merge systemd + machinectl into one module/applet (step 1)

This commit is contained in:
Damocles 2026-05-07 17:42:10 +02:00
parent 96bfc1fd5a
commit 6fc7e8bc8a
16 changed files with 274 additions and 1042 deletions

View file

@ -24,7 +24,13 @@ dirs = "6.0.0"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
zbus = { version = "5.15.0", default-features = false, features = ["tokio"] }
tokio = { version = "1.52.2", features = ["macros", "net", "rt", "sync", "time"] }
tokio = { version = "1.52.2", features = [
"macros",
"net",
"rt",
"sync",
"time",
] }
[build-dependencies]
cxx-qt-build = "0.8.1"

View file

@ -101,11 +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")]
// Systemd / machinectl bar modules.
// 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(bool, machinectl_enable, cxx_name = "machinectlEnable")]
#[qproperty(i32, machinectl_interval, cxx_name = "machinectlInterval")]
type ModulesService = super::ModulesServiceRust;
}
@ -522,8 +520,6 @@ mod data {
#[serde(default)]
pub systemd: WithInterval,
#[serde(default)]
pub machinectl: WithInterval,
#[serde(default)]
pub stats_daemon: StatsDaemon,
}
}
@ -599,8 +595,6 @@ pub struct ModulesServiceRust {
dock_applet_power: bool,
systemd_enable: bool,
systemd_interval: i32,
machinectl_enable: bool,
machinectl_interval: i32,
}
impl Default for ModulesServiceRust {
@ -684,8 +678,6 @@ impl ModulesServiceRust {
dock_applet_power: d.dock.applets.power,
systemd_enable: d.systemd.enable,
systemd_interval: d.systemd.interval,
machinectl_enable: d.machinectl.enable,
machinectl_interval: d.machinectl.interval,
}
}
}
@ -746,7 +738,6 @@ fn parse_modules(raw: &str, path: &std::path::Path) -> ModulesData {
"lock",
"dock",
"systemd",
"machinectl",
"statsDaemon",
];
for key in map.keys() {

View file

@ -12,7 +12,7 @@ use cxx_qt_lib::QString;
use serde::Serialize;
use std::sync::OnceLock;
use tokio::runtime::Runtime;
use zbus::{proxy, Connection};
use zbus::{Connection, proxy};
#[cxx_qt::bridge]
pub mod qobject {
@ -72,11 +72,8 @@ trait SystemdManager {
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>;
fn restart_unit(&self, name: &str, mode: &str)
-> zbus::Result<zbus::zvariant::OwnedObjectPath>;
}
#[proxy(
@ -242,9 +239,11 @@ impl qobject::SystemdService {
self.as_mut().set_system_state(QString::from(sys_state));
self.as_mut().set_user_state(QString::from(user_state));
self.as_mut().set_failed_units_json(QString::from(failed_json));
self.as_mut()
.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));
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

@ -1,354 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import "../services" as S
import NovaStats as NS
Column {
id: root
required property color accentColor
property bool active: true
property bool _localExpanded: true
// Localhost section header
Item {
width: root.width
height: 32
Rectangle {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
color: _localHdrHover.hovered ? NS.ThemeService.base02 : "transparent"
radius: NS.ThemeService.radius
z: -1
}
HoverHandler {
id: _localHdrHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: " localhost"
color: NS.ThemeService.base05
font.pixelSize: NS.ThemeService.fontSize
font.family: NS.ThemeService.fontFamily
}
Rectangle {
id: _localStateChip
anchors.right: _localChevron.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
visible: S.SystemdService.systemState !== "unknown"
color: {
const st = S.SystemdService.systemState;
if (st === "running")
return NS.ThemeService.base0B;
if (st === "degraded")
return NS.ThemeService.base0A;
return NS.ThemeService.base08;
}
opacity: 0.85
radius: 3
width: _localStateLbl.width + 8
height: 14
Text {
id: _localStateLbl
anchors.centerIn: parent
text: S.SystemdService.systemState
color: NS.ThemeService.base00
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
}
}
Text {
id: _localChevron
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._localExpanded ? "" : ""
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.iconFontFamily
}
TapHandler {
onTapped: root._localExpanded = !root._localExpanded
}
}
// Localhost expanded content
Column {
visible: root._localExpanded
width: root.width
// System sub-section
Item {
width: root.width
height: 22
Text {
anchors.left: parent.left
anchors.leftMargin: 24
anchors.verticalCenter: parent.verticalCenter
text: "SYSTEM"
color: NS.ThemeService.base03
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
font.letterSpacing: 1
}
}
Repeater {
model: S.SystemdService.systemUnits
delegate: SystemdUnitRow {
required property var modelData
unitName: modelData.name
description: modelData.description
subState: modelData.subState
isUser: false
machineName: ""
accentColor: root.accentColor
}
}
Item {
visible: S.SystemdService.systemUnits.length === 0
width: root.width
height: 22
Text {
anchors.centerIn: parent
text: "no failures"
color: NS.ThemeService.base0B
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.fontFamily
}
}
// User sub-section
Item {
width: root.width
height: 22
Text {
anchors.left: parent.left
anchors.leftMargin: 24
anchors.verticalCenter: parent.verticalCenter
text: "USER"
color: NS.ThemeService.base03
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
font.letterSpacing: 1
}
}
Repeater {
model: S.SystemdService.userUnits
delegate: SystemdUnitRow {
required property var modelData
unitName: modelData.name
description: modelData.description
subState: modelData.subState
isUser: true
machineName: ""
accentColor: root.accentColor
}
}
Item {
visible: S.SystemdService.userUnits.length === 0
width: root.width
height: 22
Text {
anchors.centerIn: parent
text: "no failures"
color: NS.ThemeService.base0B
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.fontFamily
}
}
}
// Containers
Repeater {
model: S.MachinectlService.machines
delegate: Column {
id: _machineSection
required property var modelData
required property int index
property bool _expanded: false
property bool _loading: false
width: root.width
Connections {
target: S.MachinectlService
function onMachineReady(machineName) {
if (machineName === _machineSection.modelData.name)
_machineSection._loading = false;
}
}
Separator {}
// Machine header
Item {
width: _machineSection.width
height: 32
Rectangle {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
color: _mHdrHover.hovered ? NS.ThemeService.base02 : "transparent"
radius: NS.ThemeService.radius
z: -1
}
HoverHandler {
id: _mHdrHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: " " + _machineSection.modelData.name
color: NS.ThemeService.base05
font.pixelSize: NS.ThemeService.fontSize
font.family: NS.ThemeService.fontFamily
elide: Text.ElideRight
width: parent.width - 100
}
Rectangle {
id: _mStateChip
anchors.right: _mChevron.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
visible: _machineSection._expanded && !_machineSection._loading
color: {
const st = S.MachinectlService.machineState(_machineSection.modelData.name);
if (st === "running")
return NS.ThemeService.base0B;
if (st === "degraded")
return NS.ThemeService.base0A;
return st === "unknown" ? "transparent" : NS.ThemeService.base08;
}
opacity: 0.85
radius: 3
width: _mStateLbl.width + 8
height: 14
Text {
id: _mStateLbl
anchors.centerIn: parent
text: S.MachinectlService.machineState(_machineSection.modelData.name)
color: NS.ThemeService.base00
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
}
}
Text {
id: _mChevron
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: _machineSection._expanded ? "" : ""
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.iconFontFamily
}
TapHandler {
onTapped: {
_machineSection._expanded = !_machineSection._expanded;
if (_machineSection._expanded) {
_machineSection._loading = true;
S.MachinectlService.fetchMachine(_machineSection.modelData.name);
}
}
}
}
// Machine expanded content
Column {
visible: _machineSection._expanded
width: _machineSection.width
Item {
visible: _machineSection._loading
width: _machineSection.width
height: 28
Text {
anchors.centerIn: parent
text: "loading..."
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.fontFamily
}
}
// System units inside the container
Item {
visible: !_machineSection._loading
width: _machineSection.width
height: 22
Text {
anchors.left: parent.left
anchors.leftMargin: 24
anchors.verticalCenter: parent.verticalCenter
text: "SYSTEM"
color: NS.ThemeService.base03
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
font.letterSpacing: 1
}
}
Repeater {
model: !_machineSection._loading ? S.MachinectlService.machineUnits(_machineSection.modelData.name) : []
delegate: SystemdUnitRow {
required property var modelData
unitName: modelData.name
description: modelData.description
subState: modelData.subState
isUser: false
machineName: _machineSection.modelData.name
accentColor: root.accentColor
}
}
Item {
visible: !_machineSection._loading && S.MachinectlService.machineUnits(_machineSection.modelData.name).length === 0
width: _machineSection.width
height: 22
Text {
anchors.centerIn: parent
text: "no failures"
color: NS.ThemeService.base0B
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.fontFamily
}
}
}
}
}
Item {
width: 1
height: 4
}
}

View file

@ -9,157 +9,44 @@ Column {
required property color accentColor
property bool active: true
// Emitted when content resizes; parent can connect to extend panel-close grace.
signal contentResized
// Section header: state label + unit count
Item {
width: root.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "SYSTEM"
color: NS.ThemeService.base03
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
font.letterSpacing: 1
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
visible: S.SystemdService.systemState !== "unknown"
color: {
const st = S.SystemdService.systemState;
if (st === "running")
return NS.ThemeService.base0B;
if (st === "degraded")
return NS.ThemeService.base0A;
return NS.ThemeService.base08;
}
opacity: 0.85
radius: 3
width: _sysStateLbl.width + 8
height: 14
Text {
id: _sysStateLbl
anchors.centerIn: parent
text: S.SystemdService.systemState
color: NS.ThemeService.base00
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
}
}
}
Repeater {
model: S.SystemdService.systemUnits
delegate: SystemdUnitRow {
required property var modelData
unitName: modelData.name
description: modelData.description
subState: modelData.subState
isUser: false
machineName: ""
accentColor: root.accentColor
onHeightChanged: root.contentResized()
}
}
Item {
visible: S.SystemdService.systemUnits.length === 0
// Local machine header + units
SystemdMachineSection {
width: root.width
height: 24
accentColor: root.accentColor
machineName: ""
title: S.SystemdService.hostname
marker: " this machine"
systemState: S.SystemdService.systemState
units: S.SystemdService.failedUnits
startExpanded: true
}
Text {
anchors.centerIn: parent
text: "no failed system units"
color: NS.ThemeService.base0B
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.fontFamily
}
}
// Containers
Repeater {
model: S.SystemdService.containers
delegate: Column {
id: _row
required property var modelData
width: root.width
Separator {}
// User section
Item {
width: root.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "USER"
color: NS.ThemeService.base03
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
font.letterSpacing: 1
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
visible: S.SystemdService.userState !== "unknown"
color: {
const st = S.SystemdService.userState;
if (st === "running")
return NS.ThemeService.base0B;
if (st === "degraded")
return NS.ThemeService.base0A;
return NS.ThemeService.base08;
}
opacity: 0.85
radius: 3
width: _userStateLbl.width + 8
height: 14
Text {
id: _userStateLbl
anchors.centerIn: parent
text: S.SystemdService.userState
color: NS.ThemeService.base00
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
}
}
}
Repeater {
model: S.SystemdService.userUnits
delegate: SystemdUnitRow {
required property var modelData
unitName: modelData.name
description: modelData.description
subState: modelData.subState
isUser: true
machineName: ""
SystemdMachineSection {
width: _row.width
accentColor: root.accentColor
onHeightChanged: root.contentResized()
machineName: _row.modelData.name
title: _row.modelData.name
marker: ""
systemState: _row.modelData.systemState ?? "unknown"
units: _row.modelData.failedUnits ?? []
startExpanded: (_row.modelData.failedUnits ?? []).length > 0
}
}
Item {
visible: S.SystemdService.userUnits.length === 0
width: root.width
height: 24
Text {
anchors.centerIn: parent
text: "no failed user units"
color: NS.ThemeService.base0B
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.fontFamily
}
}
Item {

View file

@ -0,0 +1,111 @@
pragma ComponentBehavior: Bound
import QtQuick
import "../services" as S
import NovaStats as NS
// One section of the systemd applet: a header with title + state chip,
// expandable list of failed units underneath.
Column {
id: root
required property color accentColor
required property string machineName
required property string title
required property string marker
required property string systemState
required property var units
property bool startExpanded: false
width: parent?.width ?? 0
property bool _expanded: startExpanded
Item {
width: root.width
height: 32
Rectangle {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
color: _hdrHover.hovered ? NS.ThemeService.base02 : "transparent"
radius: NS.ThemeService.radius
z: -1
}
HoverHandler {
id: _hdrHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: _stateChip.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: root.title + (root.marker !== "" ? " " + root.marker : "")
color: NS.ThemeService.base05
font.pixelSize: NS.ThemeService.fontSize
font.family: NS.ThemeService.fontFamily
elide: Text.ElideRight
}
Rectangle {
id: _stateChip
anchors.right: _chevron.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
visible: root.systemState !== "unknown"
color: {
const st = root.systemState;
if (st === "running")
return NS.ThemeService.base0B;
if (st === "degraded")
return NS.ThemeService.base0A;
return NS.ThemeService.base08;
}
opacity: 0.85
radius: 3
width: _stateLbl.width + 8
height: 14
Text {
id: _stateLbl
anchors.centerIn: parent
text: root.systemState
color: NS.ThemeService.base00
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
}
}
Text {
id: _chevron
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._expanded ? "" : ""
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.iconFontFamily
}
TapHandler {
onTapped: root._expanded = !root._expanded
}
}
Repeater {
model: root._expanded ? root.units : []
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
}
}
}

View file

@ -8,66 +8,16 @@ Item {
required property string unitName
required property string description
required property string subState
required property bool isUser
property string machineName: ""
required property string scope
required property string machineName
required property color accentColor
property string _jText: ""
property bool _expanded: false
property bool _loading: false
width: parent?.width ?? 0
height: _row.height + (_expanded ? _journalArea.height : 0)
height: 28
Behavior on height {
NumberAnimation {
duration: 150
HoverHandler {
id: _rowHover
}
}
Connections {
target: S.SystemdService
enabled: root.machineName === ""
function onJournalReady(unitName, isUser, text) {
if (unitName === root.unitName && isUser === root.isUser) {
root._jText = text;
root._loading = false;
}
}
}
Connections {
target: S.MachinectlService
enabled: root.machineName !== ""
function onMachineJournalReady(mname, unitName, text) {
if (mname === root.machineName && unitName === root.unitName) {
root._jText = text;
root._loading = false;
}
}
}
function _fetchJournal() {
root._loading = true;
root._jText = "";
if (root.machineName === "")
S.SystemdService.fetchJournal(root.unitName, root.isUser);
else
S.MachinectlService.fetchMachineJournal(root.machineName, root.unitName);
}
function _doRestart() {
if (root.machineName === "")
S.SystemdService.restartUnit(root.unitName, root.isUser);
else
S.MachinectlService.restartMachineUnit(root.machineName, root.unitName);
}
// Row
Item {
id: _row
width: parent.width
height: 32
Rectangle {
anchors.fill: parent
@ -78,10 +28,6 @@ Item {
z: -1
}
HoverHandler {
id: _rowHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
@ -116,18 +62,24 @@ Item {
}
}
// Restart button - = fa-refresh
// Restart button only visible while hovering the row. = fa-refresh
Item {
id: _restartBtn
anchors.right: _expandBtn.left
anchors.rightMargin: 2
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
width: 24
height: 24
opacity: _rowHover.hovered ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 80
}
}
Text {
anchors.centerIn: parent
text: ""
text: ""
color: _rHover.hovered ? root.accentColor : NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize
font.family: NS.ThemeService.iconFontFamily
@ -143,98 +95,8 @@ Item {
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
root._doRestart();
if (root._expanded) {
root._jText = "";
Qt.callLater(root._fetchJournal);
}
}
}
}
// Expand chevron - = fa-chevron-down, = fa-chevron-up
Item {
id: _expandBtn
anchors.right: parent.right
anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter
width: 24
height: 24
Text {
anchors.centerIn: parent
text: root._expanded ? "" : ""
color: _expHover.hovered ? root.accentColor : NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 1
font.family: NS.ThemeService.iconFontFamily
Behavior on color {
ColorAnimation {
duration: 80
}
}
}
HoverHandler {
id: _expHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
root._expanded = !root._expanded;
if (root._expanded && root._jText === "")
root._fetchJournal();
}
}
}
}
// Journal area
Item {
id: _journalArea
anchors.top: _row.bottom
width: parent.width
height: 120
visible: root._expanded
clip: true
Rectangle {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
anchors.bottomMargin: 4
color: NS.ThemeService.base01
radius: NS.ThemeService.radius
Text {
anchors.centerIn: parent
visible: root._loading
text: "loading..."
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.fontFamily
}
Flickable {
id: _flick
anchors.fill: parent
anchors.margins: 6
visible: !root._loading
contentHeight: _jContent.height
clip: true
Text {
id: _jContent
width: _flick.width
text: root._jText
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
wrapMode: Text.WrapAnywhere
}
onContentHeightChanged: contentY = Math.max(0, contentHeight - height)
}
enabled: _rowHover.hovered
onTapped: S.SystemdService.restartUnit(root.unitName, root.scope, root.machineName)
}
}
}

View file

@ -10,7 +10,6 @@ GpuApplet 1.0 GpuApplet.qml
HexWaveBackground 1.0 HexWaveBackground.qml
HoverableListItem 1.0 HoverableListItem.qml
InfoRow 1.0 InfoRow.qml
MachinectlApplet 1.0 MachinectlApplet.qml
MemoryApplet 1.0 MemoryApplet.qml
MprisApplet 1.0 MprisApplet.qml
NetworkApplet 1.0 NetworkApplet.qml
@ -19,6 +18,7 @@ PowerApplet 1.0 PowerApplet.qml
Separator 1.0 Separator.qml
SparklineCanvas 1.0 SparklineCanvas.qml
SystemdApplet 1.0 SystemdApplet.qml
SystemdMachineSection 1.0 SystemdMachineSection.qml
SystemdUnitRow 1.0 SystemdUnitRow.qml
TemperatureApplet 1.0 TemperatureApplet.qml
VolumeApplet 1.0 VolumeApplet.qml

View file

@ -187,7 +187,6 @@ PanelWindow {
M.WeatherModule {}
M.DiskModule {}
M.SystemdModule {}
M.MachinectlModule {}
}
// Power + Dock

View file

@ -1,47 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import "." as M
import "../services" as S
import "../applets" as C
import NovaStats as NS
M.BarModule {
id: root
active: NS.ModulesService.machinectlEnable
tooltip: {
const n = S.MachinectlService.machines.length;
return n === 0 ? "no containers" : n + " container" + (n === 1 ? "" : "s");
}
panelNamespace: "nova-machinectl"
panelContentWidth: 320
panelComponent: Component {
C.MachinectlApplet {
width: parent.width
accentColor: root.accentColor
active: root._showPanel
}
}
Connections {
target: S.MachinectlService
function onMachineReady() {
root.keepPanelOpen(300);
}
}
readonly property color _stateColor: S.MachinectlService.anyUnhealthy ? NS.ThemeService.base0A : root.accentColor
M.BarIcon {
icon: ""
color: root._stateColor
anchors.verticalCenter: parent.verticalCenter
}
M.BarLabel {
label: S.MachinectlService.machines.length.toString()
minText: "9"
color: root._stateColor
anchors.verticalCenter: parent.verticalCenter
}
}

View file

@ -1,7 +1,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import "." as M
import "../services" as S
import "../applets" as C
@ -11,12 +10,11 @@ M.BarModule {
id: root
active: NS.ModulesService.systemdEnable
tooltip: {
const sys = S.SystemdService.systemState;
const fc = S.SystemdService.totalFailedCount;
return "systemd: " + sys + (fc > 0 ? " (" + fc + " failed)" : "");
const fc = S.SystemdService.failedCount;
return fc === 0 ? "systemd: ok" : "systemd: " + fc + " failed";
}
panelNamespace: "nova-systemd"
panelContentWidth: 300
panelContentWidth: 320
panelComponent: Component {
C.SystemdApplet {
width: parent.width
@ -28,22 +26,12 @@ M.BarModule {
Connections {
target: S.SystemdService
function onSystemUnitsChanged() {
root.keepPanelOpen(300);
}
function onUserUnitsChanged() {
function onFailedUnitsChanged() {
root.keepPanelOpen(300);
}
}
readonly property color _stateColor: {
const st = S.SystemdService.systemState;
if (st === "running")
return root.accentColor;
if (st === "degraded")
return NS.ThemeService.base0A;
return NS.ThemeService.base08;
}
readonly property color _stateColor: S.SystemdService.failedCount > 0 ? NS.ThemeService.base0A : root.accentColor
M.BarIcon {
icon: ""
@ -51,8 +39,8 @@ M.BarModule {
anchors.verticalCenter: parent.verticalCenter
}
M.BarLabel {
label: S.SystemdService.totalFailedCount > 0 ? S.SystemdService.totalFailedCount + " failed" : S.SystemdService.systemState
minText: "degraded"
label: S.SystemdService.failedCount > 0 ? S.SystemdService.failedCount + " failed" : "ok"
minText: "0 failed"
color: root._stateColor
anchors.verticalCenter: parent.verticalCenter
}

View file

@ -16,7 +16,6 @@ DockModule 1.0 DockModule.qml
GpuModule 1.0 GpuModule.qml
HoverPanel 1.0 HoverPanel.qml
IdleInhibitorModule 1.0 IdleInhibitorModule.qml
MachinectlModule 1.0 MachinectlModule.qml
MemoryModule 1.0 MemoryModule.qml
MprisModule 1.0 MprisModule.qml
NetworkModule 1.0 NetworkModule.qml

View file

@ -1,152 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
import NovaStats as NS
QtObject {
id: root
property var machines: []
// cache: machineName -> {state, units, loading}
property var _cache: ({})
readonly property bool anyUnhealthy: {
for (const k of Object.keys(_cache)) {
if ((_cache[k]?.units?.length ?? 0) > 0)
return true;
}
return false;
}
function machineState(name) {
return _cache[name]?.state ?? "unknown";
}
function machineUnits(name) {
return _cache[name]?.units ?? [];
}
function machineLoading(name) {
return _cache[name]?.loading ?? false;
}
signal machineReady(string machineName, string state, var units)
signal machineJournalReady(string machineName, string unitName, string text)
// Per-call state: kept on root (not on the inline Process objects) so qmllint can see them.
property string _machineName: ""
property string _journalMachine: ""
property string _journalUnit: ""
property string _restartMachine: ""
function fetchMachine(name) {
const c = Object.assign({}, _cache);
c[name] = Object.assign({}, c[name] ?? {}, {
loading: true
});
_cache = c;
root._machineName = name;
_machineProc.command = ["sh", "-c", "busctl get-property --json=short --machine=" + name + " org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager SystemState 2>/dev/null || echo '{}'; " + "busctl call --json=short --machine=" + name + " org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager ListUnitsFiltered as 1 failed 2>/dev/null || echo '{}'"];
if (_machineProc.running)
_machineProc.running = false;
_machineProc.running = true;
}
function fetchMachineJournal(machineName, unitName) {
root._journalMachine = machineName;
root._journalUnit = unitName;
_machineJournalProc.command = ["journalctl", "-M", machineName, "-u", unitName, "-n", "80", "--no-pager", "--output=short-precise"];
if (_machineJournalProc.running)
_machineJournalProc.running = false;
_machineJournalProc.running = true;
}
function restartMachineUnit(machineName, unitName) {
root._restartMachine = machineName;
_machineRestartProc.command = ["pkexec", "systemctl", "-M", machineName, "restart", unitName];
if (_machineRestartProc.running)
_machineRestartProc.running = false;
_machineRestartProc.running = true;
}
property Timer _poll: Timer {
interval: NS.ModulesService.machinectlInterval ?? 15000
running: NS.ModulesService.machinectlEnable
repeat: true
triggeredOnStart: true
onTriggered: if (!root._listProc.running)
root._listProc.running = true
}
property Process _listProc: Process {
command: ["busctl", "call", "--json=short", "org.freedesktop.machine1", "/org/freedesktop/machine1", "org.freedesktop.machine1.Manager", "ListMachines"]
stdout: StdioCollector {
onStreamFinished: {
try {
const parsed = JSON.parse(text.trim());
const newMachines = (parsed.data?.[0] || []).map(m => ({
name: m[0],
class: m[1],
service: m[2]
}));
root.machines = newMachines;
// drop stale cache entries
const names = new Set(newMachines.map(m => m.name));
const c = {};
for (const k of Object.keys(root._cache)) {
if (names.has(k))
c[k] = root._cache[k];
}
root._cache = c;
} catch (e) {
root.machines = [];
}
}
}
}
// 2 lines of output: state, failed-units
property Process _machineProc: Process {
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n");
let state = "unknown";
let units = [];
try {
state = JSON.parse(lines[0] ?? "").data || "unknown";
} catch (e) {}
try {
const parsed = JSON.parse(lines[1] ?? "");
units = (parsed.data?.[0] || []).map(u => ({
name: u[0],
description: u[1],
loadState: u[2],
activeState: u[3],
subState: u[4]
}));
} catch (e) {}
const c = Object.assign({}, root._cache);
c[root._machineName] = {
state: state,
units: units,
loading: false
};
root._cache = c;
root.machineReady(root._machineName, state, units);
}
}
}
property Process _machineJournalProc: Process {
stdout: StdioCollector {
onStreamFinished: root.machineJournalReady(root._journalMachine, root._journalUnit, text)
}
}
property Process _machineRestartProc: Process {
onRunningChanged: if (!running && root._restartMachine !== "")
root.fetchMachine(root._restartMachine)
}
}

View file

@ -1,100 +1,53 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
import NovaStats as NS
// Thin wrapper around NS.SystemdService: drives the poll Timer and parses the
// JSON-encoded list properties into JS arrays for QML consumers. Restart helper
// proxies through to the Rust singleton.
QtObject {
id: root
property string systemState: "unknown"
property string userState: "unknown"
property var systemUnits: []
property var userUnits: []
readonly property int totalFailedCount: systemUnits.length + userUnits.length
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
signal journalReady(string unitName, bool isUser, string text)
function refresh() {
if (!_pollProc.running)
_pollProc.running = true;
}
// Per-fetch journal state: kept here (not on the Process) so qmllint can see them.
property string _journalUnitName: ""
property bool _journalIsUser: false
function fetchJournal(unitName, isUser) {
_journalProc.command = isUser ? ["journalctl", "--user", "-u", unitName, "-n", "80", "--no-pager", "--output=short-precise"] : ["journalctl", "-u", unitName, "-n", "80", "--no-pager", "--output=short-precise"];
root._journalUnitName = unitName;
root._journalIsUser = isUser;
if (_journalProc.running)
_journalProc.running = false;
_journalProc.running = true;
}
function restartUnit(unitName, isUser) {
_restartProc.command = isUser ? ["systemctl", "--user", "restart", unitName] : ["pkexec", "systemctl", "restart", unitName];
if (_restartProc.running)
_restartProc.running = false;
_restartProc.running = true;
}
function _parseState(json) {
// Parsed [{ name, description, subState, scope, machine }, ...]
readonly property var failedUnits: {
try {
return JSON.parse(json).data || "unknown";
} catch (e) {
return "unknown";
}
}
function _parseUnits(json) {
try {
const parsed = JSON.parse(json);
return (parsed.data?.[0] || []).map(u => ({
name: u[0],
description: u[1],
loadState: u[2],
activeState: u[3],
subState: u[4]
}));
return JSON.parse(NS.SystemdService.failedUnitsJson);
} catch (e) {
return [];
}
}
property Timer _poll: Timer {
interval: NS.ModulesService.systemdInterval ?? 15000
// Parsed [{ name, class, service, systemState, failedUnits: [...] }, ...]
readonly property var containers: {
try {
return JSON.parse(NS.SystemdService.containersJson);
} catch (e) {
return [];
}
}
function restartUnit(name, scope, machine) {
NS.SystemdService.restartUnit(name, scope ?? "system", machine ?? "");
}
function refresh() {
NS.SystemdService.poll();
}
property Timer _pollTimer: Timer {
interval: {
const ms = NS.ModulesService.systemdInterval;
return ms > 0 ? ms : 15000;
}
running: NS.ModulesService.systemdEnable
repeat: true
triggeredOnStart: true
onTriggered: if (!root._pollProc.running)
root._pollProc.running = true
}
// 4 lines of output: systemState, systemUnits, userState, userUnits
property Process _pollProc: Process {
command: ["sh", "-c", "busctl get-property --json=short org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager SystemState 2>/dev/null || echo '{}'; " + "busctl call --json=short org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager ListUnitsFiltered as 1 failed 2>/dev/null || echo '{}'; " + "busctl --user get-property --json=short org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager SystemState 2>/dev/null || echo '{}'; " + "busctl --user call --json=short org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager ListUnitsFiltered as 1 failed 2>/dev/null || echo '{}'"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n");
root.systemState = root._parseState(lines[0] ?? "");
root.systemUnits = root._parseUnits(lines[1] ?? "");
root.userState = root._parseState(lines[2] ?? "");
root.userUnits = root._parseUnits(lines[3] ?? "");
}
}
}
property Process _journalProc: Process {
stdout: StdioCollector {
onStreamFinished: root.journalReady(root._journalUnitName, root._journalIsUser, text)
}
}
property Process _restartProc: Process {
onRunningChanged: if (!running)
root.refresh()
triggeredOnStart: false
onTriggered: NS.SystemdService.poll()
}
}

View file

@ -8,7 +8,6 @@ singleton CpuService 1.0 CpuService.qml
singleton DockState 1.0 DockState.qml
singleton IdleInhibitService 1.0 IdleInhibitService.qml
singleton LockService 1.0 LockService.qml
singleton MachinectlService 1.0 MachinectlService.qml
singleton MprisService 1.0 MprisService.qml
singleton NetworkService 1.0 NetworkService.qml
singleton NiriIpc 1.0 NiriIpc.qml

View file

@ -36,14 +36,6 @@ shell/applets/HexWaveBackground.qml: Type "QColor" of property "base0E" not foun
shell/applets/HoverableListItem.qml: Type "QColor" of property "base02" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/InfoRow.qml: Type "QColor" of property "base04" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/InfoRow.qml: Type "QColor" of property "base05" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MachinectlApplet.qml: Type "QColor" of property "base00" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MachinectlApplet.qml: Type "QColor" of property "base02" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MachinectlApplet.qml: Type "QColor" of property "base03" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MachinectlApplet.qml: Type "QColor" of property "base04" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MachinectlApplet.qml: Type "QColor" of property "base05" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MachinectlApplet.qml: Type "QColor" of property "base08" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MachinectlApplet.qml: Type "QColor" of property "base0A" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MachinectlApplet.qml: Type "QColor" of property "base0B" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MemoryApplet.qml: Type "QColor" of property "base02" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MemoryApplet.qml: Type "QColor" of property "base03" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/MemoryApplet.qml: Type "QColor" of property "base0D" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
@ -78,13 +70,14 @@ shell/applets/PowerApplet.qml: Type "QColor" of property "base0D" not found. Thi
shell/applets/PowerApplet.qml: Type "QColor" of property "base0E" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/PowerApplet.qml: Unqualified access [unqualified]
shell/applets/Separator.qml: Type "QColor" of property "base03" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdApplet.qml: Type "QColor" of property "base00" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdApplet.qml: Type "QColor" of property "base03" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdApplet.qml: Type "QColor" of property "base08" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdApplet.qml: Type "QColor" of property "base0A" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdApplet.qml: Type "QColor" of property "base0B" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base00" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base02" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base04" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base05" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base08" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base0A" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base0B" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdUnitRow.qml: Type "QColor" of property "base00" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdUnitRow.qml: Type "QColor" of property "base01" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdUnitRow.qml: Type "QColor" of property "base02" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdUnitRow.qml: Type "QColor" of property "base04" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdUnitRow.qml: Type "QColor" of property "base05" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
@ -182,7 +175,6 @@ shell/modules/HoverPanel.qml: Type PanelWindow is not creatable. [uncreatable-ty
shell/modules/HoverPanel.qml: Type margins is used but it is not resolved [unresolved-type]
shell/modules/HoverPanel.qml: unknown grouped property scope margins. [unqualified]
shell/modules/IdleInhibitorModule.qml: Type "QColor" of property "base09" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/modules/MachinectlModule.qml: Type "QColor" of property "base0A" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/modules/MemoryModule.qml: Unqualified access [unqualified]
shell/modules/MprisModule.qml: Unqualified access [unqualified]
shell/modules/NetworkModule.qml: Type "QColor" of property "base08" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
@ -218,7 +210,6 @@ shell/modules/ScreenCorners.qml: Type PanelWindow is not creatable. [uncreatable
shell/modules/ScreenCorners.qml: Type margins is used but it is not resolved [unresolved-type]
shell/modules/ScreenCorners.qml: Unqualified access [unqualified]
shell/modules/ScreenCorners.qml: unknown grouped property scope margins. [unqualified]
shell/modules/SystemdModule.qml: Type "QColor" of property "base08" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/modules/SystemdModule.qml: Type "QColor" of property "base0A" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/modules/TemperatureModule.qml: Type "QColor" of property "base08" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/modules/TemperatureModule.qml: Type "QColor" of property "base0A" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]