aboutsummaryrefslogtreecommitdiffstats
path: root/hosts/Hosts.qml
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/Hosts.qml')
-rw-r--r--hosts/Hosts.qml123
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 {
}
}
}
+ }
}
}
}