diff --git a/settings/quickshell.nix b/settings/quickshell.nix index a18e95f..25400b6 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -478,6 +478,7 @@ in Bar 1.0 Bar.qml Lock 1.0 Lock.qml Cheatsheet 1.0 Cheatsheet.qml + Archie 1.0 Archie.qml ''; }; @@ -1102,6 +1103,390 @@ in ''; }; + "quickshell/Archie.qml" = { + onChange = qsRestart; + text = '' + import QtQuick + + // Archie — a black and tan dachshund with a red collar, living + // along the bottom of the screen. He trots about, wags, naps in + // his bed and visits his bowl. + // + // The art is plain text, one character per pixel, so he can be + // redrawn by editing the picture instead of editing code. He walks + // evenly on all fours and never limps; that was deliberate. + Item { + id: archie + anchors.fill: parent + // One dog. The bar is instantiated per monitor. + property bool primary: true + visible: primary + + // 2px pixels: plenty of resolution for a proper pointed + // dachshund head, and he still lands smaller on screen than + // the chunkier drawing did. + readonly property int cell: 2 + readonly property int cols: 44 + readonly property int rows: 19 + + // Not themed — he looked how he looked. + readonly property var pal: ({ + "K": "#2b2523", // black + "T": "#a86a30", // tan + "R": "#b3322c", // collar, and his bowl + "N": "#14100f", // nose and eye + "B": "#96602f", // kibble + "G": "#5a667a", // bed + "D": "#3a4454" // bed cushion + }) + + // Trotting: front leg forward, hind leg back, tail up. Paws + // point the way he is going — drawn trailing behind the leg + // they read as feet on backwards, which is exactly how the + // first pass looked. + readonly property var walkA: [ + "...............................KKKKKKKK.....", + "KK............................KKKKKKKKKK....", + "KKKK..........................KKKKKKKKKKK...", + ".KKKK.........................KKKKK.KKKKK...", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKK..TTKKKKK.", + ".KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTTT.", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTT..", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTT.....", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKTTTT.............", + ".....KKKKKKKKKKKKKKKKKKKKKTTTT..............", + "....TTKKKKKKKKKKKKKKKKKKKTTTT...............", + ".....TTTTT..............TTTT................", + "......TTTT..............TTT.................", + ".....TTT................TTT.................", + ".....TTT................TTT.................", + ".....TTT................TTT.................", + ".....TTTTT..............TTTTT..............." + ] + // Legs gathered under him, tail level. + readonly property var walkB: [ + "...............................KKKKKKKK.....", + "..............................KKKKKKKKKK....", + "KKKK..........................KKKKKKKKKKK...", + "KKKK..........................KKKKK.KKKKK...", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKK..TTKKKKK.", + ".KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTTT.", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTT..", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTT.....", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKTTTT.............", + ".....KKKKKKKKKKKKKKKKKKKKKTTTT..............", + "....TTKKKKKKKKKKKKKKKKKKKTTTT...............", + "........TTTTT........TTTT...................", + ".........TTTT........TTT....................", + "........TTT..........TTT....................", + "........TTT..........TTT....................", + "........TTT..........TTT....................", + "........TTTTT........TTTTT.................." + ] + // Standing: walkA's planted legs, walkB's tail. Flipping + // between this and walkA is the wag. + readonly property var wag: [ + "...............................KKKKKKKK.....", + "..............................KKKKKKKKKK....", + "KKKK..........................KKKKKKKKKKK...", + "KKKK..........................KKKKK.KKKKK...", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKK..TTKKKKK.", + ".KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTTT.", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTT..", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTT.....", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKTTTT.............", + ".....KKKKKKKKKKKKKKKKKKKKKTTTT..............", + "....TTKKKKKKKKKKKKKKKKKKKTTTT...............", + ".....TTTTT..............TTTT................", + "......TTTT..............TTT.................", + ".....TTT................TTT.................", + ".....TTT................TTT.................", + ".....TTT................TTT.................", + ".....TTTTT..............TTTTT..............." + ] + // Head down in the bowl. + readonly property var eat: [ + "............................................", + "KK..........................................", + "KKKK........................................", + ".KKKK.......................................", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKKKKK............", + ".KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK...........", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK..........", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKKK.........", + "...KKKKKKKKKKKKKKKKKKKKKKKKTTTTKKKKKKKK.....", + ".....KKKKKKKKKKKKKKKKKKKKKTTTTKKKKKKKKKK....", + "....TTKKKKKKKKKKKKKKKKKKKTTTT.KKKKK.KKKK....", + ".....TTTTT..............TTTT..KKKKK.TTTKK...", + "......TTTT..............TTT...KKKKTTTTTTTTT.", + ".....TTT................TTT...KKKKKTTTTTTT..", + ".....TTT................TTT....KKKKTTTT.....", + ".....TTT................TTT.................", + ".....TTTTT..............TTTTT..............." + ] + // Flat out in his bed, muzzle on the cushion. + readonly property var sleepArt: [ + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "..............................KKKKKKKKK.....", + ".............................KKKKKKKKKKKK...", + "KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKKK.KKKKK...", + "..KKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKKTTTTTKKK..", + "..KKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKKTTTTTTTTT.", + "...KKKKKKKKKKKKKKKKKKKKKKKKRRKKKKKTTTTTTT...", + "...KKKKKKKKKKKKKKKKKKKKKKKKRRTTTT...........", + "....KKKKKKKKKKKKKKKKKKKKKKRRTTTT............", + ".....TTTTTTTTKKKKKKKKKKKKTTTT...............", + "......TTTTTT..........TTTTTT................" + ] + // Mid-breath: the same pose with his ribcage lifted one cell, + // just behind the collar where a dog's chest actually is. His + // belly stays put — that side is against the cushion. + readonly property var sleepIn: [ + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "..............................KKKKKKKKK.....", + "................KKKKKKKKKK...KKKKKKKKKKKK...", + "KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKKK.KKKKK...", + "..KKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKKTTTTTKKK..", + "..KKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKKTTTTTTTTT.", + "...KKKKKKKKKKKKKKKKKKKKKKKKRRKKKKKTTTTTTT...", + "...KKKKKKKKKKKKKKKKKKKKKKKKRRTTTT...........", + "....KKKKKKKKKKKKKKKKKKKKKKRRTTTT............", + ".....TTTTTTTTKKKKKKKKKKKKTTTT...............", + "......TTTTTT..........TTTTTT................" + ] + readonly property var bowlArt: [ + ".....BBBBBB.....", + "...BBBBBBBBBB...", + ".BBBBBBBBBBBBBB.", + "RRRRRRRRRRRRRRRR", + ".RRRRRRRRRRRRRR.", + "..RRRRRRRRRRRR..", + "...RRRRRRRRRR...", + "....RRRRRRRR....", + ".....RRRRRR....." + ] + readonly property var bedArt: [ + "GGGGGGG........................................GGGGGGG", + "GGGGGGGGG....................................GGGGGGGGG", + "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", + "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", + "..GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG.." + ] + + property var frame: walkA + + // ── His patch of floor ────────────────────────────────── + readonly property real floorY: archie.height - Theme.frameWidth + readonly property real roamL: Theme.barWidth + 12 + readonly property real roamR: Math.max(roamL, + archie.width - Theme.frameWidth - cols * cell - 12) + // Bed tucked into the bottom right corner, bowl beside it — + // his corner of the room. The bowl derives from the bed so + // they cannot drift into each other. + readonly property real bedX: Math.max(roamL, + archie.width - Theme.frameWidth - 54 * cell - 8) + readonly property real bowlX: Math.max(roamL, bedX - 16 * cell - 26) + // Stand so his muzzle lands in the bowl, not his shoulder. + readonly property real bowlStandX: bowlX - 32 * cell + // Lying down, centred on the cushion. + readonly property real bedLieX: bedX + 5 * cell + + property real px: 0 + property int facing: 1 // 1 = right, -1 = left + property string mode: "walk" + property real targetX: 0 + property string errand: "" // "" | "bowl" | "bed" + property int hold: 0 + // Ticks since he last got up. Past `tired` he takes himself + // off to bed. + property int awake: 0 + readonly property int tired: 2700 // 90s on his feet + // Legs swap on distance covered, not on a tick count, so the + // gait stays in step. 3 cells is exactly how far the feet + // move in the art. + property real stride: 0 + property bool stepped: false + + // ── What he does next ─────────────────────────────────── + function wander() { + mode = "walk"; + errand = ""; + // Bed is not a random errand any more, it is what he does + // when he has been up long enough. A dog sleeps most of + // the day; pottering about non-stop is the odd bit. + if (awake > tired) { + errand = "bed"; + targetX = bedLieX; + return; + } + let dBowl = Math.abs(px - bowlStandX); + // Near enough to be worth it, and not already stood there + // — otherwise he marches half a minute for a snack, or + // rolls "go to the bowl" with his head already in it. + if (Math.random() < 0.3 && dBowl > 80 && dBowl < 900) { + errand = "bowl"; + targetX = bowlStandX; + return; + } + + let dir = Math.random() < 0.5 ? -1 : 1; + // Turn away from an end he is already near; clamping the + // target instead just parks him against the wall. + if (dir < 0 && px - roamL < 220) dir = 1; + if (dir > 0 && roamR - px < 220) dir = -1; + targetX = Math.max(roamL, Math.min(roamR, + px + dir * (150 + Math.random() * 550))); + } + + function arrive() { + if (errand === "bowl") { + mode = "eat"; + hold = 100 + Math.random() * 80; + facing = 1; + } else if (errand === "bed") { + mode = "sleep"; + hold = 9000 + Math.random() * 9000; // 5-10 minutes + // Facing into the screen. His bed is in the right + // corner, so unmirrored he sleeps nose-to-the-wall. + facing = -1; + } else { + mode = "idle"; + hold = 150 + Math.random() * 450; // 5-20s stood about + } + } + + // Placed on the first real width: anchors have not resolved at + // Component.onCompleted, so roamR collapses to roamL and he + // spawns pinned to the left edge. + property bool placed: false + onWidthChanged: { + if (placed || width <= 0) return; + placed = true; + px = roamL + Math.random() * (roamR - roamL); + wander(); + } + + Timer { + interval: 33; running: archie.primary; repeat: true + onTriggered: { + archie.awake = archie.mode === "sleep" ? 0 : archie.awake + 1; + + if (archie.mode === "walk") { + // Arrival first: checking after the step makes him + // stride backwards when he is already there. + if (Math.abs(archie.targetX - archie.px) < 2.5) { + archie.arrive(); + } else { + let dir = archie.targetX > archie.px ? 1 : -1; + let sp = 1.5; + archie.facing = dir; + archie.px += dir * sp; + archie.stride += sp; + if (archie.stride >= 3 * archie.cell) { + archie.stride = 0; + archie.stepped = !archie.stepped; + } + archie.frame = archie.stepped ? archie.walkB : archie.walkA; + } + } else { + // Breathing: in for a third of the cycle, then + // back down and a pause. 91 ticks is about three + // seconds a breath, which is where a small dog + // asleep actually sits. + archie.frame = archie.mode === "eat" ? archie.eat + : archie.mode === "sleep" + ? (archie.hold % 91 < 32 ? archie.sleepIn : archie.sleepArt) + : (archie.hold % 26 < 13 ? archie.walkA : archie.wag); + if (--archie.hold <= 0) archie.wander(); + } + } + } + + // ── Sprite: one Rectangle per pixel, colour looked up per + // frame, so switching frames just rebinds them. + component Sprite: Item { + id: sp + property var art: [] + property int w: 0 + property int h: 0 + width: w * archie.cell + height: h * archie.cell + Repeater { + // Delegates parent to the Repeater's PARENT, so `sp` + // is the only reliable way back to the sprite here. + model: sp.w * sp.h + Rectangle { + required property int index + readonly property int col: index % sp.w + readonly property int row: Math.floor(index / sp.w) + readonly property string ch: sp.art[row][col] + visible: ch !== "." + color: archie.pal[ch] || "transparent" + x: col * archie.cell + y: row * archie.cell + width: archie.cell + height: archie.cell + antialiasing: false + } + } + } + + // Bed first, so he lies in it rather than behind it. + Sprite { + art: archie.bedArt; w: 54; h: 7 + x: Math.round(archie.bedX / archie.cell) * archie.cell + y: archie.floorY - height + } + + Sprite { + art: archie.frame + w: archie.cols + h: archie.rows + // Snap x to the pixel grid or the art shimmers as he + // walks. y is exact: he never moves vertically, and + // rounding sank his paws through the floor. + x: Math.round(archie.px / archie.cell) * archie.cell + y: archie.floorY - height + // Mirror him when he heads back the other way. + transform: Scale { origin.x: archie.cols * archie.cell / 2; xScale: archie.facing } + } + + // Bowl after him, so at dinner his nose goes behind it. + Sprite { + art: archie.bowlArt; w: 16; h: 9 + x: Math.round(archie.bowlX / archie.cell) * archie.cell + y: archie.floorY - height + } + } + ''; + }; + "quickshell/Commands.qml" = { onChange = qsRestart; text = '' @@ -1483,14 +1868,6 @@ in bottom: true } - // Screen-sized: the frame band and every panel are drawn into - // this one surface. That AREA is this shell's main cost — - // Hyprland blurs this namespace, and blur is recomputed - // whenever what's behind the surface changes, which with a - // game underneath is every pixel every frame. Shrinking it to - // the bar column measured 57% -> 38% GPU. Doing that for real - // needs the frame band drawn somewhere that is NOT another - // full-screen surface; see the wallpaper window. implicitWidth: bar.screen.width exclusiveZone: Theme.barWidth color: "transparent" @@ -3908,6 +4285,207 @@ in } } + // ── 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 } + } + + 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; + } + 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; + } + } + } + + 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(); } + } + 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" + // base05, like temp: the accent was the one fully + // saturated thing in the column, which made the + // stat the eye landed on first for no reason. + valueColor: srvWidget.connected ? Theme.base05 : Theme.base03 + } + GlanceStat { + value: srvWidget.connected && srvWidget.memTotal > 0 + ? Math.round(100 * srvWidget.memUsed / srvWidget.memTotal) + "%" : "--" + caption: "ram" + valueColor: srvWidget.connected ? Theme.info : 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.err + : srvWidget.tempC >= 70 ? Theme.warn : Theme.base05 + } + GlanceStat { + value: srvWidget.connected ? srvWidget.rateVal(srvWidget.rxBps) : "--" + caption: "↓ " + srvWidget.rateUnit(srvWidget.rxBps) + valueColor: srvWidget.connected ? Theme.ok : Theme.base03 + } + GlanceStat { + value: srvWidget.connected ? srvWidget.rateVal(srvWidget.txBps) : "--" + caption: "↑ " + srvWidget.rateUnit(srvWidget.txBps) + valueColor: srvWidget.connected ? Theme.alt : 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, clock, power button. Anchored // to the bottom edge; the server glance sits just above it and @@ -5002,6 +5580,199 @@ in } // Volume dropdown + // Server monitor dropdown — the btop-at-a-distance view. + BarDropdown { + id: srvDropdown + fullWidth: srvDropdownCol.width + 2 * Theme.panelGap + fullHeight: srvDropdownCol.height + 2 * Theme.panelGap + + Column { + id: srvDropdownCol + anchors.centerIn: parent + width: 340 + spacing: 8 + + Card { + width: parent.width + + Row { + spacing: 6 + SIcon { + anchors.verticalCenter: parent.verticalCenter + text: "server" + color: srvWidget.connected ? Theme.ok : Theme.err + } + SText { + anchors.verticalCenter: parent.verticalCenter + text: "FredOS-Mediaserver" + font.weight: Font.Medium + } + } + + SText { + width: parent.width + text: srvWidget.connected + ? "up " + srvWidget.uptime + " • load " + srvWidget.load + : "unreachable" + font.pixelSize: 12 + color: srvWidget.connected ? Theme.base04 : Theme.err + elide: Text.ElideRight + } + + Row { + width: parent.width + spacing: 10 + SText { width: 42; text: "cpu"; color: Theme.base04; anchors.verticalCenter: parent.verticalCenter } + MeterBar { + width: parent.width - 42 - 76 - 20 + anchors.verticalCenter: parent.verticalCenter + value: srvWidget.cpu / 100 + fillColor: srvWidget.cpu >= 90 ? Theme.err : Theme.accent + } + SText { + width: 76 + text: srvWidget.cpu.toFixed(1) + "%" + font.weight: Font.Medium + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: 10 + SText { width: 42; text: "ram"; color: Theme.base04; anchors.verticalCenter: parent.verticalCenter } + MeterBar { + width: parent.width - 42 - 76 - 20 + anchors.verticalCenter: parent.verticalCenter + value: srvWidget.memTotal > 0 ? srvWidget.memUsed / srvWidget.memTotal : 0 + fillColor: Theme.info + } + SText { + width: 76 + text: srvWidget.memUsed.toFixed(1) + "/" + Math.round(srvWidget.memTotal) + "G" + font.weight: Font.Medium + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: 10 + SText { width: 42; text: "temp"; color: Theme.base04 } + SText { + text: srvWidget.tempC < 0 ? "--" : srvWidget.tempC + "°C" + font.weight: Font.Medium + color: srvWidget.tempC < 0 ? Theme.base03 + : srvWidget.tempC >= 85 ? Theme.err + : srvWidget.tempC >= 70 ? Theme.warn : Theme.base05 + } + } + + Row { + width: parent.width + spacing: 10 + SText { width: 42; text: "wan"; color: Theme.base04 } + Row { + spacing: 14 + Row { + spacing: 4 + SIcon { text: "arrow-down"; font.pixelSize: 14; color: Theme.ok; anchors.verticalCenter: parent.verticalCenter } + SText { + text: srvWidget.fmtRate(srvWidget.rxBps) + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + Row { + spacing: 4 + SIcon { text: "arrow-up"; font.pixelSize: 14; color: Theme.alt; anchors.verticalCenter: parent.verticalCenter } + SText { + text: srvWidget.fmtRate(srvWidget.txBps) + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + + Row { + width: parent.width + spacing: 10 + SText { width: 42; text: "ping"; color: Theme.base04 } + SText { + text: srvWidget.pingMs < 0 ? "--" : srvWidget.pingMs.toFixed(1) + " ms" + font.weight: Font.Medium + color: srvWidget.pingMs < 0 ? Theme.base03 : Theme.base05 + } + } + } + + Card { + width: parent.width + + Row { + spacing: 6 + SIcon { anchors.verticalCenter: parent.verticalCenter; text: "activity" } + SText { + anchors.verticalCenter: parent.verticalCenter + text: "Processes" + font.weight: Font.Medium + } + } + + Row { + width: parent.width + SText { width: parent.width - 112; text: "name"; font.pixelSize: 11; color: Theme.base03 } + SText { width: 56; text: "cpu%"; font.pixelSize: 11; color: Theme.base03; horizontalAlignment: Text.AlignRight } + SText { width: 56; text: "mem%"; font.pixelSize: 11; color: Theme.base03; horizontalAlignment: Text.AlignRight } + } + + Column { + width: parent.width + spacing: 3 + + Repeater { + model: srvWidget.procs + + Row { + required property var modelData + width: parent.width + height: 21 + SText { + width: parent.width - 112 + text: modelData.name + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + SText { + width: 56 + text: modelData.cpu.toFixed(1) + horizontalAlignment: Text.AlignRight + color: modelData.cpu >= 100 ? Theme.err + : modelData.cpu >= 25 ? Theme.warn : Theme.base05 + anchors.verticalCenter: parent.verticalCenter + } + SText { + width: 56 + text: modelData.mem.toFixed(1) + horizontalAlignment: Text.AlignRight + color: Theme.base04 + anchors.verticalCenter: parent.verticalCenter + } + } + } + + SText { + visible: srvWidget.procs.length === 0 + text: "no data" + color: Theme.base03 + } + } + } + } + } BarDropdown { id: volDropdown @@ -6846,16 +7617,14 @@ in property real value: 0 property color fill: Theme.accent - // Sits in the gap the server glance used to occupy: - // bottom edge 20px above the bottom cluster, which is - // the same margin that widget anchored with, so the - // OSD lands exactly where it always has. + // 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: parent.horizontalCenter // Both surfaces span the full screen height from y=0, so // the bar's coordinates carry over unchanged. - y: barBottom.y - 20 - height + y: srvWidget.y + srvWidget.height / 2 - height / 2 opacity: shown ? 1 : 0 visible: opacity > 0.01 @@ -7167,6 +7936,10 @@ in } } + ${lib.optionalString (!isMacbook) '' + // Archie. One dog, primary screen only. + Archie { id: archieDog; primary: bar.screen === Quickshell.screens[0] } + ''} } ''; };