diff --git a/plugin/src/modules_service.rs b/plugin/src/modules_service.rs index a08b119..05e33ad 100644 --- a/plugin/src/modules_service.rs +++ b/plugin/src/modules_service.rs @@ -101,10 +101,9 @@ pub mod qobject { #[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 + remotes). + // 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")] - #[qproperty(QList_QString, systemd_machines, cxx_name = "systemdMachines")] type ModulesService = super::ModulesServiceRust; } @@ -444,34 +443,6 @@ mod data { } } - // Systemd-bar group: enable + poll interval + remote SSH targets. Each - // `machines` entry is a string like "host" or "user@host"; the local - // machine is deduped by hostname even if listed. - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - pub struct Systemd { - #[serde(default = "t")] - pub enable: bool, - #[serde(default = "Systemd::d_interval")] - pub interval: i32, - #[serde(default)] - pub machines: Vec, - } - impl Systemd { - fn d_interval() -> i32 { - 15_000 - } - } - impl Default for Systemd { - fn default() -> Self { - Self { - enable: true, - interval: Self::d_interval(), - machines: Vec::new(), - } - } - } - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct StatsDaemon { @@ -547,7 +518,7 @@ mod data { #[serde(default)] pub dock: Dock, #[serde(default)] - pub systemd: Systemd, + pub systemd: WithInterval, #[serde(default)] pub stats_daemon: StatsDaemon, } @@ -624,7 +595,6 @@ pub struct ModulesServiceRust { dock_applet_power: bool, systemd_enable: bool, systemd_interval: i32, - systemd_machines: QList, } impl Default for ModulesServiceRust { @@ -708,13 +678,6 @@ impl ModulesServiceRust { dock_applet_power: d.dock.applets.power, systemd_enable: d.systemd.enable, systemd_interval: d.systemd.interval, - systemd_machines: { - let mut list = QList::::default(); - for s in &d.systemd.machines { - list.append(QString::from(s.as_str())); - } - list - }, } } } @@ -728,10 +691,6 @@ pub(crate) fn config_path(file: &str) -> PathBuf { base.join("nova-shell").join(file) } -pub(crate) fn load_systemd_machines() -> Vec { - load_modules_data().systemd.machines -} - fn load_modules_data() -> ModulesData { let path = config_path("modules.json"); let raw = match std::fs::read_to_string(&path) { diff --git a/plugin/src/systemd_service.rs b/plugin/src/systemd_service.rs index fcee982..53021d0 100644 --- a/plugin/src/systemd_service.rs +++ b/plugin/src/systemd_service.rs @@ -1,12 +1,12 @@ // In-process systemd state for nova-shell. // -// The machine tree is exposed to QML as a JSON-encoded QString (`machinesJson`) -// rather than QList. cxx-qt 0.8.1 does not implement QVariantValue -// for QVariantMap/QVariantList, and cxx-qt main on git regressed qt-build-utils -// to require QuickControls2.prl files that nixpkgs strips. Switch to -// QList when a release ships with both fixes. +// Lists of units and machines are exposed to QML as JSON-encoded QString props +// (`failedUnitsJson`, `containersJson`) rather than QList. cxx-qt +// 0.8.1 does not implement QVariantValue for QVariantMap/QVariantList, and +// cxx-qt main on git regressed qt-build-utils to require QuickControls2.prl +// files that nixpkgs strips. Switch to QList when a release ships +// with both fixes. -use crate::modules_service; use core::pin::Pin; use cxx_qt_lib::QString; use serde::Serialize; @@ -26,13 +26,13 @@ pub mod qobject { #[qml_element] #[qml_singleton] #[qproperty(QString, hostname)] - // Local failed unit count (drives the bar module label). + #[qproperty(QString, system_state, cxx_name = "systemState")] + #[qproperty(QString, user_state, cxx_name = "userState")] #[qproperty(i32, failed_count, cxx_name = "failedCount")] - // JSON array, local first then nspawn containers. Each entry: - // { name, isLocal, marker, systemState, runningCount, totalCount, - // failedUnits: [{name, description, subState, scope, machine}], - // runningUnits: [...] } - #[qproperty(QString, machines_json, cxx_name = "machinesJson")] + // JSON array: [{ name, description, subState, scope: "system"|"user", machine: "" | name }] + #[qproperty(QString, failed_units_json, cxx_name = "failedUnitsJson")] + // JSON array: [{ name, class, service, systemState, failedUnits: [...] }] + #[qproperty(QString, containers_json, cxx_name = "containersJson")] type SystemdService = super::SystemdServiceRust; #[qinvokable] @@ -46,7 +46,7 @@ pub mod qobject { impl cxx_qt::Initialize for SystemdService {} } -// systemd1.Manager.ListUnits returns a(ssssssouso): name, description, +// systemd1.Manager.ListUnitsFiltered returns a(ssssssouso): name, description, // load_state, active_state, sub_state, follower, unit_path, job_id, job_type, job_path. type UnitTuple = ( String, @@ -70,7 +70,7 @@ trait SystemdManager { #[zbus(property)] fn system_state(&self) -> zbus::Result; - fn list_units(&self) -> zbus::Result>; + fn list_units_filtered(&self, states: Vec<&str>) -> zbus::Result>; fn restart_unit(&self, name: &str, mode: &str) -> zbus::Result; @@ -88,45 +88,44 @@ trait Machined { } #[derive(Serialize)] -struct UnitJson { - name: String, - description: String, +struct UnitJson<'a> { + name: &'a str, + description: &'a str, #[serde(rename = "subState")] - sub_state: String, - scope: String, - machine: String, + sub_state: &'a str, + scope: &'a str, + machine: &'a str, } #[derive(Serialize)] -struct MachineJson { - name: String, - #[serde(rename = "isLocal")] - is_local: bool, - marker: String, +struct ContainerJson<'a> { + name: &'a str, + class: &'a str, + service: &'a str, #[serde(rename = "systemState")] - system_state: String, - #[serde(rename = "runningCount")] - running_count: i32, - #[serde(rename = "totalCount")] - total_count: i32, + system_state: &'a str, #[serde(rename = "failedUnits")] - failed_units: Vec, - #[serde(rename = "runningUnits")] - running_units: Vec, + failed_units: Vec>, } pub struct SystemdServiceRust { hostname: QString, + system_state: QString, + user_state: QString, failed_count: i32, - machines_json: QString, + failed_units_json: QString, + containers_json: QString, } impl Default for SystemdServiceRust { fn default() -> Self { Self { hostname: QString::from(read_hostname()), + system_state: QString::from("unknown"), + user_state: QString::from("unknown"), failed_count: 0, - machines_json: QString::from("[]"), + failed_units_json: QString::from("[]"), + containers_json: QString::from("[]"), } } } @@ -147,47 +146,22 @@ fn rt() -> &'static Runtime { }) } -async fn fetch_units(bus: &Connection) -> (String, Vec) { +async fn fetch_failed(bus: &Connection) -> (String, Vec) { let mut state = String::from("unknown"); let mut units = Vec::new(); if let Ok(mgr) = SystemdManagerProxy::new(bus).await { if let Ok(s) = mgr.system_state().await { state = s; } - if let Ok(u) = mgr.list_units().await { + if let Ok(u) = mgr.list_units_filtered(vec!["failed"]).await { units = u; } } (state, units) } -// Partition a unit list by active_state. (failed, running) -fn partition_units( - units: Vec, - scope: &str, - machine: &str, -) -> (Vec, Vec, i32) { - let total = units.len() as i32; - let mut failed = Vec::new(); - let mut running = Vec::new(); - for u in units { - let entry = UnitJson { - name: u.0, - description: u.1, - sub_state: u.4, - scope: scope.to_string(), - machine: machine.to_string(), - }; - match u.3.as_str() { - "failed" => failed.push(entry), - "active" => running.push(entry), - _ => {} - } - } - (failed, running, total) -} - async fn poll_async() -> ( + String, String, Vec, Vec, @@ -195,11 +169,12 @@ async fn poll_async() -> ( ) { let mut sys_state = String::from("unknown"); let mut sys_units = Vec::new(); + let mut user_state = String::from("unknown"); let mut user_units = Vec::new(); let mut machines = Vec::new(); if let Ok(c) = Connection::system().await { - let (s, u) = fetch_units(&c).await; + let (s, u) = fetch_failed(&c).await; sys_state = s; sys_units = u; if let Ok(m) = MachinedProxy::new(&c).await { @@ -213,11 +188,25 @@ async fn poll_async() -> ( } } if let Ok(c) = Connection::session().await { - let (_, u) = fetch_units(&c).await; + let (s, u) = fetch_failed(&c).await; + user_state = s; user_units = u; } - (sys_state, sys_units, user_units, machines) + (sys_state, user_state, sys_units, user_units, machines) +} + +fn unit_jsons<'a>(units: &'a [UnitTuple], scope: &'a str, machine: &'a str) -> Vec> { + units + .iter() + .map(|u| UnitJson { + name: &u.0, + description: &u.1, + sub_state: &u.4, + scope, + machine, + }) + .collect() } impl cxx_qt::Initialize for qobject::SystemdService { @@ -228,76 +217,33 @@ impl cxx_qt::Initialize for qobject::SystemdService { impl qobject::SystemdService { fn poll(mut self: Pin<&mut Self>) { - let (sys_state, sys_units, user_units, machines) = rt().block_on(poll_async()); + let (sys_state, user_state, sys_units, user_units, machines) = rt().block_on(poll_async()); - let (sys_failed, sys_running, sys_total) = partition_units(sys_units, "system", ""); - let (user_failed, user_running, user_total) = partition_units(user_units, "user", ""); + let mut all_failed: Vec = Vec::with_capacity(sys_units.len() + user_units.len()); + all_failed.extend(unit_jsons(&sys_units, "system", "")); + all_failed.extend(unit_jsons(&user_units, "user", "")); + let count = all_failed.len() as i32; + let failed_json = serde_json::to_string(&all_failed).unwrap_or_else(|_| "[]".into()); - let mut failed: Vec = Vec::with_capacity(sys_failed.len() + user_failed.len()); - failed.extend(sys_failed); - failed.extend(user_failed); - let mut running: Vec = Vec::with_capacity(sys_running.len() + user_running.len()); - running.extend(sys_running); - running.extend(user_running); - - let local_failed_count = failed.len() as i32; - let local_running_count = running.len() as i32; - let local_total_count = sys_total + user_total; - - let local = MachineJson { - name: read_hostname(), - is_local: true, - marker: "this machine".into(), - system_state: sys_state, - running_count: local_running_count, - total_count: local_total_count, - failed_units: failed, - running_units: running, - }; - - // Containers: enumerate only; unit fetching for containers comes in step 5. - let mut all_machines = Vec::with_capacity(1 + machines.len()); - all_machines.push(local); - for (name, _class, _service) in &machines { - all_machines.push(MachineJson { - name: name.clone(), - is_local: false, - marker: String::new(), - system_state: "unknown".into(), - running_count: 0, - total_count: 0, + let containers: Vec = machines + .iter() + .map(|(n, c, s)| ContainerJson { + name: n, + class: c, + service: s, + system_state: "unknown", failed_units: Vec::new(), - running_units: Vec::new(), - }); - } + }) + .collect(); + let containers_json = serde_json::to_string(&containers).unwrap_or_else(|_| "[]".into()); - // Configured remote machines (placeholders; transport lands in step 4). - // Dedup local: drop entries matching the local hostname or `localhost`, - // with or without a `user@` prefix. - let host = read_hostname(); - let cfg_machines = modules_service::load_systemd_machines(); - for target in cfg_machines { - let host_part = target.rsplit_once('@').map_or(target.as_str(), |(_, h)| h); - if host_part == host || host_part == "localhost" { - continue; - } - all_machines.push(MachineJson { - name: target.clone(), - is_local: false, - marker: String::new(), - system_state: "pending".into(), - running_count: 0, - total_count: 0, - failed_units: Vec::new(), - running_units: Vec::new(), - }); - } - - let machines_json = serde_json::to_string(&all_machines).unwrap_or_else(|_| "[]".into()); - - self.as_mut().set_failed_count(local_failed_count); + self.as_mut().set_system_state(QString::from(sys_state)); + self.as_mut().set_user_state(QString::from(user_state)); self.as_mut() - .set_machines_json(QString::from(machines_json)); + .set_failed_units_json(QString::from(failed_json)); + self.as_mut().set_failed_count(count); + self.as_mut() + .set_containers_json(QString::from(containers_json)); } fn restart_unit(self: Pin<&mut Self>, name: QString, scope: QString, machine: QString) { diff --git a/shell/applets/SystemdApplet.qml b/shell/applets/SystemdApplet.qml index 1f25f5d..bd2f1aa 100644 --- a/shell/applets/SystemdApplet.qml +++ b/shell/applets/SystemdApplet.qml @@ -13,31 +13,38 @@ Column { onHeightChanged: root.contentResized() + // Local machine header + units + SystemdMachineSection { + width: root.width + accentColor: root.accentColor + machineName: "" + title: S.SystemdService.hostname + marker: " this machine" + systemState: S.SystemdService.systemState + units: S.SystemdService.failedUnits + startExpanded: true + } + + // Containers Repeater { - model: S.SystemdService.machines + model: S.SystemdService.containers delegate: Column { id: _row required property var modelData - required property int index width: root.width - Separator { - visible: _row.index > 0 - } + Separator {} SystemdMachineSection { width: _row.width accentColor: root.accentColor - machineName: _row.modelData.isLocal ? "" : _row.modelData.name + machineName: _row.modelData.name title: _row.modelData.name - marker: _row.modelData.marker ?? "" + marker: "" systemState: _row.modelData.systemState ?? "unknown" - runningCount: _row.modelData.runningCount ?? 0 - totalCount: _row.modelData.totalCount ?? 0 - failedUnits: _row.modelData.failedUnits ?? [] - runningUnits: _row.modelData.runningUnits ?? [] - onContentResized: root.contentResized() + units: _row.modelData.failedUnits ?? [] + startExpanded: (_row.modelData.failedUnits ?? []).length > 0 } } } diff --git a/shell/applets/SystemdMachineSection.qml b/shell/applets/SystemdMachineSection.qml index b5329f2..632c9db 100644 --- a/shell/applets/SystemdMachineSection.qml +++ b/shell/applets/SystemdMachineSection.qml @@ -4,9 +4,8 @@ import QtQuick import "../services" as S import NovaStats as NS -// One section of the systemd applet: a header with title, aggregate counts, -// state chip; an auto-expanded list of failed units (hidden when empty); a -// lazy-loaded, collapsed-by-default list of running units. +// One section of the systemd applet: a header with title + state chip, +// expandable list of failed units underneath. Column { id: root @@ -15,21 +14,13 @@ Column { required property string title required property string marker required property string systemState - required property int runningCount - required property int totalCount - required property var failedUnits - required property var runningUnits - - signal contentResized - onHeightChanged: root.contentResized() + required property var units + property bool startExpanded: false width: parent?.width ?? 0 - property bool _runningExpanded: false + property bool _expanded: startExpanded - readonly property int _failedCount: (failedUnits ?? []).length - - // Header Item { width: root.width height: 32 @@ -60,29 +51,10 @@ Column { elide: Text.ElideRight } - // Aggregate counts: "n running, m/total failed" or "n running" if no failures. - Text { - id: _counts - anchors.right: _stateChip.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - text: { - if (root.totalCount === 0) - return ""; - const r = root.runningCount + " running"; - if (root._failedCount > 0) - return r + ", " + root._failedCount + "/" + root.totalCount + " failed"; - return r; - } - color: NS.ThemeService.base04 - font.pixelSize: NS.ThemeService.fontSize - 3 - font.family: NS.ThemeService.fontFamily - } - Rectangle { id: _stateChip - anchors.right: parent.right - anchors.rightMargin: 12 + anchors.right: _chevron.left + anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter visible: root.systemState !== "unknown" color: { @@ -91,8 +63,6 @@ Column { return NS.ThemeService.base0B; if (st === "degraded") return NS.ThemeService.base0A; - if (st === "pending") - return NS.ThemeService.base04; return NS.ThemeService.base08; } opacity: 0.85 @@ -109,71 +79,25 @@ Column { font.family: NS.ThemeService.fontFamily } } - } - - // Failed units (auto-expanded; entire block hidden when there are none). - Repeater { - model: root._failedCount > 0 ? root.failedUnits : [] - delegate: SystemdUnitRow { - required property var modelData - unitName: modelData.name - description: modelData.description ?? "" - subState: modelData.subState ?? "" - scope: modelData.scope ?? "system" - machineName: root.machineName - accentColor: root.accentColor - } - } - - // Running units toggle row (only meaningful when there are running units to show). - Item { - visible: (root.runningUnits ?? []).length > 0 - width: root.width - height: 26 - - Rectangle { - anchors.fill: parent - anchors.leftMargin: 4 - anchors.rightMargin: 4 - color: _runHdrHover.hovered ? NS.ThemeService.base02 : "transparent" - radius: NS.ThemeService.radius - z: -1 - } - - HoverHandler { - id: _runHdrHover - } - - Text { - anchors.left: parent.left - anchors.leftMargin: 24 - anchors.verticalCenter: parent.verticalCenter - text: "running units (" + (root.runningUnits ?? []).length + ")" - color: NS.ThemeService.base04 - font.pixelSize: NS.ThemeService.fontSize - 2 - font.family: NS.ThemeService.fontFamily - font.letterSpacing: 1 - } Text { + id: _chevron anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter - text: root._runningExpanded ? "" : "" + text: root._expanded ? "" : "" color: NS.ThemeService.base04 font.pixelSize: NS.ThemeService.fontSize - 2 font.family: NS.ThemeService.iconFontFamily } TapHandler { - onTapped: root._runningExpanded = !root._runningExpanded + onTapped: root._expanded = !root._expanded } } - // Lazy-loaded running units list. Repeater materializes rows only when the - // model is non-empty, so feeding `[]` while collapsed avoids per-row cost. Repeater { - model: root._runningExpanded ? root.runningUnits : [] + model: root._expanded ? root.units : [] delegate: SystemdUnitRow { required property var modelData unitName: modelData.name diff --git a/shell/services/SystemdService.qml b/shell/services/SystemdService.qml index 3a82048..4ba5f38 100644 --- a/shell/services/SystemdService.qml +++ b/shell/services/SystemdService.qml @@ -4,18 +4,29 @@ import QtQuick import NovaStats as NS // Thin wrapper around NS.SystemdService: drives the poll Timer and parses the -// JSON-encoded machine tree into a JS array for QML consumers. +// JSON-encoded list properties into JS arrays for QML consumers. Restart helper +// proxies through to the Rust singleton. QtObject { id: root readonly property string hostname: NS.SystemdService.hostname + readonly property string systemState: NS.SystemdService.systemState + readonly property string userState: NS.SystemdService.userState readonly property int failedCount: NS.SystemdService.failedCount - // [{ name, isLocal, marker, systemState, runningCount, totalCount, - // failedUnits: [...], runningUnits: [...] }, ...] - readonly property var machines: { + // Parsed [{ name, description, subState, scope, machine }, ...] + readonly property var failedUnits: { try { - return JSON.parse(NS.SystemdService.machinesJson); + return JSON.parse(NS.SystemdService.failedUnitsJson); + } catch (e) { + return []; + } + } + + // Parsed [{ name, class, service, systemState, failedUnits: [...] }, ...] + readonly property var containers: { + try { + return JSON.parse(NS.SystemdService.containersJson); } catch (e) { return []; }