mirror of
https://github.com/tuxdotrs/tpanel.git
synced 2026-09-19 10:19:02 +05:30
feat(gpu): add cardwire integration and gpu widget
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
result
|
||||||
|
result-bin
|
||||||
|
plugin/build/
|
||||||
|
plugin/dist/
|
||||||
35
assets/icons/nvidia.svg
Normal file
35
assets/icons/nvidia.svg
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
width="128" height="128" version="1.1">
|
||||||
|
<title>Nvidia Logo</title>
|
||||||
|
<desc>This is shape (source) for Clarity vector icon theme for gtk</desc>
|
||||||
|
<metadata>
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work rdf:about="">
|
||||||
|
<dc:title>Nvidia Logo</dc:title>
|
||||||
|
<dc:description>This is shape (source) for Clarity vector icon theme for gtk</dc:description>
|
||||||
|
<dc:creator>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Jakub Jankiewicz</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:creator>
|
||||||
|
<dc:rights>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Jakub Jankiewicz</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:rights>
|
||||||
|
<dc:date>2010</dc:date>
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<path d="m 19.777894,58.887563 c 0,0 10.05027,-14.828437 30.117316,-16.362058 l 0,-5.380182 c -22.2268,1.785057 -41.4742019,20.61011 -41.4742019,20.61011 0,0 10.9013539,31.514726 41.4742019,34.400204 l 0,-5.718258 C 27.458955,83.614426 19.777894,58.887563 19.777894,58.887563 z m 30.117316,16.177165 0,5.236824 C 32.938729,77.278522 28.232018,59.651693 28.232018,59.651693 c 0,0 8.141505,-9.019072 21.663192,-10.481238 l 0,5.746393 c -0.01032,0 -0.01741,-0.0031 -0.0259,-0.0031 -7.09691,-0.851665 -12.640994,5.777655 -12.640994,5.777655 0,0 3.106991,11.16141 12.666896,14.373351 m 0,-47.841948 0,9.922543 c 0.652482,-0.05047 1.304964,-0.09245 1.961018,-0.114779 25.269481,-0.851665 41.734256,20.724887 41.734256,20.724887 0,0 -18.910353,22.994504 -38.611635,22.994504 -1.805601,0 -3.495979,-0.167475 -5.083639,-0.448385 l 0,6.135827 c 1.357662,0.172387 2.765343,0.273767 4.234208,0.273767 18.333347,0 31.59154,-9.362955 44.429483,-20.444422 2.128489,1.704667 10.842539,5.851342 12.633849,7.667216 -12.206453,10.219531 -40.654381,18.457501 -56.782421,18.457501 -1.554612,0 -3.047594,-0.09424 -4.515119,-0.235803 l 0,8.6216 69.68378,0 0,-73.554458 z m 0,21.947675 0,-6.64495 c 0.645782,-0.04511 1.296477,-0.07995 1.961018,-0.100929 18.171232,-0.570753 30.0932,15.615345 30.0932,15.615345 0,0 -12.876351,17.883175 -26.682522,17.883175 -1.98692,0 -3.768404,-0.319318 -5.371696,-0.858364 l 0,-20.147881 c 7.074133,0.854792 8.497892,3.979647 12.750856,11.06941 l 9.458973,-7.976263 c 0,0 -6.904871,-9.055693 -18.544589,-9.055693 -1.266109,0 -2.476839,0.08888 -3.66524,0.216154"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -11,7 +11,7 @@ Singleton {
|
|||||||
return `${Quickshell.shellPath("modules/bar")}/${name}.qml`;
|
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"),]
|
property var center: [root.widget("CavaVisualizer"), root.widget("Workspaces"), root.widget("CavaVisualizer"),]
|
||||||
|
|
||||||
|
|||||||
17
flake.nix
17
flake.nix
@@ -39,6 +39,7 @@
|
|||||||
programs = {
|
programs = {
|
||||||
nixfmt.enable = true;
|
nixfmt.enable = true;
|
||||||
qmlformat.enable = true;
|
qmlformat.enable = true;
|
||||||
|
clang-format.enable = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
@@ -57,7 +58,23 @@
|
|||||||
quickshellWithModules
|
quickshellWithModules
|
||||||
pkgs.cava
|
pkgs.cava
|
||||||
pkgs.wallust
|
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
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
23
modules/bar/GPUWidget.qml
Normal file
23
modules/bar/GPUWidget.qml
Normal file
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
60
package.nix
60
package.nix
@@ -3,7 +3,10 @@
|
|||||||
stdenv,
|
stdenv,
|
||||||
makeWrapper,
|
makeWrapper,
|
||||||
makeFontsConf,
|
makeFontsConf,
|
||||||
|
cmake,
|
||||||
|
ninja,
|
||||||
quickshellWithModules,
|
quickshellWithModules,
|
||||||
|
qt6,
|
||||||
cava,
|
cava,
|
||||||
wallust,
|
wallust,
|
||||||
nerd-fonts,
|
nerd-fonts,
|
||||||
@@ -21,41 +24,52 @@ let
|
|||||||
fontconfig = makeFontsConf {
|
fontconfig = makeFontsConf {
|
||||||
fontDirectories = [ nerd-fonts.fira-code ];
|
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
|
in
|
||||||
stdenv.mkDerivation {
|
stdenv.mkDerivation {
|
||||||
inherit version;
|
inherit version src;
|
||||||
pname = "tshell";
|
pname = "tshell";
|
||||||
src = lib.fileset.toSource {
|
|
||||||
root = ./.;
|
|
||||||
fileset = lib.fileset.unions [
|
|
||||||
./shell.qml
|
|
||||||
./assets
|
|
||||||
./config
|
|
||||||
./modules
|
|
||||||
./services
|
|
||||||
./ui
|
|
||||||
./windows
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
nativeBuildInputs = [ makeWrapper ];
|
dontWrapQtApps = true;
|
||||||
buildInputs = [ quickshellWithModules ];
|
|
||||||
|
preConfigure = ''
|
||||||
|
cd plugin
|
||||||
|
'';
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
cmake
|
||||||
|
ninja
|
||||||
|
makeWrapper
|
||||||
|
];
|
||||||
|
buildInputs = [
|
||||||
|
qt6.qtbase
|
||||||
|
qt6.qtdeclarative
|
||||||
|
];
|
||||||
propagatedBuildInputs = runtimeDeps;
|
propagatedBuildInputs = runtimeDeps;
|
||||||
|
|
||||||
dontBuild = true;
|
postInstall = ''
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
runHook preInstall
|
|
||||||
|
|
||||||
mkdir -p $out/bin $out/share/tshell
|
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 \
|
makeWrapper ${quickshellWithModules}/bin/qs $out/bin/tshell \
|
||||||
--prefix PATH : "${lib.makeBinPath runtimeDeps}" \
|
--prefix PATH : "${lib.makeBinPath runtimeDeps}" \
|
||||||
--set FONTCONFIG_FILE "${fontconfig}" \
|
--set FONTCONFIG_FILE "${fontconfig}" \
|
||||||
|
--suffix NIXPKGS_QT6_QML_IMPORT_PATH : "$out/lib/qt-6/qml" \
|
||||||
--add-flags "-p $out/share/tshell"
|
--add-flags "-p $out/share/tshell"
|
||||||
|
|
||||||
runHook postInstall
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
|
|||||||
46
plugin/CMakeLists.txt
Normal file
46
plugin/CMakeLists.txt
Normal file
@@ -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}")
|
||||||
405
plugin/src/cardwire.cpp
Normal file
405
plugin/src/cardwire.cpp
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
#include "cardwire.hpp"
|
||||||
|
|
||||||
|
#include <qdbusargument.h>
|
||||||
|
#include <qdbusconnection.h>
|
||||||
|
#include <qdbusextratypes.h>
|
||||||
|
#include <qdbusmessage.h>
|
||||||
|
#include <qdbusmetatype.h>
|
||||||
|
#include <qdbuspendingcall.h>
|
||||||
|
#include <qdbusservicewatcher.h>
|
||||||
|
#include <qloggingcategory.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
// a{oa{sa{sv}}} as returned by GetManagedObjects
|
||||||
|
using InterfacesMap = QMap<QString, QVariantMap>;
|
||||||
|
using ManagedObjects = QMap<QDBusObjectPath, InterfacesMap>;
|
||||||
|
|
||||||
|
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<InterfacesMap>();
|
||||||
|
qDBusRegisterMetaType<ManagedObjects>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<QDBusPendingCallWatcher *>(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<QDBusArgument>()) {
|
||||||
|
// 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<QDBusArgument>();
|
||||||
|
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<QDBusPendingCallWatcher *>(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<QVariantMap>(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<QVariantMap>(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<QStringList>(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<QDBusPendingCallWatcher *>(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<CardwireGpu *> seen;
|
||||||
|
const auto objects = qdbus_cast<ManagedObjects>(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<InterfacesMap>(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<QStringList>(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();
|
||||||
|
}
|
||||||
129
plugin/src/cardwire.hpp
Normal file
129
plugin/src/cardwire.hpp
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qdbusmessage.h>
|
||||||
|
#include <qdbusservicewatcher.h>
|
||||||
|
#include <qlist.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qstring.h>
|
||||||
|
|
||||||
|
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<CardwireGpu *> 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<CardwireGpu *> 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<CardwireGpu *> mGpus;
|
||||||
|
CardwireGpu *mCurrentNvidiaGpu = nullptr;
|
||||||
|
bool mConnected = false;
|
||||||
|
quint32 mGeneration = 0;
|
||||||
|
};
|
||||||
12
services/GPU.qml
Normal file
12
services/GPU.qml
Normal file
@@ -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`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user