This repository has been archived on 2026-07-13. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
nova-shell/plugin/src/modules_service.rs

762 lines
25 KiB
Rust

//! 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<cxx_qt_lib::QString>;
}
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 `<group>.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<String> {
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<String>,
#[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<QString>,
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::<QString>::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::<serde_json::Value>(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;
}
}