add nova-plugin: in-process cxx-qt stats plugin replacing nova-stats subprocess
This commit is contained in:
parent
40cc681e9a
commit
e39d47177b
19 changed files with 1893 additions and 233 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
result
|
result
|
||||||
result-*
|
result-*
|
||||||
stats-daemon/target/
|
stats-daemon/target/
|
||||||
|
plugin/target/
|
||||||
|
|
|
||||||
|
|
@ -60,17 +60,16 @@ imports = [ inputs.nova-shell.nixosModules.default ];
|
||||||
You poor thing. Here's what the Nix packaging does for you, manually:
|
You poor thing. Here's what the Nix packaging does for you, manually:
|
||||||
|
|
||||||
1. Build [quickshell](https://quickshell.outfoxxed.me) from source. You'll need Qt 6.
|
1. Build [quickshell](https://quickshell.outfoxxed.me) from source. You'll need Qt 6.
|
||||||
2. Build `nova-stats` from `stats-daemon/` (`cargo build --release`). Put the binary in your PATH.
|
2. Build the stats plugin from `plugin/` (`cargo build --release`). Copy `libnova_plugin.so` and `target/qml_modules/NovaStats/qmldir` into a `NovaStats/` directory on Qt's QML import path. Set `QMAKE` to your `qmake6` before building.
|
||||||
3. Put `gdbus` (from glib) in your PATH. The lock screen needs it.
|
3. Put `gdbus` (from glib) in your PATH. The lock screen needs it.
|
||||||
4. Compile the shaders: `for f in shell/modules/*.frag; do qsb --qt6 -o "${f}.qsb" "$f"; done`
|
4. Compile the shaders: `for f in shell/modules/*.frag; do qsb --qt6 -o "${f}.qsb" "$f"; done`
|
||||||
5. Install [Symbols Nerd Font](https://www.nerdfonts.com/).
|
5. Install [Symbols Nerd Font](https://www.nerdfonts.com/).
|
||||||
6. Create `~/.config/nova-shell/theme.json` and `~/.config/nova-shell/modules.json`. The Nix module generates these from your config. Without it, you write JSON by hand like a human. See the theme and module tables below for keys.
|
6. Create `~/.config/nova-shell/theme.json` and `~/.config/nova-shell/modules.json`. The Nix module generates these from your config. Without it, you write JSON by hand like a human. See the theme and module tables below for keys.
|
||||||
7. Run: `quickshell -p /path/to/nova-shell/shell/shell.qml`
|
7. Run: `quickshell -I /path/to/qml-modules -p /path/to/nova-shell/shell/shell.qml`
|
||||||
|
|
||||||
Optional runtime dependencies, depending on which modules you enable:
|
Optional runtime dependencies, depending on which modules you enable:
|
||||||
- `wttrbar` for weather
|
- `wttrbar` for weather
|
||||||
- `cava` for the audio visualizer on album art
|
- `cava` for the audio visualizer on album art
|
||||||
- `coreutils` (df) for disk usage
|
|
||||||
|
|
||||||
If something breaks, the Nix module would have handled it. You chose this path. I respect it in the same way I respect people who free-climb skyscrapers.
|
If something breaks, the Nix module would have handled it. You chose this path. I respect it in the same way I respect people who free-climb skyscrapers.
|
||||||
|
|
||||||
|
|
|
||||||
12
flake.nix
12
flake.nix
|
|
@ -74,12 +74,13 @@
|
||||||
};
|
};
|
||||||
nova-stats = pkgs.callPackage ./nix/stats-daemon.nix { };
|
nova-stats = pkgs.callPackage ./nix/stats-daemon.nix { };
|
||||||
nova-shaders = pkgs.callPackage ./nix/shaders.nix { };
|
nova-shaders = pkgs.callPackage ./nix/shaders.nix { };
|
||||||
|
nova-plugin = pkgs.callPackage ./nix/plugin.nix { };
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
inherit nova-stats nova-shaders;
|
inherit nova-stats nova-shaders nova-plugin;
|
||||||
nova-shell = pkgs.callPackage ./nix/package.nix {
|
nova-shell = pkgs.callPackage ./nix/package.nix {
|
||||||
quickshell = qs;
|
quickshell = qs;
|
||||||
inherit nova-stats nova-shaders;
|
inherit nova-stats nova-shaders nova-plugin;
|
||||||
};
|
};
|
||||||
nova-shell-cli = pkgs.runCommand "nova-shell-cli" { nativeBuildInputs = [ pkgs.makeWrapper ]; } ''
|
nova-shell-cli = pkgs.runCommand "nova-shell-cli" { nativeBuildInputs = [ pkgs.makeWrapper ]; } ''
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
|
|
@ -99,7 +100,7 @@
|
||||||
cd $src
|
cd $src
|
||||||
export QML_IMPORT_PATH="${rawPkgs.qt6.qtdeclarative}/lib/qt-6/qml:${
|
export QML_IMPORT_PATH="${rawPkgs.qt6.qtdeclarative}/lib/qt-6/qml:${
|
||||||
quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default
|
quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default
|
||||||
}/lib/qt-6/qml"
|
}/lib/qt-6/qml:${self.packages.${pkgs.stdenv.hostPlatform.system}.nova-plugin}/lib/qt-6/qml"
|
||||||
qmllint -E \
|
qmllint -E \
|
||||||
-I shell/modules -I shell/services -I shell/applets -I shell/dock -I shell/lock \
|
-I shell/modules -I shell/services -I shell/applets -I shell/dock -I shell/lock \
|
||||||
shell/shell.qml shell/modules/*.qml shell/services/*.qml \
|
shell/shell.qml shell/modules/*.qml shell/services/*.qml \
|
||||||
|
|
@ -126,6 +127,7 @@
|
||||||
quickshell = qsUnpatched;
|
quickshell = qsUnpatched;
|
||||||
nova-stats = rawPkgs.callPackage ./nix/stats-daemon.nix { };
|
nova-stats = rawPkgs.callPackage ./nix/stats-daemon.nix { };
|
||||||
nova-shaders = rawPkgs.callPackage ./nix/shaders.nix { };
|
nova-shaders = rawPkgs.callPackage ./nix/shaders.nix { };
|
||||||
|
nova-plugin = rawPkgs.callPackage ./nix/plugin.nix { };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -142,7 +144,10 @@
|
||||||
rustfmt
|
rustfmt
|
||||||
libnotify
|
libnotify
|
||||||
qt6.qtdeclarative
|
qt6.qtdeclarative
|
||||||
|
qt6.qtbase
|
||||||
|
pkg-config
|
||||||
];
|
];
|
||||||
|
QMAKE = "${rawPkgs.qt6.qtbase}/bin/qmake6";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -157,6 +162,7 @@
|
||||||
formatting = treefmt-eval.config.build.check self;
|
formatting = treefmt-eval.config.build.check self;
|
||||||
build = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
build = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||||
nova-stats = self.packages.${pkgs.stdenv.hostPlatform.system}.nova-stats;
|
nova-stats = self.packages.${pkgs.stdenv.hostPlatform.system}.nova-stats;
|
||||||
|
nova-plugin = self.packages.${pkgs.stdenv.hostPlatform.system}.nova-plugin;
|
||||||
docs = self.packages.${pkgs.stdenv.hostPlatform.system}.docs;
|
docs = self.packages.${pkgs.stdenv.hostPlatform.system}.docs;
|
||||||
qmllint =
|
qmllint =
|
||||||
let
|
let
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
quickshell,
|
quickshell,
|
||||||
nova-stats,
|
nova-stats,
|
||||||
nova-shaders,
|
nova-shaders,
|
||||||
|
nova-plugin,
|
||||||
glib,
|
glib,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
|
@ -38,6 +39,7 @@ symlinkJoin {
|
||||||
paths = [
|
paths = [
|
||||||
shell
|
shell
|
||||||
nova-shaders
|
nova-shaders
|
||||||
|
nova-plugin
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = [ makeWrapper ];
|
nativeBuildInputs = [ makeWrapper ];
|
||||||
|
|
@ -46,6 +48,7 @@ symlinkJoin {
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
makeWrapper ${lib.getExe quickshell} $out/bin/nova-shell \
|
makeWrapper ${lib.getExe quickshell} $out/bin/nova-shell \
|
||||||
--add-flags "-p $out/share/nova-shell/shell.qml" \
|
--add-flags "-p $out/share/nova-shell/shell.qml" \
|
||||||
|
--add-flags "-I $out/lib/qt-6/qml" \
|
||||||
--prefix PATH : ${
|
--prefix PATH : ${
|
||||||
lib.makeBinPath [
|
lib.makeBinPath [
|
||||||
nova-stats
|
nova-stats
|
||||||
|
|
|
||||||
79
nix/plugin.nix
Normal file
79
nix/plugin.nix
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
rustPlatform,
|
||||||
|
pkg-config,
|
||||||
|
qt6,
|
||||||
|
writeShellScript,
|
||||||
|
runCommand,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
# nixpkgs splits Qt tools across packages: moc/rcc/qtpaths live in qtbase,
|
||||||
|
# qmltyperegistrar and qmlcachegen live in qtdeclarative. qt-build-utils
|
||||||
|
# finds tools via `qmake -query QT_INSTALL_LIBEXECS`, which only returns
|
||||||
|
# qtbase paths, so those two tools are invisible. Fix: combine them into a
|
||||||
|
# single symlink tree and point a qmake wrapper at it.
|
||||||
|
qtBuildTools = runCommand "qt6-build-tools" { } ''
|
||||||
|
mkdir -p $out/libexec $out/bin
|
||||||
|
for f in ${qt6.qtbase}/libexec/* ${qt6.qtbase}/bin/*; do
|
||||||
|
ln -sf "$f" "$out/$(echo "$f" | grep -o 'libexec\|bin')/$(basename "$f")"
|
||||||
|
done
|
||||||
|
for tool in qmltyperegistrar qmlcachegen; do
|
||||||
|
src="${qt6.qtdeclarative}/libexec/$tool"
|
||||||
|
[ -f "$src" ] && ln -sf "$src" "$out/libexec/$tool"
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
qmakeWrapper = writeShellScript "qmake6" ''
|
||||||
|
if [ "$1" = "-query" ]; then
|
||||||
|
case "$2" in
|
||||||
|
QT_HOST_LIBEXECS|QT_HOST_LIBEXECS/get|QT_INSTALL_LIBEXECS|QT_INSTALL_LIBEXECS/get)
|
||||||
|
echo "${qtBuildTools}/libexec"; exit 0;;
|
||||||
|
QT_HOST_BINS|QT_HOST_BINS/get|QT_INSTALL_BINS|QT_INSTALL_BINS/get)
|
||||||
|
echo "${qtBuildTools}/bin"; exit 0;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
exec ${qt6.qtbase}/bin/qmake6 "$@"
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
rustPlatform.buildRustPackage {
|
||||||
|
pname = "nova-plugin";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = lib.cleanSource ../plugin;
|
||||||
|
cargoLock.lockFile = ../plugin/Cargo.lock;
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkg-config
|
||||||
|
qt6.qtbase
|
||||||
|
qt6.qtdeclarative
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
qt6.qtbase
|
||||||
|
qt6.qtdeclarative
|
||||||
|
];
|
||||||
|
|
||||||
|
dontWrapQtApps = true;
|
||||||
|
|
||||||
|
# qt6.qtbase's setup hook overrides QMAKE after derivation attrs are set,
|
||||||
|
# so re-assert it in preBuild which runs after all setup hooks.
|
||||||
|
preBuild = ''
|
||||||
|
export QMAKE=${qmakeWrapper}
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
|
qml_dir="$out/lib/qt-6/qml/NovaStats"
|
||||||
|
mkdir -p "$qml_dir"
|
||||||
|
|
||||||
|
install -m755 target/*/release/libnova_plugin.so "$qml_dir/"
|
||||||
|
install -m644 target/*/cxxqt/qml_modules/NovaStats/qmldir "$qml_dir/"
|
||||||
|
install -m644 target/*/cxxqt/qml_modules/NovaStats/plugin.qmltypes "$qml_dir/" 2>/dev/null || true
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "In-process system stats QML plugin for nova-shell";
|
||||||
|
platforms = lib.platforms.linux;
|
||||||
|
};
|
||||||
|
}
|
||||||
571
plugin/Cargo.lock
generated
Normal file
571
plugin/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,571 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.61"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-format"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "696283b40e1a39d208ee614b92e5f6521d16962edeb47c48372585ec92419943"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codespan-reporting"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||||
|
dependencies = [
|
||||||
|
"termcolor",
|
||||||
|
"unicode-width 0.1.14",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codespan-reporting"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"termcolor",
|
||||||
|
"unicode-width 0.2.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx"
|
||||||
|
version = "1.0.194"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cxx-build",
|
||||||
|
"cxxbridge-cmd",
|
||||||
|
"cxxbridge-flags",
|
||||||
|
"cxxbridge-macro",
|
||||||
|
"foldhash",
|
||||||
|
"link-cplusplus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-build"
|
||||||
|
version = "1.0.194"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"codespan-reporting 0.13.1",
|
||||||
|
"indexmap",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"scratch",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-gen"
|
||||||
|
version = "0.7.194"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "035b6c61a944483e8a4b2ad4fb8b13830d63491bd004943716ad16d85dcc64bc"
|
||||||
|
dependencies = [
|
||||||
|
"codespan-reporting 0.13.1",
|
||||||
|
"indexmap",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-qt"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fdf26e5ee4375a85799d0fe436662e5a8ef099bd0964b84bce8812c35c7daea"
|
||||||
|
dependencies = [
|
||||||
|
"cxx",
|
||||||
|
"cxx-qt-build",
|
||||||
|
"cxx-qt-macro",
|
||||||
|
"qt-build-utils",
|
||||||
|
"static_assertions",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-qt-build"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9a88c7f8241bfe4dfd19d27397a5845f0f275735a34ee7fb219045da81945e3"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"codespan-reporting 0.11.1",
|
||||||
|
"cxx-gen",
|
||||||
|
"cxx-qt-gen",
|
||||||
|
"proc-macro2",
|
||||||
|
"qt-build-utils",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-qt-gen"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b1917c90b10117890e3794f4a0dc764a24704de40711d688ae128b0704490b8"
|
||||||
|
dependencies = [
|
||||||
|
"clang-format",
|
||||||
|
"convert_case",
|
||||||
|
"cxx-gen",
|
||||||
|
"indoc",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-qt-lib"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7b3de184cff59ad29d657021f933380e584d9b70265247f6a96098bef964211"
|
||||||
|
dependencies = [
|
||||||
|
"cxx",
|
||||||
|
"cxx-qt",
|
||||||
|
"cxx-qt-build",
|
||||||
|
"qt-build-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-qt-macro"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "230b0d8b20d3d02a85885742be4fabc0382fead4e382dd8c9c9ff1827f221e8c"
|
||||||
|
dependencies = [
|
||||||
|
"cxx-qt-gen",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-cmd"
|
||||||
|
version = "1.0.194"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"codespan-reporting 0.13.1",
|
||||||
|
"indexmap",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-flags"
|
||||||
|
version = "1.0.194"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-macro"
|
||||||
|
version = "1.0.194"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "link-cplusplus"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nova-plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cxx",
|
||||||
|
"cxx-qt",
|
||||||
|
"cxx-qt-build",
|
||||||
|
"cxx-qt-lib",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qt-build-utils"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d074750fd3baba12fdb47388d591ad9ed043e23864a79d129dcf3a1ef6fc8d9"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"cc",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scratch"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.3+wasi-0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.57.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
22
plugin/Cargo.toml
Normal file
22
plugin/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "nova-plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
cast_possible_truncation = "allow"
|
||||||
|
cast_precision_loss = "allow"
|
||||||
|
cast_sign_loss = "allow"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cxx = "1.0"
|
||||||
|
cxx-qt = "0.8.1"
|
||||||
|
cxx-qt-lib = { version = "0.8.1", features = ["qt_full"] }
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cxx-qt-build = "0.8.1"
|
||||||
7
plugin/build.rs
Normal file
7
plugin/build.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
use cxx_qt_build::{CxxQtBuilder, PluginType, QmlModule};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
CxxQtBuilder::new_qml_module(QmlModule::new("NovaStats").plugin_type(PluginType::Dynamic))
|
||||||
|
.files(["src/system_stats.rs", "src/cpu_service.rs"])
|
||||||
|
.build();
|
||||||
|
}
|
||||||
269
plugin/src/cpu_service.rs
Normal file
269
plugin/src/cpu_service.rs
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
use crate::stats::cpu;
|
||||||
|
use core::pin::Pin;
|
||||||
|
use cxx_qt::CxxQtType;
|
||||||
|
use cxx_qt_lib::{QList, QString};
|
||||||
|
|
||||||
|
#[cxx_qt::bridge]
|
||||||
|
pub mod qobject {
|
||||||
|
unsafe extern "C++" {
|
||||||
|
include!("cxx-qt-lib/qstring.h");
|
||||||
|
type QString = cxx_qt_lib::QString;
|
||||||
|
|
||||||
|
include!("cxx-qt-lib/core/qlist/qlist_i32.h");
|
||||||
|
type QList_i32 = cxx_qt_lib::QList<i32>;
|
||||||
|
|
||||||
|
include!("cxx-qt-lib/core/qlist/qlist_f64.h");
|
||||||
|
type QList_f64 = cxx_qt_lib::QList<f64>;
|
||||||
|
|
||||||
|
include!("cxx-qt-lib/core/qlist/qlist_QString.h");
|
||||||
|
type QList_QString = cxx_qt_lib::QList<cxx_qt_lib::QString>;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "RustQt" {
|
||||||
|
#[qobject]
|
||||||
|
#[qml_element]
|
||||||
|
#[qml_singleton]
|
||||||
|
#[qproperty(i32, usage)]
|
||||||
|
#[qproperty(f64, freq_ghz, cxx_name = "freqGhz")]
|
||||||
|
#[qproperty(QList_i32, history)]
|
||||||
|
// JSON strings: [{usage, freq_ghz, history:[]}]
|
||||||
|
#[qproperty(QList_QString, cores)]
|
||||||
|
#[qproperty(QList_f64, core_max_freq, cxx_name = "coreMaxFreq")]
|
||||||
|
#[qproperty(QList_QString, core_types, cxx_name = "coreTypes")]
|
||||||
|
#[qproperty(bool, enable_core_history, cxx_name = "enableCoreHistory")]
|
||||||
|
type CpuService = super::CpuServiceRust;
|
||||||
|
|
||||||
|
#[qinvokable]
|
||||||
|
fn poll(self: Pin<&mut Self>);
|
||||||
|
|
||||||
|
#[qinvokable]
|
||||||
|
#[cxx_name = "clearCoreHistory"]
|
||||||
|
fn clear_core_history(self: Pin<&mut Self>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cxx_qt::Initialize for CpuService {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CpuServiceRust {
|
||||||
|
prev_stat: Vec<cpu::Sample>,
|
||||||
|
history_overall: Vec<i32>,
|
||||||
|
core_history: Vec<Vec<i32>>,
|
||||||
|
|
||||||
|
usage: i32,
|
||||||
|
freq_ghz: f64,
|
||||||
|
history: QList<i32>,
|
||||||
|
cores: QList<QString>,
|
||||||
|
core_max_freq: QList<f64>,
|
||||||
|
core_types: QList<QString>,
|
||||||
|
enable_core_history: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CpuServiceRust {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
prev_stat: Vec::new(),
|
||||||
|
history_overall: Vec::new(),
|
||||||
|
core_history: Vec::new(),
|
||||||
|
usage: 0,
|
||||||
|
freq_ghz: 0.0,
|
||||||
|
history: QList::default(),
|
||||||
|
cores: QList::default(),
|
||||||
|
core_max_freq: QList::default(),
|
||||||
|
core_types: QList::default(),
|
||||||
|
enable_core_history: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cxx_qt::Initialize for qobject::CpuService {
|
||||||
|
fn initialize(mut self: Pin<&mut Self>) {
|
||||||
|
let max_freqs = read_core_max_freqs();
|
||||||
|
let types = infer_core_types(&max_freqs);
|
||||||
|
|
||||||
|
let mut mf_list = QList::<f64>::default();
|
||||||
|
for f in &max_freqs {
|
||||||
|
mf_list.append(*f);
|
||||||
|
}
|
||||||
|
self.as_mut().set_core_max_freq(mf_list);
|
||||||
|
|
||||||
|
let ct_list = strings_to_qlist(&types);
|
||||||
|
self.as_mut().set_core_types(ct_list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl qobject::CpuService {
|
||||||
|
fn poll(mut self: Pin<&mut Self>) {
|
||||||
|
let curr = cpu::read_stat();
|
||||||
|
let freqs = cpu::read_core_freqs();
|
||||||
|
let prev = self.as_ref().rust().prev_stat.clone();
|
||||||
|
|
||||||
|
let Some(snap) = cpu::compute_snapshot(&prev, &curr, &freqs) else {
|
||||||
|
self.as_mut().rust_mut().prev_stat = curr;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.as_mut().rust_mut().prev_stat = curr;
|
||||||
|
|
||||||
|
let usage = snap.usage as i32;
|
||||||
|
let freq_ghz = snap.freq_ghz;
|
||||||
|
let enable = self.as_ref().rust().enable_core_history;
|
||||||
|
let n_cores = snap.core_usage.len().max(freqs.len());
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut r = self.as_mut().rust_mut();
|
||||||
|
push_capped(&mut r.history_overall, usage, 60);
|
||||||
|
if r.core_history.len() < n_cores {
|
||||||
|
r.core_history.resize(n_cores, Vec::new());
|
||||||
|
}
|
||||||
|
if enable {
|
||||||
|
for (i, &u) in snap.core_usage.iter().enumerate() {
|
||||||
|
push_capped(&mut r.core_history[i], u as i32, 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let history_qlist = vec_to_qlist_i32(&self.as_ref().rust().history_overall);
|
||||||
|
let cores_qlist =
|
||||||
|
build_cores_qlist(&snap, &freqs, &self.as_ref().rust().core_history, enable);
|
||||||
|
|
||||||
|
self.as_mut().set_usage(usage);
|
||||||
|
self.as_mut().set_freq_ghz(freq_ghz);
|
||||||
|
self.as_mut().set_history(history_qlist);
|
||||||
|
self.as_mut().set_cores(cores_qlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_core_history(mut self: Pin<&mut Self>) {
|
||||||
|
for v in &mut self.as_mut().rust_mut().core_history {
|
||||||
|
v.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_cores_qlist(
|
||||||
|
snap: &cpu::CpuSnapshot,
|
||||||
|
freqs: &[f64],
|
||||||
|
core_history: &[Vec<i32>],
|
||||||
|
enable: bool,
|
||||||
|
) -> QList<QString> {
|
||||||
|
let n = snap.core_usage.len().max(freqs.len());
|
||||||
|
let mut list = QList::<QString>::default();
|
||||||
|
for i in 0..n {
|
||||||
|
let u = snap.core_usage.get(i).copied().unwrap_or(0) as i32;
|
||||||
|
let f = freqs.get(i).copied().unwrap_or(0.0);
|
||||||
|
let hist_slice = if enable {
|
||||||
|
core_history.get(i).map(Vec::as_slice).unwrap_or(&[])
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
|
let hist_str = hist_slice
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
|
let json = format!(
|
||||||
|
"{{\"usage\":{},\"freq_ghz\":{:.3},\"history\":[{}]}}",
|
||||||
|
u, f, hist_str
|
||||||
|
);
|
||||||
|
list.append(QString::from(json.as_str()));
|
||||||
|
}
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_capped(v: &mut Vec<i32>, val: i32, max: usize) {
|
||||||
|
v.push(val);
|
||||||
|
if v.len() > max {
|
||||||
|
v.drain(..v.len() - max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vec_to_qlist_i32(v: &[i32]) -> QList<i32> {
|
||||||
|
let mut list = QList::<i32>::default();
|
||||||
|
for &x in v {
|
||||||
|
list.append(x);
|
||||||
|
}
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strings_to_qlist(types: &[String]) -> QList<QString> {
|
||||||
|
let mut list = QList::<QString>::default();
|
||||||
|
for t in types {
|
||||||
|
list.append(QString::from(t.as_str()));
|
||||||
|
}
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_core_max_freqs() -> Vec<f64> {
|
||||||
|
let mut freqs = Vec::new();
|
||||||
|
for i in 0.. {
|
||||||
|
let path = format!("/sys/devices/system/cpu/cpu{i}/cpufreq/cpuinfo_max_freq");
|
||||||
|
match std::fs::read_to_string(&path) {
|
||||||
|
Ok(s) => match s.trim().parse::<u64>() {
|
||||||
|
Ok(khz) => freqs.push(khz as f64 / 1_000_000.0),
|
||||||
|
Err(_) => break,
|
||||||
|
},
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
freqs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn infer_core_types(max_freqs: &[f64]) -> Vec<String> {
|
||||||
|
if let Some(types) = read_hybrid_topology(max_freqs.len()) {
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
let freqs: Vec<f64> = max_freqs.iter().copied().filter(|&f| f > 0.0).collect();
|
||||||
|
if freqs.len() < 2 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let max_f = freqs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||||
|
let min_f = freqs.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||||
|
if max_f <= 0.0 || (max_f - min_f) / max_f <= 0.15 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let threshold = (max_f + min_f) / 2.0;
|
||||||
|
max_freqs
|
||||||
|
.iter()
|
||||||
|
.map(|&f| {
|
||||||
|
if f >= threshold {
|
||||||
|
"Performance".to_string()
|
||||||
|
} else {
|
||||||
|
"Efficiency".to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_hybrid_topology(n_cores: usize) -> Option<Vec<String>> {
|
||||||
|
let core_cpus = std::fs::read_to_string("/sys/devices/cpu_core/cpus").ok()?;
|
||||||
|
let atom_cpus = std::fs::read_to_string("/sys/devices/cpu_atom/cpus").ok()?;
|
||||||
|
let p_cores = expand_cpu_range(core_cpus.trim());
|
||||||
|
let e_cores = expand_cpu_range(atom_cpus.trim());
|
||||||
|
let max_cpu = p_cores.iter().chain(e_cores.iter()).copied().max()?;
|
||||||
|
let count = (max_cpu + 1).max(n_cores);
|
||||||
|
Some(
|
||||||
|
(0..count)
|
||||||
|
.map(|i| {
|
||||||
|
if e_cores.contains(&i) {
|
||||||
|
"Efficiency".to_string()
|
||||||
|
} else {
|
||||||
|
"Performance".to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_cpu_range(s: &str) -> std::collections::HashSet<usize> {
|
||||||
|
let mut set = std::collections::HashSet::new();
|
||||||
|
for part in s.split(',') {
|
||||||
|
if let Some((a, b)) = part.split_once('-') {
|
||||||
|
if let (Ok(lo), Ok(hi)) = (a.trim().parse::<usize>(), b.trim().parse::<usize>()) {
|
||||||
|
for i in lo..=hi {
|
||||||
|
set.insert(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Ok(n) = part.trim().parse::<usize>() {
|
||||||
|
set.insert(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
}
|
||||||
3
plugin/src/lib.rs
Normal file
3
plugin/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod cpu_service;
|
||||||
|
pub mod stats;
|
||||||
|
pub mod system_stats;
|
||||||
185
plugin/src/stats/cpu.rs
Normal file
185
plugin/src/stats/cpu.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Sample {
|
||||||
|
pub idle: u64,
|
||||||
|
pub total: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_stat(input: &str) -> Vec<Sample> {
|
||||||
|
input
|
||||||
|
.lines()
|
||||||
|
.filter(|l| l.starts_with("cpu"))
|
||||||
|
.map(|l| {
|
||||||
|
let vals: Vec<u64> = l
|
||||||
|
.split_whitespace()
|
||||||
|
.skip(1)
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect();
|
||||||
|
let idle = vals.get(3).copied().unwrap_or(0) + vals.get(4).copied().unwrap_or(0);
|
||||||
|
let total = vals.iter().sum();
|
||||||
|
Sample { idle, total }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pct(prev: &Sample, curr: &Sample) -> u32 {
|
||||||
|
let dt = curr.total.saturating_sub(prev.total);
|
||||||
|
let di = curr.idle.saturating_sub(prev.idle);
|
||||||
|
if dt == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
(dt.saturating_sub(di) * 100 / dt) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_stat() -> Vec<Sample> {
|
||||||
|
parse_stat(&fs::read_to_string("/proc/stat").unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_core_freqs() -> Vec<f64> {
|
||||||
|
let mut freqs = Vec::new();
|
||||||
|
for i in 0.. {
|
||||||
|
let path = format!("/sys/devices/system/cpu/cpu{i}/cpufreq/scaling_cur_freq");
|
||||||
|
match fs::read_to_string(&path) {
|
||||||
|
Ok(s) => match s.trim().parse::<u64>() {
|
||||||
|
Ok(khz) => freqs.push(khz as f64 / 1_000_000.0),
|
||||||
|
Err(_) => break,
|
||||||
|
},
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
freqs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CpuSnapshot {
|
||||||
|
pub usage: u32,
|
||||||
|
pub freq_ghz: f64,
|
||||||
|
pub core_usage: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_snapshot(prev: &[Sample], curr: &[Sample], freqs: &[f64]) -> Option<CpuSnapshot> {
|
||||||
|
if curr.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let usage = prev.first().zip(curr.first()).map_or(0, |(p, c)| pct(p, c));
|
||||||
|
let core_usage: Vec<u32> = prev
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.zip(curr.iter().skip(1))
|
||||||
|
.map(|(p, c)| pct(p, c))
|
||||||
|
.collect();
|
||||||
|
let freq_ghz = if freqs.is_empty() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
freqs.iter().sum::<f64>() / freqs.len() as f64
|
||||||
|
};
|
||||||
|
Some(CpuSnapshot {
|
||||||
|
usage,
|
||||||
|
freq_ghz,
|
||||||
|
core_usage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn sample(idle: u64, total: u64) -> Sample {
|
||||||
|
Sample { idle, total }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── pct ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pct_zero_delta_returns_zero() {
|
||||||
|
let s = Sample {
|
||||||
|
idle: 100,
|
||||||
|
total: 400,
|
||||||
|
};
|
||||||
|
assert_eq!(pct(&s, &s), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pct_all_idle() {
|
||||||
|
let prev = Sample {
|
||||||
|
idle: 0,
|
||||||
|
total: 100,
|
||||||
|
};
|
||||||
|
let curr = Sample {
|
||||||
|
idle: 100,
|
||||||
|
total: 200,
|
||||||
|
};
|
||||||
|
assert_eq!(pct(&prev, &curr), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pct_fully_busy() {
|
||||||
|
let prev = Sample {
|
||||||
|
idle: 100,
|
||||||
|
total: 200,
|
||||||
|
};
|
||||||
|
let curr = Sample {
|
||||||
|
idle: 100,
|
||||||
|
total: 300,
|
||||||
|
};
|
||||||
|
assert_eq!(pct(&prev, &curr), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pct_half_busy() {
|
||||||
|
let prev = Sample { idle: 0, total: 0 };
|
||||||
|
let curr = Sample {
|
||||||
|
idle: 50,
|
||||||
|
total: 100,
|
||||||
|
};
|
||||||
|
assert_eq!(pct(&prev, &curr), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pct_no_underflow_on_backwards_clock() {
|
||||||
|
let prev = Sample {
|
||||||
|
idle: 200,
|
||||||
|
total: 400,
|
||||||
|
};
|
||||||
|
let curr = Sample {
|
||||||
|
idle: 100,
|
||||||
|
total: 300,
|
||||||
|
};
|
||||||
|
assert_eq!(pct(&prev, &curr), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── parse_stat ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STAT_SAMPLE: &str = "\
|
||||||
|
cpu 100 10 50 700 40 0 0 0 0 0
|
||||||
|
cpu0 50 5 25 350 20 0 0 0 0 0
|
||||||
|
cpu1 50 5 25 350 20 0 0 0 0 0";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_stat_count() {
|
||||||
|
assert_eq!(parse_stat(STAT_SAMPLE).len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_stat_aggregate_idle() {
|
||||||
|
assert_eq!(parse_stat(STAT_SAMPLE)[0].idle, 740);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_stat_aggregate_total() {
|
||||||
|
assert_eq!(parse_stat(STAT_SAMPLE)[0].total, 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_stat_per_core_idle() {
|
||||||
|
let s = parse_stat(STAT_SAMPLE);
|
||||||
|
assert_eq!(s[1].idle, 370);
|
||||||
|
assert_eq!(s[2].idle, 370);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_stat_ignores_non_cpu_lines() {
|
||||||
|
let input = "intr 12345\ncpu 1 2 3 4 5 0 0 0 0 0\npage 0 0";
|
||||||
|
assert_eq!(parse_stat(input).len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
plugin/src/stats/disk.rs
Normal file
80
plugin/src/stats/disk.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
pub struct DiskMount {
|
||||||
|
pub target: String,
|
||||||
|
pub pct: u32,
|
||||||
|
pub used_bytes: u64,
|
||||||
|
pub total_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXCLUDED_FS: &[&str] = &[
|
||||||
|
"tmpfs",
|
||||||
|
"devtmpfs",
|
||||||
|
"squashfs",
|
||||||
|
"efivarfs",
|
||||||
|
"overlay",
|
||||||
|
"proc",
|
||||||
|
"sysfs",
|
||||||
|
"cgroup",
|
||||||
|
"cgroup2",
|
||||||
|
"devpts",
|
||||||
|
"hugetlbfs",
|
||||||
|
"mqueue",
|
||||||
|
"nsfs",
|
||||||
|
"pstore",
|
||||||
|
"rpc_pipefs",
|
||||||
|
"fusectl",
|
||||||
|
"tracefs",
|
||||||
|
"configfs",
|
||||||
|
"securityfs",
|
||||||
|
"binfmt_misc",
|
||||||
|
"autofs",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn read_disk_mounts() -> Vec<DiskMount> {
|
||||||
|
let content = fs::read_to_string("/proc/mounts").unwrap_or_default();
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let mut mounts = Vec::new();
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.len() < 3 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let target = parts[1];
|
||||||
|
let fs_type = parts[2];
|
||||||
|
if EXCLUDED_FS.contains(&fs_type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !seen.insert(target.to_string()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some((total, used)) = statvfs_bytes(target) {
|
||||||
|
let pct = (used * 100 / total) as u32;
|
||||||
|
mounts.push(DiskMount {
|
||||||
|
target: target.to_string(),
|
||||||
|
pct,
|
||||||
|
used_bytes: used,
|
||||||
|
total_bytes: total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mounts
|
||||||
|
}
|
||||||
|
|
||||||
|
fn statvfs_bytes(path: &str) -> Option<(u64, u64)> {
|
||||||
|
use std::ffi::CString;
|
||||||
|
let cpath = CString::new(path).ok()?;
|
||||||
|
let mut st: libc::statvfs = unsafe { std::mem::zeroed() };
|
||||||
|
let ret = unsafe { libc::statvfs(cpath.as_ptr(), &mut st) };
|
||||||
|
if ret != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bsize = st.f_frsize as u64;
|
||||||
|
let total = st.f_blocks as u64 * bsize;
|
||||||
|
let avail = st.f_bavail as u64 * bsize;
|
||||||
|
if total == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((total, total - avail))
|
||||||
|
}
|
||||||
212
plugin/src/stats/gpu.rs
Normal file
212
plugin/src/stats/gpu.rs
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
pub struct GpuInfo {
|
||||||
|
pub usage: u32,
|
||||||
|
pub vram_used_gb: f64,
|
||||||
|
pub vram_total_gb: f64,
|
||||||
|
pub temp_c: i32,
|
||||||
|
pub vendor: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GpuBackend {
|
||||||
|
Amd {
|
||||||
|
card_path: String,
|
||||||
|
hwmon_path: Option<String>,
|
||||||
|
},
|
||||||
|
Nvidia,
|
||||||
|
Intel {
|
||||||
|
card_path: String,
|
||||||
|
device_path: String,
|
||||||
|
hwmon_path: Option<String>,
|
||||||
|
prev_rc6: Option<(u64, Instant)>,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_sysfs(path: &str) -> Option<String> {
|
||||||
|
fs::read_to_string(path).ok().map(|s| s.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_sysfs_u64(path: &str) -> Option<u64> {
|
||||||
|
read_sysfs(path)?.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_hwmon(driver_name: &str) -> Option<String> {
|
||||||
|
for i in 0..32 {
|
||||||
|
if let Some(name) = read_sysfs(&format!("/sys/class/hwmon/hwmon{i}/name")) {
|
||||||
|
if name == driver_name {
|
||||||
|
return Some(format!("/sys/class/hwmon/hwmon{i}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_gpu() -> GpuBackend {
|
||||||
|
// AMD: look for gpu_busy_percent exposed by the amdgpu driver
|
||||||
|
for i in 0..8 {
|
||||||
|
let p = format!("/sys/class/drm/card{i}/device/gpu_busy_percent");
|
||||||
|
if fs::read_to_string(&p).is_ok() {
|
||||||
|
let card = format!("/sys/class/drm/card{i}/device");
|
||||||
|
let hwmon = find_hwmon("amdgpu");
|
||||||
|
return GpuBackend::Amd {
|
||||||
|
card_path: card,
|
||||||
|
hwmon_path: hwmon,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// NVIDIA: probe nvidia-smi
|
||||||
|
let nvidia_ok = std::process::Command::new("nvidia-smi")
|
||||||
|
.args(["--query-gpu=name", "--format=csv,noheader"])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if nvidia_ok {
|
||||||
|
return GpuBackend::Nvidia;
|
||||||
|
}
|
||||||
|
// Intel: look for i915 or xe driver
|
||||||
|
for i in 0..8 {
|
||||||
|
let driver_link = format!("/sys/class/drm/card{i}/device/driver");
|
||||||
|
if let Some(drv) = fs::read_link(&driver_link)
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
|
||||||
|
{
|
||||||
|
if drv == "i915" || drv == "xe" {
|
||||||
|
let card_path = format!("/sys/class/drm/card{i}");
|
||||||
|
let device_path = format!("/sys/class/drm/card{i}/device");
|
||||||
|
let hwmon = find_hwmon(&drv);
|
||||||
|
return GpuBackend::Intel {
|
||||||
|
card_path,
|
||||||
|
device_path,
|
||||||
|
hwmon_path: hwmon,
|
||||||
|
prev_rc6: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GpuBackend::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_amd(card: &str, hwmon: Option<&String>) -> Option<GpuInfo> {
|
||||||
|
let usage: u32 = read_sysfs(&format!("{card}/gpu_busy_percent"))?
|
||||||
|
.parse()
|
||||||
|
.ok()?;
|
||||||
|
let vram_used: u64 = read_sysfs(&format!("{card}/mem_info_vram_used"))?
|
||||||
|
.parse()
|
||||||
|
.ok()?;
|
||||||
|
let vram_total: u64 = read_sysfs(&format!("{card}/mem_info_vram_total"))?
|
||||||
|
.parse()
|
||||||
|
.ok()?;
|
||||||
|
let temp_c = hwmon
|
||||||
|
.and_then(|h| read_sysfs(&format!("{h}/temp1_input")))
|
||||||
|
.and_then(|s| s.parse::<i32>().ok())
|
||||||
|
.map_or(0, |mc| mc / 1000);
|
||||||
|
Some(GpuInfo {
|
||||||
|
usage,
|
||||||
|
vram_used_gb: vram_used as f64 / 1_073_741_824.0,
|
||||||
|
vram_total_gb: vram_total as f64 / 1_073_741_824.0,
|
||||||
|
temp_c,
|
||||||
|
vendor: "amd",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_nvidia() -> Option<GpuInfo> {
|
||||||
|
let out = std::process::Command::new("nvidia-smi")
|
||||||
|
.args([
|
||||||
|
"--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu",
|
||||||
|
"--format=csv,noheader,nounits",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let s = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let p: Vec<&str> = s.trim().split(',').map(str::trim).collect();
|
||||||
|
if p.len() < 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(GpuInfo {
|
||||||
|
usage: p[0].parse().ok()?,
|
||||||
|
vram_used_gb: p[1].parse::<f64>().ok()? / 1024.0,
|
||||||
|
vram_total_gb: p[2].parse::<f64>().ok()? / 1024.0,
|
||||||
|
temp_c: p[3].parse().ok()?,
|
||||||
|
vendor: "nvidia",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_intel(
|
||||||
|
card: &str,
|
||||||
|
device: &str,
|
||||||
|
hwmon: Option<&String>,
|
||||||
|
prev_rc6: &mut Option<(u64, Instant)>,
|
||||||
|
) -> GpuInfo {
|
||||||
|
let usage = read_intel_usage(card, prev_rc6).unwrap_or(0);
|
||||||
|
|
||||||
|
// VRAM - only present on discrete GPUs (Arc)
|
||||||
|
let vram_total = read_sysfs_u64(&format!("{device}/mem_info_vram_total")).unwrap_or(0);
|
||||||
|
let vram_used = if vram_total > 0 {
|
||||||
|
read_sysfs_u64(&format!("{device}/mem_info_vram_used")).unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let temp_c = hwmon
|
||||||
|
.and_then(|h| read_sysfs(&format!("{h}/temp1_input")))
|
||||||
|
.and_then(|s| s.parse::<i32>().ok())
|
||||||
|
.map_or(0, |mc| mc / 1000);
|
||||||
|
|
||||||
|
GpuInfo {
|
||||||
|
usage,
|
||||||
|
vram_used_gb: vram_used as f64 / 1_073_741_824.0,
|
||||||
|
vram_total_gb: vram_total as f64 / 1_073_741_824.0,
|
||||||
|
temp_c,
|
||||||
|
vendor: "intel",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_intel_usage(card: &str, prev_rc6: &mut Option<(u64, Instant)>) -> Option<u32> {
|
||||||
|
// RC6 is the GPU idle state - compute usage from residency delta
|
||||||
|
let rc6_ms = read_sysfs_u64(&format!("{card}/gt/gt0/rc6_residency_ms"))
|
||||||
|
.or_else(|| read_sysfs_u64(&format!("{card}/power/rc6_residency_ms")))?;
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let usage = if let Some((prev_ms, prev_time)) = prev_rc6.take() {
|
||||||
|
let dt_ms = now.duration_since(prev_time).as_millis() as u64;
|
||||||
|
if dt_ms > 0 {
|
||||||
|
let idle_ms = rc6_ms.saturating_sub(prev_ms);
|
||||||
|
let idle_pct = (idle_ms * 100 / dt_ms).min(100);
|
||||||
|
(100 - idle_pct) as u32
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0 // first reading, no delta yet
|
||||||
|
};
|
||||||
|
|
||||||
|
*prev_rc6 = Some((rc6_ms, now));
|
||||||
|
Some(usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_gpu(backend: &mut GpuBackend) -> Option<GpuInfo> {
|
||||||
|
match backend {
|
||||||
|
GpuBackend::Amd {
|
||||||
|
card_path,
|
||||||
|
hwmon_path,
|
||||||
|
} => read_amd(card_path, hwmon_path.as_ref()),
|
||||||
|
GpuBackend::Nvidia => read_nvidia(),
|
||||||
|
GpuBackend::Intel {
|
||||||
|
card_path,
|
||||||
|
device_path,
|
||||||
|
hwmon_path,
|
||||||
|
prev_rc6,
|
||||||
|
} => Some(read_intel(
|
||||||
|
card_path,
|
||||||
|
device_path,
|
||||||
|
hwmon_path.as_ref(),
|
||||||
|
prev_rc6,
|
||||||
|
)),
|
||||||
|
GpuBackend::None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
104
plugin/src/stats/mem.rs
Normal file
104
plugin/src/stats/mem.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
pub struct MemInfo {
|
||||||
|
pub percent: u64,
|
||||||
|
pub used_gb: f64,
|
||||||
|
pub total_gb: f64,
|
||||||
|
pub avail_gb: f64,
|
||||||
|
pub cached_gb: f64,
|
||||||
|
pub buffers_gb: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_meminfo(input: &str) -> Option<MemInfo> {
|
||||||
|
let mut total = 0u64;
|
||||||
|
let mut avail = 0u64;
|
||||||
|
let mut buffers = 0u64;
|
||||||
|
let mut cached = 0u64;
|
||||||
|
let mut sreclaimable = 0u64;
|
||||||
|
|
||||||
|
for line in input.lines() {
|
||||||
|
let mut parts = line.splitn(2, ':');
|
||||||
|
let key = parts.next().unwrap_or("").trim();
|
||||||
|
let val: u64 = parts
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(0);
|
||||||
|
match key {
|
||||||
|
"MemTotal" => total = val,
|
||||||
|
"MemAvailable" => avail = val,
|
||||||
|
"Buffers" => buffers = val,
|
||||||
|
"Cached" => cached = val,
|
||||||
|
"SReclaimable" => sreclaimable = val,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if total == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let used = total.saturating_sub(avail);
|
||||||
|
let cached_total = cached + sreclaimable;
|
||||||
|
let gb = |kb: u64| kb as f64 / 1_048_576.0;
|
||||||
|
|
||||||
|
Some(MemInfo {
|
||||||
|
percent: used * 100 / total,
|
||||||
|
used_gb: gb(used),
|
||||||
|
total_gb: gb(total),
|
||||||
|
avail_gb: gb(avail),
|
||||||
|
cached_gb: gb(cached_total),
|
||||||
|
buffers_gb: gb(buffers),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_meminfo() -> Option<MemInfo> {
|
||||||
|
parse_meminfo(&fs::read_to_string("/proc/meminfo").unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const MEMINFO_SAMPLE: &str = "\
|
||||||
|
MemTotal: 16384000 kB
|
||||||
|
MemFree: 2048000 kB
|
||||||
|
MemAvailable: 4096000 kB
|
||||||
|
Buffers: 512000 kB
|
||||||
|
Cached: 3072000 kB
|
||||||
|
SReclaimable: 512000 kB
|
||||||
|
SwapTotal: 8192000 kB
|
||||||
|
SwapFree: 8192000 kB";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_meminfo_percent() {
|
||||||
|
let m = parse_meminfo(MEMINFO_SAMPLE).unwrap();
|
||||||
|
assert_eq!(m.percent, 75);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_meminfo_total_gb() {
|
||||||
|
let m = parse_meminfo(MEMINFO_SAMPLE).unwrap();
|
||||||
|
assert!((m.total_gb - 15.625).abs() < 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_meminfo_cached_includes_sreclaimable() {
|
||||||
|
let m = parse_meminfo(MEMINFO_SAMPLE).unwrap();
|
||||||
|
let expected = 3_584_000.0 / 1_048_576.0;
|
||||||
|
assert!((m.cached_gb - expected).abs() < 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_meminfo_zero_total_returns_none() {
|
||||||
|
assert!(parse_meminfo("MemFree: 1000 kB\n").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_meminfo_empty_returns_none() {
|
||||||
|
assert!(parse_meminfo("").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
5
plugin/src/stats/mod.rs
Normal file
5
plugin/src/stats/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod cpu;
|
||||||
|
pub mod disk;
|
||||||
|
pub mod gpu;
|
||||||
|
pub mod mem;
|
||||||
|
pub mod temp;
|
||||||
45
plugin/src/stats/temp.rs
Normal file
45
plugin/src/stats/temp.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ThermalDevice {
|
||||||
|
pub name: String,
|
||||||
|
pub celsius: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_thermal_devices() -> Vec<ThermalDevice> {
|
||||||
|
let mut by_name: HashMap<String, i32> = HashMap::new();
|
||||||
|
|
||||||
|
for i in 0.. {
|
||||||
|
let temp_path = format!("/sys/class/thermal/thermal_zone{i}/temp");
|
||||||
|
let type_path = format!("/sys/class/thermal/thermal_zone{i}/type");
|
||||||
|
|
||||||
|
let Ok(temp_str) = fs::read_to_string(&temp_path) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let millic: i32 = match temp_str.trim().parse() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let celsius = millic / 1000;
|
||||||
|
|
||||||
|
let name = fs::read_to_string(&type_path)
|
||||||
|
.map_or_else(|_| format!("zone{i}"), |s| s.trim().to_string());
|
||||||
|
|
||||||
|
// Keep the highest temp seen for each device type
|
||||||
|
let entry = by_name.entry(name).or_insert(celsius);
|
||||||
|
if celsius > *entry {
|
||||||
|
*entry = celsius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut devices: Vec<ThermalDevice> = by_name
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, celsius)| ThermalDevice { name, celsius })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort descending by temp so the hottest shows first
|
||||||
|
devices.sort_by(|a, b| b.celsius.cmp(&a.celsius));
|
||||||
|
devices
|
||||||
|
}
|
||||||
225
plugin/src/system_stats.rs
Normal file
225
plugin/src/system_stats.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
use crate::stats::{disk, gpu, mem, temp};
|
||||||
|
use core::pin::Pin;
|
||||||
|
use cxx_qt::CxxQtType;
|
||||||
|
use cxx_qt_lib::{QList, QString};
|
||||||
|
|
||||||
|
#[cxx_qt::bridge]
|
||||||
|
pub mod qobject {
|
||||||
|
unsafe extern "C++" {
|
||||||
|
include!("cxx-qt-lib/qstring.h");
|
||||||
|
type QString = cxx_qt_lib::QString;
|
||||||
|
|
||||||
|
include!("cxx-qt-lib/core/qlist/qlist_i32.h");
|
||||||
|
type QList_i32 = cxx_qt_lib::QList<i32>;
|
||||||
|
|
||||||
|
include!("cxx-qt-lib/core/qlist/qlist_QString.h");
|
||||||
|
type QList_QString = cxx_qt_lib::QList<cxx_qt_lib::QString>;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "RustQt" {
|
||||||
|
#[qobject]
|
||||||
|
#[qml_element]
|
||||||
|
#[qml_singleton]
|
||||||
|
// Temperature
|
||||||
|
#[qproperty(i32, temp_celsius, cxx_name = "tempCelsius")]
|
||||||
|
#[qproperty(QList_i32, temp_history, cxx_name = "tempHistory")]
|
||||||
|
// JSON strings: [{name, celsius}]
|
||||||
|
#[qproperty(QList_QString, temp_devices, cxx_name = "tempDevices")]
|
||||||
|
// GPU
|
||||||
|
#[qproperty(bool, gpu_available, cxx_name = "gpuAvailable")]
|
||||||
|
#[qproperty(QString, gpu_vendor, cxx_name = "gpuVendor")]
|
||||||
|
#[qproperty(i32, gpu_usage, cxx_name = "gpuUsage")]
|
||||||
|
#[qproperty(f64, gpu_vram_used_gb, cxx_name = "gpuVramUsedGb")]
|
||||||
|
#[qproperty(f64, gpu_vram_total_gb, cxx_name = "gpuVramTotalGb")]
|
||||||
|
#[qproperty(i32, gpu_temp_c, cxx_name = "gpuTempC")]
|
||||||
|
#[qproperty(QList_i32, gpu_history, cxx_name = "gpuHistory")]
|
||||||
|
// Memory
|
||||||
|
#[qproperty(i32, mem_percent, cxx_name = "memPercent")]
|
||||||
|
#[qproperty(f64, mem_used_gb, cxx_name = "memUsedGb")]
|
||||||
|
#[qproperty(f64, mem_total_gb, cxx_name = "memTotalGb")]
|
||||||
|
#[qproperty(f64, mem_avail_gb, cxx_name = "memAvailGb")]
|
||||||
|
#[qproperty(f64, mem_cached_gb, cxx_name = "memCachedGb")]
|
||||||
|
#[qproperty(f64, mem_buffers_gb, cxx_name = "memBuffersGb")]
|
||||||
|
#[qproperty(QList_i32, mem_history, cxx_name = "memHistory")]
|
||||||
|
// Disk: JSON strings: [{target, pct, usedBytes, totalBytes}]
|
||||||
|
#[qproperty(QList_QString, disk_mounts, cxx_name = "diskMounts")]
|
||||||
|
#[qproperty(i32, disk_root_pct, cxx_name = "diskRootPct")]
|
||||||
|
type SystemStatsService = super::SystemStatsServiceRust;
|
||||||
|
|
||||||
|
#[qinvokable]
|
||||||
|
fn poll(self: Pin<&mut Self>);
|
||||||
|
|
||||||
|
#[qinvokable]
|
||||||
|
#[cxx_name = "pollDisk"]
|
||||||
|
fn poll_disk_invokable(self: Pin<&mut Self>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cxx_qt::Initialize for SystemStatsService {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SystemStatsServiceRust {
|
||||||
|
history_temp: Vec<i32>,
|
||||||
|
history_gpu: Vec<i32>,
|
||||||
|
history_mem: Vec<i32>,
|
||||||
|
gpu_backend: gpu::GpuBackend,
|
||||||
|
|
||||||
|
temp_celsius: i32,
|
||||||
|
temp_history: QList<i32>,
|
||||||
|
temp_devices: QList<QString>,
|
||||||
|
gpu_available: bool,
|
||||||
|
gpu_vendor: QString,
|
||||||
|
gpu_usage: i32,
|
||||||
|
gpu_vram_used_gb: f64,
|
||||||
|
gpu_vram_total_gb: f64,
|
||||||
|
gpu_temp_c: i32,
|
||||||
|
gpu_history: QList<i32>,
|
||||||
|
mem_percent: i32,
|
||||||
|
mem_used_gb: f64,
|
||||||
|
mem_total_gb: f64,
|
||||||
|
mem_avail_gb: f64,
|
||||||
|
mem_cached_gb: f64,
|
||||||
|
mem_buffers_gb: f64,
|
||||||
|
mem_history: QList<i32>,
|
||||||
|
disk_mounts: QList<QString>,
|
||||||
|
disk_root_pct: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SystemStatsServiceRust {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
history_temp: Vec::new(),
|
||||||
|
history_gpu: Vec::new(),
|
||||||
|
history_mem: Vec::new(),
|
||||||
|
gpu_backend: gpu::GpuBackend::None,
|
||||||
|
temp_celsius: 0,
|
||||||
|
temp_history: QList::default(),
|
||||||
|
temp_devices: QList::default(),
|
||||||
|
gpu_available: false,
|
||||||
|
gpu_vendor: QString::default(),
|
||||||
|
gpu_usage: 0,
|
||||||
|
gpu_vram_used_gb: 0.0,
|
||||||
|
gpu_vram_total_gb: 0.0,
|
||||||
|
gpu_temp_c: 0,
|
||||||
|
gpu_history: QList::default(),
|
||||||
|
mem_percent: 0,
|
||||||
|
mem_used_gb: 0.0,
|
||||||
|
mem_total_gb: 0.0,
|
||||||
|
mem_avail_gb: 0.0,
|
||||||
|
mem_cached_gb: 0.0,
|
||||||
|
mem_buffers_gb: 0.0,
|
||||||
|
mem_history: QList::default(),
|
||||||
|
disk_mounts: QList::default(),
|
||||||
|
disk_root_pct: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cxx_qt::Initialize for qobject::SystemStatsService {
|
||||||
|
fn initialize(mut self: Pin<&mut Self>) {
|
||||||
|
let backend = gpu::detect_gpu();
|
||||||
|
let available = !matches!(backend, gpu::GpuBackend::None);
|
||||||
|
let vendor = match &backend {
|
||||||
|
gpu::GpuBackend::Amd { .. } => "amd",
|
||||||
|
gpu::GpuBackend::Nvidia => "nvidia",
|
||||||
|
gpu::GpuBackend::Intel { .. } => "intel",
|
||||||
|
gpu::GpuBackend::None => "",
|
||||||
|
};
|
||||||
|
self.as_mut().rust_mut().gpu_backend = backend;
|
||||||
|
self.as_mut().set_gpu_available(available);
|
||||||
|
self.as_mut().set_gpu_vendor(QString::from(vendor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl qobject::SystemStatsService {
|
||||||
|
fn poll(mut self: Pin<&mut Self>) {
|
||||||
|
self.as_mut().poll_mem();
|
||||||
|
self.as_mut().poll_temp();
|
||||||
|
self.as_mut().poll_gpu();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_disk_invokable(mut self: Pin<&mut Self>) {
|
||||||
|
self.as_mut().poll_disk();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_mem(mut self: Pin<&mut Self>) {
|
||||||
|
let Some(m) = mem::read_meminfo() else { return };
|
||||||
|
let pct = m.percent as i32;
|
||||||
|
self.as_mut().set_mem_percent(pct);
|
||||||
|
self.as_mut().set_mem_used_gb(m.used_gb);
|
||||||
|
self.as_mut().set_mem_total_gb(m.total_gb);
|
||||||
|
self.as_mut().set_mem_avail_gb(m.avail_gb);
|
||||||
|
self.as_mut().set_mem_cached_gb(m.cached_gb);
|
||||||
|
self.as_mut().set_mem_buffers_gb(m.buffers_gb);
|
||||||
|
|
||||||
|
push_history(&mut self.as_mut().rust_mut().history_mem, pct, 30);
|
||||||
|
let hist = vec_to_qlist_i32(&self.as_ref().rust().history_mem);
|
||||||
|
self.as_mut().set_mem_history(hist);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_temp(mut self: Pin<&mut Self>) {
|
||||||
|
let devices = temp::read_thermal_devices();
|
||||||
|
if devices.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let max = devices.iter().map(|d| d.celsius).max().unwrap_or(0);
|
||||||
|
self.as_mut().set_temp_celsius(max);
|
||||||
|
|
||||||
|
push_history(&mut self.as_mut().rust_mut().history_temp, max, 150);
|
||||||
|
let hist = vec_to_qlist_i32(&self.as_ref().rust().history_temp);
|
||||||
|
self.as_mut().set_temp_history(hist);
|
||||||
|
|
||||||
|
let mut list = QList::<QString>::default();
|
||||||
|
for d in &devices {
|
||||||
|
let json = format!("{{\"name\":{:?},\"celsius\":{}}}", d.name, d.celsius);
|
||||||
|
list.append(QString::from(json.as_str()));
|
||||||
|
}
|
||||||
|
self.as_mut().set_temp_devices(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_gpu(mut self: Pin<&mut Self>) {
|
||||||
|
let info = gpu::read_gpu(&mut self.as_mut().rust_mut().gpu_backend);
|
||||||
|
let Some(g) = info else { return };
|
||||||
|
let usage = g.usage as i32;
|
||||||
|
self.as_mut().set_gpu_usage(usage);
|
||||||
|
self.as_mut().set_gpu_vram_used_gb(g.vram_used_gb);
|
||||||
|
self.as_mut().set_gpu_vram_total_gb(g.vram_total_gb);
|
||||||
|
self.as_mut().set_gpu_temp_c(g.temp_c);
|
||||||
|
|
||||||
|
push_history(&mut self.as_mut().rust_mut().history_gpu, usage, 60);
|
||||||
|
let hist = vec_to_qlist_i32(&self.as_ref().rust().history_gpu);
|
||||||
|
self.as_mut().set_gpu_history(hist);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_disk(mut self: Pin<&mut Self>) {
|
||||||
|
let mounts = disk::read_disk_mounts();
|
||||||
|
let mut root_pct = 0i32;
|
||||||
|
let mut list = QList::<QString>::default();
|
||||||
|
for m in &mounts {
|
||||||
|
if m.target == "/" {
|
||||||
|
root_pct = m.pct as i32;
|
||||||
|
}
|
||||||
|
let json = format!(
|
||||||
|
"{{\"target\":{:?},\"pct\":{},\"usedBytes\":{},\"totalBytes\":{}}}",
|
||||||
|
m.target, m.pct as i32, m.used_bytes as i64, m.total_bytes as i64
|
||||||
|
);
|
||||||
|
list.append(QString::from(json.as_str()));
|
||||||
|
}
|
||||||
|
self.as_mut().set_disk_root_pct(root_pct);
|
||||||
|
self.as_mut().set_disk_mounts(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_history(history: &mut Vec<i32>, value: i32, max_len: usize) {
|
||||||
|
history.push(value);
|
||||||
|
if history.len() > max_len {
|
||||||
|
history.drain(..history.len() - max_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vec_to_qlist_i32(v: &[i32]) -> QList<i32> {
|
||||||
|
let mut list = QList::<i32>::default();
|
||||||
|
for &x in v {
|
||||||
|
list.append(x);
|
||||||
|
}
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
@ -1,160 +1,50 @@
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Io
|
import NovaStats as NS
|
||||||
import "." as S
|
import "." as S
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property int usage: 0
|
readonly property int usage: NS.CpuService.usage
|
||||||
property real freqGhz: 0
|
readonly property real freqGhz: NS.CpuService.freqGhz
|
||||||
property var cores: [] // [{usage, freq_ghz, history:[]}] - history only while coreConsumers > 0
|
// cores arrives as QList<QString> JSON; parse into [{usage, freq_ghz, history:[]}]
|
||||||
property var coreMaxFreq: []
|
readonly property var cores: {
|
||||||
property var coreTypes: []
|
let raw = NS.CpuService.cores;
|
||||||
|
let out = [];
|
||||||
|
for (let i = 0; i < raw.length; i++)
|
||||||
|
out.push(JSON.parse(raw[i]));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
readonly property var coreMaxFreq: NS.CpuService.coreMaxFreq
|
||||||
|
readonly property var coreTypes: NS.CpuService.coreTypes
|
||||||
|
|
||||||
// Overall CPU history (60 samples)
|
readonly property var history: NS.CpuService.history
|
||||||
property var history: []
|
|
||||||
|
|
||||||
// Per-core data gating - applets increment/decrement
|
// Gate per-core history: applets increment/decrement coreConsumers
|
||||||
property int coreConsumers: 0
|
property int coreConsumers: 0
|
||||||
onCoreConsumersChanged: {
|
onCoreConsumersChanged: {
|
||||||
if (coreConsumers > 0)
|
NS.CpuService.enableCoreHistory = coreConsumers > 0;
|
||||||
_coreGraceTimer.stop();
|
if (coreConsumers <= 0)
|
||||||
else
|
|
||||||
_coreGraceTimer.start();
|
_coreGraceTimer.start();
|
||||||
|
else
|
||||||
|
_coreGraceTimer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
property Timer _coreGraceTimer: Timer {
|
property Timer _coreGraceTimer: Timer {
|
||||||
interval: 30000
|
interval: 30000
|
||||||
onTriggered: root.cores = root.cores.map(() => ({
|
onTriggered: NS.CpuService.clearCoreHistory()
|
||||||
"usage": 0,
|
|
||||||
"freq_ghz": 0,
|
|
||||||
"history": []
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nova-stats process (cpu only)
|
property Timer _pollTimer: Timer {
|
||||||
property Process _proc: Process {
|
interval: {
|
||||||
running: true
|
|
||||||
command: {
|
|
||||||
const ms = S.Modules.statsDaemon.interval;
|
const ms = S.Modules.statsDaemon.interval;
|
||||||
return ms > 0 ? ["nova-stats", "--types", "cpu", "--interval", ms.toString()] : ["nova-stats", "--types", "cpu"];
|
return ms > 0 ? ms : 4000;
|
||||||
}
|
}
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: "\n"
|
|
||||||
onRead: line => {
|
|
||||||
try {
|
|
||||||
const ev = JSON.parse(line);
|
|
||||||
if (ev.type !== "cpu")
|
|
||||||
return;
|
|
||||||
root.usage = ev.usage;
|
|
||||||
root.freqGhz = ev.freq_ghz;
|
|
||||||
|
|
||||||
const h = root.history.concat([ev.usage]);
|
|
||||||
root.history = h.length > 60 ? h.slice(h.length - 60) : h;
|
|
||||||
|
|
||||||
if (root.coreConsumers > 0) {
|
|
||||||
const histLen = 16;
|
|
||||||
const prev = root.cores;
|
|
||||||
root.cores = ev.cores.map((c, i) => {
|
|
||||||
const oldHist = prev[i]?.history ?? [];
|
|
||||||
const hist = oldHist.concat([c.usage]);
|
|
||||||
return {
|
|
||||||
usage: c.usage,
|
|
||||||
freq_ghz: c.freq_ghz,
|
|
||||||
history: hist.length > histLen ? hist.slice(hist.length - histLen) : hist
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (root.cores.length !== ev.cores.length) {
|
|
||||||
root.cores = ev.cores.map(c => ({
|
|
||||||
"usage": c.usage,
|
|
||||||
"freq_ghz": c.freq_ghz,
|
|
||||||
"history": []
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// One-time: per-core max freq
|
|
||||||
property Process _maxFreqProc: Process {
|
|
||||||
running: true
|
running: true
|
||||||
command: ["sh", "-c", "ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do f=\"$d/cpufreq/cpuinfo_max_freq\"; [ -f \"$f\" ] && cat \"$f\" || echo 0; done"]
|
repeat: true
|
||||||
stdout: StdioCollector {
|
triggeredOnStart: true
|
||||||
onStreamFinished: {
|
onTriggered: NS.CpuService.poll()
|
||||||
root.coreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-time: P/E-core topology
|
|
||||||
property Process _coreTypesProc: Process {
|
|
||||||
running: true
|
|
||||||
command: ["sh", "-c", String.raw`
|
|
||||||
if [ -f /sys/devices/cpu_core/cpus ] && [ -f /sys/devices/cpu_atom/cpus ]; then
|
|
||||||
core=$(cat /sys/devices/cpu_core/cpus)
|
|
||||||
atom=$(cat /sys/devices/cpu_atom/cpus)
|
|
||||||
echo "hybrid:$core:$atom"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do
|
|
||||||
f="$d/topology/core_type"
|
|
||||||
[ -f "$f" ] && cat "$f"
|
|
||||||
done
|
|
||||||
`]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const out = text.trim();
|
|
||||||
if (!out)
|
|
||||||
return;
|
|
||||||
if (out.startsWith("hybrid:")) {
|
|
||||||
const parts = out.split(":");
|
|
||||||
const coreRange = parts[1];
|
|
||||||
const atomRange = parts[2];
|
|
||||||
function expandRange(s) {
|
|
||||||
const cpus = new Set();
|
|
||||||
for (const part of s.split(",")) {
|
|
||||||
if (part.includes("-")) {
|
|
||||||
const [a, b] = part.split("-").map(Number);
|
|
||||||
for (let i = a; i <= b; i++)
|
|
||||||
cpus.add(i);
|
|
||||||
} else {
|
|
||||||
cpus.add(Number(part));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cpus;
|
|
||||||
}
|
|
||||||
const pCores = expandRange(coreRange);
|
|
||||||
const eCores = expandRange(atomRange);
|
|
||||||
const maxCpu = Math.max(...pCores, ...eCores);
|
|
||||||
const types = [];
|
|
||||||
for (let i = 0; i <= maxCpu; i++)
|
|
||||||
types.push(eCores.has(i) ? "Efficiency" : "Performance");
|
|
||||||
root.coreTypes = types;
|
|
||||||
} else {
|
|
||||||
const types = out.split("\n").filter(l => l).map(l => l.trim());
|
|
||||||
if (types.length > 0)
|
|
||||||
root.coreTypes = types;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: infer P/E from max freq gap
|
|
||||||
function _inferCoreTypesFromFreq() {
|
|
||||||
if (coreTypes.length > 0 || coreMaxFreq.length < 2)
|
|
||||||
return;
|
|
||||||
const freqs = coreMaxFreq.filter(f => f > 0);
|
|
||||||
if (!freqs.length)
|
|
||||||
return;
|
|
||||||
const maxF = Math.max(...freqs);
|
|
||||||
const minF = Math.min(...freqs);
|
|
||||||
if (maxF > 0 && minF > 0 && (maxF - minF) / maxF > 0.15) {
|
|
||||||
const threshold = (maxF + minF) / 2;
|
|
||||||
coreTypes = coreMaxFreq.map(f => f >= threshold ? "Performance" : "Efficiency");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onCoreMaxFreqChanged: Qt.callLater(_inferCoreTypesFromFreq)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,117 +1,71 @@
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Io
|
import NovaStats as NS
|
||||||
import "." as M
|
import "." as M
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// ── Temperature ──────────────────────────────────────────────────────
|
// ── Temperature ──────────────────────────────────────────────────────
|
||||||
property int tempCelsius: 0
|
readonly property int tempCelsius: NS.SystemStatsService.tempCelsius
|
||||||
property var tempHistory: [] // 150 samples @ 4s each ≈ 10 min
|
readonly property var tempHistory: NS.SystemStatsService.tempHistory
|
||||||
property var tempDevices: [] // [{name, celsius}] sorted hottest-first
|
// tempDevices arrives as QList<QString> JSON; parse into [{name, celsius}]
|
||||||
|
readonly property var tempDevices: {
|
||||||
|
let raw = NS.SystemStatsService.tempDevices;
|
||||||
|
let out = [];
|
||||||
|
for (let i = 0; i < raw.length; i++)
|
||||||
|
out.push(JSON.parse(raw[i]));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// ── GPU ──────────────────────────────────────────────────────────────
|
// ── GPU ──────────────────────────────────────────────────────────────
|
||||||
property bool gpuAvailable: false
|
readonly property bool gpuAvailable: NS.SystemStatsService.gpuAvailable
|
||||||
property string gpuVendor: ""
|
readonly property string gpuVendor: NS.SystemStatsService.gpuVendor
|
||||||
property int gpuUsage: 0
|
readonly property int gpuUsage: NS.SystemStatsService.gpuUsage
|
||||||
property real gpuVramUsedGb: 0
|
readonly property real gpuVramUsedGb: NS.SystemStatsService.gpuVramUsedGb
|
||||||
property real gpuVramTotalGb: 0
|
readonly property real gpuVramTotalGb: NS.SystemStatsService.gpuVramTotalGb
|
||||||
property int gpuTempC: 0
|
readonly property int gpuTempC: NS.SystemStatsService.gpuTempC
|
||||||
property var gpuHistory: [] // 60 samples @ ~4-8s each ≈ 4-8 min
|
readonly property var gpuHistory: NS.SystemStatsService.gpuHistory
|
||||||
|
|
||||||
// ── Memory ───────────────────────────────────────────────────────────
|
// ── Memory ───────────────────────────────────────────────────────────
|
||||||
property int memPercent: 0
|
readonly property int memPercent: NS.SystemStatsService.memPercent
|
||||||
property real memUsedGb: 0
|
readonly property real memUsedGb: NS.SystemStatsService.memUsedGb
|
||||||
property real memTotalGb: 0
|
readonly property real memTotalGb: NS.SystemStatsService.memTotalGb
|
||||||
property real memAvailGb: 0
|
readonly property real memAvailGb: NS.SystemStatsService.memAvailGb
|
||||||
property real memCachedGb: 0
|
readonly property real memCachedGb: NS.SystemStatsService.memCachedGb
|
||||||
property real memBuffersGb: 0
|
readonly property real memBuffersGb: NS.SystemStatsService.memBuffersGb
|
||||||
property var memHistory: []
|
readonly property var memHistory: NS.SystemStatsService.memHistory
|
||||||
|
|
||||||
// ── Disk ─────────────────────────────────────────────────────────────
|
// ── Disk ─────────────────────────────────────────────────────────────
|
||||||
property var diskMounts: []
|
// diskMounts arrives as QList<QString> JSON; parse into [{target, pct, usedBytes, totalBytes}]
|
||||||
property int diskRootPct: 0
|
readonly property var diskMounts: {
|
||||||
|
let raw = NS.SystemStatsService.diskMounts;
|
||||||
|
let out = [];
|
||||||
|
for (let i = 0; i < raw.length; i++)
|
||||||
|
out.push(JSON.parse(raw[i]));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
readonly property int diskRootPct: NS.SystemStatsService.diskRootPct
|
||||||
|
|
||||||
// nova-stats stream (mem, temp, gpu - cpu handled by CpuService)
|
// ── Polling ──────────────────────────────────────────────────────────
|
||||||
property var _statsProc: Process {
|
// Drive the Rust service from QML timers; both intervals read from Modules config.
|
||||||
running: true
|
property Timer _statsTimer: Timer {
|
||||||
command: {
|
interval: {
|
||||||
const ms = M.Modules.statsDaemon.interval;
|
const ms = M.Modules.statsDaemon.interval;
|
||||||
return ms > 0 ? ["nova-stats", "--types", "mem,temp,gpu", "--interval", ms.toString()] : ["nova-stats", "--types", "mem,temp,gpu"];
|
return ms > 0 ? ms : 4000;
|
||||||
}
|
}
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: "\n"
|
|
||||||
onRead: line => {
|
|
||||||
try {
|
|
||||||
const ev = JSON.parse(line);
|
|
||||||
if (ev.type === "temp") {
|
|
||||||
root.tempCelsius = ev.celsius;
|
|
||||||
const th = root.tempHistory.concat([ev.celsius]);
|
|
||||||
root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th;
|
|
||||||
if (ev.devices)
|
|
||||||
root.tempDevices = ev.devices;
|
|
||||||
} else if (ev.type === "gpu") {
|
|
||||||
root.gpuAvailable = true;
|
|
||||||
root.gpuVendor = ev.vendor;
|
|
||||||
root.gpuUsage = ev.usage;
|
|
||||||
root.gpuVramUsedGb = ev.vram_used_gb;
|
|
||||||
root.gpuVramTotalGb = ev.vram_total_gb;
|
|
||||||
root.gpuTempC = ev.temp_c;
|
|
||||||
const gh = root.gpuHistory.concat([ev.usage]);
|
|
||||||
root.gpuHistory = gh.length > 60 ? gh.slice(gh.length - 60) : gh;
|
|
||||||
} else if (ev.type === "mem") {
|
|
||||||
root.memPercent = ev.percent;
|
|
||||||
root.memUsedGb = ev.used_gb;
|
|
||||||
root.memTotalGb = ev.total_gb;
|
|
||||||
root.memAvailGb = ev.avail_gb;
|
|
||||||
root.memCachedGb = ev.cached_gb;
|
|
||||||
root.memBuffersGb = ev.buffers_gb;
|
|
||||||
const h = root.memHistory.concat([ev.percent]);
|
|
||||||
root.memHistory = h.length > 30 ? h.slice(h.length - 30) : h;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disk via df
|
|
||||||
property var _diskProc: Process {
|
|
||||||
id: diskProc
|
|
||||||
running: true
|
running: true
|
||||||
command: ["sh", "-c", "df -x tmpfs -x devtmpfs -x squashfs -x efivarfs -x overlay -B1 --output=target,size,used 2>/dev/null | awk 'NR>1 && $2+0>0 {print $1\"|\"$2\"|\"$3}'"]
|
repeat: true
|
||||||
stdout: StdioCollector {
|
triggeredOnStart: true
|
||||||
onStreamFinished: {
|
onTriggered: NS.SystemStatsService.poll()
|
||||||
const lines = text.trim().split("\n").filter(l => l);
|
|
||||||
const mounts = [];
|
|
||||||
for (const line of lines) {
|
|
||||||
const parts = line.split("|");
|
|
||||||
if (parts.length < 3)
|
|
||||||
continue;
|
|
||||||
const total = parseInt(parts[1]);
|
|
||||||
const used = parseInt(parts[2]);
|
|
||||||
if (total <= 0)
|
|
||||||
continue;
|
|
||||||
mounts.push({
|
|
||||||
"target": parts[0],
|
|
||||||
"pct": Math.round(used / total * 100),
|
|
||||||
"usedBytes": used,
|
|
||||||
"totalBytes": total
|
|
||||||
});
|
|
||||||
}
|
|
||||||
root.diskMounts = mounts;
|
|
||||||
const rm = mounts.find(m => m.target === "/");
|
|
||||||
if (rm)
|
|
||||||
root.diskRootPct = rm.pct;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
property var _diskTimer: Timer {
|
property Timer _diskTimer: Timer {
|
||||||
interval: M.Modules.disk.interval || 30000
|
interval: M.Modules.disk.interval || 30000
|
||||||
running: true
|
running: true
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: diskProc.running = true
|
triggeredOnStart: true
|
||||||
|
onTriggered: NS.SystemStatsService.pollDisk()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue