plugin: rust-side modules + theme services with serde-typed config

This commit is contained in:
Damocles 2026-05-04 22:58:12 +02:00
parent a86e90e927
commit f34f3f2f4e
95 changed files with 2477 additions and 1011 deletions

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import "." as S
import NovaStats as NS
QtObject {
id: root
@ -12,7 +13,7 @@ QtObject {
readonly property bool available: _blDev !== ""
function adjust(delta) {
const step = S.Modules.backlight.step || 5;
const step = NS.ModulesService.backlightStep || 5;
_adjProc.cmd = delta > 0 ? "light -A " + step : "light -U " + step;
_adjProc.running = true;
}

View file

@ -3,6 +3,7 @@ pragma Singleton
import QtQuick
import Quickshell.Services.UPower
import "." as S
import NovaStats as NS
QtObject {
id: root
@ -20,11 +21,11 @@ QtObject {
readonly property bool healthSupported: dev?.healthSupported ?? false
readonly property real healthPercent: (dev?.healthPercentage ?? 1) * 100
readonly property int critThresh: S.Modules.battery.critical || 15
readonly property int warnThresh: S.Modules.battery.warning || 25
readonly property int critThresh: NS.ModulesService.batteryCritical || 15
readonly property int warnThresh: NS.ModulesService.batteryWarning || 25
readonly property bool critical: percent < critThresh && !charging
readonly property color stateColor: charging ? S.Theme.base0B : critical ? S.Theme.base09 : percent < warnThresh ? S.Theme.base0A : S.Theme.base05
readonly property color stateColor: charging ? NS.ThemeService.base0B : critical ? NS.ThemeService.base09 : percent < warnThresh ? NS.ThemeService.base0A : NS.ThemeService.base05
// 24h history (1440 samples @ 60s)
property var history: []

View file

@ -3,6 +3,7 @@ pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
import NovaStats as NS
QtObject {
id: root
@ -38,7 +39,7 @@ QtObject {
// Status polling (bar icon state)
property Process _statusProc: Process {
running: S.Modules.bluetooth.enable
running: NS.ModulesService.bluetoothEnable
command: ["sh", "-c", "s=$(bluetoothctl show 2>/dev/null); " + "[ -z \"$s\" ] && echo unavailable && exit; " + "echo \"$s\" | grep -q 'Powered: yes' || { echo off:; exit; }; " + "info=$(bluetoothctl info 2>/dev/null); " + "d=$(echo \"$info\" | awk -F': ' '/\\tName:/{n=$2}/Connected: yes/{c=1}END{if(c)print n}'); " + "[ -n \"$d\" ] && echo \"connected:$d\" || { echo on:; exit; }; " + "bat=$(echo \"$info\" | awk -F': ' '/Battery Percentage.*\\(/{gsub(/[^0-9]/,\"\",$2);print $2}'); " + "[ -n \"$bat\" ] && echo \"bat:$bat\""]
stdout: StdioCollector {
onStreamFinished: {
@ -58,7 +59,7 @@ QtObject {
// Event-driven: watch BlueZ DBus property changes
property Process _monitor: Process {
running: S.Modules.bluetooth.enable
running: NS.ModulesService.bluetoothEnable
command: ["sh", "-c", "dbus-monitor --system \"interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path_namespace='/org/bluez'\" 2>/dev/null"]
stdout: SplitParser {
splitMarker: "\n"
@ -73,7 +74,7 @@ QtObject {
property Timer _fallbackPoll: Timer {
interval: 60000
running: S.Modules.bluetooth.enable
running: NS.ModulesService.bluetoothEnable
repeat: true
onTriggered: root.refresh()
}

View file

@ -39,7 +39,7 @@ QtObject {
property Timer _pollTimer: Timer {
interval: {
const ms = S.Modules.statsDaemon.interval;
const ms = NS.ModulesService.statsDaemonInterval;
return ms > 0 ? ms : 4000;
}
running: true

View file

@ -3,11 +3,12 @@ pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
import NovaStats as NS
QtObject {
id: root
readonly property bool enabled: S.Modules.lock.enable
readonly property bool enabled: NS.ModulesService.lockEnable
property bool locked: false
property string sessionPath: ""

View file

@ -3,6 +3,7 @@ pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
import NovaStats as NS
QtObject {
id: root
@ -71,8 +72,8 @@ QtObject {
}
property Timer _poll: Timer {
interval: S.Modules.machinectl.interval ?? 15000
running: S.Modules.machinectl.enable
interval: NS.ModulesService.machinectlInterval ?? 15000
running: NS.ModulesService.machinectlEnable
repeat: true
triggeredOnStart: true
onTriggered: if (!root._listProc.running)

View file

@ -1,180 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
QtObject {
id: root
property var workspaces: ({
enable: true
})
property var tray: ({
enable: true
})
property var windowTitle: ({
enable: true
})
property var clock: ({
enable: true
})
property var notifications: ({
enable: true,
timeout: 3000,
maxPopups: 4,
maxVisible: 10,
maxHistory: -1
})
property var mpris: ({
enable: true
})
property var volume: ({
enable: true
})
property var bluetooth: ({
enable: true
})
property var backlight: ({
enable: true,
step: 5
})
property var network: ({
enable: true
})
property var powerProfile: ({
enable: true
})
property var idleInhibitor: ({
enable: true
})
property var weather: ({
enable: true,
args: ["--nerd"],
interval: 3600000
})
property var temperature: ({
enable: true,
warm: 80,
hot: 90,
device: ""
})
property var gpu: ({
enable: true,
warm: 70,
hot: 85
})
property var cpu: ({
enable: true
})
property var memory: ({
enable: true
})
property var disk: ({
enable: true,
interval: 30000,
warnThreshold: 85
})
property var battery: ({
enable: true,
warning: 25,
critical: 15
})
property var privacy: ({
enable: true
})
property var screenCorners: ({
enable: true
})
property var power: ({
enable: true
})
property var backgroundOverlay: ({
enable: true
})
property var overviewBackdrop: ({
enable: true
})
property var lock: ({
enable: true,
screenshot: true,
notifications: true,
mpris: true,
volume: true,
weather: true,
threatEffect: true
})
property var dock: ({
enable: true,
width: 300,
applets: {
clock: true,
cpu: true,
gpu: true,
memory: true,
temperature: true,
disk: true,
battery: true,
network: true,
bluetooth: true,
volume: true,
backlight: true,
weather: true,
mpris: true,
notifications: true,
power: true
}
})
property var systemd: ({
enable: true,
interval: 15000
})
property var machinectl: ({
enable: true,
interval: 15000
})
property var statsDaemon: ({
interval: -1
})
// All module keys that have an enable flag used to default-enable anything
// not explicitly mentioned in modules.json
readonly property var _moduleKeys: ["workspaces", "tray", "windowTitle", "clock", "notifications", "mpris", "volume", "bluetooth", "backlight", "network", "powerProfile", "idleInhibitor", "weather", "temperature", "gpu", "cpu", "memory", "disk", "battery", "privacy", "screenCorners", "power", "backgroundOverlay", "overviewBackdrop", "lock", "dock", "systemd", "machinectl"]
// Fallback: if modules.json doesn't exist, enable everything
Component.onCompleted: _apply("{}")
property FileView _file: FileView {
path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/nova-shell/modules.json"
watchChanges: true
onFileChanged: reload()
onLoaded: root._apply(text())
}
function _apply(raw) {
let data = {};
try {
data = JSON.parse(raw);
} catch (e) {}
// Enable all modules that aren't explicitly mentioned in the JSON
for (const k of _moduleKeys) {
if (!(k in data))
root[k] = Object.assign({}, root[k], {
enable: true
});
}
// Apply JSON overrides
for (const k of Object.keys(data)) {
if (!(k in root))
continue;
const v = data[k];
if (typeof v === "object" && v !== null)
root[k] = Object.assign({}, root[k], v);
else if (typeof v === "boolean")
root[k] = Object.assign({}, root[k], {
enable: v
});
}
}
}

View file

@ -3,6 +3,7 @@ pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
import NovaStats as NS
QtObject {
id: root
@ -40,7 +41,7 @@ QtObject {
// Status polling
property Process _statusProc: Process {
running: S.Modules.network.enable
running: NS.ModulesService.networkEnable
command: ["sh", "-c", "line=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active 2>/dev/null | head -1); if [ -z \"$line\" ]; then dev=$(nmcli -t -f DEVICE,STATE device 2>/dev/null | grep ':connected' | grep -v ':unmanaged\\|:unavailable\\|:disconnected\\|:connecting' | head -1 | cut -d: -f1); [ -n \"$dev\" ] && line=\"linked:linked:$dev\"; fi; [ -z \"$line\" ] && exit 0; echo \"$line\"; dev=$(echo \"$line\" | cut -d: -f3); ip=$(nmcli -t -f IP4.ADDRESS device show \"$dev\" 2>/dev/null | head -1 | cut -d: -f2); echo \"ip:${ip:-}\"; sig=$(nmcli -t -f GENERAL.SIGNAL device show \"$dev\" 2>/dev/null | head -1 | cut -d: -f2); echo \"sig:${sig:-}\""]
stdout: StdioCollector {
onStreamFinished: {
@ -76,7 +77,7 @@ QtObject {
// Event-driven monitor
property Process _monitor: Process {
running: S.Modules.network.enable
running: NS.ModulesService.networkEnable
command: ["nmcli", "monitor"]
stdout: SplitParser {
splitMarker: "\n"
@ -92,7 +93,7 @@ QtObject {
// Fallback poll
property Timer _fallbackPoll: Timer {
interval: 60000
running: S.Modules.network.enable
running: NS.ModulesService.networkEnable
repeat: true
onTriggered: root.refresh()
}

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "." as S
import NovaStats as NS
QtObject {
id: root
@ -96,7 +97,7 @@ QtObject {
});
// Trim excess popups
const max = S.Modules.notifications.maxPopups || 4;
const max = NS.ModulesService.notificationsMaxPopups || 4;
const currentPopups = root.list.filter(n => n.popup);
if (currentPopups.length > max) {
for (let i = max; i < currentPopups.length; i++)
@ -106,13 +107,13 @@ QtObject {
// Auto-expire popup (skip for critical)
if (item.popup && !isCritical) {
const timeout = notif.expireTimeout > 0 ? notif.expireTimeout : (S.Modules.notifications.timeout || 3000);
const timeout = notif.expireTimeout > 0 ? notif.expireTimeout : (NS.ModulesService.notificationsTimeout || 3000);
item._expireTimer.interval = timeout;
item._expireTimer.running = true;
}
// Trim history (-1 = unlimited)
const maxHistory = S.Modules.notifications.maxHistory ?? -1;
const maxHistory = NS.ModulesService.notificationsMaxHistory ?? -1;
while (maxHistory > 0 && root.list.length > maxHistory) {
const old = root.list.pop();
old.finishDismiss();

View file

@ -3,6 +3,7 @@ pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
import NovaStats as NS
QtObject {
id: root
@ -11,7 +12,7 @@ QtObject {
readonly property bool powerSaver: profile === "power-saver"
property var _proc: Process {
running: S.Modules.powerProfile.enable
running: NS.ModulesService.powerProfileEnable
command: ["powerprofilesctl", "get"]
stdout: StdioCollector {
onStreamFinished: root.profile = text.trim()
@ -19,7 +20,7 @@ QtObject {
}
property var _monitor: Process {
running: S.Modules.powerProfile.enable
running: NS.ModulesService.powerProfileEnable
command: ["sh", "-c", "dbus-monitor --system \"interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='/net/hadess/PowerProfiles'\" 2>/dev/null"]
stdout: SplitParser {
splitMarker: "\n"
@ -34,7 +35,7 @@ QtObject {
property var _poll: Timer {
interval: 60000
running: S.Modules.powerProfile.enable
running: NS.ModulesService.powerProfileEnable
repeat: true
onTriggered: root._proc.running = true
}

View file

@ -52,7 +52,7 @@ QtObject {
// Drive the Rust service from QML timers; both intervals read from Modules config.
property Timer _statsTimer: Timer {
interval: {
const ms = M.Modules.statsDaemon.interval;
const ms = NS.ModulesService.statsDaemonInterval;
return ms > 0 ? ms : 4000;
}
running: true
@ -62,7 +62,7 @@ QtObject {
}
property Timer _diskTimer: Timer {
interval: M.Modules.disk.interval || 30000
interval: NS.ModulesService.diskInterval || 30000
running: true
repeat: true
triggeredOnStart: true

View file

@ -3,6 +3,7 @@ pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
import NovaStats as NS
QtObject {
id: root
@ -64,8 +65,8 @@ QtObject {
}
property Timer _poll: Timer {
interval: S.Modules.systemd.interval ?? 15000
running: S.Modules.systemd.enable
interval: NS.ModulesService.systemdInterval ?? 15000
running: NS.ModulesService.systemdEnable
repeat: true
triggeredOnStart: true
onTriggered: if (!root._pollProc.running)

View file

@ -1,95 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
QtObject {
id: root
// base16 palette, overwritten from ~/.config/nova-shell/theme.json
property color base00: "#1e1e2e"
property color base01: "#181825"
property color base02: "#313244"
property color base03: "#45475a"
property color base04: "#585b70"
property color base05: "#cdd6f4"
property color base06: "#f5e0dc"
property color base07: "#b4befe"
property color base08: "#f38ba8"
property color base09: "#fab387"
property color base0A: "#f9e2af"
property color base0B: "#a6e3a1"
property color base0C: "#94e2d5"
property color base0D: "#89b4fa"
property color base0E: "#cba6f7"
property color base0F: "#f2cdcd"
property string fontFamily: "sans-serif"
property string iconFontFamily: "Symbols Nerd Font"
property int fontSize: 12
property real barOpacity: 0.9
property int barHeight: 32
property int barPadding: 8
property int moduleSpacing: 4
property int groupSpacing: 6
property int groupPadding: 8
property int radius: 4
property int screenRadius: 15
property bool _reducedMotionConfig: false
readonly property bool reducedMotion: _reducedMotionConfig || PowerProfileService.powerSaver
// Green -> yellow -> red gradient for 0-100% load/usage values
function loadColor(pct) {
const t = Math.max(0, Math.min(100, pct)) / 100;
const a = t < 0.5 ? root.base0B : root.base0A;
const b = t < 0.5 ? root.base0A : root.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 FileView _themeFile: FileView {
path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/nova-shell/theme.json"
watchChanges: true
onFileChanged: reload()
onLoaded: root._apply(text())
}
function _apply(raw) {
let data;
try {
data = JSON.parse(raw);
} catch (e) {
return;
}
const c = data.colors || {};
for (const k of Object.keys(c)) {
if (k in root)
root[k] = c[k];
}
if (data.fontFamily)
root.fontFamily = data.fontFamily;
if (data.iconFontFamily)
root.iconFontFamily = data.iconFontFamily;
if (data.fontSize)
root.fontSize = data.fontSize;
if (data.barOpacity !== undefined)
root.barOpacity = data.barOpacity;
if (data.barHeight !== undefined)
root.barHeight = data.barHeight;
if (data.barPadding !== undefined)
root.barPadding = data.barPadding;
if (data.moduleSpacing !== undefined)
root.moduleSpacing = data.moduleSpacing;
if (data.groupSpacing !== undefined)
root.groupSpacing = data.groupSpacing;
if (data.groupPadding !== undefined)
root.groupPadding = data.groupPadding;
if (data.radius !== undefined)
root.radius = data.radius;
if (data.screenRadius !== undefined)
root.screenRadius = data.screenRadius;
if (data.reducedMotion !== undefined)
root._reducedMotionConfig = data.reducedMotion;
}
}

View file

@ -0,0 +1,23 @@
pragma Singleton
import QtQuick
import NovaStats as NS
import "." as S
// Helpers that need QML-side state (PowerProfileService) or compute over the
// raw theme primitives. Plain values come straight from NS.ThemeService.
QtObject {
id: root
// Effective reduced-motion: configured value OR power-saver active.
readonly property bool reducedMotion: NS.ThemeService.reducedMotionConfig || S.PowerProfileService.powerSaver
// Green -> yellow -> red gradient for 0-100% load/usage values.
function loadColor(pct) {
const t = Math.max(0, Math.min(100, pct)) / 100;
const a = t < 0.5 ? NS.ThemeService.base0B : NS.ThemeService.base0A;
const b = t < 0.5 ? NS.ThemeService.base0A : NS.ThemeService.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);
}
}

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import "." as S
import NovaStats as NS
QtObject {
id: root
@ -13,8 +14,8 @@ QtObject {
readonly property bool available: icon !== ""
property Process _proc: Process {
running: S.Modules.weather.enable
command: ["wttrbar"].concat(S.Modules.weather.args)
running: NS.ModulesService.weatherEnable
command: ["wttrbar"].concat(NS.ModulesService.weatherArgs)
stdout: StdioCollector {
onStreamFinished: {
try {
@ -30,8 +31,8 @@ QtObject {
}
property Timer _poll: Timer {
interval: S.Modules.weather.interval || 3600000
running: S.Modules.weather.enable
interval: NS.ModulesService.weatherInterval || 3600000
running: NS.ModulesService.weatherEnable
repeat: true
onTriggered: root._proc.running = true
}

View file

@ -9,7 +9,6 @@ singleton DockState 1.0 DockState.qml
singleton IdleInhibitService 1.0 IdleInhibitService.qml
singleton LockService 1.0 LockService.qml
singleton MachinectlService 1.0 MachinectlService.qml
singleton Modules 1.0 Modules.qml
singleton MprisService 1.0 MprisService.qml
singleton NetworkService 1.0 NetworkService.qml
singleton NiriIpc 1.0 NiriIpc.qml
@ -20,6 +19,6 @@ singleton ScreenshotService 1.0 ScreenshotService.qml
singleton SleepService 1.0 SleepService.qml
singleton SystemStats 1.0 SystemStats.qml
singleton SystemdService 1.0 SystemdService.qml
singleton Theme 1.0 Theme.qml
singleton ThemeUtil 1.0 ThemeUtil.qml
singleton WeatherService 1.0 WeatherService.qml
# keep-sorted end