From 7bfd1e25c3c45e586da7a39aebfc12bf1bfae0df Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 28 Jul 2026 22:10:47 +0100 Subject: [PATCH] quickshell: segment bar groups, pin server glance --- settings/quickshell.nix | 393 ++++++++++++++++++++++------------------ 1 file changed, 214 insertions(+), 179 deletions(-) diff --git a/settings/quickshell.nix b/settings/quickshell.nix index c2f3e18..bf5bbdd 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -775,6 +775,20 @@ in font.pixelSize: 16 } + // ── BarSep: hairline between widget groups in the bar column. + // Wrapped in a plain Item because a Column refuses to position + // children that set their own horizontalCenter anchor. + component BarSep: Item { + width: Theme.barWidth + height: 1 + Rectangle { + anchors.centerIn: parent + width: 16 + height: 1 + color: Theme.base02 + } + } + // ── GlanceStat: a bar-column stat — clock-sized value over a // faded 9px caption (the server glance rows). component GlanceStat: Column { @@ -1621,9 +1635,11 @@ in precision: SystemClock.Minutes } - // ── Top cluster: logo, clock, workspaces. The bar is a column - // now, so the two clusters anchor to opposite ends and the gap - // between them is the OSD slot. + // ── Top cluster: logo, clock, workspaces — the three groups + // that belong to "this machine, right now", split by hairlines. + // The server glance is NOT here: it anchors above the bottom + // cluster instead, so adding a workspace grows this column + // downward into empty space rather than shoving the stats. Column { id: barTop anchors.top: parent.top @@ -1663,6 +1679,8 @@ in } } + BarSep {} + // Clock — HH over mm; "HH:mm" does not fit a 46px column. Item { id: clockText @@ -1713,6 +1731,8 @@ in } } + BarSep {} + // Workspace dots: accent pill for the focused workspace, // dim dots otherwise. Stacked vertically now, so the // focused pill grows along HEIGHT rather than width. @@ -1753,197 +1773,210 @@ in } } - // Server monitor: one persistent SSH streaming `top -b` - // from the mediaserver (2s ticks, parsed line by line — - // no per-poll reconnects) plus a streaming ping to the - // router. Glance = CPU/RAM numbers + ping ms; click - // opens the btop-style dropdown. - Item { - id: srvWidget - width: Theme.barWidth - height: srvGlanceCol.height + } - property bool connected: false - property real cpu: 0 - property real memUsed: 0 // GiB - property real memTotal: 0 - property real pingMs: -1 - property string uptime: "" - property string load: "" - property var procs: [] - property int tempC: -1 - property real rxBps: 0 - property real txBps: 0 + // ── Mid cluster: the server glance, pinned just above the + // bottom cluster. Its own anchor (not a barTop child) is the + // whole point: workspace count then changes nothing here. + // + // Server monitor: one persistent SSH streaming `top -b` + // from the mediaserver (2s ticks, parsed line by line — + // no per-poll reconnects) plus a streaming ping to the + // router. Glance = CPU/RAM numbers + ping ms; click + // opens the btop-style dropdown. + Item { + id: srvWidget + width: Theme.barWidth + height: srvGlanceCol.height + anchors.horizontalCenter: barBgRect.horizontalCenter + anchors.bottom: barBottom.top + anchors.bottomMargin: 20 + // Fades out while the OSD takes this slot. + opacity: osdItem.shown ? 0 : 1 + Behavior on opacity { + NumberAnimation { duration: Theme.animContent } + } - function fmtRate(bps) { - const b = bps * 8; - if (b >= 1e9) return (b / 1e9).toFixed(2) + " Gb/s"; - if (b >= 1e6) return (b / 1e6).toFixed(1) + " Mb/s"; - if (b >= 1e3) return Math.round(b / 1e3) + " Kb/s"; - return Math.round(b) + " b/s"; + property bool connected: false + property real cpu: 0 + property real memUsed: 0 // GiB + property real memTotal: 0 + property real pingMs: -1 + property string uptime: "" + property string load: "" + property var procs: [] + property int tempC: -1 + property real rxBps: 0 + property real txBps: 0 + + function fmtRate(bps) { + const b = bps * 8; + if (b >= 1e9) return (b / 1e9).toFixed(2) + " Gb/s"; + if (b >= 1e6) return (b / 1e6).toFixed(1) + " Mb/s"; + if (b >= 1e3) return Math.round(b / 1e3) + " Kb/s"; + return Math.round(b) + " b/s"; + } + + // Split variants for the bar glance: number and unit + // live on separate lines, so both stay narrow. + function rateVal(bps) { + const b = bps * 8; + if (b >= 1e9) return (b / 1e9).toFixed(1); + if (b >= 1e7) return Math.round(b / 1e6).toString(); + if (b >= 1e6) return (b / 1e6).toFixed(1); + return Math.round(b / 1e3).toString(); + } + function rateUnit(bps) { + const b = bps * 8; + if (b >= 1e9) return "gb/s"; + if (b >= 1e6) return "mb/s"; + return "kb/s"; + } + + property var _procsPending: [] + property bool _collecting: false + + function _parseLine(line) { + if (line.indexOf("@STAT ") === 0) { + const t = line.match(/temp=(\d+)/); + if (t) tempC = parseInt(t[1]); + const rx = line.match(/rxbps=(-?\d+)/); + if (rx) rxBps = Math.max(0, parseInt(rx[1])); + const tx = line.match(/txbps=(-?\d+)/); + if (tx) txBps = Math.max(0, parseInt(tx[1])); + return; } - - // Split variants for the bar glance: number and unit - // live on separate lines, so both stay narrow. - function rateVal(bps) { - const b = bps * 8; - if (b >= 1e9) return (b / 1e9).toFixed(1); - if (b >= 1e7) return Math.round(b / 1e6).toString(); - if (b >= 1e6) return (b / 1e6).toFixed(1); - return Math.round(b / 1e3).toString(); + if (line.indexOf("top - ") === 0) { + if (_collecting) procs = _procsPending; + _collecting = false; + const up = line.match(/up\s+(.*?),\s*\d+\s+users?,/); + if (up) uptime = up[1].replace(/\s+/g, " "); + const la = line.match(/load average:\s*(.*)$/); + if (la) load = la[1]; + return; } - function rateUnit(bps) { - const b = bps * 8; - if (b >= 1e9) return "gb/s"; - if (b >= 1e6) return "mb/s"; - return "kb/s"; + if (line.indexOf("%Cpu(s)") === 0) { + const m = line.match(/([\d.]+)\s+id/); + if (m) { cpu = Math.max(0, 100 - parseFloat(m[1])); connected = true; } + return; } - - property var _procsPending: [] - property bool _collecting: false - - function _parseLine(line) { - if (line.indexOf("@STAT ") === 0) { - const t = line.match(/temp=(\d+)/); - if (t) tempC = parseInt(t[1]); - const rx = line.match(/rxbps=(-?\d+)/); - if (rx) rxBps = Math.max(0, parseInt(rx[1])); - const tx = line.match(/txbps=(-?\d+)/); - if (tx) txBps = Math.max(0, parseInt(tx[1])); - return; - } - if (line.indexOf("top - ") === 0) { - if (_collecting) procs = _procsPending; - _collecting = false; - const up = line.match(/up\s+(.*?),\s*\d+\s+users?,/); - if (up) uptime = up[1].replace(/\s+/g, " "); - const la = line.match(/load average:\s*(.*)$/); - if (la) load = la[1]; - return; - } - if (line.indexOf("%Cpu(s)") === 0) { - const m = line.match(/([\d.]+)\s+id/); - if (m) { cpu = Math.max(0, 100 - parseFloat(m[1])); connected = true; } - return; - } - const mem = line.match(/^(\S+) Mem\s*:\s*([\d.]+)\s+total,\s*[\d.]+\s+free,\s*([\d.]+)\s+used/); - if (mem) { - const div = mem[1] === "KiB" ? 1048576 : mem[1] === "MiB" ? 1024 : 1; - memTotal = parseFloat(mem[2]) / div; - memUsed = parseFloat(mem[3]) / div; - return; - } - if (/^\s*PID\s/.test(line)) { _collecting = true; _procsPending = []; return; } - if (_collecting) { - const f = line.trim().split(/\s+/); - if (f.length >= 12) { - _procsPending.push({ pid: f[0], cpu: parseFloat(f[8]), mem: parseFloat(f[9]), name: f.slice(11).join(" ") }); - // top sorts by %CPU; first 10 rows are the story - if (_procsPending.length >= 10) { procs = _procsPending; _collecting = false; } - } else { - procs = _procsPending; _collecting = false; - } + const mem = line.match(/^(\S+) Mem\s*:\s*([\d.]+)\s+total,\s*[\d.]+\s+free,\s*([\d.]+)\s+used/); + if (mem) { + const div = mem[1] === "KiB" ? 1048576 : mem[1] === "MiB" ? 1024 : 1; + memTotal = parseFloat(mem[2]) / div; + memUsed = parseFloat(mem[3]) / div; + return; + } + if (/^\s*PID\s/.test(line)) { _collecting = true; _procsPending = []; return; } + if (_collecting) { + const f = line.trim().split(/\s+/); + if (f.length >= 12) { + _procsPending.push({ pid: f[0], cpu: parseFloat(f[8]), mem: parseFloat(f[9]), name: f.slice(11).join(" ") }); + // top sorts by %CPU; first 10 rows are the story + if (_procsPending.length >= 10) { procs = _procsPending; _collecting = false; } + } else { + procs = _procsPending; _collecting = false; } } + } - Process { - id: srvTop - command: [Commands.ssh, "-o", "BatchMode=yes", "-o", "ConnectTimeout=3", - "-o", "StrictHostKeyChecking=accept-new", - "-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=2", - "fred@10.0.0.1", "qs-stats"] - running: true - stdout: SplitParser { - onRead: data => srvWidget._parseLine(data) - } - onRunningChanged: if (!running) { srvWidget.connected = false; srvRestart.start(); } + Process { + id: srvTop + command: [Commands.ssh, "-o", "BatchMode=yes", "-o", "ConnectTimeout=3", + "-o", "StrictHostKeyChecking=accept-new", + "-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=2", + "fred@10.0.0.1", "qs-stats"] + running: true + stdout: SplitParser { + onRead: data => srvWidget._parseLine(data) } - Timer { id: srvRestart; interval: 10000; onTriggered: srvTop.running = true } + onRunningChanged: if (!running) { srvWidget.connected = false; srvRestart.start(); } + } + Timer { id: srvRestart; interval: 10000; onTriggered: srvTop.running = true } - // Ping is deliberately separate from the SSH stream: - // ping up + ssh down = box alive but wedged (seen it). - Process { - id: srvPing - command: [Commands.ping, "-n", "-i", "2", "10.0.0.1"] - running: true - stdout: SplitParser { - onRead: data => { - const m = data.match(/time=([\d.]+)\s*ms/); - if (m) { srvWidget.pingMs = parseFloat(m[1]); pingStale.restart(); } - } - } - onRunningChanged: if (!running) pingRestart.start() - } - Timer { id: pingRestart; interval: 10000; onTriggered: srvPing.running = true } - // No echo reply for 3 intervals -> show dashes, not a stale number. - Timer { id: pingStale; interval: 6000; onTriggered: srvWidget.pingMs = -1 } - - function openSrvDropdown() { - bar.toggleDropdown(srvDropdown, function() { - let pos = srvWidget.mapToItem(bar.contentItem, 0, srvWidget.height / 2); - srvDropdown.dropdownY = pos.y; - }); - } - - // Stat stack, clock-sized values over faded captions. - Column { - id: srvGlanceCol - anchors.centerIn: parent - spacing: 5 - - GlanceStat { - value: srvWidget.connected ? Math.round(srvWidget.cpu) + "%" : "--" - caption: "cpu" - valueColor: srvWidget.connected ? Theme.base0D : Theme.base03 - } - GlanceStat { - value: srvWidget.connected && srvWidget.memTotal > 0 - ? Math.round(100 * srvWidget.memUsed / srvWidget.memTotal) + "%" : "--" - caption: "ram" - valueColor: srvWidget.connected ? Theme.base0C : Theme.base03 - } - GlanceStat { - value: srvWidget.connected && srvWidget.tempC >= 0 ? srvWidget.tempC + "°" : "--" - caption: "temp" - valueColor: !srvWidget.connected || srvWidget.tempC < 0 ? Theme.base03 - : srvWidget.tempC >= 85 ? Theme.base08 - : srvWidget.tempC >= 70 ? Theme.base0A : Theme.base05 - } - GlanceStat { - value: srvWidget.connected ? srvWidget.rateVal(srvWidget.rxBps) : "--" - caption: "↓ " + srvWidget.rateUnit(srvWidget.rxBps) - valueColor: srvWidget.connected ? Theme.base0B : Theme.base03 - } - GlanceStat { - value: srvWidget.connected ? srvWidget.rateVal(srvWidget.txBps) : "--" - caption: "↑ " + srvWidget.rateUnit(srvWidget.txBps) - valueColor: srvWidget.connected ? Theme.base09 : Theme.base03 - } - GlanceStat { - value: srvWidget.pingMs < 0 ? "--" - : srvWidget.pingMs < 10 ? srvWidget.pingMs.toFixed(1) - : Math.round(srvWidget.pingMs).toString() - caption: "ms" - valueColor: srvWidget.pingMs < 0 ? Theme.base03 : Theme.base04 + // Ping is deliberately separate from the SSH stream: + // ping up + ssh down = box alive but wedged (seen it). + Process { + id: srvPing + command: [Commands.ping, "-n", "-i", "2", "10.0.0.1"] + running: true + stdout: SplitParser { + onRead: data => { + const m = data.match(/time=([\d.]+)\s*ms/); + if (m) { srvWidget.pingMs = parseFloat(m[1]); pingStale.restart(); } } } + onRunningChanged: if (!running) pingRestart.start() + } + Timer { id: pingRestart; interval: 10000; onTriggered: srvPing.running = true } + // No echo reply for 3 intervals -> show dashes, not a stale number. + Timer { id: pingStale; interval: 6000; onTriggered: srvWidget.pingMs = -1 } - MouseArea { - anchors.fill: parent - hoverEnabled: true - onClicked: srvWidget.openSrvDropdown() - onEntered: { - if (bar.activeDropdown) { - if (bar.activeDropdown !== srvDropdown) srvWidget.openSrvDropdown(); - } + function openSrvDropdown() { + bar.toggleDropdown(srvDropdown, function() { + let pos = srvWidget.mapToItem(bar.contentItem, 0, srvWidget.height / 2); + srvDropdown.dropdownY = pos.y; + }); + } + + // Stat stack, clock-sized values over faded captions. + Column { + id: srvGlanceCol + anchors.centerIn: parent + spacing: 5 + + GlanceStat { + value: srvWidget.connected ? Math.round(srvWidget.cpu) + "%" : "--" + caption: "cpu" + valueColor: srvWidget.connected ? Theme.base0D : Theme.base03 + } + GlanceStat { + value: srvWidget.connected && srvWidget.memTotal > 0 + ? Math.round(100 * srvWidget.memUsed / srvWidget.memTotal) + "%" : "--" + caption: "ram" + valueColor: srvWidget.connected ? Theme.base0C : Theme.base03 + } + GlanceStat { + value: srvWidget.connected && srvWidget.tempC >= 0 ? srvWidget.tempC + "°" : "--" + caption: "temp" + valueColor: !srvWidget.connected || srvWidget.tempC < 0 ? Theme.base03 + : srvWidget.tempC >= 85 ? Theme.base08 + : srvWidget.tempC >= 70 ? Theme.base0A : Theme.base05 + } + GlanceStat { + value: srvWidget.connected ? srvWidget.rateVal(srvWidget.rxBps) : "--" + caption: "↓ " + srvWidget.rateUnit(srvWidget.rxBps) + valueColor: srvWidget.connected ? Theme.base0B : Theme.base03 + } + GlanceStat { + value: srvWidget.connected ? srvWidget.rateVal(srvWidget.txBps) : "--" + caption: "↑ " + srvWidget.rateUnit(srvWidget.txBps) + valueColor: srvWidget.connected ? Theme.base09 : Theme.base03 + } + GlanceStat { + value: srvWidget.pingMs < 0 ? "--" + : srvWidget.pingMs < 10 ? srvWidget.pingMs.toFixed(1) + : Math.round(srvWidget.pingMs).toString() + caption: "ms" + valueColor: srvWidget.pingMs < 0 ? Theme.base03 : Theme.base04 + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onClicked: srvWidget.openSrvDropdown() + onEntered: { + if (bar.activeDropdown) { + if (bar.activeDropdown !== srvDropdown) srvWidget.openSrvDropdown(); } } } } // ── Bottom cluster: status icons, tray, power button. Anchored - // to the bottom edge so the gap above it — between here and the - // workspace dots — stays free as the OSD slot. + // to the bottom edge; the server glance sits just above it and + // the OSD borrows that same slot while it is on screen. Column { id: barBottom anchors.bottom: parent.bottom @@ -2294,7 +2327,7 @@ in // Group separator: status icons / tray / power read // as three clusters instead of one clump. - Item { width: Theme.barWidth; height: 10 } + BarSep {} // Tray icons — stacked, like everything else in the column Column { @@ -2382,7 +2415,7 @@ in // Group separator: status icons / tray / power read // as three clusters instead of one clump. - Item { width: Theme.barWidth; height: 10 } + BarSep {} // Power menu opener — bottom-left corner. Opens the same // session menu as Super+L, which now unfolds from here. @@ -4168,10 +4201,12 @@ in property real value: 0 property color fill: Theme.base0D + // Takes over the server glance's slot: fixed position, no + // dependence on how tall the workspace column has grown. width: Theme.barWidth + height: 170 anchors.horizontalCenter: barBgRect.horizontalCenter - anchors.top: barTop.bottom - anchors.bottom: barBottom.top + anchors.verticalCenter: srvWidget.verticalCenter opacity: shown ? 1 : 0 visible: opacity > 0.01