Compare commits

..
5 changed files with 127 additions and 280 deletions

View file

@ -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<String>,
}
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<QString>,
}
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::<QString>::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<String> {
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) {

View file

@ -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<QVariantMap>. 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<QVariantMap> 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<QVariantMap>. 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<QVariantMap> 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<String>;
fn list_units(&self) -> zbus::Result<Vec<UnitTuple>>;
fn list_units_filtered(&self, states: Vec<&str>) -> zbus::Result<Vec<UnitTuple>>;
fn restart_unit(&self, name: &str, mode: &str)
-> zbus::Result<zbus::zvariant::OwnedObjectPath>;
@ -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<UnitJson>,
#[serde(rename = "runningUnits")]
running_units: Vec<UnitJson>,
failed_units: Vec<UnitJson<'a>>,
}
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<UnitTuple>) {
async fn fetch_failed(bus: &Connection) -> (String, Vec<UnitTuple>) {
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<UnitTuple>,
scope: &str,
machine: &str,
) -> (Vec<UnitJson>, Vec<UnitJson>, 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<UnitTuple>,
Vec<UnitTuple>,
@ -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<UnitJson<'a>> {
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<UnitJson> = 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<UnitJson> = Vec::with_capacity(sys_failed.len() + user_failed.len());
failed.extend(sys_failed);
failed.extend(user_failed);
let mut running: Vec<UnitJson> = 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<ContainerJson> = 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) {

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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 [];
}