diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2d77b7a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+result
+result-bin
+plugin/build/
+plugin/dist/
diff --git a/assets/icons/nvidia.svg b/assets/icons/nvidia.svg
new file mode 100644
index 0000000..01d8637
--- /dev/null
+++ b/assets/icons/nvidia.svg
@@ -0,0 +1,35 @@
+
+
+
\ No newline at end of file
diff --git a/config/BarLayout.qml b/config/BarLayout.qml
index ee47f19..ff12c9e 100644
--- a/config/BarLayout.qml
+++ b/config/BarLayout.qml
@@ -11,7 +11,7 @@ Singleton {
return `${Quickshell.shellPath("modules/bar")}/${name}.qml`;
}
- property var left: [root.widget("LauncherButton"), root.widget("PowerProfileWidget"), root.widget("HomeWidget"), root.widget("MusicWidget")]
+ property var left: [root.widget("LauncherButton"), root.widget("PowerProfileWidget"), root.widget("GPUWidget"), root.widget("HomeWidget"), root.widget("MusicWidget")]
property var center: [root.widget("CavaVisualizer"), root.widget("Workspaces"), root.widget("CavaVisualizer"),]
diff --git a/flake.nix b/flake.nix
index 53bf147..43c56a7 100644
--- a/flake.nix
+++ b/flake.nix
@@ -39,6 +39,7 @@
programs = {
nixfmt.enable = true;
qmlformat.enable = true;
+ clang-format.enable = true;
};
};
in
@@ -57,7 +58,23 @@
quickshellWithModules
pkgs.cava
pkgs.wallust
+ pkgs.dbus
+ pkgs.cmake
+ pkgs.ninja
+ pkgs.qt6.qtbase
+ pkgs.qt6.qtdeclarative
];
+
+ shellHook = ''
+ if [ -d "$PWD/plugin/dist/lib/qt-6/qml" ]; then
+ export NIXPKGS_QT6_QML_IMPORT_PATH="$PWD/plugin/dist/lib/qt-6/qml''${NIXPKGS_QT6_QML_IMPORT_PATH:+:$NIXPKGS_QT6_QML_IMPORT_PATH}"
+ export QML2_IMPORT_PATH="$PWD/plugin/dist/lib/qt-6/qml''${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}"
+ else
+ echo "gpu plugin is not built; run from the repo root to enable 'import Tshell.Cardwire':"
+ echo " cmake -S plugin -B plugin/dist -DCMAKE_INSTALL_PREFIX=\$PWD/plugin/dist"
+ echo " cmake --build plugin/dist --target install"
+ fi
+ '';
};
};
}
diff --git a/modules/bar/GPUWidget.qml b/modules/bar/GPUWidget.qml
new file mode 100644
index 0000000..275d20b
--- /dev/null
+++ b/modules/bar/GPUWidget.qml
@@ -0,0 +1,23 @@
+pragma ComponentBehavior: Bound
+
+import Quickshell
+import qs.config
+import qs.ui
+import qs.services
+
+BarButton {
+ id: root
+
+ hoverHighlight: false
+ pointerCursor: false
+ iconSource: GPU.icon
+
+ visible: GPU.connected && GPU.gpu !== null
+ opacity: GPU.gpu !== null && GPU.gpu.known ? 1 : 0
+
+ label: {
+ if (!GPU.gpu)
+ return "NA";
+ return GPU.gpu.blocked ? "Blocked" : "Active";
+ }
+}
diff --git a/package.nix b/package.nix
index d62b9d0..a2cbbe9 100644
--- a/package.nix
+++ b/package.nix
@@ -3,7 +3,10 @@
stdenv,
makeWrapper,
makeFontsConf,
+ cmake,
+ ninja,
quickshellWithModules,
+ qt6,
cava,
wallust,
nerd-fonts,
@@ -21,41 +24,52 @@ let
fontconfig = makeFontsConf {
fontDirectories = [ nerd-fonts.fira-code ];
};
+
+ # Unlike lib.fileset, this keeps untracked files (the flake only sees
+ # git-tracked paths) while dropping build artifacts.
+ src = lib.cleanSourceWith {
+ src = lib.cleanSource ./.;
+ filter =
+ path: _:
+ let
+ name = baseNameOf path;
+ in
+ !(name == "dist" || name == "result" || name == "result-bin" || lib.hasPrefix "build" name);
+ };
in
stdenv.mkDerivation {
- inherit version;
+ inherit version src;
pname = "tshell";
- src = lib.fileset.toSource {
- root = ./.;
- fileset = lib.fileset.unions [
- ./shell.qml
- ./assets
- ./config
- ./modules
- ./services
- ./ui
- ./windows
- ];
- };
- nativeBuildInputs = [ makeWrapper ];
- buildInputs = [ quickshellWithModules ];
+ dontWrapQtApps = true;
+
+ preConfigure = ''
+ cd plugin
+ '';
+
+ nativeBuildInputs = [
+ cmake
+ ninja
+ makeWrapper
+ ];
+ buildInputs = [
+ qt6.qtbase
+ qt6.qtdeclarative
+ ];
propagatedBuildInputs = runtimeDeps;
- dontBuild = true;
-
- installPhase = ''
- runHook preInstall
-
+ postInstall = ''
mkdir -p $out/bin $out/share/tshell
- cp -r . $out/share/tshell
+
+ for entry in shell.qml assets config modules services ui windows; do
+ cp -r "$src/$entry" $out/share/tshell/
+ done
makeWrapper ${quickshellWithModules}/bin/qs $out/bin/tshell \
--prefix PATH : "${lib.makeBinPath runtimeDeps}" \
--set FONTCONFIG_FILE "${fontconfig}" \
+ --suffix NIXPKGS_QT6_QML_IMPORT_PATH : "$out/lib/qt-6/qml" \
--add-flags "-p $out/share/tshell"
-
- runHook postInstall
'';
meta = {
diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt
new file mode 100644
index 0000000..60e92d5
--- /dev/null
+++ b/plugin/CMakeLists.txt
@@ -0,0 +1,46 @@
+cmake_minimum_required(VERSION 3.20)
+project(tshell-cardwire LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_AUTOMOC ON)
+
+find_package(Qt6 REQUIRED COMPONENTS Core Qml DBus)
+
+qt_add_library(tshellcardwire SHARED src/cardwire.cpp src/cardwire.hpp)
+target_include_directories(tshellcardwire PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
+
+qt_add_qml_module(tshellcardwire
+ URI Tshell.Cardwire
+ VERSION 0.1
+ DEPENDENCIES QtQml
+ OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Tshell/Cardwire"
+)
+
+target_link_libraries(tshellcardwire PRIVATE Qt6::Core Qt6::Qml Qt6::DBus)
+
+# Keep the backing library next to the plugin so "$ORIGIN" rpaths resolve both
+# in the build tree and after install.
+set(module_build_dir "${CMAKE_CURRENT_BINARY_DIR}/Tshell/Cardwire")
+set_target_properties(tshellcardwire tshellcardwireplugin PROPERTIES
+ LIBRARY_OUTPUT_DIRECTORY "${module_build_dir}"
+ RUNTIME_OUTPUT_DIRECTORY "${module_build_dir}"
+ ARCHIVE_OUTPUT_DIRECTORY "${module_build_dir}"
+ BUILD_RPATH "$ORIGIN"
+ INSTALL_RPATH "$ORIGIN"
+)
+
+qt_query_qml_module(tshellcardwire
+ PLUGIN_TARGET module_plugin_target
+ TARGET_PATH module_target_path
+ QMLDIR module_qmldir
+ TYPEINFO module_typeinfo
+)
+
+set(module_dir "${CMAKE_INSTALL_PREFIX}/lib/qt-6/qml/${module_target_path}")
+
+install(TARGETS tshellcardwire "${module_plugin_target}"
+ LIBRARY DESTINATION "${module_dir}"
+ RUNTIME DESTINATION "${module_dir}"
+)
+install(FILES "${module_qmldir}" "${module_typeinfo}" DESTINATION "${module_dir}")
diff --git a/plugin/src/cardwire.cpp b/plugin/src/cardwire.cpp
new file mode 100644
index 0000000..601acda
--- /dev/null
+++ b/plugin/src/cardwire.cpp
@@ -0,0 +1,405 @@
+#include "cardwire.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// a{oa{sa{sv}}} as returned by GetManagedObjects
+using InterfacesMap = QMap;
+using ManagedObjects = QMap;
+
+Q_DECLARE_METATYPE(InterfacesMap)
+Q_DECLARE_METATYPE(ManagedObjects)
+
+namespace {
+
+Q_LOGGING_CATEGORY(logCardwire, "tshell.cardwire", QtWarningMsg);
+
+const QString SERVICE = "org.opengamingcollective.cardwire";
+const QString ROOT_PATH = "/org/opengamingcollective/cardwire";
+const QString GPU_PREFIX = "/org/opengamingcollective/cardwire/Gpu/";
+const QString GPU_IFACE = "org.opengamingcollective.cardwire.Gpu";
+const QString PROPS_IFACE = "org.freedesktop.DBus.Properties";
+const QString OBJMGR_IFACE = "org.freedesktop.DBus.ObjectManager";
+
+void registerMetaTypes() {
+ qDBusRegisterMetaType();
+ qDBusRegisterMetaType();
+}
+
+QDBusMessage methodCall(const QString &path, const QString &iface,
+ const QString &member) {
+ return QDBusMessage::createMethodCall(SERVICE, path, iface, member);
+}
+
+QDBusPendingCallWatcher *asyncCall(QObject *parent,
+ const QDBusMessage &message) {
+ return new QDBusPendingCallWatcher(
+ QDBusConnection::systemBus().asyncCall(message), parent);
+}
+
+void logError(const QString &what, const QDBusMessage &reply) {
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qCWarning(logCardwire).nospace()
+ << what << ": " << reply.errorName() << " " << reply.errorMessage();
+ }
+}
+
+} // namespace
+
+CardwireGpu::CardwireGpu(QString objectPath, QObject *parent)
+ : QObject(parent), mObjectPath(std::move(objectPath)) {
+ auto bus = QDBusConnection::systemBus();
+ bus.connect(SERVICE, this->mObjectPath, PROPS_IFACE, "PropertiesChanged",
+ this, SLOT(onPropertiesChanged(QDBusMessage)));
+ bus.connect(SERVICE, this->mObjectPath, GPU_IFACE, "PowerStateChanged", this,
+ SLOT(onPowerStateChanged(QString)));
+
+ this->refreshDevice();
+ this->refreshProperties();
+}
+
+void CardwireGpu::refreshDevice() {
+ QObject::connect(
+ asyncCall(this, methodCall(this->mObjectPath, GPU_IFACE, "GetDevice")),
+ &QDBusPendingCallWatcher::finished, this,
+ &CardwireGpu::onGetDeviceFinished);
+}
+
+void CardwireGpu::onGetDeviceFinished() {
+ auto *watcher = qobject_cast(this->sender());
+ if (watcher == nullptr)
+ return;
+ watcher->deleteLater();
+
+ const QDBusMessage reply = watcher->reply();
+ logError("GetDevice failed for " + this->mObjectPath, reply);
+ if (reply.type() == QDBusMessage::ErrorMessage)
+ return;
+
+ // (ssuubbbbssbs): name pci render card default discrete virtual available
+ // vendor driver nvidia nvidia_minor
+ QString pci;
+ QString vendor;
+ QString driver;
+ QString nvidiaMinor;
+ uint render = 0;
+ uint card = 0;
+ bool virtualGpu = false;
+
+ const auto arguments = reply.arguments();
+ if (arguments.first().userType() != qMetaTypeId()) {
+ // The daemon actually sends the fields as separate out arguments.
+ if (arguments.size() < 12) {
+ qCWarning(logCardwire)
+ << "GetDevice returned" << arguments.size() << "arguments";
+ return;
+ }
+ this->mName = arguments.at(0).toString();
+ pci = arguments.at(1).toString();
+ render = arguments.at(2).toUInt();
+ card = arguments.at(3).toUInt();
+ this->mDefault = arguments.at(4).toBool();
+ this->mDiscrete = arguments.at(5).toBool();
+ virtualGpu = arguments.at(6).toBool();
+ this->mAvailable = arguments.at(7).toBool();
+ vendor = arguments.at(8).toString();
+ driver = arguments.at(9).toString();
+ this->mNvidia = arguments.at(10).toBool();
+ nvidiaMinor = arguments.at(11).toString();
+ } else {
+ // Documented shape: all fields packed into a single struct.
+ // Must be const: demarshalling uses the const (read) overloads.
+ const QDBusArgument arg = arguments.first().value();
+ arg.beginStructure();
+ arg >> this->mName >> pci >> render >> card >> this->mDefault >>
+ this->mDiscrete >> virtualGpu >> this->mAvailable >> vendor >> driver >>
+ this->mNvidia >> nvidiaMinor;
+ arg.endStructure();
+ }
+
+ if (!this->mKnown) {
+ this->mKnown = true;
+ qCInfo(logCardwire).nospace() << "found gpu \"" << this->mName << "\" at "
+ << this->mObjectPath << " (" << pci << ")";
+ }
+
+ emit this->deviceChanged();
+}
+
+void CardwireGpu::refreshProperties() {
+ QDBusMessage call = methodCall(this->mObjectPath, PROPS_IFACE, "GetAll");
+ call << GPU_IFACE;
+
+ QObject::connect(asyncCall(this, call), &QDBusPendingCallWatcher::finished,
+ this, &CardwireGpu::onGetAllFinished);
+}
+
+void CardwireGpu::onGetAllFinished() {
+ auto *watcher = qobject_cast(this->sender());
+ if (watcher == nullptr)
+ return;
+ watcher->deleteLater();
+
+ const QDBusMessage reply = watcher->reply();
+ logError("GetAll failed for " + this->mObjectPath, reply);
+ if (reply.type() == QDBusMessage::ErrorMessage)
+ return;
+
+ const auto properties = qdbus_cast(reply.arguments().at(0));
+ for (auto it = properties.constBegin(); it != properties.constEnd(); ++it) {
+ if (it.key() == "Block")
+ this->setBlockedValue(it.value().toBool());
+ else if (it.key() == "Launchable")
+ this->setLaunchableValue(it.value().toBool());
+ }
+}
+
+void CardwireGpu::setBlocked(bool blocked) {
+ QDBusMessage call = methodCall(this->mObjectPath, PROPS_IFACE, "Set");
+ call << GPU_IFACE << QStringLiteral("Block")
+ << QVariant::fromValue(QDBusVariant(blocked));
+
+ // The resulting state is confirmed asynchronously via PropertiesChanged.
+ asyncCall(this, call);
+}
+
+void CardwireGpu::onPropertiesChanged(const QDBusMessage &message) {
+ const auto arguments = message.arguments();
+ if (arguments.isEmpty() || arguments.first().toString() != GPU_IFACE)
+ return;
+
+ if (arguments.size() > 1) {
+ const auto properties = qdbus_cast(arguments.at(1));
+ for (auto it = properties.constBegin(); it != properties.constEnd(); ++it) {
+ if (it.key() == "Block")
+ this->setBlockedValue(it.value().toBool());
+ else if (it.key() == "Launchable")
+ this->setLaunchableValue(it.value().toBool());
+ }
+ }
+
+ if (arguments.size() > 2) { // invalidated properties: refetch everything
+ const auto invalidated = qdbus_cast(arguments.at(2));
+ if (invalidated.contains("Block") || invalidated.contains("Launchable")) {
+ this->refreshProperties();
+ }
+ }
+}
+
+void CardwireGpu::onPowerStateChanged(const QString &state) {
+ emit this->powerStateChanged(state);
+}
+
+void CardwireGpu::setBlockedValue(bool blocked) {
+ if (blocked == this->mBlocked)
+ return;
+ this->mBlocked = blocked;
+ qCDebug(logCardwire).nospace()
+ << this->mObjectPath << ": block = " << blocked;
+ emit this->blockedChanged();
+}
+
+void CardwireGpu::setLaunchableValue(bool launchable) {
+ if (launchable == this->mLaunchable)
+ return;
+ this->mLaunchable = launchable;
+ emit this->launchableChanged();
+}
+
+Cardwire::Cardwire(QObject *parent) : QObject(parent), mWatcher(this) {}
+
+Cardwire *Cardwire::instance() {
+ static Cardwire *instance =
+ nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+ if (instance == nullptr) {
+ registerMetaTypes();
+ instance = new Cardwire();
+ instance->connectSignals();
+ }
+ return instance;
+}
+
+Cardwire *Cardwire::create(QQmlEngine * /*qmlEngine*/,
+ QJSEngine * /*jsEngine*/) {
+ auto *instance = Cardwire::instance();
+ QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership);
+ return instance;
+}
+
+CardwireGpu *Cardwire::nvidiaGpu() const {
+ for (auto *gpu : this->mGpus) {
+ if (gpu->isNvidia())
+ return gpu;
+ }
+ return nullptr;
+}
+
+void Cardwire::connectSignals() {
+ // cardwired runs as a system service, so its API is on the system bus.
+ this->mWatcher.setConnection(QDBusConnection::systemBus());
+ QObject::connect(&this->mWatcher, &QDBusServiceWatcher::serviceOwnerChanged,
+ this, &Cardwire::onServiceOwnerChanged);
+ this->mWatcher.setWatchMode(QDBusServiceWatcher::WatchForOwnerChange);
+
+ auto bus = QDBusConnection::systemBus();
+ bus.connect(SERVICE, ROOT_PATH, OBJMGR_IFACE, "InterfacesAdded", this,
+ SLOT(onInterfacesAdded(QDBusMessage)));
+ bus.connect(SERVICE, ROOT_PATH, OBJMGR_IFACE, "InterfacesRemoved", this,
+ SLOT(onInterfacesRemoved(QDBusMessage)));
+
+ // Kick off the initial sync; if the daemon is not running yet the service
+ // watcher triggers another sync once it appears.
+ this->sync();
+}
+
+void Cardwire::onServiceOwnerChanged(const QString &serviceName,
+ const QString &oldOwner,
+ const QString &newOwner) {
+ if (serviceName != SERVICE)
+ return;
+ qCInfo(logCardwire).nospace()
+ << SERVICE << " owner: " << oldOwner << " -> " << newOwner;
+
+ this->sync();
+}
+
+void Cardwire::sync() {
+ const auto generation = ++this->mGeneration;
+ auto *watcher =
+ asyncCall(this, methodCall(ROOT_PATH, OBJMGR_IFACE, "GetManagedObjects"));
+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this,
+ [this, generation]() { this->handleSyncReply(generation); });
+}
+
+void Cardwire::handleSyncReply(quint32 generation) {
+ auto *watcher = qobject_cast(this->sender());
+ if (watcher == nullptr)
+ return;
+ watcher->deleteLater();
+
+ if (generation != this->mGeneration)
+ return; // a newer sync superseded this one
+
+ const QDBusMessage reply = watcher->reply();
+ logError("GetManagedObjects failed", reply);
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ if (this->mConnected) {
+ this->mConnected = false;
+ emit this->connectedChanged();
+ }
+
+ for (auto *gpu : this->mGpus)
+ gpu->deleteLater();
+ if (!this->mGpus.isEmpty()) {
+ this->mGpus.clear();
+ emit this->gpusChanged();
+ }
+ return;
+ }
+
+ QList seen;
+ const auto objects = qdbus_cast(reply.arguments().at(0));
+ for (auto it = objects.constBegin(); it != objects.constEnd(); ++it) {
+ if (!it.value().contains(GPU_IFACE))
+ continue;
+
+ const QString &path = it.key().path();
+ if (!path.startsWith(GPU_PREFIX))
+ continue;
+
+ this->ensureGpu(path);
+ seen.append(this->findGpu(path));
+ }
+
+ // Remove GPUs that disappeared (hotplug).
+ for (auto *gpu : this->mGpus) {
+ if (!seen.contains(gpu))
+ gpu->deleteLater();
+ }
+
+ if (seen != this->mGpus) {
+ this->mGpus = seen;
+ emit this->gpusChanged();
+ }
+
+ if (!this->mConnected) {
+ this->mConnected = true;
+ qCInfo(logCardwire) << "connected to" << SERVICE;
+ emit this->connectedChanged();
+ }
+}
+
+void Cardwire::ensureGpu(const QString &path) {
+ if (this->findGpu(path) != nullptr)
+ return;
+
+ auto *gpu = new CardwireGpu(path, this);
+ // The nvidia flag only becomes known once GetDevice replies.
+ QObject::connect(gpu, &CardwireGpu::deviceChanged, this,
+ &Cardwire::updateNvidiaGpu);
+ this->mGpus.append(gpu);
+ qCInfo(logCardwire) << "tracking" << path;
+ emit this->gpusChanged();
+}
+
+CardwireGpu *Cardwire::findGpu(const QString &path) const {
+ for (auto *gpu : this->mGpus) {
+ if (gpu->objectPath() == path)
+ return gpu;
+ }
+ return nullptr;
+}
+
+void Cardwire::removeGpu(const QString &path) {
+ auto *gpu = this->findGpu(path);
+ if (gpu == nullptr)
+ return;
+
+ gpu->deleteLater();
+ this->mGpus.removeOne(gpu);
+ qCInfo(logCardwire) << "stopped tracking" << path;
+ emit this->gpusChanged();
+}
+
+void Cardwire::onInterfacesAdded(const QDBusMessage &message) {
+ const auto arguments = message.arguments();
+ if (arguments.size() < 2)
+ return;
+
+ const auto path = arguments.first().toString();
+ if (!path.startsWith(GPU_PREFIX))
+ return;
+
+ if (qdbus_cast(arguments.at(1)).contains(GPU_IFACE))
+ this->ensureGpu(path);
+}
+
+void Cardwire::onInterfacesRemoved(const QDBusMessage &message) {
+ const auto arguments = message.arguments();
+ if (arguments.size() < 2)
+ return;
+
+ const auto path = arguments.first().toString();
+ if (!path.startsWith(GPU_PREFIX))
+ return;
+
+ if (qdbus_cast(arguments.at(1)).contains(GPU_IFACE))
+ this->removeGpu(path);
+}
+
+void Cardwire::updateNvidiaGpu() {
+ auto *current = this->nvidiaGpu();
+ if (current == this->mCurrentNvidiaGpu)
+ return;
+
+ this->mCurrentNvidiaGpu = current;
+ emit this->nvidiaGpuChanged();
+}
diff --git a/plugin/src/cardwire.hpp b/plugin/src/cardwire.hpp
new file mode 100644
index 0000000..1fc7552
--- /dev/null
+++ b/plugin/src/cardwire.hpp
@@ -0,0 +1,129 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class QDBusPendingCallWatcher;
+class QJSEngine;
+class QQmlEngine;
+
+/// A single GPU exposed by cardwire at
+/// /org/opengamingcollective/cardwire/Gpu/{id}.
+///
+/// Mirrors org.opengamingcollective.cardwire.Gpu: device details are fetched
+/// once via GetDevice, while Block/Launchable are kept in sync through
+/// org.freedesktop.DBus.Properties.PropertiesChanged.
+class CardwireGpu : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("Gpu objects are managed by the Cardwire singleton")
+
+ Q_PROPERTY(QString objectPath READ objectPath CONSTANT)
+ Q_PROPERTY(bool known READ known NOTIFY deviceChanged)
+ Q_PROPERTY(QString name READ name NOTIFY deviceChanged)
+ Q_PROPERTY(bool nvidia READ isNvidia NOTIFY deviceChanged)
+ Q_PROPERTY(bool defaultGpu READ isDefault NOTIFY deviceChanged)
+ Q_PROPERTY(bool discrete READ isDiscrete NOTIFY deviceChanged)
+ Q_PROPERTY(bool available READ isAvailable NOTIFY deviceChanged)
+ Q_PROPERTY(bool blocked READ isBlocked NOTIFY blockedChanged)
+ Q_PROPERTY(bool launchable READ isLaunchable NOTIFY launchableChanged)
+
+public:
+ explicit CardwireGpu(QString objectPath, QObject *parent = nullptr);
+
+ [[nodiscard]] const QString &objectPath() const { return this->mObjectPath; }
+ [[nodiscard]] bool known() const { return this->mKnown; }
+ [[nodiscard]] const QString &name() const { return this->mName; }
+ [[nodiscard]] bool isNvidia() const { return this->mNvidia; }
+ [[nodiscard]] bool isDefault() const { return this->mDefault; }
+ [[nodiscard]] bool isDiscrete() const { return this->mDiscrete; }
+ [[nodiscard]] bool isAvailable() const { return this->mAvailable; }
+ [[nodiscard]] bool isBlocked() const { return this->mBlocked; }
+ [[nodiscard]] bool isLaunchable() const { return this->mLaunchable; }
+
+ // Writes the Block property. Only succeeds when the daemon mode allows it
+ // (the daemon rejects the write otherwise and the state stays unchanged).
+ Q_INVOKABLE void setBlocked(bool blocked);
+
+signals:
+ void deviceChanged();
+ void blockedChanged();
+ void launchableChanged();
+ void powerStateChanged(const QString &state);
+
+private slots:
+ void onPropertiesChanged(const QDBusMessage &message);
+ void onPowerStateChanged(const QString &state);
+ void onGetDeviceFinished();
+ void onGetAllFinished();
+
+private:
+ void refreshDevice();
+ void refreshProperties();
+ void setBlockedValue(bool blocked);
+ void setLaunchableValue(bool launchable);
+
+ QString mObjectPath;
+ bool mKnown = false;
+ QString mName;
+ bool mNvidia = false;
+ bool mDefault = false;
+ bool mDiscrete = false;
+ bool mAvailable = false;
+ bool mBlocked = false;
+ bool mLaunchable = false;
+};
+
+/// Exposes the GPUs tracked by the cardwire daemon over D-Bus.
+class Cardwire : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+ QML_SINGLETON
+
+ Q_PROPERTY(bool connected READ isConnected NOTIFY connectedChanged)
+ Q_PROPERTY(QList gpus READ gpus NOTIFY gpusChanged)
+ Q_PROPERTY(CardwireGpu *nvidiaGpu READ nvidiaGpu NOTIFY nvidiaGpuChanged)
+
+public:
+ static Cardwire *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);
+ static Cardwire *instance();
+
+ [[nodiscard]] bool isConnected() const { return this->mConnected; }
+ [[nodiscard]] QList gpus() const { return this->mGpus; }
+
+ /// The first NVIDIA GPU found by the daemon, or nullptr if there is none.
+ [[nodiscard]] CardwireGpu *nvidiaGpu() const;
+
+signals:
+ void connectedChanged();
+ void gpusChanged();
+ void nvidiaGpuChanged();
+
+private slots:
+ void onServiceOwnerChanged(const QString &serviceName,
+ const QString &oldOwner, const QString &newOwner);
+ void onInterfacesAdded(const QDBusMessage &message);
+ void onInterfacesRemoved(const QDBusMessage &message);
+
+private:
+ explicit Cardwire(QObject *parent = nullptr);
+
+ void connectSignals();
+ void sync();
+ void handleSyncReply(quint32 generation);
+ void ensureGpu(const QString &path);
+ [[nodiscard]] CardwireGpu *findGpu(const QString &path) const;
+ void removeGpu(const QString &path);
+ void updateNvidiaGpu();
+
+ QDBusServiceWatcher mWatcher;
+ QList mGpus;
+ CardwireGpu *mCurrentNvidiaGpu = nullptr;
+ bool mConnected = false;
+ quint32 mGeneration = 0;
+};
diff --git a/services/GPU.qml b/services/GPU.qml
new file mode 100644
index 0000000..0e8a6c7
--- /dev/null
+++ b/services/GPU.qml
@@ -0,0 +1,12 @@
+pragma Singleton
+
+import Quickshell
+import Tshell.Cardwire
+
+Singleton {
+ id: root
+
+ readonly property bool connected: Cardwire.connected
+ readonly property CardwireGpu gpu: Cardwire.nvidiaGpu
+ readonly property url icon: `${Quickshell.shellPath("assets")}/icons/nvidia.svg`
+}