add systemd and machinectl bar modules with applets
This commit is contained in:
parent
7ab784e101
commit
8ab3fc5f6b
12 changed files with 1117 additions and 1 deletions
351
shell/applets/MachinectlApplet.qml
Normal file
351
shell/applets/MachinectlApplet.qml
Normal 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
|
||||
}
|
||||
}
|
||||
164
shell/applets/SystemdApplet.qml
Normal file
164
shell/applets/SystemdApplet.qml
Normal 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
|
||||
}
|
||||
}
|
||||
239
shell/applets/SystemdUnitRow.qml
Normal file
239
shell/applets/SystemdUnitRow.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ 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
|
||||
|
|
@ -17,6 +18,8 @@ NotifApplet 1.0 NotifApplet.qml
|
|||
PowerApplet 1.0 PowerApplet.qml
|
||||
Separator 1.0 Separator.qml
|
||||
SparklineCanvas 1.0 SparklineCanvas.qml
|
||||
SystemdApplet 1.0 SystemdApplet.qml
|
||||
SystemdUnitRow 1.0 SystemdUnitRow.qml
|
||||
TemperatureApplet 1.0 TemperatureApplet.qml
|
||||
VolumeApplet 1.0 VolumeApplet.qml
|
||||
WeatherApplet 1.0 WeatherApplet.qml
|
||||
|
|
|
|||
|
|
@ -185,6 +185,8 @@ PanelWindow {
|
|||
M.TemperatureModule {}
|
||||
M.WeatherModule {}
|
||||
M.DiskModule {}
|
||||
M.SystemdModule {}
|
||||
M.MachinectlModule {}
|
||||
}
|
||||
|
||||
// Power + Dock
|
||||
|
|
|
|||
44
shell/modules/MachinectlModule.qml
Normal file
44
shell/modules/MachinectlModule.qml
Normal 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
|
||||
}
|
||||
}
|
||||
55
shell/modules/SystemdModule.qml
Normal file
55
shell/modules/SystemdModule.qml
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ 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
|
||||
|
|
@ -31,6 +32,7 @@ ProcessList 1.0 ProcessList.qml
|
|||
PulseAnimation 1.0 PulseAnimation.qml
|
||||
ScreenCapture 1.0 ScreenCapture.qml
|
||||
ScreenCorners 1.0 ScreenCorners.qml
|
||||
SystemdModule 1.0 SystemdModule.qml
|
||||
TemperatureModule 1.0 TemperatureModule.qml
|
||||
ThemedIcon 1.0 ThemedIcon.qml
|
||||
Tooltip 1.0 Tooltip.qml
|
||||
|
|
|
|||
149
shell/services/MachinectlService.qml
Normal file
149
shell/services/MachinectlService.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -125,13 +125,21 @@ QtObject {
|
|||
power: true
|
||||
}
|
||||
})
|
||||
property var systemd: ({
|
||||
enable: true,
|
||||
interval: 15000
|
||||
})
|
||||
property var machinectl: ({
|
||||
enable: true,
|
||||
interval: 15000
|
||||
})
|
||||
property var statsDaemon: ({
|
||||
interval: -1
|
||||
})
|
||||
|
||||
// All module keys that have an enable flag — used to default-enable anything
|
||||
// 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
|
||||
Component.onCompleted: _apply("{}")
|
||||
|
|
|
|||
97
shell/services/SystemdService.qml
Normal file
97
shell/services/SystemdService.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ 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 Modules 1.0 Modules.qml
|
||||
singleton MprisService 1.0 MprisService.qml
|
||||
singleton NetworkService 1.0 NetworkService.qml
|
||||
|
|
@ -18,6 +19,7 @@ singleton PowerProfileService 1.0 PowerProfileService.qml
|
|||
singleton ScreenshotService 1.0 ScreenshotService.qml
|
||||
singleton SleepService 1.0 SleepService.qml
|
||||
singleton SystemStats 1.0 SystemStats.qml
|
||||
singleton SystemdService 1.0 SystemdService.qml
|
||||
singleton Theme 1.0 Theme.qml
|
||||
singleton WeatherService 1.0 WeatherService.qml
|
||||
# keep-sorted end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue