From c9310e802be62bc149cc596ff3531faadda37e0e Mon Sep 17 00:00:00 2001 From: tux Date: Tue, 25 Aug 2026 00:38:14 +0530 Subject: [PATCH] feat(gpu): add cardwire integration and gpu widget --- .gitignore | 4 + assets/icons/nvidia.svg | 35 ++++ config/BarLayout.qml | 2 +- flake.nix | 17 ++ modules/bar/GPUWidget.qml | 23 +++ package.nix | 60 +++--- plugin/CMakeLists.txt | 46 +++++ plugin/src/cardwire.cpp | 405 ++++++++++++++++++++++++++++++++++++++ plugin/src/cardwire.hpp | 129 ++++++++++++ services/GPU.qml | 12 ++ 10 files changed, 709 insertions(+), 24 deletions(-) create mode 100644 .gitignore create mode 100644 assets/icons/nvidia.svg create mode 100644 modules/bar/GPUWidget.qml create mode 100644 plugin/CMakeLists.txt create mode 100644 plugin/src/cardwire.cpp create mode 100644 plugin/src/cardwire.hpp create mode 100644 services/GPU.qml 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 @@ + + + + Nvidia Logo + This is shape (source) for Clarity vector icon theme for gtk + + + + Nvidia Logo + This is shape (source) for Clarity vector icon theme for gtk + + + Jakub Jankiewicz + + + + + Jakub Jankiewicz + + + 2010 + image/svg+xml + + + + + + + \ 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` +}