move Theme, SystemStats, Modules to shell/services/

This commit is contained in:
Damocles 2026-04-17 22:07:00 +02:00
parent 197f6976e0
commit 989182d603
59 changed files with 432 additions and 388 deletions

142
shell/services/Modules.qml Normal file
View file

@ -0,0 +1,142 @@
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
})
property var cpu: ({
enable: true
})
property var memory: ({
enable: true
})
property var disk: ({
enable: true,
interval: 30000
})
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
})
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"]
// 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

@ -0,0 +1,185 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import "." as M
QtObject {
id: root
// CPU
property int cpuUsage: 0
property real cpuFreqGhz: 0
property var cpuCores: [] // [{usage, freq_ghz, history:[]}] only rebuilt while coreConsumers > 0
property int coreConsumers: 0
onCoreConsumersChanged: {
if (coreConsumers > 0)
_coreGraceTimer.stop();
else
_coreGraceTimer.start();
}
property var _coreGraceTimer: Timer {
interval: 30000
onTriggered: root.cpuCores = root.cpuCores.map(() => ({
"usage": 0,
"freq_ghz": 0,
"history": []
}))
}
property var cpuCoreMaxFreq: []
property var cpuCoreTypes: []
// Temperature
property int tempCelsius: 0
property var tempHistory: [] // 150 samples @ 4s each 10 min
property var tempDevices: [] // [{name, celsius}] sorted hottest-first
// GPU
property bool gpuAvailable: false
property string gpuVendor: ""
property int gpuUsage: 0
property real gpuVramUsedGb: 0
property real gpuVramTotalGb: 0
property int gpuTempC: 0
property var gpuHistory: [] // 60 samples @ ~4-8s each 4-8 min
// Memory
property int memPercent: 0
property real memUsedGb: 0
property real memTotalGb: 0
property real memAvailGb: 0
property real memCachedGb: 0
property real memBuffersGb: 0
property var memHistory: []
// Disk
property var diskMounts: []
property int diskRootPct: 0
// nova-stats stream (cpu + mem)
property var _statsProc: Process {
running: true
command: {
const ms = M.Modules.statsDaemon.interval;
return ms > 0 ? ["nova-stats", "--interval", ms.toString()] : ["nova-stats"];
}
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
try {
const ev = JSON.parse(line);
if (ev.type === "cpu") {
root.cpuUsage = ev.usage;
root.cpuFreqGhz = ev.freq_ghz;
if (root.coreConsumers > 0) {
const histLen = 16;
const prev = root.cpuCores;
root.cpuCores = ev.cores.map((c, i) => {
const oldHist = prev[i]?.history ?? [];
const hist = oldHist.concat([c.usage]);
return {
usage: c.usage,
freq_ghz: c.freq_ghz,
history: hist.length > histLen ? hist.slice(hist.length - histLen) : hist
};
});
} else if (root.cpuCores.length !== ev.cores.length) {
// Keep count in sync so panel can size correctly before consumers activate
root.cpuCores = ev.cores.map(c => ({
"usage": c.usage,
"freq_ghz": c.freq_ghz,
"history": []
}));
}
} else if (ev.type === "temp") {
root.tempCelsius = ev.celsius;
const th = root.tempHistory.concat([ev.celsius]);
root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th;
if (ev.devices)
root.tempDevices = ev.devices;
} else if (ev.type === "gpu") {
root.gpuAvailable = true;
root.gpuVendor = ev.vendor;
root.gpuUsage = ev.usage;
root.gpuVramUsedGb = ev.vram_used_gb;
root.gpuVramTotalGb = ev.vram_total_gb;
root.gpuTempC = ev.temp_c;
const gh = root.gpuHistory.concat([ev.usage]);
root.gpuHistory = gh.length > 60 ? gh.slice(gh.length - 60) : gh;
} else if (ev.type === "mem") {
root.memPercent = ev.percent;
root.memUsedGb = ev.used_gb;
root.memTotalGb = ev.total_gb;
root.memAvailGb = ev.avail_gb;
root.memCachedGb = ev.cached_gb;
root.memBuffersGb = ev.buffers_gb;
const h = root.memHistory.concat([ev.percent]);
root.memHistory = h.length > 30 ? h.slice(h.length - 30) : h;
}
} catch (e) {}
}
}
}
// One-time: per-core max freq
property var _maxFreqProc: Process {
running: true
command: ["sh", "-c", "for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do [ -f \"$f\" ] && cat \"$f\" || echo 0; done 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
root.cpuCoreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6);
}
}
}
// One-time: P/E-core topology
property var _coreTypesProc: Process {
running: true
command: ["sh", "-c", "for d in /sys/devices/system/cpu/cpu[0-9]*/topology/core_type; do [ -f \"$d\" ] && cat \"$d\" || echo Performance; done 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
root.cpuCoreTypes = text.trim().split("\n").filter(l => l).map(l => l.trim());
}
}
}
// Disk via df
property var _diskProc: Process {
id: diskProc
running: true
command: ["sh", "-c", "df -x tmpfs -x devtmpfs -x squashfs -x efivarfs -x overlay -B1 --output=target,size,used 2>/dev/null | awk 'NR>1 && $2+0>0 {print $1\"|\"$2\"|\"$3}'"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n").filter(l => l);
const mounts = [];
for (const line of lines) {
const parts = line.split("|");
if (parts.length < 3)
continue;
const total = parseInt(parts[1]);
const used = parseInt(parts[2]);
if (total <= 0)
continue;
mounts.push({
"target": parts[0],
"pct": Math.round(used / total * 100),
"usedBytes": used,
"totalBytes": total
});
}
root.diskMounts = mounts;
const rm = mounts.find(m => m.target === "/");
if (rm)
root.diskRootPct = rm.pct;
}
}
}
property var _diskTimer: Timer {
interval: M.Modules.disk.interval || 30000
running: true
repeat: true
onTriggered: diskProc.running = true
}
}

86
shell/services/Theme.qml Normal file
View file

@ -0,0 +1,86 @@
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
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;
}
}

4
shell/services/qmldir Normal file
View file

@ -0,0 +1,4 @@
module services
singleton Theme 1.0 Theme.qml
singleton SystemStats 1.0 SystemStats.qml
singleton Modules 1.0 Modules.qml