//! Settings service: parses `$XDG_CONFIG_HOME/nova-shell/modules.json` once at //! plugin load and exposes per-leaf primitive Q_PROPERTYs to QML. Defaults //! mirror the previous QML implementation; a missing file yields all defaults //! silently. Read errors / malformed JSON crash so misconfigurations are loud. //! Unknown fields warn via `tracing` and are otherwise ignored. use core::pin::Pin; use cxx_qt::CxxQtType; use cxx_qt_lib::{QList, QString}; use serde::Deserialize; use std::path::PathBuf; #[cxx_qt::bridge] pub mod qobject { unsafe extern "C++" { include!("cxx-qt-lib/qstring.h"); type QString = cxx_qt_lib::QString; include!("cxx-qt-lib/core/qlist/qlist_QString.h"); type QList_QString = cxx_qt_lib::QList; } extern "RustQt" { #[qobject] #[qml_element] #[qml_singleton] // Stats polling interval (-1 = use default). #[qproperty(i32, stats_daemon_interval, cxx_name = "statsDaemonInterval")] // Per-module enables (mirrors `.enable`). #[qproperty(bool, workspaces_enable, cxx_name = "workspacesEnable")] #[qproperty(bool, tray_enable, cxx_name = "trayEnable")] #[qproperty(bool, window_title_enable, cxx_name = "windowTitleEnable")] #[qproperty(bool, clock_enable, cxx_name = "clockEnable")] #[qproperty(bool, mpris_enable, cxx_name = "mprisEnable")] #[qproperty(bool, volume_enable, cxx_name = "volumeEnable")] #[qproperty(bool, bluetooth_enable, cxx_name = "bluetoothEnable")] #[qproperty(bool, network_enable, cxx_name = "networkEnable")] #[qproperty(bool, power_profile_enable, cxx_name = "powerProfileEnable")] #[qproperty(bool, idle_inhibitor_enable, cxx_name = "idleInhibitorEnable")] #[qproperty(bool, cpu_enable, cxx_name = "cpuEnable")] #[qproperty(bool, memory_enable, cxx_name = "memoryEnable")] #[qproperty(bool, privacy_enable, cxx_name = "privacyEnable")] #[qproperty(bool, screen_corners_enable, cxx_name = "screenCornersEnable")] #[qproperty(bool, power_enable, cxx_name = "powerEnable")] #[qproperty(bool, background_overlay_enable, cxx_name = "backgroundOverlayEnable")] #[qproperty(bool, overview_backdrop_enable, cxx_name = "overviewBackdropEnable")] // Notifications. #[qproperty(bool, notifications_enable, cxx_name = "notificationsEnable")] #[qproperty(i32, notifications_timeout, cxx_name = "notificationsTimeout")] #[qproperty(i32, notifications_max_popups, cxx_name = "notificationsMaxPopups")] #[qproperty(i32, notifications_max_visible, cxx_name = "notificationsMaxVisible")] #[qproperty(i32, notifications_max_history, cxx_name = "notificationsMaxHistory")] // Backlight. #[qproperty(bool, backlight_enable, cxx_name = "backlightEnable")] #[qproperty(i32, backlight_step, cxx_name = "backlightStep")] // Weather. #[qproperty(bool, weather_enable, cxx_name = "weatherEnable")] #[qproperty(QList_QString, weather_args, cxx_name = "weatherArgs")] #[qproperty(i32, weather_interval, cxx_name = "weatherInterval")] // Temperature. #[qproperty(bool, temperature_enable, cxx_name = "temperatureEnable")] #[qproperty(i32, temperature_warm, cxx_name = "temperatureWarm")] #[qproperty(i32, temperature_hot, cxx_name = "temperatureHot")] #[qproperty(QString, temperature_device, cxx_name = "temperatureDevice")] // GPU. #[qproperty(bool, gpu_enable, cxx_name = "gpuEnable")] #[qproperty(i32, gpu_warm, cxx_name = "gpuWarm")] #[qproperty(i32, gpu_hot, cxx_name = "gpuHot")] // Disk. #[qproperty(bool, disk_enable, cxx_name = "diskEnable")] #[qproperty(i32, disk_interval, cxx_name = "diskInterval")] #[qproperty(i32, disk_warn_threshold, cxx_name = "diskWarnThreshold")] // Battery. #[qproperty(bool, battery_enable, cxx_name = "batteryEnable")] #[qproperty(bool, battery_poweralertd, cxx_name = "batteryPoweralertd")] #[qproperty(i32, battery_warning, cxx_name = "batteryWarning")] #[qproperty(i32, battery_critical, cxx_name = "batteryCritical")] // Lock screen. #[qproperty(bool, lock_enable, cxx_name = "lockEnable")] #[qproperty(bool, lock_mpris, cxx_name = "lockMpris")] #[qproperty(bool, lock_notifications, cxx_name = "lockNotifications")] #[qproperty(bool, lock_screenshot, cxx_name = "lockScreenshot")] #[qproperty(bool, lock_threat_effect, cxx_name = "lockThreatEffect")] #[qproperty(bool, lock_volume, cxx_name = "lockVolume")] #[qproperty(bool, lock_weather, cxx_name = "lockWeather")] // Dock. #[qproperty(bool, dock_enable, cxx_name = "dockEnable")] #[qproperty(i32, dock_width, cxx_name = "dockWidth")] #[qproperty(bool, dock_applet_clock, cxx_name = "dockAppletClock")] #[qproperty(bool, dock_applet_cpu, cxx_name = "dockAppletCpu")] #[qproperty(bool, dock_applet_gpu, cxx_name = "dockAppletGpu")] #[qproperty(bool, dock_applet_memory, cxx_name = "dockAppletMemory")] #[qproperty(bool, dock_applet_temperature, cxx_name = "dockAppletTemperature")] #[qproperty(bool, dock_applet_disk, cxx_name = "dockAppletDisk")] #[qproperty(bool, dock_applet_battery, cxx_name = "dockAppletBattery")] #[qproperty(bool, dock_applet_network, cxx_name = "dockAppletNetwork")] #[qproperty(bool, dock_applet_bluetooth, cxx_name = "dockAppletBluetooth")] #[qproperty(bool, dock_applet_volume, cxx_name = "dockAppletVolume")] #[qproperty(bool, dock_applet_backlight, cxx_name = "dockAppletBacklight")] #[qproperty(bool, dock_applet_weather, cxx_name = "dockAppletWeather")] #[qproperty(bool, dock_applet_mpris, cxx_name = "dockAppletMpris")] #[qproperty(bool, dock_applet_notifications, cxx_name = "dockAppletNotifications")] #[qproperty(bool, dock_applet_power, cxx_name = "dockAppletPower")] // Unified systemd bar module (covers local + nspawn containers, and later remotes). #[qproperty(bool, systemd_enable, cxx_name = "systemdEnable")] #[qproperty(i32, systemd_interval, cxx_name = "systemdInterval")] type ModulesService = super::ModulesServiceRust; } impl cxx_qt::Initialize for ModulesService {} } // ── Pure Rust data layer ───────────────────────────────────────────────────── // One struct per group, plus a top-level `ModulesData`. Defaults mirror the // previous QML implementation, NOT serde's type defaults. `#[serde(default)]` // on every leaf so partial JSON objects get filled in from `Default`. mod data { use super::Deserialize; fn t() -> bool { true } fn weather_args_default() -> Vec { vec!["--nerd".to_owned()] } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Plain { #[serde(default = "t")] pub enable: bool, } impl Default for Plain { fn default() -> Self { Self { enable: true } } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Notifications { #[serde(default = "t")] pub enable: bool, #[serde(default = "Notifications::d_timeout")] pub timeout: i32, #[serde(default = "Notifications::d_max_popups")] pub max_popups: i32, #[serde(default = "Notifications::d_max_visible")] pub max_visible: i32, #[serde(default = "Notifications::d_max_history")] pub max_history: i32, } impl Notifications { fn d_timeout() -> i32 { 3000 } fn d_max_popups() -> i32 { 4 } fn d_max_visible() -> i32 { 10 } fn d_max_history() -> i32 { -1 } } impl Default for Notifications { fn default() -> Self { Self { enable: true, timeout: Self::d_timeout(), max_popups: Self::d_max_popups(), max_visible: Self::d_max_visible(), max_history: Self::d_max_history(), } } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Backlight { #[serde(default = "t")] pub enable: bool, #[serde(default = "Backlight::d_step")] pub step: i32, } impl Backlight { fn d_step() -> i32 { 5 } } impl Default for Backlight { fn default() -> Self { Self { enable: true, step: Self::d_step(), } } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Weather { #[serde(default = "t")] pub enable: bool, #[serde(default = "weather_args_default")] pub args: Vec, #[serde(default = "Weather::d_interval")] pub interval: i32, } impl Weather { fn d_interval() -> i32 { 3_600_000 } } impl Default for Weather { fn default() -> Self { Self { enable: true, args: weather_args_default(), interval: Self::d_interval(), } } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Temperature { #[serde(default = "t")] pub enable: bool, #[serde(default = "Temperature::d_warm")] pub warm: i32, #[serde(default = "Temperature::d_hot")] pub hot: i32, #[serde(default)] pub device: String, } impl Temperature { fn d_warm() -> i32 { 80 } fn d_hot() -> i32 { 90 } } impl Default for Temperature { fn default() -> Self { Self { enable: true, warm: Self::d_warm(), hot: Self::d_hot(), device: String::new(), } } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Gpu { #[serde(default = "t")] pub enable: bool, #[serde(default = "Gpu::d_warm")] pub warm: i32, #[serde(default = "Gpu::d_hot")] pub hot: i32, } impl Gpu { fn d_warm() -> i32 { 70 } fn d_hot() -> i32 { 85 } } impl Default for Gpu { fn default() -> Self { Self { enable: true, warm: Self::d_warm(), hot: Self::d_hot(), } } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Disk { #[serde(default = "t")] pub enable: bool, #[serde(default = "Disk::d_interval")] pub interval: i32, #[serde(default = "Disk::d_warn_threshold")] pub warn_threshold: i32, } impl Disk { fn d_interval() -> i32 { 30_000 } fn d_warn_threshold() -> i32 { 85 } } impl Default for Disk { fn default() -> Self { Self { enable: true, interval: Self::d_interval(), warn_threshold: Self::d_warn_threshold(), } } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Battery { #[serde(default = "t")] pub enable: bool, #[serde(default = "t")] pub poweralertd: bool, #[serde(default = "Battery::d_warning")] pub warning: i32, #[serde(default = "Battery::d_critical")] pub critical: i32, } impl Battery { fn d_warning() -> i32 { 25 } fn d_critical() -> i32 { 15 } } impl Default for Battery { fn default() -> Self { Self { enable: true, poweralertd: true, warning: Self::d_warning(), critical: Self::d_critical(), } } } #[derive(Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct Lock { #[serde(default = "t")] pub enable: bool, #[serde(default = "t")] pub mpris: bool, #[serde(default = "t")] pub notifications: bool, #[serde(default = "t")] pub screenshot: bool, #[serde(default = "t")] pub threat_effect: bool, #[serde(default = "t")] pub volume: bool, #[serde(default = "t")] pub weather: bool, } #[derive(Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct DockApplets { #[serde(default = "t")] pub clock: bool, #[serde(default = "t")] pub cpu: bool, #[serde(default = "t")] pub gpu: bool, #[serde(default = "t")] pub memory: bool, #[serde(default = "t")] pub temperature: bool, #[serde(default = "t")] pub disk: bool, #[serde(default = "t")] pub battery: bool, #[serde(default = "t")] pub network: bool, #[serde(default = "t")] pub bluetooth: bool, #[serde(default = "t")] pub volume: bool, #[serde(default = "t")] pub backlight: bool, #[serde(default = "t")] pub weather: bool, #[serde(default = "t")] pub mpris: bool, #[serde(default = "t")] pub notifications: bool, #[serde(default = "t")] pub power: bool, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Dock { #[serde(default = "t")] pub enable: bool, #[serde(default = "Dock::d_width")] pub width: i32, #[serde(default)] pub applets: DockApplets, } impl Dock { fn d_width() -> i32 { 300 } } impl Default for Dock { fn default() -> Self { Self { enable: true, width: Self::d_width(), applets: DockApplets::default(), } } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct WithInterval { #[serde(default = "t")] pub enable: bool, #[serde(default = "WithInterval::d_interval")] pub interval: i32, } impl WithInterval { fn d_interval() -> i32 { 15_000 } } impl Default for WithInterval { fn default() -> Self { Self { enable: true, interval: Self::d_interval(), } } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct StatsDaemon { #[serde(default = "StatsDaemon::d_interval")] pub interval: i32, } impl StatsDaemon { fn d_interval() -> i32 { -1 } } impl Default for StatsDaemon { fn default() -> Self { Self { interval: Self::d_interval(), } } } #[derive(Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct ModulesData { #[serde(default)] pub workspaces: Plain, #[serde(default)] pub tray: Plain, #[serde(default)] pub window_title: Plain, #[serde(default)] pub clock: Plain, #[serde(default)] pub mpris: Plain, #[serde(default)] pub volume: Plain, #[serde(default)] pub bluetooth: Plain, #[serde(default)] pub network: Plain, #[serde(default)] pub power_profile: Plain, #[serde(default)] pub idle_inhibitor: Plain, #[serde(default)] pub cpu: Plain, #[serde(default)] pub memory: Plain, #[serde(default)] pub privacy: Plain, #[serde(default)] pub screen_corners: Plain, #[serde(default)] pub power: Plain, #[serde(default)] pub background_overlay: Plain, #[serde(default)] pub overview_backdrop: Plain, #[serde(default)] pub notifications: Notifications, #[serde(default)] pub backlight: Backlight, #[serde(default)] pub weather: Weather, #[serde(default)] pub temperature: Temperature, #[serde(default)] pub gpu: Gpu, #[serde(default)] pub disk: Disk, #[serde(default)] pub battery: Battery, #[serde(default)] pub lock: Lock, #[serde(default)] pub dock: Dock, #[serde(default)] pub systemd: WithInterval, #[serde(default)] pub stats_daemon: StatsDaemon, } } use data::ModulesData; pub struct ModulesServiceRust { stats_daemon_interval: i32, workspaces_enable: bool, tray_enable: bool, window_title_enable: bool, clock_enable: bool, mpris_enable: bool, volume_enable: bool, bluetooth_enable: bool, network_enable: bool, power_profile_enable: bool, idle_inhibitor_enable: bool, cpu_enable: bool, memory_enable: bool, privacy_enable: bool, screen_corners_enable: bool, power_enable: bool, background_overlay_enable: bool, overview_backdrop_enable: bool, notifications_enable: bool, notifications_timeout: i32, notifications_max_popups: i32, notifications_max_visible: i32, notifications_max_history: i32, backlight_enable: bool, backlight_step: i32, weather_enable: bool, weather_args: QList, weather_interval: i32, temperature_enable: bool, temperature_warm: i32, temperature_hot: i32, temperature_device: QString, gpu_enable: bool, gpu_warm: i32, gpu_hot: i32, disk_enable: bool, disk_interval: i32, disk_warn_threshold: i32, battery_enable: bool, battery_poweralertd: bool, battery_warning: i32, battery_critical: i32, lock_enable: bool, lock_mpris: bool, lock_notifications: bool, lock_screenshot: bool, lock_threat_effect: bool, lock_volume: bool, lock_weather: bool, dock_enable: bool, dock_width: i32, dock_applet_clock: bool, dock_applet_cpu: bool, dock_applet_gpu: bool, dock_applet_memory: bool, dock_applet_temperature: bool, dock_applet_disk: bool, dock_applet_battery: bool, dock_applet_network: bool, dock_applet_bluetooth: bool, dock_applet_volume: bool, dock_applet_backlight: bool, dock_applet_weather: bool, dock_applet_mpris: bool, dock_applet_notifications: bool, dock_applet_power: bool, systemd_enable: bool, systemd_interval: i32, } impl Default for ModulesServiceRust { fn default() -> Self { Self::from_data(load_modules_data()) } } impl ModulesServiceRust { fn from_data(d: ModulesData) -> Self { let mut weather_args = QList::::default(); for s in &d.weather.args { weather_args.append(QString::from(s.as_str())); } Self { stats_daemon_interval: d.stats_daemon.interval, workspaces_enable: d.workspaces.enable, tray_enable: d.tray.enable, window_title_enable: d.window_title.enable, clock_enable: d.clock.enable, mpris_enable: d.mpris.enable, volume_enable: d.volume.enable, bluetooth_enable: d.bluetooth.enable, network_enable: d.network.enable, power_profile_enable: d.power_profile.enable, idle_inhibitor_enable: d.idle_inhibitor.enable, cpu_enable: d.cpu.enable, memory_enable: d.memory.enable, privacy_enable: d.privacy.enable, screen_corners_enable: d.screen_corners.enable, power_enable: d.power.enable, background_overlay_enable: d.background_overlay.enable, overview_backdrop_enable: d.overview_backdrop.enable, notifications_enable: d.notifications.enable, notifications_timeout: d.notifications.timeout, notifications_max_popups: d.notifications.max_popups, notifications_max_visible: d.notifications.max_visible, notifications_max_history: d.notifications.max_history, backlight_enable: d.backlight.enable, backlight_step: d.backlight.step, weather_enable: d.weather.enable, weather_args, weather_interval: d.weather.interval, temperature_enable: d.temperature.enable, temperature_warm: d.temperature.warm, temperature_hot: d.temperature.hot, temperature_device: QString::from(d.temperature.device.as_str()), gpu_enable: d.gpu.enable, gpu_warm: d.gpu.warm, gpu_hot: d.gpu.hot, disk_enable: d.disk.enable, disk_interval: d.disk.interval, disk_warn_threshold: d.disk.warn_threshold, battery_enable: d.battery.enable, battery_poweralertd: d.battery.poweralertd, battery_warning: d.battery.warning, battery_critical: d.battery.critical, lock_enable: d.lock.enable, lock_mpris: d.lock.mpris, lock_notifications: d.lock.notifications, lock_screenshot: d.lock.screenshot, lock_threat_effect: d.lock.threat_effect, lock_volume: d.lock.volume, lock_weather: d.lock.weather, dock_enable: d.dock.enable, dock_width: d.dock.width, dock_applet_clock: d.dock.applets.clock, dock_applet_cpu: d.dock.applets.cpu, dock_applet_gpu: d.dock.applets.gpu, dock_applet_memory: d.dock.applets.memory, dock_applet_temperature: d.dock.applets.temperature, dock_applet_disk: d.dock.applets.disk, dock_applet_battery: d.dock.applets.battery, dock_applet_network: d.dock.applets.network, dock_applet_bluetooth: d.dock.applets.bluetooth, dock_applet_volume: d.dock.applets.volume, dock_applet_backlight: d.dock.applets.backlight, dock_applet_weather: d.dock.applets.weather, dock_applet_mpris: d.dock.applets.mpris, dock_applet_notifications: d.dock.applets.notifications, dock_applet_power: d.dock.applets.power, systemd_enable: d.systemd.enable, systemd_interval: d.systemd.interval, } } } pub(crate) fn config_path(file: &str) -> PathBuf { let base = dirs::config_dir().unwrap_or_else(|| { // Treat a missing HOME/XDG as a user error, not a silent default; matches // the rule that perm/format issues should surface immediately. panic!("[nova-plugin] could not resolve $XDG_CONFIG_HOME or $HOME for config lookup"); }); base.join("nova-shell").join(file) } fn load_modules_data() -> ModulesData { let path = config_path("modules.json"); let raw = match std::fs::read_to_string(&path) { Ok(s) => s, Err(e) if e.kind() == std::io::ErrorKind::NotFound => { tracing::info!( target: "nova_plugin::config", "modules.json not found at {}, using defaults", path.display() ); return ModulesData::default(); } Err(e) => panic!("[nova-plugin] cannot read {}: {e}", path.display()), }; parse_modules(&raw, &path) } fn parse_modules(raw: &str, path: &std::path::Path) -> ModulesData { // First pass: catch unknown keys via a generic Value, warn but keep going. if let Ok(serde_json::Value::Object(map)) = serde_json::from_str::(raw) { let known: &[&str] = &[ "workspaces", "tray", "windowTitle", "clock", "mpris", "volume", "bluetooth", "network", "powerProfile", "idleInhibitor", "cpu", "memory", "privacy", "screenCorners", "power", "backgroundOverlay", "overviewBackdrop", "notifications", "backlight", "weather", "temperature", "gpu", "disk", "battery", "lock", "dock", "systemd", "statsDaemon", ]; for key in map.keys() { if !known.contains(&key.as_str()) { tracing::warn!( target: "nova_plugin::config", "{}: unknown top-level key `{key}` ignored", path.display() ); } } } serde_json::from_str(raw) .unwrap_or_else(|e| panic!("[nova-plugin] {}: malformed JSON: {e}", path.display())) } impl cxx_qt::Initialize for qobject::ModulesService { fn initialize(self: Pin<&mut Self>) { // Default-construct populates from disk; nothing else to do. let _ = self; } }