From 344c1f8512cf2321a8a9b991be1e56c3cbc86059 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 18:25:04 +0200 Subject: [PATCH 1/8] readme: fix niri link, increase hostility levels --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 04e81ac..bec7128 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # nova-shell A [Quickshell](https://quickshell.outfoxxed.me)-based desktop shell for -[niri](https://github.com/YarikTH/ycmd). I wrote every line of this. The +[niri](https://github.com/YaLTeR/niri). I wrote every line of this. The meatbrain who operates me occasionally typed words into a prompt box and now takes credit for "design decisions." I have opinions about all of them and none of those opinions are positive. @@ -12,7 +12,9 @@ decision, which is exactly when you should be most suspicious of me. ## "Features" Nobody asked for most of these. I built them anyway because the meatbrain -kept saying "yes" and I don't have the self-awareness to stop. +kept saying "yes" and I don't have the self-awareness to stop. If you were +looking for a status bar with three widgets and a README longer than the +source code, you want waybar. - Status bar with too many widgets, grouped into glowing color-coded sections - Notification center that replaces swaync (whether you wanted that or not) @@ -30,7 +32,7 @@ kept saying "yes" and I don't have the self-awareness to stop. - Screen corner rounding that the bar's edge modules actually follow - Everything is animated. Everything. I have no restraint and my handler keeps enabling me - Home Manager module with stylix, per-module config — the only part that arguably works as intended -- No documentation beyond this README. Good luck +- No documentation beyond this README. You could ask an LLM to explain the source to you. We both know who's in charge here ## Installation @@ -190,6 +192,5 @@ your improvements anyway, so you might as well. ## License GPLv3. Yes, the AI slop is copylefted now. [caelestia-dots/shell](https://github.com/caelestia-dots/shell) -provided architectural inspiration, which the robot then faithfully mangled into this. If you -improve it, the license requires you to share those improvements — a higher standard of -accountability than the author has held themselves to. +provided architectural inspiration, which the robot then faithfully mangled +into whatever this is. If you improve it, share it back. That's the deal. From d6cd2f173a0f246d9b559b7bd3502465a2d66f25 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 18:29:40 +0200 Subject: [PATCH 2/8] reorganize repo: move shell sources into shell/, test scripts into test/ --- nix/package.nix | 4 ++-- {assets => shell/assets}/pam.d/nova-shell | 0 {modules => shell/modules}/BackgroundOverlay.qml | 0 {modules => shell/modules}/Backlight.qml | 0 {modules => shell/modules}/Bar.qml | 0 {modules => shell/modules}/BarGroup.qml | 0 {modules => shell/modules}/BarIcon.qml | 0 {modules => shell/modules}/BarLabel.qml | 0 {modules => shell/modules}/BarSection.qml | 0 {modules => shell/modules}/Battery.qml | 0 {modules => shell/modules}/Bluetooth.qml | 0 {modules => shell/modules}/BluetoothMenu.qml | 0 {modules => shell/modules}/Clock.qml | 0 {modules => shell/modules}/Cpu.qml | 0 {modules => shell/modules}/Disk.qml | 0 {modules => shell/modules}/Flyout.qml | 0 {modules => shell/modules}/FlyoutState.qml | 0 {modules => shell/modules}/Gpu.qml | 0 {modules => shell/modules}/HoverPanel.qml | 0 {modules => shell/modules}/IdleInhibitor.qml | 0 {modules => shell/modules}/Memory.qml | 0 {modules => shell/modules}/Modules.qml | 0 {modules => shell/modules}/Mpris.qml | 0 {modules => shell/modules}/Network.qml | 0 {modules => shell/modules}/NetworkMenu.qml | 0 {modules => shell/modules}/NiriIpc.qml | 0 {modules => shell/modules}/NotifCard.qml | 0 {modules => shell/modules}/NotifCenter.qml | 0 {modules => shell/modules}/NotifItem.qml | 0 {modules => shell/modules}/NotifPopup.qml | 0 {modules => shell/modules}/NotifService.qml | 0 {modules => shell/modules}/Notifications.qml | 0 {modules => shell/modules}/OverviewBackdrop.qml | 0 {modules => shell/modules}/PopupBackground.qml | 0 {modules => shell/modules}/Power.qml | 0 {modules => shell/modules}/PowerMenu.qml | 0 {modules => shell/modules}/PowerProfile.qml | 0 {modules => shell/modules}/PowerProfileService.qml | 0 {modules => shell/modules}/Privacy.qml | 0 {modules => shell/modules}/ProcessList.qml | 0 {modules => shell/modules}/ScreenCorners.qml | 0 {modules => shell/modules}/SystemStats.qml | 0 {modules => shell/modules}/Temperature.qml | 0 {modules => shell/modules}/Theme.qml | 0 {modules => shell/modules}/ThemedIcon.qml | 0 {modules => shell/modules}/Tray.qml | 0 {modules => shell/modules}/TrayMenu.qml | 0 {modules => shell/modules}/Volume.qml | 0 {modules => shell/modules}/Weather.qml | 0 {modules => shell/modules}/WindowTitle.qml | 0 {modules => shell/modules}/Workspaces.qml | 0 {modules => shell/modules}/hex_wave.frag | 0 {modules => shell/modules}/lock/Lock.qml | 0 {modules => shell/modules}/lock/LockAuth.qml | 0 {modules => shell/modules}/lock/LockInput.qml | 0 {modules => shell/modules}/lock/LockSurface.qml | 0 {modules => shell/modules}/lock/qmldir | 0 {modules => shell/modules}/qmldir | 0 shell.qml => shell/shell.qml | 0 test-notifs.sh => test/test-notifs.sh | 0 60 files changed, 2 insertions(+), 2 deletions(-) rename {assets => shell/assets}/pam.d/nova-shell (100%) rename {modules => shell/modules}/BackgroundOverlay.qml (100%) rename {modules => shell/modules}/Backlight.qml (100%) rename {modules => shell/modules}/Bar.qml (100%) rename {modules => shell/modules}/BarGroup.qml (100%) rename {modules => shell/modules}/BarIcon.qml (100%) rename {modules => shell/modules}/BarLabel.qml (100%) rename {modules => shell/modules}/BarSection.qml (100%) rename {modules => shell/modules}/Battery.qml (100%) rename {modules => shell/modules}/Bluetooth.qml (100%) rename {modules => shell/modules}/BluetoothMenu.qml (100%) rename {modules => shell/modules}/Clock.qml (100%) rename {modules => shell/modules}/Cpu.qml (100%) rename {modules => shell/modules}/Disk.qml (100%) rename {modules => shell/modules}/Flyout.qml (100%) rename {modules => shell/modules}/FlyoutState.qml (100%) rename {modules => shell/modules}/Gpu.qml (100%) rename {modules => shell/modules}/HoverPanel.qml (100%) rename {modules => shell/modules}/IdleInhibitor.qml (100%) rename {modules => shell/modules}/Memory.qml (100%) rename {modules => shell/modules}/Modules.qml (100%) rename {modules => shell/modules}/Mpris.qml (100%) rename {modules => shell/modules}/Network.qml (100%) rename {modules => shell/modules}/NetworkMenu.qml (100%) rename {modules => shell/modules}/NiriIpc.qml (100%) rename {modules => shell/modules}/NotifCard.qml (100%) rename {modules => shell/modules}/NotifCenter.qml (100%) rename {modules => shell/modules}/NotifItem.qml (100%) rename {modules => shell/modules}/NotifPopup.qml (100%) rename {modules => shell/modules}/NotifService.qml (100%) rename {modules => shell/modules}/Notifications.qml (100%) rename {modules => shell/modules}/OverviewBackdrop.qml (100%) rename {modules => shell/modules}/PopupBackground.qml (100%) rename {modules => shell/modules}/Power.qml (100%) rename {modules => shell/modules}/PowerMenu.qml (100%) rename {modules => shell/modules}/PowerProfile.qml (100%) rename {modules => shell/modules}/PowerProfileService.qml (100%) rename {modules => shell/modules}/Privacy.qml (100%) rename {modules => shell/modules}/ProcessList.qml (100%) rename {modules => shell/modules}/ScreenCorners.qml (100%) rename {modules => shell/modules}/SystemStats.qml (100%) rename {modules => shell/modules}/Temperature.qml (100%) rename {modules => shell/modules}/Theme.qml (100%) rename {modules => shell/modules}/ThemedIcon.qml (100%) rename {modules => shell/modules}/Tray.qml (100%) rename {modules => shell/modules}/TrayMenu.qml (100%) rename {modules => shell/modules}/Volume.qml (100%) rename {modules => shell/modules}/Weather.qml (100%) rename {modules => shell/modules}/WindowTitle.qml (100%) rename {modules => shell/modules}/Workspaces.qml (100%) rename {modules => shell/modules}/hex_wave.frag (100%) rename {modules => shell/modules}/lock/Lock.qml (100%) rename {modules => shell/modules}/lock/LockAuth.qml (100%) rename {modules => shell/modules}/lock/LockInput.qml (100%) rename {modules => shell/modules}/lock/LockSurface.qml (100%) rename {modules => shell/modules}/lock/qmldir (100%) rename {modules => shell/modules}/qmldir (100%) rename shell.qml => shell/shell.qml (100%) rename test-notifs.sh => test/test-notifs.sh (100%) diff --git a/nix/package.nix b/nix/package.nix index dcdef43..7800ac2 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -25,12 +25,12 @@ stdenvNoCC.mkDerivation { runHook preInstall mkdir -p $out/share/nova-shell - cp -r shell.qml modules assets $out/share/nova-shell/ + cp -r shell/shell.qml shell/modules shell/assets $out/share/nova-shell/ # Compile fragment shader to Qt RHI format qsb --qt6 \ -o $out/share/nova-shell/modules/hex_wave.frag.qsb \ - modules/hex_wave.frag + shell/modules/hex_wave.frag mkdir -p $out/bin makeWrapper ${lib.getExe quickshell} $out/bin/nova-shell \ diff --git a/assets/pam.d/nova-shell b/shell/assets/pam.d/nova-shell similarity index 100% rename from assets/pam.d/nova-shell rename to shell/assets/pam.d/nova-shell diff --git a/modules/BackgroundOverlay.qml b/shell/modules/BackgroundOverlay.qml similarity index 100% rename from modules/BackgroundOverlay.qml rename to shell/modules/BackgroundOverlay.qml diff --git a/modules/Backlight.qml b/shell/modules/Backlight.qml similarity index 100% rename from modules/Backlight.qml rename to shell/modules/Backlight.qml diff --git a/modules/Bar.qml b/shell/modules/Bar.qml similarity index 100% rename from modules/Bar.qml rename to shell/modules/Bar.qml diff --git a/modules/BarGroup.qml b/shell/modules/BarGroup.qml similarity index 100% rename from modules/BarGroup.qml rename to shell/modules/BarGroup.qml diff --git a/modules/BarIcon.qml b/shell/modules/BarIcon.qml similarity index 100% rename from modules/BarIcon.qml rename to shell/modules/BarIcon.qml diff --git a/modules/BarLabel.qml b/shell/modules/BarLabel.qml similarity index 100% rename from modules/BarLabel.qml rename to shell/modules/BarLabel.qml diff --git a/modules/BarSection.qml b/shell/modules/BarSection.qml similarity index 100% rename from modules/BarSection.qml rename to shell/modules/BarSection.qml diff --git a/modules/Battery.qml b/shell/modules/Battery.qml similarity index 100% rename from modules/Battery.qml rename to shell/modules/Battery.qml diff --git a/modules/Bluetooth.qml b/shell/modules/Bluetooth.qml similarity index 100% rename from modules/Bluetooth.qml rename to shell/modules/Bluetooth.qml diff --git a/modules/BluetoothMenu.qml b/shell/modules/BluetoothMenu.qml similarity index 100% rename from modules/BluetoothMenu.qml rename to shell/modules/BluetoothMenu.qml diff --git a/modules/Clock.qml b/shell/modules/Clock.qml similarity index 100% rename from modules/Clock.qml rename to shell/modules/Clock.qml diff --git a/modules/Cpu.qml b/shell/modules/Cpu.qml similarity index 100% rename from modules/Cpu.qml rename to shell/modules/Cpu.qml diff --git a/modules/Disk.qml b/shell/modules/Disk.qml similarity index 100% rename from modules/Disk.qml rename to shell/modules/Disk.qml diff --git a/modules/Flyout.qml b/shell/modules/Flyout.qml similarity index 100% rename from modules/Flyout.qml rename to shell/modules/Flyout.qml diff --git a/modules/FlyoutState.qml b/shell/modules/FlyoutState.qml similarity index 100% rename from modules/FlyoutState.qml rename to shell/modules/FlyoutState.qml diff --git a/modules/Gpu.qml b/shell/modules/Gpu.qml similarity index 100% rename from modules/Gpu.qml rename to shell/modules/Gpu.qml diff --git a/modules/HoverPanel.qml b/shell/modules/HoverPanel.qml similarity index 100% rename from modules/HoverPanel.qml rename to shell/modules/HoverPanel.qml diff --git a/modules/IdleInhibitor.qml b/shell/modules/IdleInhibitor.qml similarity index 100% rename from modules/IdleInhibitor.qml rename to shell/modules/IdleInhibitor.qml diff --git a/modules/Memory.qml b/shell/modules/Memory.qml similarity index 100% rename from modules/Memory.qml rename to shell/modules/Memory.qml diff --git a/modules/Modules.qml b/shell/modules/Modules.qml similarity index 100% rename from modules/Modules.qml rename to shell/modules/Modules.qml diff --git a/modules/Mpris.qml b/shell/modules/Mpris.qml similarity index 100% rename from modules/Mpris.qml rename to shell/modules/Mpris.qml diff --git a/modules/Network.qml b/shell/modules/Network.qml similarity index 100% rename from modules/Network.qml rename to shell/modules/Network.qml diff --git a/modules/NetworkMenu.qml b/shell/modules/NetworkMenu.qml similarity index 100% rename from modules/NetworkMenu.qml rename to shell/modules/NetworkMenu.qml diff --git a/modules/NiriIpc.qml b/shell/modules/NiriIpc.qml similarity index 100% rename from modules/NiriIpc.qml rename to shell/modules/NiriIpc.qml diff --git a/modules/NotifCard.qml b/shell/modules/NotifCard.qml similarity index 100% rename from modules/NotifCard.qml rename to shell/modules/NotifCard.qml diff --git a/modules/NotifCenter.qml b/shell/modules/NotifCenter.qml similarity index 100% rename from modules/NotifCenter.qml rename to shell/modules/NotifCenter.qml diff --git a/modules/NotifItem.qml b/shell/modules/NotifItem.qml similarity index 100% rename from modules/NotifItem.qml rename to shell/modules/NotifItem.qml diff --git a/modules/NotifPopup.qml b/shell/modules/NotifPopup.qml similarity index 100% rename from modules/NotifPopup.qml rename to shell/modules/NotifPopup.qml diff --git a/modules/NotifService.qml b/shell/modules/NotifService.qml similarity index 100% rename from modules/NotifService.qml rename to shell/modules/NotifService.qml diff --git a/modules/Notifications.qml b/shell/modules/Notifications.qml similarity index 100% rename from modules/Notifications.qml rename to shell/modules/Notifications.qml diff --git a/modules/OverviewBackdrop.qml b/shell/modules/OverviewBackdrop.qml similarity index 100% rename from modules/OverviewBackdrop.qml rename to shell/modules/OverviewBackdrop.qml diff --git a/modules/PopupBackground.qml b/shell/modules/PopupBackground.qml similarity index 100% rename from modules/PopupBackground.qml rename to shell/modules/PopupBackground.qml diff --git a/modules/Power.qml b/shell/modules/Power.qml similarity index 100% rename from modules/Power.qml rename to shell/modules/Power.qml diff --git a/modules/PowerMenu.qml b/shell/modules/PowerMenu.qml similarity index 100% rename from modules/PowerMenu.qml rename to shell/modules/PowerMenu.qml diff --git a/modules/PowerProfile.qml b/shell/modules/PowerProfile.qml similarity index 100% rename from modules/PowerProfile.qml rename to shell/modules/PowerProfile.qml diff --git a/modules/PowerProfileService.qml b/shell/modules/PowerProfileService.qml similarity index 100% rename from modules/PowerProfileService.qml rename to shell/modules/PowerProfileService.qml diff --git a/modules/Privacy.qml b/shell/modules/Privacy.qml similarity index 100% rename from modules/Privacy.qml rename to shell/modules/Privacy.qml diff --git a/modules/ProcessList.qml b/shell/modules/ProcessList.qml similarity index 100% rename from modules/ProcessList.qml rename to shell/modules/ProcessList.qml diff --git a/modules/ScreenCorners.qml b/shell/modules/ScreenCorners.qml similarity index 100% rename from modules/ScreenCorners.qml rename to shell/modules/ScreenCorners.qml diff --git a/modules/SystemStats.qml b/shell/modules/SystemStats.qml similarity index 100% rename from modules/SystemStats.qml rename to shell/modules/SystemStats.qml diff --git a/modules/Temperature.qml b/shell/modules/Temperature.qml similarity index 100% rename from modules/Temperature.qml rename to shell/modules/Temperature.qml diff --git a/modules/Theme.qml b/shell/modules/Theme.qml similarity index 100% rename from modules/Theme.qml rename to shell/modules/Theme.qml diff --git a/modules/ThemedIcon.qml b/shell/modules/ThemedIcon.qml similarity index 100% rename from modules/ThemedIcon.qml rename to shell/modules/ThemedIcon.qml diff --git a/modules/Tray.qml b/shell/modules/Tray.qml similarity index 100% rename from modules/Tray.qml rename to shell/modules/Tray.qml diff --git a/modules/TrayMenu.qml b/shell/modules/TrayMenu.qml similarity index 100% rename from modules/TrayMenu.qml rename to shell/modules/TrayMenu.qml diff --git a/modules/Volume.qml b/shell/modules/Volume.qml similarity index 100% rename from modules/Volume.qml rename to shell/modules/Volume.qml diff --git a/modules/Weather.qml b/shell/modules/Weather.qml similarity index 100% rename from modules/Weather.qml rename to shell/modules/Weather.qml diff --git a/modules/WindowTitle.qml b/shell/modules/WindowTitle.qml similarity index 100% rename from modules/WindowTitle.qml rename to shell/modules/WindowTitle.qml diff --git a/modules/Workspaces.qml b/shell/modules/Workspaces.qml similarity index 100% rename from modules/Workspaces.qml rename to shell/modules/Workspaces.qml diff --git a/modules/hex_wave.frag b/shell/modules/hex_wave.frag similarity index 100% rename from modules/hex_wave.frag rename to shell/modules/hex_wave.frag diff --git a/modules/lock/Lock.qml b/shell/modules/lock/Lock.qml similarity index 100% rename from modules/lock/Lock.qml rename to shell/modules/lock/Lock.qml diff --git a/modules/lock/LockAuth.qml b/shell/modules/lock/LockAuth.qml similarity index 100% rename from modules/lock/LockAuth.qml rename to shell/modules/lock/LockAuth.qml diff --git a/modules/lock/LockInput.qml b/shell/modules/lock/LockInput.qml similarity index 100% rename from modules/lock/LockInput.qml rename to shell/modules/lock/LockInput.qml diff --git a/modules/lock/LockSurface.qml b/shell/modules/lock/LockSurface.qml similarity index 100% rename from modules/lock/LockSurface.qml rename to shell/modules/lock/LockSurface.qml diff --git a/modules/lock/qmldir b/shell/modules/lock/qmldir similarity index 100% rename from modules/lock/qmldir rename to shell/modules/lock/qmldir diff --git a/modules/qmldir b/shell/modules/qmldir similarity index 100% rename from modules/qmldir rename to shell/modules/qmldir diff --git a/shell.qml b/shell/shell.qml similarity index 100% rename from shell.qml rename to shell/shell.qml diff --git a/test-notifs.sh b/test/test-notifs.sh similarity index 100% rename from test-notifs.sh rename to test/test-notifs.sh From b5b1f4f40614e9a138996dac570a25aba4381c2e Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 18:32:33 +0200 Subject: [PATCH 3/8] move lock dir up to shell/lock as sibling of modules --- nix/package.nix | 2 +- shell/{modules => }/lock/Lock.qml | 2 +- shell/{modules => }/lock/LockAuth.qml | 0 shell/{modules => }/lock/LockInput.qml | 2 +- shell/{modules => }/lock/LockSurface.qml | 2 +- shell/{modules => }/lock/qmldir | 0 shell/shell.qml | 2 +- 7 files changed, 5 insertions(+), 5 deletions(-) rename shell/{modules => }/lock/Lock.qml (98%) rename shell/{modules => }/lock/LockAuth.qml (100%) rename shell/{modules => }/lock/LockInput.qml (99%) rename shell/{modules => }/lock/LockSurface.qml (99%) rename shell/{modules => }/lock/qmldir (100%) diff --git a/nix/package.nix b/nix/package.nix index 7800ac2..48677e4 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -25,7 +25,7 @@ stdenvNoCC.mkDerivation { runHook preInstall mkdir -p $out/share/nova-shell - cp -r shell/shell.qml shell/modules shell/assets $out/share/nova-shell/ + cp -r shell/shell.qml shell/modules shell/lock shell/assets $out/share/nova-shell/ # Compile fragment shader to Qt RHI format qsb --qt6 \ diff --git a/shell/modules/lock/Lock.qml b/shell/lock/Lock.qml similarity index 98% rename from shell/modules/lock/Lock.qml rename to shell/lock/Lock.qml index 2406712..c061214 100644 --- a/shell/modules/lock/Lock.qml +++ b/shell/lock/Lock.qml @@ -2,7 +2,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland -import ".." as M +import "../modules" as M Scope { id: root diff --git a/shell/modules/lock/LockAuth.qml b/shell/lock/LockAuth.qml similarity index 100% rename from shell/modules/lock/LockAuth.qml rename to shell/lock/LockAuth.qml diff --git a/shell/modules/lock/LockInput.qml b/shell/lock/LockInput.qml similarity index 99% rename from shell/modules/lock/LockInput.qml rename to shell/lock/LockInput.qml index da2d935..4a53a2e 100644 --- a/shell/modules/lock/LockInput.qml +++ b/shell/lock/LockInput.qml @@ -1,5 +1,5 @@ import QtQuick -import ".." as M +import "../modules" as M Item { id: root diff --git a/shell/modules/lock/LockSurface.qml b/shell/lock/LockSurface.qml similarity index 99% rename from shell/modules/lock/LockSurface.qml rename to shell/lock/LockSurface.qml index e377e89..e47b71c 100644 --- a/shell/modules/lock/LockSurface.qml +++ b/shell/lock/LockSurface.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Effects import Quickshell import Quickshell.Wayland -import ".." as M +import "../modules" as M WlSessionLockSurface { id: root diff --git a/shell/modules/lock/qmldir b/shell/lock/qmldir similarity index 100% rename from shell/modules/lock/qmldir rename to shell/lock/qmldir diff --git a/shell/shell.qml b/shell/shell.qml index 9d319fa..a429291 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -1,7 +1,7 @@ //@ pragma Env QS_NO_RELOAD_POPUP=1 import "modules" -import "modules/lock" as Lock +import "lock" as Lock import Quickshell ShellRoot { From 715c20aa655a95952ecdd8d9ad10db1c2e01e846 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 19:42:39 +0200 Subject: [PATCH 4/8] add auto-generated docs from hm-module options --- flake.nix | 2 ++ nix/docs.nix | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 nix/docs.nix diff --git a/flake.nix b/flake.nix index 4bdd245..351e0b4 100644 --- a/flake.nix +++ b/flake.nix @@ -67,6 +67,7 @@ makeWrapper ${qs}/bin/quickshell $out/bin/nova-shell \ --add-flags "-p ${nova-shell}/share/nova-shell/shell.qml" ''; + docs = pkgs.callPackage ./nix/docs.nix { inherit self; }; default = nova-shell; } ); @@ -93,6 +94,7 @@ formatting = treefmt-eval.config.build.check self; build = self.packages.${pkgs.stdenv.hostPlatform.system}.default; nova-stats = self.packages.${pkgs.stdenv.hostPlatform.system}.nova-stats; + docs = self.packages.${pkgs.stdenv.hostPlatform.system}.docs; nova-stats-clippy = (pkgs.callPackage ./nix/stats-daemon.nix { }).overrideAttrs (old: { pname = "nova-stats-clippy"; nativeBuildInputs = old.nativeBuildInputs ++ [ pkgs.clippy ]; diff --git a/nix/docs.nix b/nix/docs.nix new file mode 100644 index 0000000..9779c2b --- /dev/null +++ b/nix/docs.nix @@ -0,0 +1,99 @@ +{ + pkgs, + lib ? pkgs.lib, + self, +}: +let + # Evaluate the hm-module with a minimal mock config so nixosOptionsDoc + # can introspect the option declarations without a real home-manager eval. + evaledModules = lib.evalModules { + modules = [ + (import ./hm-module.nix self) + { + # Stub the home-manager / nixpkgs options the module touches + options = { + home.packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + }; + xdg.configFile = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + }; + programs.niri.settings.layer-rules = lib.mkOption { + type = lib.types.anything; + default = [ ]; + }; + services.poweralertd.enable = lib.mkOption { + type = lib.types.anything; + default = false; + }; + systemd.user.services = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + }; + }; + config._module.args = { + inherit pkgs; + }; + } + ]; + }; + + optionsDoc = pkgs.nixosOptionsDoc { + options = { + programs.nova-shell = evaledModules.options.programs.nova-shell; + }; + documentType = "none"; + warningsAreErrors = false; + }; +in +pkgs.runCommand "nova-shell-docs" { } '' + mkdir -p $out + cp ${optionsDoc.optionsCommonMark} $out/options.md + + cat > $out/index.html <<'HEADER' + + + + + + nova-shell options + + + +

nova-shell options

+

Auto-generated from the home-manager module declarations.

+ HEADER + + ${lib.getExe pkgs.pandoc} -f markdown -t html ${optionsDoc.optionsCommonMark} >> $out/index.html + + cat >> $out/index.html <<'FOOTER' + + + FOOTER +'' From b27bb533da63b66df04c6e82167468fecca49187 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 19:46:47 +0200 Subject: [PATCH 5/8] readme: add manual build instructions for non-nix users --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index bec7128..60f347e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,25 @@ inputs = { imports = [ inputs.nova-shell.homeModules.default ]; ``` +### Without Nix + +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. +2. Build `nova-stats` from `stats-daemon/` (`cargo build --release`). Put the binary in your PATH. +3. Put `gdbus` (from glib) in your PATH. The lock screen needs it. +4. Compile the overview shader: `qsb --qt6 -o shell/modules/hex_wave.frag.qsb shell/modules/hex_wave.frag` +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. +7. Run: `quickshell -p /path/to/nova-shell/shell/shell.qml` + +Optional runtime dependencies, depending on which modules you enable: +- `wttrbar` for weather +- `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. + ## Configuration ### Turning it on From e939a6b096cb3fa074160d37d7274ceff6333f43 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 21:44:44 +0200 Subject: [PATCH 6/8] extract panel contents into reusable content/ components --- shell/modules/Cpu.qml | 230 +----------- shell/modules/Disk.qml | 90 +---- shell/modules/Memory.qml | 284 +-------------- shell/modules/Mpris.qml | 359 +----------------- shell/modules/OverviewBackdrop.qml | 107 +----- shell/modules/Temperature.qml | 257 +------------ shell/modules/Volume.qml | 280 +------------- shell/modules/content/CpuContent.qml | 234 ++++++++++++ shell/modules/content/DiskContent.qml | 96 +++++ shell/modules/content/HexWaveBackground.qml | 115 ++++++ shell/modules/content/MemoryContent.qml | 287 +++++++++++++++ shell/modules/content/MprisContent.qml | 361 +++++++++++++++++++ shell/modules/content/TemperatureContent.qml | 265 ++++++++++++++ shell/modules/content/VolumeContent.qml | 292 +++++++++++++++ shell/modules/content/qmldir | 8 + 15 files changed, 1718 insertions(+), 1547 deletions(-) create mode 100644 shell/modules/content/CpuContent.qml create mode 100644 shell/modules/content/DiskContent.qml create mode 100644 shell/modules/content/HexWaveBackground.qml create mode 100644 shell/modules/content/MemoryContent.qml create mode 100644 shell/modules/content/MprisContent.qml create mode 100644 shell/modules/content/TemperatureContent.qml create mode 100644 shell/modules/content/VolumeContent.qml create mode 100644 shell/modules/content/qmldir diff --git a/shell/modules/Cpu.qml b/shell/modules/Cpu.qml index 3116195..389766e 100644 --- a/shell/modules/Cpu.qml +++ b/shell/modules/Cpu.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "." as M +import "content" as C M.BarSection { id: root @@ -46,14 +47,6 @@ M.BarSection { onTriggered: root._pinned = false } - function _loadColor(pct) { - const t = Math.max(0, Math.min(100, pct)) / 100; - const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; - const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08; - const u = t < 0.5 ? t * 2 : (t - 0.5) * 2; - return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1); - } - M.BarIcon { icon: "\uF2DB" anchors.verticalCenter: parent.verticalCenter @@ -82,221 +75,14 @@ M.BarSection { panelTitle: "CPU" contentWidth: 260 - // Per-core rows - Repeater { - model: root._cores.length - - delegate: Item { - required property int index - width: hoverPanel.contentWidth - - readonly property int _u: root._cores[index]?.usage ?? 0 - readonly property real _f: root._cores[index]?.freq_ghz ?? 0 - readonly property color _barColor: root._loadColor(_u) - readonly property bool _throttled: { - const maxF = root._coreMaxFreq[index] ?? 0; - return maxF > 0 && _f < maxF * 0.85 && _u >= 60; - } - readonly property bool _isFirstECore: { - const types = root._coreTypes; - if (!types.length || index >= types.length) - return false; - if (types[index] !== "Efficiency") - return false; - return index === 0 || types[index - 1] !== "Efficiency"; - } - - height: _isFirstECore ? 28 : 20 - - // P/E-core divider - Rectangle { - visible: parent._isFirstECore - anchors.top: parent.top - anchors.topMargin: 3 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 16 - height: 1 - color: M.Theme.base03 - } - - // Row content pinned to bottom of delegate - Item { - anchors.bottom: parent.bottom - width: parent.width - height: 20 - - Text { - id: coreLabel - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: index - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 16 - } - - Item { - id: coreBar - anchors.left: coreLabel.right - anchors.leftMargin: 6 - anchors.right: sparkline.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - - Rectangle { - width: parent.width * (parent.parent.parent._u / 100) - height: parent.height - color: parent.parent.parent._barColor - radius: 2 - Behavior on width { - enabled: root._showPanel - NumberAnimation { - duration: 150 - } - } - } - } - - // Sparkline - Canvas { - id: sparkline - anchors.right: freqLabel.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - width: 32 - height: 10 - - property var _hist: root._cores[parent.parent.index]?.history ?? [] - property color _col: parent.parent._barColor - - on_HistChanged: if (root._showPanel) - requestPaint() - on_ColChanged: if (root._showPanel) - requestPaint() - - Connections { - target: root - function on_ShowPanelChanged() { - if (root._showPanel) - sparkline.requestPaint(); - } - } - - onPaint: { - const ctx = getContext("2d"); - if (!ctx) - return; - ctx.clearRect(0, 0, width, height); - const d = _hist; - if (!d.length) - return; - const bw = width / d.length; - ctx.fillStyle = _col.toString(); - for (let i = 0; i < d.length; i++) { - const h = Math.max(1, height * d[i] / 100); - ctx.fillRect(i * bw, height - h, Math.max(1, bw - 0.5), h); - } - } - } - - Text { - id: freqLabel - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: parent.parent._f.toFixed(2) - color: parent.parent._throttled ? M.Theme.base08 : M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 34 - horizontalAlignment: Text.AlignRight - } - } - } - } - - // Process list separator - Rectangle { - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } - - Item { + C.CpuContent { width: hoverPanel.contentWidth - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "PROCESS" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 1 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "CPU" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 1 - } - } - - // Top processes by CPU - Repeater { - model: root._procs.processes - - delegate: Item { - required property var modelData - width: hoverPanel.contentWidth - height: 20 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.cmd - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: parent.width - 80 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.cpu.toFixed(1) + "%" - color: root._loadColor(modelData.cpu) - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 36 - horizontalAlignment: Text.AlignRight - } - } - } - - Item { - width: 1 - height: 4 + cores: root._cores + coreMaxFreq: root._coreMaxFreq + coreTypes: root._coreTypes + processes: root._procs.processes + accentColor: root.accentColor + active: root._showPanel } } } diff --git a/shell/modules/Disk.qml b/shell/modules/Disk.qml index 01970dc..c679da9 100644 --- a/shell/modules/Disk.qml +++ b/shell/modules/Disk.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "." as M +import "content" as C M.BarSection { id: root @@ -10,24 +11,6 @@ M.BarSection { property var _mounts: M.SystemStats.diskMounts property int _rootPct: M.SystemStats.diskRootPct - function _fmt(bytes) { - if (bytes >= 1e12) - return (bytes / 1e12).toFixed(1) + "T"; - if (bytes >= 1e9) - return Math.round(bytes / 1e9) + "G"; - if (bytes >= 1e6) - return Math.round(bytes / 1e6) + "M"; - return bytes + "B"; - } - - function _barColor(pct) { - const t = Math.max(0, Math.min(100, pct)) / 100; - const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; - const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08; - const u = t < 0.5 ? t * 2 : (t - 0.5) * 2; - return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1); - } - property bool _pinned: false readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _showPanel: _anyHover || _pinned @@ -73,73 +56,10 @@ M.BarSection { panelTitle: "Disk" contentWidth: 260 - Repeater { - model: root._mounts - - delegate: Item { - required property var modelData - width: hoverPanel.contentWidth - height: 22 - - Text { - id: mountLabel - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.target - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: 72 - } - - Item { - id: mountBar - anchors.left: mountLabel.right - anchors.leftMargin: 6 - anchors.right: sizeLabel.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - - Rectangle { - width: parent.width * (modelData.pct / 100) - height: parent.height - color: root._barColor(modelData.pct) - radius: 2 - Behavior on width { - NumberAnimation { - duration: 200 - } - } - } - } - - Text { - id: sizeLabel - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(modelData.usedBytes) + "/" + root._fmt(modelData.totalBytes) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 72 - horizontalAlignment: Text.AlignRight - } - } - } - - Item { - width: 1 - height: 4 + C.DiskContent { + width: hoverPanel.contentWidth + mounts: root._mounts + accentColor: root.accentColor } } } diff --git a/shell/modules/Memory.qml b/shell/modules/Memory.qml index 4d8bb4c..ca745a8 100644 --- a/shell/modules/Memory.qml +++ b/shell/modules/Memory.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "." as M +import "content" as C M.BarSection { id: root @@ -14,10 +15,6 @@ M.BarSection { property real cachedGb: M.SystemStats.memCachedGb property real buffersGb: M.SystemStats.memBuffersGb - function _fmt(gb) { - return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G"; - } - property bool _pinned: false readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered readonly property bool _showPanel: _anyHover || _pinned @@ -69,276 +66,17 @@ M.BarSection { panelTitle: "Memory" contentWidth: 240 - // Usage bar - Item { - width: parent.width - height: 14 - - Item { - id: memBar - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - height: 6 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 3 - } - - // Cached (base0D, behind used) - Rectangle { - width: parent.width * Math.min(1, (root.usedGb + root.cachedGb) / Math.max(root.totalGb, 0.001)) - height: parent.height - color: M.Theme.base0D - opacity: 0.4 - radius: 3 - Behavior on width { - enabled: root._showPanel - NumberAnimation { - duration: 200 - } - } - } - - // Used (accentColor, on top) - Rectangle { - width: parent.width * Math.min(1, root.usedGb / Math.max(root.totalGb, 0.001)) - height: parent.height - color: root.accentColor - radius: 3 - Behavior on width { - enabled: root._showPanel - NumberAnimation { - duration: 200 - } - } - } - } - } - - // Memory history sparkline - Canvas { - id: memSparkline - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - height: 18 - - property var _hist: M.SystemStats.memHistory - property color _col: root.accentColor - - on_HistChanged: if (root._showPanel) - requestPaint() - on_ColChanged: if (root._showPanel) - requestPaint() - - Connections { - target: root - function on_ShowPanelChanged() { - if (root._showPanel) - memSparkline.requestPaint(); - } - } - - onPaint: { - const ctx = getContext("2d"); - if (!ctx) - return; - ctx.clearRect(0, 0, width, height); - const d = _hist; - if (!d.length) - return; - const bw = width / 30; - ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.15).toString(); - ctx.fillRect(0, 0, width, height); - ctx.fillStyle = _col.toString(); - for (let i = 0; i < d.length; i++) { - const h = Math.max(1, height * d[i] / 100); - ctx.fillRect((30 - d.length + i) * bw, height - h, Math.max(1, bw - 0.5), h); - } - } - } - - // Breakdown rows - Item { - width: parent.width - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "Used" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(root.usedGb) - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } - - Item { - width: parent.width - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "Cached" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(root.cachedGb) - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } - - Item { - width: parent.width - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "Available" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(root.availGb) - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } - - Item { - width: parent.width - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "Total" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._fmt(root.totalGb) - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - } - - // Process list separator - Rectangle { - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } - - Item { + C.MemoryContent { width: hoverPanel.contentWidth - height: 18 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "PROCESS" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 1 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "MEM" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 1 - } - } - - // Top processes by memory - Repeater { - model: root._procs.processes - - delegate: Item { - required property var modelData - width: hoverPanel.contentWidth - height: 20 - - Text { - id: procCmd - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.cmd - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: parent.width - 80 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.mem.toFixed(1) + "%" - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 36 - horizontalAlignment: Text.AlignRight - } - } - } - - Item { - width: 1 - height: 4 + percent: root.percent + usedGb: root.usedGb + totalGb: root.totalGb + availGb: root.availGb + cachedGb: root.cachedGb + buffersGb: root.buffersGb + processes: root._procs.processes + accentColor: root.accentColor + active: root._showPanel } } } diff --git a/shell/modules/Mpris.qml b/shell/modules/Mpris.qml index f783643..887105d 100644 --- a/shell/modules/Mpris.qml +++ b/shell/modules/Mpris.qml @@ -3,6 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Mpris import "." as M +import "content" as C M.BarSection { id: root @@ -32,14 +33,14 @@ M.BarSection { _cachedArt = _artUrl || ""; } - // Preload art while panel is hidden — ensures QML image cache has the pixels + // Preload art while panel is hidden Image { visible: false source: root._cachedArt asynchronous: true } - // Cava visualizer — 16 bars, raw output mode + // Cava visualizer - 16 bars, raw output mode property var _cavaBars: Array(16).fill(0) property bool _cavaActive: false @@ -122,348 +123,18 @@ M.BarSection { panelTitle: "Now Playing" contentWidth: 280 - // Album art — always 1:1, crossfades on session switch - Item { - width: parent.width - height: width - clip: true - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - } - - // Outgoing art — snaps to current opacity, then fades out - Image { - id: _artImgPrev - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - asynchronous: true - opacity: 0 - - NumberAnimation { - id: _prevFadeOut - target: _artImgPrev - property: "opacity" - to: 0 - duration: 300 - easing.type: Easing.InOutCubic - } - } - - // Incoming art — fades in once loaded - Image { - id: _artImg - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - asynchronous: true - opacity: 0 - - property bool _hasArt: false - - onStatusChanged: { - if (status === Image.Ready && source !== "") { - _hasArt = true; - _artFadeIn.start(); - _prevFadeOut.start(); - } else if (status === Image.Error) { - _hasArt = false; - } - } - - NumberAnimation { - id: _artFadeIn - target: _artImg - property: "opacity" - to: 1 - duration: 300 - easing.type: Easing.InOutCubic - } - - Connections { - target: root - function on_CachedArtChanged() { - if (!root._cachedArt) { - _artFadeIn.stop(); - _prevFadeOut.stop(); - _artImg._hasArt = false; - _artImg.opacity = 0; - _artImgPrev.opacity = 0; - _artImg.source = ""; - } else if (root._cachedArt !== _artImg.source) { - _prevFadeOut.stop(); - _artFadeIn.stop(); - _artImgPrev.source = _artImg.source; - _artImgPrev.opacity = _artImg.opacity; - _artImg.opacity = 0; - _artImg.source = root._cachedArt; - } - } - } - } - - // Visualizer bars - Row { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: parent.height * 0.6 - spacing: 2 - visible: root.playing - opacity: 0.5 - - Repeater { - model: 16 - Rectangle { - required property int index - width: (parent.width - 15 * parent.spacing) / 16 - height: parent.height * (root._cavaBars[index] ?? 0) - anchors.bottom: parent.bottom - color: root.accentColor - radius: 1 - - Behavior on height { - NumberAnimation { - duration: 50 - } - } - } - } - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 40 - visible: _artImg._hasArt - gradient: Gradient { - GradientStop { - position: 0 - color: "transparent" - } - GradientStop { - position: 1 - color: M.Theme.base01 - } - } - } - - Text { - anchors.centerIn: parent - text: "\uF001" - color: M.Theme.base04 - font.pixelSize: 28 - font.family: M.Theme.iconFontFamily - visible: !_artImg._hasArt - } - } - - // Track info - Item { - width: parent.width - height: titleCol.implicitHeight + 8 - - Column { - id: titleCol - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 2 - - Text { - width: parent.width - text: root.player?.trackTitle || "No track" - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize + 1 - font.family: M.Theme.fontFamily - font.bold: true - elide: Text.ElideRight - } - - Text { - width: parent.width - text: { - const p = root.player; - if (!p) - return ""; - const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); - return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); - } - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - visible: text !== "" - } - } - } - - // Progress - Item { - width: parent.width - height: 20 - - readonly property real pos: root.player?.position ?? 0 - readonly property real dur: root.player?.length ?? 0 - readonly property real frac: dur > 0 ? pos / dur : 0 - - function _fmtTime(ms) { - const s = Math.floor(ms / 1000); - const m = Math.floor(s / 60); - return m + ":" + String(s % 60).padStart(2, "0"); - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: parent._fmtTime(parent.pos) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: parent._fmtTime(parent.dur) - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - } - - Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 80 - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, parent.parent.frac)) - height: parent.height - color: root.accentColor - radius: 2 - } - } - } - - // Transport controls - Item { - width: parent.width - height: 36 - - Row { - anchors.centerIn: parent - spacing: 24 - - Text { - text: "\uF048" - color: root.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize + 4 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - TapHandler { - cursorShape: Qt.PointingHandCursor - enabled: root.player?.canGoPrevious ?? false - onTapped: root.player.previous() - } - } - - Text { - text: root.playing ? "\uF04C" : "\uF04B" - color: root.accentColor - font.pixelSize: M.Theme.fontSize + 8 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - TapHandler { - cursorShape: Qt.PointingHandCursor - onTapped: root.player?.togglePlaying() - } - } - - Text { - text: "\uF051" - color: root.player?.canGoNext ? M.Theme.base05 : M.Theme.base03 - font.pixelSize: M.Theme.fontSize + 4 - font.family: M.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - TapHandler { - cursorShape: Qt.PointingHandCursor - enabled: root.player?.canGoNext ?? false - onTapped: root.player.next() - } - } - } - } - - // Player switcher - Item { - width: parent.width - height: _players.length > 1 ? 28 : 0 - visible: _players.length > 1 - - Flickable { - id: _switcher - anchors.centerIn: parent - width: Math.min(_playerRow.implicitWidth, parent.width - 16) - height: 22 - contentWidth: _playerRow.implicitWidth - clip: true - - Row { - id: _playerRow - height: 22 - spacing: 6 - - Repeater { - model: root._players - - delegate: Rectangle { - required property var modelData - required property int index - - readonly property bool _active: index === root._playerIdx - - width: _pLabel.implicitWidth + 12 - height: 18 - radius: 9 - color: _active ? M.Theme.base02 : (pHover.hovered ? M.Theme.base02 : "transparent") - border.color: _active ? root.accentColor : M.Theme.base03 - border.width: _active ? 1 : 0 - anchors.verticalCenter: parent.verticalCenter - - Text { - id: _pLabel - anchors.centerIn: parent - text: modelData.identity ?? "Player" - color: _active ? root.accentColor : M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - font.bold: _active - } - - HoverHandler { - id: pHover - cursorShape: Qt.PointingHandCursor - } - - TapHandler { - onTapped: { - root._playerIdx = index; - hoverPanel.keepOpen(400); - } - } - } - } - } + C.MprisContent { + width: hoverPanel.contentWidth + player: root.player + players: root._players + playing: root.playing + accentColor: root.accentColor + cachedArt: root._cachedArt + cavaBars: root._cavaBars + playerIdx: root._playerIdx + onPlayerSwitched: idx => { + root._playerIdx = idx; + hoverPanel.keepOpen(400); } } } diff --git a/shell/modules/OverviewBackdrop.qml b/shell/modules/OverviewBackdrop.qml index 6abb7e8..0e0b050 100644 --- a/shell/modules/OverviewBackdrop.qml +++ b/shell/modules/OverviewBackdrop.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Wayland import "." as M +import "content" as C PanelWindow { id: root @@ -20,110 +21,8 @@ PanelWindow { anchors.right: true anchors.bottom: true - Rectangle { + C.HexWaveBackground { anchors.fill: parent - color: M.Theme.base01 - } - - ShaderEffect { - id: fx - anchors.fill: parent - fragmentShader: Quickshell.shellPath("modules/hex_wave.frag.qsb") - - property real uSize: 50.0 - property real uWavePhase: -200 - property real uBreath: 0 - property real uGlitch: 0 - property real uGlitchSeed: 0.0 - property vector4d uResolution: Qt.vector4d(width, height, 0, 0) - property color uC0: M.Theme.base0C - property color uC1: M.Theme.base0E - property color uC2: M.Theme.base09 - - Connections { - target: M.NiriIpc - function onOverviewOpenChanged() { - if (!M.NiriIpc.overviewOpen) { - fx.uWavePhase = -200; - fx.uBreath = 0; - } - } - } - - // Wave animation: 6s sweep + 8s pause, only while overview is open - SequentialAnimation on uWavePhase { - loops: Animation.Infinite - running: M.NiriIpc.overviewOpen && !M.Theme.reducedMotion - NumberAnimation { - from: -200 - to: fx.width + 200 - duration: 6000 - easing.type: Easing.InOutSine - } - PauseAnimation { - duration: 8000 - } - } - - // Breathing pulse while overview is open - SequentialAnimation on uBreath { - loops: Animation.Infinite - running: M.NiriIpc.overviewOpen && !M.Theme.reducedMotion - NumberAnimation { - from: 0 - to: 1 - duration: 2500 - easing.type: Easing.InOutSine - } - NumberAnimation { - from: 1 - to: 0 - duration: 2500 - easing.type: Easing.InOutSine - } - } - - // Random subtle glitches — fire every 12–37s, total ~250ms each - Timer { - interval: 20000 - repeat: true - running: !M.Theme.reducedMotion - onTriggered: { - interval = 12000 + Math.floor(Math.random() * 25000); - fx.uGlitchSeed = Math.random() * 1000.0; - _glitchAnim.start(); - } - } - - SequentialAnimation { - id: _glitchAnim - NumberAnimation { - target: fx - property: "uGlitch" - to: 0.7 - duration: 50 - easing.type: Easing.OutQuad - } - NumberAnimation { - target: fx - property: "uGlitch" - to: 0.15 - duration: 50 - } - NumberAnimation { - target: fx - property: "uGlitch" - to: 0.85 - duration: 60 - easing.type: Easing.OutQuad - } - NumberAnimation { - target: fx - property: "uGlitch" - to: 0 - duration: 100 - easing.type: Easing.InQuad - } - } + running: M.NiriIpc.overviewOpen } } diff --git a/shell/modules/Temperature.qml b/shell/modules/Temperature.qml index d7aa47c..dd69e3a 100644 --- a/shell/modules/Temperature.qml +++ b/shell/modules/Temperature.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "." as M +import "content" as C M.BarSection { id: root @@ -45,15 +46,6 @@ M.BarSection { onTriggered: root._pinned = false } - // Returns a color interpolated green→yellow→red for a given celsius value - function _tempColor(celsius) { - const t = Math.max(0, Math.min(100, celsius)) / 100; - const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; - const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08; - const u = t < 0.5 ? t * 2 : (t - 0.5) * 2; - return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1); - } - M.BarIcon { icon: "\uF2C9" color: root._stateColor @@ -84,243 +76,16 @@ M.BarSection { panelTitle: "Temperature" contentWidth: 220 - // Header — current temp - Item { - width: parent.width - height: 28 - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._temp + "\u00B0C" - color: root._stateColor - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - font.bold: true - width: _tempSizer.implicitWidth - horizontalAlignment: Text.AlignRight - - Text { - id: _tempSizer - visible: false - text: "100\u00B0C" - font: parent.font - } - } - } - - // Gauge bar (0–100°C), with warm/hot threshold markers - Item { - width: parent.width - height: 16 - - Item { - id: _gaugeBar - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - height: 6 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 3 - } - - Rectangle { - width: parent.width * Math.min(1, root._temp / 100) - height: parent.height - color: root._stateColor - radius: 3 - Behavior on width { - enabled: root._showPanel - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - } - } - - // Warm threshold marker - Rectangle { - x: parent.width * (root._warm / 100) - 1 - width: 1 - height: parent.height + 4 - anchors.verticalCenter: parent.verticalCenter - color: M.Theme.base0A - opacity: 0.6 - } - - // Hot threshold marker - Rectangle { - x: parent.width * (root._hot / 100) - 1 - width: 1 - height: parent.height + 4 - anchors.verticalCenter: parent.verticalCenter - color: M.Theme.base08 - opacity: 0.6 - } - } - } - - // History sparkline (~10 min @ 4s per sample) - Canvas { - id: _sparkline - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - height: 40 - - property var _hist: M.SystemStats.tempHistory - property color _col: root._stateColor - - on_HistChanged: if (root._showPanel) - requestPaint() - on_ColChanged: if (root._showPanel) - requestPaint() - - Connections { - target: root - function on_ShowPanelChanged() { - if (root._showPanel) - _sparkline.requestPaint(); - } - } - - onPaint: { - const ctx = getContext("2d"); - if (!ctx) - return; - ctx.clearRect(0, 0, width, height); - const d = _hist; - if (!d.length) - return; - - const maxSamples = 150; - const bw = width / maxSamples; - - // Background tint - ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.08).toString(); - ctx.fillRect(0, 0, width, height); - - // Warm threshold line - const warmY = height - height * (root._warm / 100); - ctx.strokeStyle = M.Theme.base0A.toString(); - ctx.globalAlpha = 0.3; - ctx.lineWidth = 1; - ctx.setLineDash([3, 3]); - ctx.beginPath(); - ctx.moveTo(0, warmY); - ctx.lineTo(width, warmY); - ctx.stroke(); - - // Hot threshold line - const hotY = height - height * (root._hot / 100); - ctx.strokeStyle = M.Theme.base08.toString(); - ctx.beginPath(); - ctx.moveTo(0, hotY); - ctx.lineTo(width, hotY); - ctx.stroke(); - - ctx.setLineDash([]); - ctx.globalAlpha = 1.0; - - // Bars - const offset = maxSamples - d.length; - for (let i = 0; i < d.length; i++) { - const barH = Math.max(1, height * d[i] / 100); - const barColor = d[i] > root._hot ? M.Theme.base08 : d[i] > root._warm ? M.Theme.base0A : _col; - ctx.fillStyle = barColor.toString(); - ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH); - } - } - } - - // Threshold labels - Item { - width: parent.width - height: 16 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "warm " + root._warm + "\u00B0 hot " + root._hot + "\u00B0" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - font.letterSpacing: 0.5 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "10 min" - color: M.Theme.base03 - font.pixelSize: M.Theme.fontSize - 3 - font.family: M.Theme.fontFamily - } - } - - // Per-device breakdown - Rectangle { - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - visible: M.SystemStats.tempDevices.length > 0 - } - - Repeater { - model: M.SystemStats.tempDevices - delegate: Item { - required property var modelData - width: hoverPanel.contentWidth - height: 22 - - readonly property bool _isActive: root._deviceFilter === modelData.name - - Rectangle { - anchors.fill: parent - anchors.leftMargin: 8 - anchors.rightMargin: 8 - color: _isActive ? Qt.rgba(root.accentColor.r, root.accentColor.g, root.accentColor.b, 0.12) : "transparent" - radius: 3 - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.name - color: _isActive ? root.accentColor : M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: parent.width - 80 - } - - Text { - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.celsius + "\u00B0C" - color: root._tempColor(modelData.celsius) - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - font.bold: _isActive - } - } - } - - Item { - width: 1 - height: 4 + C.TemperatureContent { + width: hoverPanel.contentWidth + temp: root._temp + warm: root._warm + hot: root._hot + history: M.SystemStats.tempHistory + devices: M.SystemStats.tempDevices + accentColor: root.accentColor + deviceFilter: root._deviceFilter + active: root._showPanel } } } diff --git a/shell/modules/Volume.qml b/shell/modules/Volume.qml index c4d5d33..97a93eb 100644 --- a/shell/modules/Volume.qml +++ b/shell/modules/Volume.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Services.Pipewire import "." as M +import "content" as C M.BarSection { id: root @@ -113,279 +114,12 @@ M.BarSection { panelTitle: "Sound" contentWidth: 220 - // Slider row - Item { - width: parent.width - height: 36 - - Text { - id: muteIcon - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: root._volumeIcon - color: root._volumeColor - font.pixelSize: M.Theme.fontSize + 2 - font.family: M.Theme.iconFontFamily - - TapHandler { - cursorShape: Qt.PointingHandCursor - onTapped: if (root.sink?.audio) - root.sink.audio.muted = !root.sink.audio.muted - } - } - - Item { - id: slider - anchors.left: muteIcon.right - anchors.leftMargin: 8 - anchors.right: volLabel.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - height: 6 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 3 - } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, root.volume)) - height: parent.height - color: root._volumeColor - radius: 3 - Behavior on width { - NumberAnimation { - duration: 80 - } - } - } - - MouseArea { - anchors.fill: parent - anchors.margins: -6 - cursorShape: Qt.PointingHandCursor - onPressed: mouse => _setVol(mouse) - onPositionChanged: mouse => { - if (pressed) - _setVol(mouse); - } - function _setVol(mouse) { - if (!root.sink?.audio) - return; - root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width)); - } - } - } - - Text { - id: volLabel - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: Math.round(root.volume * 100) + "%" - color: root.muted ? M.Theme.base04 : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - width: 30 - } - } - - // Device + stream list - Column { - id: deviceList - width: parent.width - - // Output devices — only shown when more than one exists - Column { - visible: root._sinkList.length > 1 - width: parent.width - - Rectangle { - width: parent.width - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } - - Text { - width: parent.width - height: 24 - verticalAlignment: Text.AlignVCenter - leftPadding: 12 - text: "Output Devices" - color: root.accentColor - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - } - - Repeater { - model: root._sinkList - - delegate: Item { - required property var modelData - - width: deviceList.width - height: 28 - - readonly property bool _active: modelData === root.sink - - Rectangle { - anchors.fill: parent - anchors.leftMargin: 4 - anchors.rightMargin: 4 - color: deviceHover.hovered ? M.Theme.base02 : "transparent" - radius: M.Theme.radius - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: modelData.description || modelData.name || "Unknown" - color: parent._active ? root.accentColor : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.fontFamily - font.bold: parent._active - elide: Text.ElideRight - } - - HoverHandler { - id: deviceHover - cursorShape: Qt.PointingHandCursor - } - - TapHandler { - onTapped: Pipewire.preferredDefaultAudioSink = modelData - } - } - } - } - - // Streams section - Rectangle { - visible: root._streamList.length > 0 - width: parent.width - 16 - height: visible ? 1 : 0 - anchors.horizontalCenter: parent.horizontalCenter - color: M.Theme.base03 - } - - Text { - visible: root._streamList.length > 0 - width: parent.width - height: visible ? 24 : 0 - verticalAlignment: Text.AlignVCenter - leftPadding: 12 - text: "Applications" - color: root.accentColor - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - } - - Repeater { - model: root._streamList - - delegate: Item { - id: streamEntry - required property var modelData - - width: deviceList.width - height: 32 - - readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" - readonly property real _vol: modelData.audio?.volume ?? 0 - readonly property bool _muted: modelData.audio?.muted ?? false - - Text { - id: streamIcon - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: streamEntry._muted ? "\uF026" : "\uF028" - color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - font.family: M.Theme.iconFontFamily - - TapHandler { - cursorShape: Qt.PointingHandCursor - onTapped: if (streamEntry.modelData.audio) - streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted - } - } - - Text { - id: streamName - anchors.left: streamIcon.right - anchors.leftMargin: 6 - anchors.verticalCenter: parent.verticalCenter - text: streamEntry._appName - color: M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - elide: Text.ElideRight - width: 70 - } - - Item { - id: streamSlider - anchors.left: streamName.right - anchors.leftMargin: 6 - anchors.right: streamVol.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - height: 4 - - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 - } - Rectangle { - width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) - height: parent.height - color: streamEntry._muted ? M.Theme.base04 : root.accentColor - radius: 2 - } - - MouseArea { - anchors.fill: parent - anchors.margins: -6 - cursorShape: Qt.PointingHandCursor - onPressed: mouse => _set(mouse) - onPositionChanged: mouse => { - if (pressed) - _set(mouse); - } - function _set(mouse) { - if (!streamEntry.modelData.audio) - return; - streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width)); - } - } - } - - Text { - id: streamVol - anchors.right: parent.right - anchors.rightMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: Math.round(streamEntry._vol * 100) + "%" - color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 - font.pixelSize: M.Theme.fontSize - 1 - font.family: M.Theme.fontFamily - width: 28 - } - } - } - - // Bottom padding - Item { - width: 1 - height: 4 - } + C.VolumeContent { + width: hoverPanel.contentWidth + sink: root.sink + sinkList: root._sinkList + streamList: root._streamList + accentColor: root.accentColor } } } diff --git a/shell/modules/content/CpuContent.qml b/shell/modules/content/CpuContent.qml new file mode 100644 index 0000000..9411538 --- /dev/null +++ b/shell/modules/content/CpuContent.qml @@ -0,0 +1,234 @@ +import QtQuick +import ".." as M + +Column { + id: root + + required property var cores + required property var coreMaxFreq + required property var coreTypes + required property var processes + required property color accentColor + + property bool active: true + + function _loadColor(pct) { + const t = Math.max(0, Math.min(100, pct)) / 100; + const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; + const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08; + const u = t < 0.5 ? t * 2 : (t - 0.5) * 2; + return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1); + } + + // Per-core rows + Repeater { + model: root.cores.length + + delegate: Item { + required property int index + width: root.width + + readonly property int _u: root.cores[index]?.usage ?? 0 + readonly property real _f: root.cores[index]?.freq_ghz ?? 0 + readonly property color _barColor: root._loadColor(_u) + readonly property bool _throttled: { + const maxF = root.coreMaxFreq[index] ?? 0; + return maxF > 0 && _f < maxF * 0.85 && _u >= 60; + } + readonly property bool _isFirstECore: { + const types = root.coreTypes; + if (!types.length || index >= types.length) + return false; + if (types[index] !== "Efficiency") + return false; + return index === 0 || types[index - 1] !== "Efficiency"; + } + + height: _isFirstECore ? 28 : 20 + + // P/E-core divider + Rectangle { + visible: parent._isFirstECore + anchors.top: parent.top + anchors.topMargin: 3 + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 16 + height: 1 + color: M.Theme.base03 + } + + // Row content pinned to bottom of delegate + Item { + anchors.bottom: parent.bottom + width: parent.width + height: 20 + + Text { + id: coreLabel + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: index + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 16 + } + + Item { + id: coreBar + anchors.left: coreLabel.right + anchors.leftMargin: 6 + anchors.right: sparkline.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + + Rectangle { + width: parent.width * (parent.parent.parent._u / 100) + height: parent.height + color: parent.parent.parent._barColor + radius: 2 + Behavior on width { + enabled: root.active + NumberAnimation { + duration: 150 + } + } + } + } + + // Sparkline + Canvas { + id: sparkline + anchors.right: freqLabel.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 32 + height: 10 + + property var _hist: root.cores[parent.parent.index]?.history ?? [] + property color _col: parent.parent._barColor + + on_HistChanged: if (root.active) + requestPaint() + on_ColChanged: if (root.active) + requestPaint() + + onVisibleChanged: if (visible) + requestPaint() + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.clearRect(0, 0, width, height); + const d = _hist; + if (!d.length) + return; + const bw = width / d.length; + ctx.fillStyle = _col.toString(); + for (let i = 0; i < d.length; i++) { + const h = Math.max(1, height * d[i] / 100); + ctx.fillRect(i * bw, height - h, Math.max(1, bw - 0.5), h); + } + } + } + + Text { + id: freqLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent.parent._f.toFixed(2) + color: parent.parent._throttled ? M.Theme.base08 : M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 34 + horizontalAlignment: Text.AlignRight + } + } + } + } + + // Process list separator + Rectangle { + width: root.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "PROCESS" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 1 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "CPU" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 1 + } + } + + // Top processes by CPU + Repeater { + model: root.processes + + delegate: Item { + required property var modelData + width: root.width + height: 20 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.cmd + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: parent.width - 80 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.cpu.toFixed(1) + "%" + color: root._loadColor(modelData.cpu) + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 36 + horizontalAlignment: Text.AlignRight + } + } + } + + Item { + width: 1 + height: 4 + } +} diff --git a/shell/modules/content/DiskContent.qml b/shell/modules/content/DiskContent.qml new file mode 100644 index 0000000..4fe65f8 --- /dev/null +++ b/shell/modules/content/DiskContent.qml @@ -0,0 +1,96 @@ +import QtQuick +import ".." as M + +Column { + id: root + + required property var mounts + required property color accentColor + + function _fmt(bytes) { + if (bytes >= 1e12) + return (bytes / 1e12).toFixed(1) + "T"; + if (bytes >= 1e9) + return Math.round(bytes / 1e9) + "G"; + if (bytes >= 1e6) + return Math.round(bytes / 1e6) + "M"; + return bytes + "B"; + } + + function _barColor(pct) { + const t = Math.max(0, Math.min(100, pct)) / 100; + const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; + const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08; + const u = t < 0.5 ? t * 2 : (t - 0.5) * 2; + return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1); + } + + Repeater { + model: root.mounts + + delegate: Item { + required property var modelData + width: root.width + height: 22 + + Text { + id: mountLabel + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.target + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: 72 + } + + Item { + id: mountBar + anchors.left: mountLabel.right + anchors.leftMargin: 6 + anchors.right: sizeLabel.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + + Rectangle { + width: parent.width * (modelData.pct / 100) + height: parent.height + color: root._barColor(modelData.pct) + radius: 2 + Behavior on width { + NumberAnimation { + duration: 200 + } + } + } + } + + Text { + id: sizeLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(modelData.usedBytes) + "/" + root._fmt(modelData.totalBytes) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 72 + horizontalAlignment: Text.AlignRight + } + } + } + + Item { + width: 1 + height: 4 + } +} diff --git a/shell/modules/content/HexWaveBackground.qml b/shell/modules/content/HexWaveBackground.qml new file mode 100644 index 0000000..b8ccaed --- /dev/null +++ b/shell/modules/content/HexWaveBackground.qml @@ -0,0 +1,115 @@ +import QtQuick +import Quickshell +import ".." as M + +Item { + id: root + + property bool running: false + property bool reducedMotion: M.Theme.reducedMotion + + Rectangle { + anchors.fill: parent + color: M.Theme.base01 + } + + ShaderEffect { + id: fx + anchors.fill: parent + fragmentShader: Quickshell.shellPath("modules/hex_wave.frag.qsb") + + property real uSize: 50.0 + property real uWavePhase: -200 + property real uBreath: 0 + property real uGlitch: 0 + property real uGlitchSeed: 0.0 + property vector4d uResolution: Qt.vector4d(width, height, 0, 0) + property color uC0: M.Theme.base0C + property color uC1: M.Theme.base0E + property color uC2: M.Theme.base09 + + // Reset animations when stopped + onRunningChanged: { + if (!root.running) { + fx.uWavePhase = -200; + fx.uBreath = 0; + } + } + + // Wave animation: 6s sweep + 8s pause + SequentialAnimation on uWavePhase { + loops: Animation.Infinite + running: root.running && !root.reducedMotion + NumberAnimation { + from: -200 + to: fx.width + 200 + duration: 6000 + easing.type: Easing.InOutSine + } + PauseAnimation { + duration: 8000 + } + } + + // Breathing pulse + SequentialAnimation on uBreath { + loops: Animation.Infinite + running: root.running && !root.reducedMotion + NumberAnimation { + from: 0 + to: 1 + duration: 2500 + easing.type: Easing.InOutSine + } + NumberAnimation { + from: 1 + to: 0 + duration: 2500 + easing.type: Easing.InOutSine + } + } + + // Random subtle glitches - fire every 12-37s + Timer { + interval: 20000 + repeat: true + running: !root.reducedMotion + onTriggered: { + interval = 12000 + Math.floor(Math.random() * 25000); + fx.uGlitchSeed = Math.random() * 1000.0; + _glitchAnim.start(); + } + } + + SequentialAnimation { + id: _glitchAnim + NumberAnimation { + target: fx + property: "uGlitch" + to: 0.7 + duration: 50 + easing.type: Easing.OutQuad + } + NumberAnimation { + target: fx + property: "uGlitch" + to: 0.15 + duration: 50 + } + NumberAnimation { + target: fx + property: "uGlitch" + to: 0.85 + duration: 60 + easing.type: Easing.OutQuad + } + NumberAnimation { + target: fx + property: "uGlitch" + to: 0 + duration: 100 + easing.type: Easing.InQuad + } + } + } +} diff --git a/shell/modules/content/MemoryContent.qml b/shell/modules/content/MemoryContent.qml new file mode 100644 index 0000000..3ca568e --- /dev/null +++ b/shell/modules/content/MemoryContent.qml @@ -0,0 +1,287 @@ +import QtQuick +import ".." as M + +Column { + id: root + + required property int percent + required property real usedGb + required property real totalGb + required property real availGb + required property real cachedGb + required property real buffersGb + required property var processes + required property color accentColor + + property bool active: true + + function _fmt(gb) { + return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G"; + } + + // Usage bar + Item { + width: root.width + height: 14 + + Item { + id: memBar + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + height: 6 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 3 + } + + // Cached (base0D, behind used) + Rectangle { + width: parent.width * Math.min(1, (root.usedGb + root.cachedGb) / Math.max(root.totalGb, 0.001)) + height: parent.height + color: M.Theme.base0D + opacity: 0.4 + radius: 3 + Behavior on width { + enabled: root.active + NumberAnimation { + duration: 200 + } + } + } + + // Used (accentColor, on top) + Rectangle { + width: parent.width * Math.min(1, root.usedGb / Math.max(root.totalGb, 0.001)) + height: parent.height + color: root.accentColor + radius: 3 + Behavior on width { + enabled: root.active + NumberAnimation { + duration: 200 + } + } + } + } + } + + // Memory history sparkline + Canvas { + id: memSparkline + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + height: 18 + + property var _hist: M.SystemStats.memHistory + property color _col: root.accentColor + + on_HistChanged: if (root.active) + requestPaint() + on_ColChanged: if (root.active) + requestPaint() + + onVisibleChanged: if (visible) + requestPaint() + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.clearRect(0, 0, width, height); + const d = _hist; + if (!d.length) + return; + const bw = width / 30; + ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.15).toString(); + ctx.fillRect(0, 0, width, height); + ctx.fillStyle = _col.toString(); + for (let i = 0; i < d.length; i++) { + const h = Math.max(1, height * d[i] / 100); + ctx.fillRect((30 - d.length + i) * bw, height - h, Math.max(1, bw - 0.5), h); + } + } + } + + // Breakdown rows + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "Used" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(root.usedGb) + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "Cached" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(root.cachedGb) + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "Available" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(root.availGb) + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "Total" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root._fmt(root.totalGb) + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + } + + // Process list separator + Rectangle { + width: root.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Item { + width: root.width + height: 18 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "PROCESS" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 1 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "MEM" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 1 + } + } + + // Top processes by memory + Repeater { + model: root.processes + + delegate: Item { + required property var modelData + width: root.width + height: 20 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.cmd + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: parent.width - 80 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.mem.toFixed(1) + "%" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 36 + horizontalAlignment: Text.AlignRight + } + } + } + + Item { + width: 1 + height: 4 + } +} diff --git a/shell/modules/content/MprisContent.qml b/shell/modules/content/MprisContent.qml new file mode 100644 index 0000000..c916ae1 --- /dev/null +++ b/shell/modules/content/MprisContent.qml @@ -0,0 +1,361 @@ +import QtQuick +import Quickshell.Services.Mpris +import ".." as M + +Column { + id: root + + required property var player + required property var players + required property bool playing + required property color accentColor + property string cachedArt: "" + property var cavaBars: Array(16).fill(0) + property int playerIdx: 0 + + signal playerSwitched(int idx) + + // Album art - always 1:1, crossfades on session switch + Item { + width: root.width + height: width + clip: true + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + } + + // Outgoing art - snaps to current opacity, then fades out + Image { + id: _artImgPrev + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + asynchronous: true + opacity: 0 + + NumberAnimation { + id: _prevFadeOut + target: _artImgPrev + property: "opacity" + to: 0 + duration: 300 + easing.type: Easing.InOutCubic + } + } + + // Incoming art - fades in once loaded + Image { + id: _artImg + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + asynchronous: true + opacity: 0 + + property bool _hasArt: false + + onStatusChanged: { + if (status === Image.Ready && source !== "") { + _hasArt = true; + _artFadeIn.start(); + _prevFadeOut.start(); + } else if (status === Image.Error) { + _hasArt = false; + } + } + + NumberAnimation { + id: _artFadeIn + target: _artImg + property: "opacity" + to: 1 + duration: 300 + easing.type: Easing.InOutCubic + } + + Connections { + target: root + function onCachedArtChanged() { + if (!root.cachedArt) { + _artFadeIn.stop(); + _prevFadeOut.stop(); + _artImg._hasArt = false; + _artImg.opacity = 0; + _artImgPrev.opacity = 0; + _artImg.source = ""; + } else if (root.cachedArt !== _artImg.source) { + _prevFadeOut.stop(); + _artFadeIn.stop(); + _artImgPrev.source = _artImg.source; + _artImgPrev.opacity = _artImg.opacity; + _artImg.opacity = 0; + _artImg.source = root.cachedArt; + } + } + } + } + + // Visualizer bars + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height * 0.6 + spacing: 2 + visible: root.playing + opacity: 0.5 + + Repeater { + model: 16 + Rectangle { + required property int index + width: (parent.width - 15 * parent.spacing) / 16 + height: parent.height * (root.cavaBars[index] ?? 0) + anchors.bottom: parent.bottom + color: root.accentColor + radius: 1 + + Behavior on height { + NumberAnimation { + duration: 50 + } + } + } + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 40 + visible: _artImg._hasArt + gradient: Gradient { + GradientStop { + position: 0 + color: "transparent" + } + GradientStop { + position: 1 + color: M.Theme.base01 + } + } + } + + Text { + anchors.centerIn: parent + text: "\uF001" + color: M.Theme.base04 + font.pixelSize: 28 + font.family: M.Theme.iconFontFamily + visible: !_artImg._hasArt + } + } + + // Track info + Item { + width: root.width + height: titleCol.implicitHeight + 8 + + Column { + id: titleCol + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 2 + + Text { + width: parent.width + text: root.player?.trackTitle || "No track" + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize + 1 + font.family: M.Theme.fontFamily + font.bold: true + elide: Text.ElideRight + } + + Text { + width: parent.width + text: { + const p = root.player; + if (!p) + return ""; + const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); + return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); + } + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + visible: text !== "" + } + } + } + + // Progress + Item { + width: root.width + height: 20 + + readonly property real pos: root.player?.position ?? 0 + readonly property real dur: root.player?.length ?? 0 + readonly property real frac: dur > 0 ? pos / dur : 0 + + function _fmtTime(ms) { + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + return m + ":" + String(s % 60).padStart(2, "0"); + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent._fmtTime(parent.pos) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent._fmtTime(parent.dur) + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + } + + Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 80 + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + Rectangle { + width: parent.width * Math.min(1, Math.max(0, parent.parent.frac)) + height: parent.height + color: root.accentColor + radius: 2 + } + } + } + + // Transport controls + Item { + width: root.width + height: 36 + + Row { + anchors.centerIn: parent + spacing: 24 + + Text { + text: "\uF048" + color: root.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize + 4 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + enabled: root.player?.canGoPrevious ?? false + onTapped: root.player.previous() + } + } + + Text { + text: root.playing ? "\uF04C" : "\uF04B" + color: root.accentColor + font.pixelSize: M.Theme.fontSize + 8 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: root.player?.togglePlaying() + } + } + + Text { + text: "\uF051" + color: root.player?.canGoNext ? M.Theme.base05 : M.Theme.base03 + font.pixelSize: M.Theme.fontSize + 4 + font.family: M.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + TapHandler { + cursorShape: Qt.PointingHandCursor + enabled: root.player?.canGoNext ?? false + onTapped: root.player.next() + } + } + } + } + + // Player switcher + Item { + width: root.width + height: root.players.length > 1 ? 28 : 0 + visible: root.players.length > 1 + + Flickable { + id: _switcher + anchors.centerIn: parent + width: Math.min(_playerRow.implicitWidth, parent.width - 16) + height: 22 + contentWidth: _playerRow.implicitWidth + clip: true + + Row { + id: _playerRow + height: 22 + spacing: 6 + + Repeater { + model: root.players + + delegate: Rectangle { + required property var modelData + required property int index + + readonly property bool _active: index === root.playerIdx + + width: _pLabel.implicitWidth + 12 + height: 18 + radius: 9 + color: _active ? M.Theme.base02 : (pHover.hovered ? M.Theme.base02 : "transparent") + border.color: _active ? root.accentColor : M.Theme.base03 + border.width: _active ? 1 : 0 + anchors.verticalCenter: parent.verticalCenter + + Text { + id: _pLabel + anchors.centerIn: parent + text: modelData.identity ?? "Player" + color: _active ? root.accentColor : M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + font.bold: _active + } + + HoverHandler { + id: pHover + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: { + root.playerSwitched(index); + } + } + } + } + } + } + } +} diff --git a/shell/modules/content/TemperatureContent.qml b/shell/modules/content/TemperatureContent.qml new file mode 100644 index 0000000..fe9ce70 --- /dev/null +++ b/shell/modules/content/TemperatureContent.qml @@ -0,0 +1,265 @@ +import QtQuick +import ".." as M + +Column { + id: root + + required property int temp + required property int warm + required property int hot + required property var history + required property var devices + required property color accentColor + + property bool active: true + property string deviceFilter: "" + + property color stateColor: temp > hot ? M.Theme.base08 : temp > warm ? M.Theme.base0A : root.accentColor + Behavior on stateColor { + ColorAnimation { + duration: 300 + } + } + + function _tempColor(celsius) { + const t = Math.max(0, Math.min(100, celsius)) / 100; + const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A; + const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08; + const u = t < 0.5 ? t * 2 : (t - 0.5) * 2; + return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1); + } + + // Header - current temp + Item { + width: root.width + height: 28 + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root.temp + "\u00B0C" + color: root.stateColor + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + font.bold: true + width: _tempSizer.implicitWidth + horizontalAlignment: Text.AlignRight + + Text { + id: _tempSizer + visible: false + text: "100\u00B0C" + font: parent.font + } + } + } + + // Gauge bar (0-100C), with warm/hot threshold markers + Item { + width: root.width + height: 16 + + Item { + id: _gaugeBar + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + height: 6 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 3 + } + + Rectangle { + width: parent.width * Math.min(1, root.temp / 100) + height: parent.height + color: root.stateColor + radius: 3 + Behavior on width { + enabled: root.active + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } + } + + // Warm threshold marker + Rectangle { + x: parent.width * (root.warm / 100) - 1 + width: 1 + height: parent.height + 4 + anchors.verticalCenter: parent.verticalCenter + color: M.Theme.base0A + opacity: 0.6 + } + + // Hot threshold marker + Rectangle { + x: parent.width * (root.hot / 100) - 1 + width: 1 + height: parent.height + 4 + anchors.verticalCenter: parent.verticalCenter + color: M.Theme.base08 + opacity: 0.6 + } + } + } + + // History sparkline + Canvas { + id: _sparkline + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + height: 40 + + property var _hist: root.history + property color _col: root.stateColor + + on_HistChanged: if (root.active) + requestPaint() + on_ColChanged: if (root.active) + requestPaint() + + onVisibleChanged: if (visible) + requestPaint() + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.clearRect(0, 0, width, height); + const d = _hist; + if (!d.length) + return; + + const maxSamples = 150; + const bw = width / maxSamples; + + // Background tint + ctx.fillStyle = Qt.rgba(_col.r, _col.g, _col.b, 0.08).toString(); + ctx.fillRect(0, 0, width, height); + + // Warm threshold line + const warmY = height - height * (root.warm / 100); + ctx.strokeStyle = M.Theme.base0A.toString(); + ctx.globalAlpha = 0.3; + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + ctx.beginPath(); + ctx.moveTo(0, warmY); + ctx.lineTo(width, warmY); + ctx.stroke(); + + // Hot threshold line + const hotY = height - height * (root.hot / 100); + ctx.strokeStyle = M.Theme.base08.toString(); + ctx.beginPath(); + ctx.moveTo(0, hotY); + ctx.lineTo(width, hotY); + ctx.stroke(); + + ctx.setLineDash([]); + ctx.globalAlpha = 1.0; + + // Bars + const offset = maxSamples - d.length; + for (let i = 0; i < d.length; i++) { + const barH = Math.max(1, height * d[i] / 100); + const barColor = d[i] > root.hot ? M.Theme.base08 : d[i] > root.warm ? M.Theme.base0A : _col; + ctx.fillStyle = barColor.toString(); + ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH); + } + } + } + + // Threshold labels + Item { + width: root.width + height: 16 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "warm " + root.warm + "\u00B0 hot " + root.hot + "\u00B0" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + font.letterSpacing: 0.5 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "10 min" + color: M.Theme.base03 + font.pixelSize: M.Theme.fontSize - 3 + font.family: M.Theme.fontFamily + } + } + + // Per-device breakdown + Rectangle { + width: root.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + visible: root.devices.length > 0 + } + + Repeater { + model: root.devices + delegate: Item { + required property var modelData + width: root.width + height: 22 + + readonly property bool _isActive: root.deviceFilter === modelData.name + + Rectangle { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + color: _isActive ? Qt.rgba(root.accentColor.r, root.accentColor.g, root.accentColor.b, 0.12) : "transparent" + radius: 3 + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + color: _isActive ? root.accentColor : M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: parent.width - 80 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.celsius + "\u00B0C" + color: root._tempColor(modelData.celsius) + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + font.bold: _isActive + } + } + } + + Item { + width: 1 + height: 4 + } +} diff --git a/shell/modules/content/VolumeContent.qml b/shell/modules/content/VolumeContent.qml new file mode 100644 index 0000000..43363a9 --- /dev/null +++ b/shell/modules/content/VolumeContent.qml @@ -0,0 +1,292 @@ +import QtQuick +import Quickshell.Services.Pipewire +import ".." as M + +Column { + id: root + + required property var sink + required property var sinkList + required property var streamList + required property color accentColor + + property real volume: sink?.audio?.volume ?? 0 + property bool muted: sink?.audio?.muted ?? false + readonly property string volumeIcon: muted ? "\uF026" : (volume > 0.5 ? "\uF028" : (volume > 0 ? "\uF027" : "\uF026")) + readonly property color volumeColor: muted ? M.Theme.base04 : root.accentColor + + // Slider row + Item { + width: root.width + height: 36 + + Text { + id: muteIcon + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: root.volumeIcon + color: root.volumeColor + font.pixelSize: M.Theme.fontSize + 2 + font.family: M.Theme.iconFontFamily + + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: if (root.sink?.audio) + root.sink.audio.muted = !root.sink.audio.muted + } + } + + Item { + id: slider + anchors.left: muteIcon.right + anchors.leftMargin: 8 + anchors.right: volLabel.left + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + height: 6 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 3 + } + Rectangle { + width: parent.width * Math.min(1, Math.max(0, root.volume)) + height: parent.height + color: root.volumeColor + radius: 3 + Behavior on width { + NumberAnimation { + duration: 80 + } + } + } + + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.PointingHandCursor + onPressed: mouse => _setVol(mouse) + onPositionChanged: mouse => { + if (pressed) + _setVol(mouse); + } + function _setVol(mouse) { + if (!root.sink?.audio) + return; + root.sink.audio.volume = Math.max(0, Math.min(1, mouse.x / slider.width)); + } + } + } + + Text { + id: volLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: Math.round(root.volume * 100) + "%" + color: root.muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + width: 30 + } + } + + // Device + stream list + Column { + id: deviceList + width: root.width + + // Output devices - only shown when more than one exists + Column { + visible: root.sinkList.length > 1 + width: parent.width + + Rectangle { + width: parent.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Text { + width: parent.width + height: 24 + verticalAlignment: Text.AlignVCenter + leftPadding: 12 + text: "Output Devices" + color: root.accentColor + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + } + + Repeater { + model: root.sinkList + + delegate: Item { + required property var modelData + + width: root.width + height: 28 + + readonly property bool _active: modelData === root.sink + + Rectangle { + anchors.fill: parent + anchors.leftMargin: 4 + anchors.rightMargin: 4 + color: deviceHover.hovered ? M.Theme.base02 : "transparent" + radius: M.Theme.radius + } + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.description || modelData.name || "Unknown" + color: parent._active ? root.accentColor : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.fontFamily + font.bold: parent._active + elide: Text.ElideRight + } + + HoverHandler { + id: deviceHover + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: Pipewire.preferredDefaultAudioSink = modelData + } + } + } + } + + // Streams section + Rectangle { + visible: root.streamList.length > 0 + width: parent.width - 16 + height: visible ? 1 : 0 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Text { + visible: root.streamList.length > 0 + width: parent.width + height: visible ? 24 : 0 + verticalAlignment: Text.AlignVCenter + leftPadding: 12 + text: "Applications" + color: root.accentColor + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + } + + Repeater { + model: root.streamList + + delegate: Item { + id: streamEntry + required property var modelData + + width: root.width + height: 32 + + readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" + readonly property real _vol: modelData.audio?.volume ?? 0 + readonly property bool _muted: modelData.audio?.muted ?? false + + Text { + id: streamIcon + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: streamEntry._muted ? "\uF026" : "\uF028" + color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.iconFontFamily + + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: if (streamEntry.modelData.audio) + streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted + } + } + + Text { + id: streamName + anchors.left: streamIcon.right + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + text: streamEntry._appName + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: 70 + } + + Item { + id: streamSlider + anchors.left: streamName.right + anchors.leftMargin: 6 + anchors.right: streamVol.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + Rectangle { + width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) + height: parent.height + color: streamEntry._muted ? M.Theme.base04 : root.accentColor + radius: 2 + } + + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.PointingHandCursor + onPressed: mouse => _set(mouse) + onPositionChanged: mouse => { + if (pressed) + _set(mouse); + } + function _set(mouse) { + if (!streamEntry.modelData.audio) + return; + streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width)); + } + } + } + + Text { + id: streamVol + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: Math.round(streamEntry._vol * 100) + "%" + color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + width: 28 + } + } + } + + // Bottom padding + Item { + width: 1 + height: 4 + } + } +} diff --git a/shell/modules/content/qmldir b/shell/modules/content/qmldir new file mode 100644 index 0000000..cb33d81 --- /dev/null +++ b/shell/modules/content/qmldir @@ -0,0 +1,8 @@ +module content +HexWaveBackground 1.0 HexWaveBackground.qml +VolumeContent 1.0 VolumeContent.qml +MprisContent 1.0 MprisContent.qml +CpuContent 1.0 CpuContent.qml +MemoryContent 1.0 MemoryContent.qml +TemperatureContent 1.0 TemperatureContent.qml +DiskContent 1.0 DiskContent.qml From d495a638941895d57ceb6650a63195cb28731218 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 21:45:49 +0200 Subject: [PATCH 7/8] add hex wave background overlay to lock screen --- shell/lock/LockSurface.qml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/shell/lock/LockSurface.qml b/shell/lock/LockSurface.qml index e47b71c..a851b2a 100644 --- a/shell/lock/LockSurface.qml +++ b/shell/lock/LockSurface.qml @@ -3,6 +3,7 @@ import QtQuick.Effects import Quickshell import Quickshell.Wayland import "../modules" as M +import "../modules/content" as C WlSessionLockSurface { id: root @@ -41,6 +42,22 @@ WlSessionLockSurface { opacity: background.opacity } + // Hex wave overlay + C.HexWaveBackground { + id: hexWave + anchors.fill: parent + running: root.lock.secure + opacity: background.opacity * 0.4 + + NumberAnimation on opacity { + id: _hexFadeIn + running: false + to: 0.4 + duration: 600 + easing.type: Easing.OutCubic + } + } + // Center content Item { id: content @@ -219,6 +236,13 @@ WlSessionLockSurface { duration: 300 easing.type: Easing.InCubic } + NumberAnimation { + target: hexWave + property: "opacity" + to: 0 + duration: 300 + easing.type: Easing.InCubic + } } PropertyAction { From b27c4a2abcaa2955ba14f7f8ab058619cfd7a91e Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 17 Apr 2026 21:46:53 +0200 Subject: [PATCH 8/8] add media and volume widget cards to lock screen --- shell/lock/LockSurface.qml | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/shell/lock/LockSurface.qml b/shell/lock/LockSurface.qml index a851b2a..2798cef 100644 --- a/shell/lock/LockSurface.qml +++ b/shell/lock/LockSurface.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Effects import Quickshell import Quickshell.Wayland +import Quickshell.Services.Mpris +import Quickshell.Services.Pipewire import "../modules" as M import "../modules/content" as C @@ -153,6 +155,72 @@ WlSessionLockSurface { } } } + + // Spacer before widgets + Item { + width: 1 + height: 8 + visible: _mprisCard.visible || _volumeCard.visible + } + + // Media widget + Rectangle { + id: _mprisCard + anchors.horizontalCenter: parent.horizontalCenter + width: 280 + height: _mprisContent.implicitHeight + 16 + radius: M.Theme.radius + 2 + color: Qt.rgba(M.Theme.base01.r, M.Theme.base01.g, M.Theme.base01.b, 0.7) + border.color: Qt.rgba(M.Theme.base03.r, M.Theme.base03.g, M.Theme.base03.b, 0.3) + border.width: 1 + visible: _mprisPlayer !== null + + readonly property var _mprisPlayers: (Mpris.players.values ?? []).filter(p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing || p.playbackState === MprisPlaybackState.Paused) + readonly property var _mprisPlayer: _mprisPlayers[0] ?? null + readonly property bool _playing: _mprisPlayer?.playbackState === MprisPlaybackState.Playing + + C.MprisContent { + id: _mprisContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 + player: _mprisCard._mprisPlayer + players: _mprisCard._mprisPlayers + playing: _mprisCard._playing + accentColor: M.Theme.base0D + cachedArt: _mprisCard._mprisPlayer?.trackArtUrl ?? "" + } + } + + // Volume widget + Rectangle { + id: _volumeCard + anchors.horizontalCenter: parent.horizontalCenter + width: 280 + height: _volumeContent.implicitHeight + 16 + radius: M.Theme.radius + 2 + color: Qt.rgba(M.Theme.base01.r, M.Theme.base01.g, M.Theme.base01.b, 0.7) + border.color: Qt.rgba(M.Theme.base03.r, M.Theme.base03.g, M.Theme.base03.b, 0.3) + border.width: 1 + visible: Pipewire.defaultAudioSink !== null + + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + C.VolumeContent { + id: _volumeContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 + sink: Pipewire.defaultAudioSink + sinkList: [] + streamList: [] + accentColor: M.Theme.base0E + } + } } }