diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-27 17:50:00 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-27 17:50:00 +0100 |
| commit | e59745a38b56d2517b8c006279ea6d07f8047678 (patch) | |
| tree | 884cbc3a24d9c48b57f5446624dfa81cd40689df /hosts/Hosts.qml | |
| parent | 77f4df8b0a9969019581813ddb5e3cab05b6bf0a (diff) | |
| download | quickshell-master.tar.gz quickshell-master.zip | |
The flat grid mixed Proxmox services with the phone, printers and IoT.
hosts.py now gives every entry a group: the node named in its note's
`node`, its own name when other notes name it as their node (so the node
card heads its section), "Devices" for anything else on a private address,
"Remote" for the rest. The list is sorted node by node, node card first,
and the grid starts a titled section at each group change. Up/Down move by
visual row and keep the column, since rows are no longer a fixed five.
`hosts.py --probe` checks every host at once, about a second for the whole
list, and runs beside the list rather than ahead of it. A host with a port
or url is up only when that port accepts a TCP connect, so a service whose
CT is up and whose daemon is down reads red; anything else gets one ping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'hosts/Hosts.qml')
| -rw-r--r-- | hosts/Hosts.qml | 123 |
1 files changed, 105 insertions, 18 deletions
diff --git a/hosts/Hosts.qml b/hosts/Hosts.qml index fb2c0e1..6dc4ea2 100644 --- a/hosts/Hosts.qml +++ b/hosts/Hosts.qml @@ -20,6 +20,9 @@ Scope { property bool open: false property string monitor: "DP-1" property var hosts: [] + // name -> reachable, from `hosts.py --probe`; a host missing from it is + // not probed yet and its dot stays grey. + property var up: ({}) property string query: "" property int current: 0 // Non-empty while loading or after a failure; the list shows only when @@ -30,13 +33,36 @@ Scope { readonly property var shown: { const words = root.query.toLowerCase().split(/\s+/).filter(w => w); return root.hosts.filter(h => { - const hay = [h.name, h.ip, h.port, h.hostname, h.node, h.ct_id] + const hay = [h.name, h.ip, h.port, h.hostname, h.node, h.ct_id, h.group] .concat(h.aliases).join(" ").toLowerCase(); return words.every(w => hay.includes(w)); }); } onShownChanged: root.current = 0 + // The grid's rows: a group starts a new row, which carries the group's + // title, and no row holds more than `columns` cards. `start` is the + // index in `shown` of the row's first card. + readonly property int columns: 5 + readonly property var rows: { + const out = []; + root.shown.forEach((h, i) => { + const last = out[out.length - 1]; + if (last && last.group === h.group && last.items.length < root.columns) + last.items.push(h); + else + out.push({ group: h.group, title: last && last.group === h.group ? "" : h.group, + start: i, items: [h] }); + }); + return out; + } + + function rowOf(i) { + for (let r = root.rows.length - 1; r >= 0; r--) + if (root.rows[r].start <= i) return r; + return 0; + } + readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] @@ -67,6 +93,14 @@ Scope { root.current = Math.max(0, Math.min(root.shown.length - 1, root.current + step)); } + // Up and Down keep the column, clamped to a shorter row's last card. + function moveRow(step) { + const r = root.rowOf(root.current); + const to = root.rows[r + step]; + if (to) + root.current = to.start + Math.min(root.current - root.rows[r].start, to.items.length - 1); + } + function runFirst(h) { const a = h ? root.actions(h) : []; if (a.length) root.run(a[0]); @@ -77,9 +111,24 @@ Scope { onOpenChanged: if (open) { root.status = "Loading..."; root.query = ""; + root.up = {}; // A Process already running ignores running = true. proc.running = false; proc.running = true; + probe.running = false; + probe.running = true; + } + + // Takes about a second, every host at once, so it runs beside the list + // rather than holding it up. + Process { + id: probe + command: ["/usr/bin/python3", `${Quickshell.shellDir}/hosts.py`, "--probe"] + stdout: StdioCollector { + onStreamFinished: { + try { root.up = JSON.parse(text); } catch (e) {} + } + } } Process { @@ -164,8 +213,8 @@ Scope { // text: a query is short enough to retype. Keys.onLeftPressed: root.move(-1) Keys.onRightPressed: root.move(1) - Keys.onUpPressed: root.move(-grid.columns) - Keys.onDownPressed: root.move(grid.columns) + Keys.onUpPressed: root.moveRow(-1) + Keys.onDownPressed: root.moveRow(1) Keys.onReturnPressed: root.runFirst(root.shown[root.current]) Keys.onEnterPressed: root.runFirst(root.shown[root.current]) @@ -187,14 +236,14 @@ Scope { font.pixelSize: 18 } - // Five columns in rows, each row as tall as its - // busiest card, so one with many chips grows its own row - // and no other. The margins keep the selected card's + // Five columns in rows grouped by node, then Devices and + // Remote, each row as tall as its busiest card, so one + // with many chips grows its own row and no other. The margins keep the selected card's // glow inside the clip. Rows rather than a GridView, // whose cells all share one height. Flickable { id: grid - readonly property int columns: 5 + readonly property int columns: root.columns visible: root.status === "" width: parent.width height: parent.height - y @@ -204,7 +253,7 @@ Scope { // Keep the selected card in view as the arrows move. function reveal(i) { - const r = rowRepeater.itemAt(Math.floor(i / columns)); + const r = rowRepeater.itemAt(root.rowOf(i)); if (!r) return; const top = rows.y + r.y - 24; const bottom = rows.y + r.y + r.height + 24; @@ -229,17 +278,40 @@ Scope { Repeater { id: rowRepeater - model: { - const out = []; - for (let i = 0; i < root.shown.length; i += grid.columns) - out.push(root.shown.slice(i, i + grid.columns)); - return out; - } + model: root.rows + + Column { + id: section + required property var modelData + spacing: 12 + // A group's first row opens it with a + // title and a rule; later rows are bare. + topPadding: modelData.title && modelData.start > 0 ? 20 : 0 + + Column { + visible: section.modelData.title !== "" + spacing: 6 + + Text { + text: section.modelData.title + color: section.modelData.group === "Devices" + || section.modelData.group === "Remote" + ? Theme.subtext : Theme.accent + font.family: Theme.fontFamily + font.pixelSize: 18 + font.bold: true + } + Rectangle { + width: rows.width + height: 1 + color: Theme.overlay + } + } Row { id: row - required property var modelData - required property int index + readonly property var modelData: section.modelData.items + readonly property int start: section.modelData.start readonly property real tallest: { let m = 0; for (const c of children) @@ -255,7 +327,7 @@ Scope { id: card required property var modelData required property int index - readonly property int at: row.index * grid.columns + index + readonly property int at: row.start + index readonly property bool selected: at === root.current // What this card needs; the row takes the most. readonly property real need: info.implicitHeight + 24 @@ -279,11 +351,25 @@ Scope { onClicked: root.runFirst(card.modelData) } + // Reachable now: green, not: red, + // not probed yet: grey. + Rectangle { + readonly property var state: root.up[card.modelData.name] + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 14 + width: 10 + height: 10 + radius: 5 + color: state === true ? Theme.green + : state === false ? Theme.red : Theme.overlay + } + Column { id: info x: 16 y: 12 - width: parent.width - 32 + width: parent.width - 48 spacing: 4 Text { @@ -351,6 +437,7 @@ Scope { } } } + } } } } |
