add systemd and machinectl bar modules with applets

This commit is contained in:
Damocles 2026-05-01 18:43:35 +02:00
parent 7ab784e101
commit 8ab3fc5f6b
12 changed files with 1117 additions and 1 deletions

View file

@ -0,0 +1,351 @@
import QtQuick
import "../services" as S
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 ? S.Theme.base02 : "transparent"
radius: S.Theme.radius
z: -1
}
HoverHandler {
id: _localHdrHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: " localhost"
color: S.Theme.base05
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.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 S.Theme.base0B;
if (st === "degraded")
return S.Theme.base0A;
return S.Theme.base08;
}
opacity: 0.85
radius: 3
width: _localStateLbl.width + 8
height: 14
Text {
id: _localStateLbl
anchors.centerIn: parent
text: S.SystemdService.systemState
color: S.Theme.base00
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
}
}
Text {
id: _localChevron
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._localExpanded ? "" : ""
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.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: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.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: S.Theme.base0B
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.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: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.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: S.Theme.base0B
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.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 ? S.Theme.base02 : "transparent"
radius: S.Theme.radius
z: -1
}
HoverHandler {
id: _mHdrHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: " " + _machineSection.modelData.name
color: S.Theme.base05
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.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 S.Theme.base0B;
if (st === "degraded")
return S.Theme.base0A;
return st === "unknown" ? "transparent" : S.Theme.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: S.Theme.base00
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
}
}
Text {
id: _mChevron
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: _machineSection._expanded ? "" : ""
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.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: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.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: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.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: S.Theme.base0B
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
}
}
}
}
Item {
width: 1
height: 4
}
}

View file

@ -0,0 +1,164 @@
import QtQuick
import "../services" as S
Column {
id: root
required property color accentColor
property bool active: true
// 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: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.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 S.Theme.base0B;
if (st === "degraded")
return S.Theme.base0A;
return S.Theme.base08;
}
opacity: 0.85
radius: 3
width: _sysStateLbl.width + 8
height: 14
Text {
id: _sysStateLbl
anchors.centerIn: parent
text: S.SystemdService.systemState
color: S.Theme.base00
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.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.keepPanelOpen?.(300)
}
}
Item {
visible: S.SystemdService.systemUnits.length === 0
width: root.width
height: 24
Text {
anchors.centerIn: parent
text: "no failed system units"
color: S.Theme.base0B
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
}
Separator {}
// User section
Item {
width: root.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "USER"
color: S.Theme.base03
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.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 S.Theme.base0B;
if (st === "degraded")
return S.Theme.base0A;
return S.Theme.base08;
}
opacity: 0.85
radius: 3
width: _userStateLbl.width + 8
height: 14
Text {
id: _userStateLbl
anchors.centerIn: parent
text: S.SystemdService.userState
color: S.Theme.base00
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
}
}
}
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
onHeightChanged: root.keepPanelOpen?.(300)
}
}
Item {
visible: S.SystemdService.userUnits.length === 0
width: root.width
height: 24
Text {
anchors.centerIn: parent
text: "no failed user units"
color: S.Theme.base0B
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.fontFamily
}
}
Item {
width: 1
height: 4
}
}

View file

@ -0,0 +1,239 @@
import QtQuick
import "../services" as S
Item {
id: root
required property string unitName
required property string description
required property string subState
required property bool isUser
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)
Behavior on height {
NumberAnimation {
duration: 150
}
}
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
anchors.leftMargin: 4
anchors.rightMargin: 4
color: _rowHover.hovered ? S.Theme.base02 : "transparent"
radius: S.Theme.radius
z: -1
}
HoverHandler {
id: _rowHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: _subStateTag.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: root.unitName
color: S.Theme.base05
font.pixelSize: S.Theme.fontSize - 1
font.family: S.Theme.fontFamily
elide: Text.ElideRight
}
Rectangle {
id: _subStateTag
anchors.right: _restartBtn.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
color: S.Theme.base08
opacity: 0.85
radius: 3
width: _subStateLbl.width + 8
height: 14
Text {
id: _subStateLbl
anchors.centerIn: parent
text: root.subState
color: S.Theme.base00
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
}
}
// Restart button - = fa-refresh
Item {
id: _restartBtn
anchors.right: _expandBtn.left
anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter
width: 24
height: 24
Text {
anchors.centerIn: parent
text: ""
color: _rHover.hovered ? root.accentColor : S.Theme.base04
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily
Behavior on color {
ColorAnimation {
duration: 80
}
}
}
HoverHandler {
id: _rHover
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 : S.Theme.base04
font.pixelSize: S.Theme.fontSize - 1
font.family: S.Theme.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: S.Theme.base01
radius: S.Theme.radius
Text {
anchors.centerIn: parent
visible: root._loading
text: "loading..."
color: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 2
font.family: S.Theme.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: S.Theme.base04
font.pixelSize: S.Theme.fontSize - 3
font.family: S.Theme.fontFamily
wrapMode: Text.WrapAnywhere
}
onContentHeightChanged: contentY = Math.max(0, contentHeight - height)
}
}
}
}

View file

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

View file

@ -185,6 +185,8 @@ PanelWindow {
M.TemperatureModule {} M.TemperatureModule {}
M.WeatherModule {} M.WeatherModule {}
M.DiskModule {} M.DiskModule {}
M.SystemdModule {}
M.MachinectlModule {}
} }
// Power + Dock // Power + Dock

View file

@ -0,0 +1,44 @@
import QtQuick
import Quickshell
import "." as M
import "../services" as S
import "../applets" as C
M.BarModule {
id: root
active: S.Modules.machinectl.enable
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 ? S.Theme.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

@ -0,0 +1,55 @@
import QtQuick
import Quickshell
import "." as M
import "../services" as S
import "../applets" as C
M.BarModule {
id: root
active: S.Modules.systemd.enable
tooltip: {
const sys = S.SystemdService.systemState;
const fc = S.SystemdService.totalFailedCount;
return "systemd: " + sys + (fc > 0 ? " (" + fc + " failed)" : "");
}
panelNamespace: "nova-systemd"
panelContentWidth: 300
panelComponent: Component {
C.SystemdApplet {
width: parent.width
accentColor: root.accentColor
active: root._showPanel
}
}
Connections {
target: S.SystemdService
function onSystemUnitsChanged() {
root.keepPanelOpen(300);
}
function onUserUnitsChanged() {
root.keepPanelOpen(300);
}
}
readonly property color _stateColor: {
const st = S.SystemdService.systemState;
if (st === "running")
return root.accentColor;
if (st === "degraded")
return S.Theme.base0A;
return S.Theme.base08;
}
M.BarIcon {
icon: ""
color: root._stateColor
anchors.verticalCenter: parent.verticalCenter
}
M.BarLabel {
label: S.SystemdService.totalFailedCount > 0 ? S.SystemdService.totalFailedCount + " failed" : S.SystemdService.systemState
minText: "degraded"
color: root._stateColor
anchors.verticalCenter: parent.verticalCenter
}
}

View file

@ -16,6 +16,7 @@ DockModule 1.0 DockModule.qml
GpuModule 1.0 GpuModule.qml GpuModule 1.0 GpuModule.qml
HoverPanel 1.0 HoverPanel.qml HoverPanel 1.0 HoverPanel.qml
IdleInhibitorModule 1.0 IdleInhibitorModule.qml IdleInhibitorModule 1.0 IdleInhibitorModule.qml
MachinectlModule 1.0 MachinectlModule.qml
MemoryModule 1.0 MemoryModule.qml MemoryModule 1.0 MemoryModule.qml
MprisModule 1.0 MprisModule.qml MprisModule 1.0 MprisModule.qml
NetworkModule 1.0 NetworkModule.qml NetworkModule 1.0 NetworkModule.qml
@ -31,6 +32,7 @@ ProcessList 1.0 ProcessList.qml
PulseAnimation 1.0 PulseAnimation.qml PulseAnimation 1.0 PulseAnimation.qml
ScreenCapture 1.0 ScreenCapture.qml ScreenCapture 1.0 ScreenCapture.qml
ScreenCorners 1.0 ScreenCorners.qml ScreenCorners 1.0 ScreenCorners.qml
SystemdModule 1.0 SystemdModule.qml
TemperatureModule 1.0 TemperatureModule.qml TemperatureModule 1.0 TemperatureModule.qml
ThemedIcon 1.0 ThemedIcon.qml ThemedIcon 1.0 ThemedIcon.qml
Tooltip 1.0 Tooltip.qml Tooltip 1.0 Tooltip.qml

View file

@ -0,0 +1,149 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
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)
function fetchMachine(name) {
const c = Object.assign({}, _cache);
c[name] = Object.assign({}, c[name] ?? {}, {
loading: true
});
_cache = c;
_machineProc._name = 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) {
_machineJournalProc._machine = machineName;
_machineJournalProc._unit = 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) {
_machineRestartProc._machine = machineName;
_machineRestartProc.command = ["pkexec", "systemctl", "-M", machineName, "restart", unitName];
if (_machineRestartProc.running)
_machineRestartProc.running = false;
_machineRestartProc.running = true;
}
property Timer _poll: Timer {
interval: S.Modules.machinectl.interval ?? 15000
running: S.Modules.machinectl.enable
repeat: true
triggeredOnStart: true
onTriggered: if (!_listProc.running)
_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 || []).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 {
property string _name: ""
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 || []).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._machineProc._name] = {
state: state,
units: units,
loading: false
};
root._cache = c;
root.machineReady(root._machineProc._name, state, units);
}
}
}
property Process _machineJournalProc: Process {
property string _machine: ""
property string _unit: ""
stdout: StdioCollector {
onStreamFinished: root.machineJournalReady(root._machineJournalProc._machine, root._machineJournalProc._unit, text)
}
}
property Process _machineRestartProc: Process {
property string _machine: ""
onRunningChanged: if (!running && _machine !== "")
root.fetchMachine(_machine)
}
}

View file

@ -125,13 +125,21 @@ QtObject {
power: true power: true
} }
}) })
property var systemd: ({
enable: true,
interval: 15000
})
property var machinectl: ({
enable: true,
interval: 15000
})
property var statsDaemon: ({ property var statsDaemon: ({
interval: -1 interval: -1
}) })
// All module keys that have an enable flag used to default-enable anything // All module keys that have an enable flag used to default-enable anything
// not explicitly mentioned in modules.json // not explicitly mentioned in modules.json
readonly property var _moduleKeys: ["workspaces", "tray", "windowTitle", "clock", "notifications", "mpris", "volume", "bluetooth", "backlight", "network", "powerProfile", "idleInhibitor", "weather", "temperature", "gpu", "cpu", "memory", "disk", "battery", "privacy", "screenCorners", "power", "backgroundOverlay", "overviewBackdrop", "lock", "dock"] readonly property var _moduleKeys: ["workspaces", "tray", "windowTitle", "clock", "notifications", "mpris", "volume", "bluetooth", "backlight", "network", "powerProfile", "idleInhibitor", "weather", "temperature", "gpu", "cpu", "memory", "disk", "battery", "privacy", "screenCorners", "power", "backgroundOverlay", "overviewBackdrop", "lock", "dock", "systemd", "machinectl"]
// Fallback: if modules.json doesn't exist, enable everything // Fallback: if modules.json doesn't exist, enable everything
Component.onCompleted: _apply("{}") Component.onCompleted: _apply("{}")

View file

@ -0,0 +1,97 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import "." as S
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
signal journalReady(string unitName, bool isUser, string text)
function refresh() {
if (!_pollProc.running)
_pollProc.running = true;
}
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"];
_journalProc._unitName = unitName;
_journalProc._isUser = 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) {
try {
return JSON.parse(json).data || "unknown";
} catch (e) {
return "unknown";
}
}
function _parseUnits(json) {
try {
const parsed = JSON.parse(json);
return (parsed.data || []).map(u => ({
name: u[0],
description: u[1],
loadState: u[2],
activeState: u[3],
subState: u[4]
}));
} catch (e) {
return [];
}
}
property Timer _poll: Timer {
interval: S.Modules.systemd.interval ?? 15000
running: S.Modules.systemd.enable
repeat: true
triggeredOnStart: true
onTriggered: if (!_pollProc.running)
_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 {
property string _unitName: ""
property bool _isUser: false
stdout: StdioCollector {
onStreamFinished: root.journalReady(root._journalProc._unitName, root._journalProc._isUser, text)
}
}
property Process _restartProc: Process {
onRunningChanged: if (!running)
root.refresh()
}
}

View file

@ -8,6 +8,7 @@ singleton CpuService 1.0 CpuService.qml
singleton DockState 1.0 DockState.qml singleton DockState 1.0 DockState.qml
singleton IdleInhibitService 1.0 IdleInhibitService.qml singleton IdleInhibitService 1.0 IdleInhibitService.qml
singleton LockService 1.0 LockService.qml singleton LockService 1.0 LockService.qml
singleton MachinectlService 1.0 MachinectlService.qml
singleton Modules 1.0 Modules.qml singleton Modules 1.0 Modules.qml
singleton MprisService 1.0 MprisService.qml singleton MprisService 1.0 MprisService.qml
singleton NetworkService 1.0 NetworkService.qml singleton NetworkService 1.0 NetworkService.qml
@ -18,6 +19,7 @@ singleton PowerProfileService 1.0 PowerProfileService.qml
singleton ScreenshotService 1.0 ScreenshotService.qml singleton ScreenshotService 1.0 ScreenshotService.qml
singleton SleepService 1.0 SleepService.qml singleton SleepService 1.0 SleepService.qml
singleton SystemStats 1.0 SystemStats.qml singleton SystemStats 1.0 SystemStats.qml
singleton SystemdService 1.0 SystemdService.qml
singleton Theme 1.0 Theme.qml singleton Theme 1.0 Theme.qml
singleton WeatherService 1.0 WeatherService.qml singleton WeatherService 1.0 WeatherService.qml
# keep-sorted end # keep-sorted end