pragma ComponentBehavior: Bound import Quickshell import Quickshell.Io import QtQuick import QtQuick.Layouts import qs.hyprchrome.theme import qs.hyprchrome.widgets.panels // Host identity in the hyprchrome panel chrome: the machine's name set large, // with its timezone and the current date and time. // // Expanded: name on the left, clock stack on the right. // Collapsed: name, time and zone abbreviation on the header line. // // The name and the zone come from one shell call at startup — neither changes // while the shell runs, so there is nothing to poll. The clock is a plain // Timer; `now` is the single source both densities read, so they always agree // down to the second. BarPanel { id: panel panelId: "HST" title: "HOST" meta: panel.zoneAbbrev property string hostName: "LOCAL" property string zoneName: "" // IANA, e.g. EUROPE/BERLIN property string userName: "" // whoever is logged in, e.g. DARMAN property string localIp: "" // interface holding the default route property string tailnetIp: "" // tailscale0, when the tailnet is up property date now: new Date() // Qt resolves the abbreviation ("CEST") against the same zone the offset // comes from, so the two can never disagree. readonly property string zoneAbbrev: Qt.formatDateTime(panel.now, "t") readonly property string utcOffset: { const hours = -panel.now.getTimezoneOffset() / 60; return "UTC" + (hours >= 0 ? "+" : "") + (Number.isInteger(hours) ? hours : hours.toFixed(1)); } function two(value) { return value < 10 ? "0" + value : String(value); } function timeText(value) { return panel.two(value.getHours()) + ":" + panel.two(value.getMinutes()) + ":" + panel.two(value.getSeconds()); } function dateText(value) { const days = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]; return days[value.getDay()] + " // " + panel.two(value.getDate()) + " " + months[value.getMonth()] + " " + value.getFullYear(); } Timer { interval: 1000 running: true repeat: true triggeredOnStart: true onTriggered: panel.now = new Date() } Process { running: true // /etc/localtime is a symlink into the zoneinfo tree; its tail is the // IANA name, which no environment variable reliably carries. command: ["sh", "-c", "cat /proc/sys/kernel/hostname; readlink -f /etc/localtime; id -un"] stdout: StdioCollector { onStreamFinished: { const lines = this.text.trim().split("\n"); if (lines.length > 0 && lines[0].trim().length > 0) panel.hostName = lines[0].trim().toUpperCase(); if (lines.length > 1) { const zone = lines[1].match(/zoneinfo\/(.+)$/); if (zone) panel.zoneName = zone[1].toUpperCase(); } if (lines.length > 2 && lines[2].trim().length > 0) panel.userName = lines[2].trim().toUpperCase(); } } } // Addresses, unlike the name and the zone, can change under a running // shell — a lease renewal, `tailscale up`/`down` — so this one polls. // // The local address is taken from the interface carrying the default // route, with tailscale0 excluded: as an exit node it holds the default // route itself, and the panel would then show the tailnet address twice. Process { id: addresses command: ["sh", "-c", "dev=$(ip -4 route show default | grep -v tailscale0 | awk '{print $5; exit}');" + " ip -4 -o addr show dev \"$dev\" scope global 2>/dev/null | awk '{split($4,a,\"/\"); print a[1]; exit}';" + " ip -4 -o addr show dev tailscale0 scope global 2>/dev/null | awk '{split($4,a,\"/\"); print a[1]; exit}'"] stdout: StdioCollector { onStreamFinished: { const lines = this.text.trim().split("\n"); panel.localIp = lines.length > 0 ? lines[0].trim() : ""; panel.tailnetIp = lines.length > 1 ? lines[1].trim() : ""; } } } Timer { interval: 30000 running: true repeat: true triggeredOnStart: true onTriggered: { if (!addresses.running) addresses.running = true; } } // Collapsed: who and when, nothing else. summary: Row { spacing: 10 Text { id: hostLabel text: panel.hostName color: Theme.text font.family: Theme.displayFont font.pixelSize: 13 font.bold: true font.letterSpacing: 1.2 } // The pieces are set at two sizes. A Row positions its children at the // top and has no item alignment of its own (that is Grid), and a // vertical anchor inside a positioner is ignored — so the smaller // pieces take the tallest one's height and centre their text in it. Text { text: panel.timeText(panel.now) color: Theme.accent font.family: Theme.displayFont font.pixelSize: 13 font.bold: true height: hostLabel.implicitHeight verticalAlignment: Text.AlignVCenter } Text { text: panel.dateText(panel.now) color: Theme.text font.family: Theme.microFont font.pixelSize: 11 height: hostLabel.implicitHeight verticalAlignment: Text.AlignVCenter } Text { text: panel.zoneAbbrev color: Theme.muted font.family: Theme.microFont font.pixelSize: 11 height: hostLabel.implicitHeight verticalAlignment: Text.AlignVCenter } } // Expanded: name left, clock stack right. RowLayout { width: parent.width // No explicit height: a third line in the identity column has to grow // the panel, and BarPanel measures the body to decide how tall it is. spacing: 16 Column { Layout.fillWidth: true Layout.fillHeight: true Text { id: hostText text: panel.hostName color: Theme.text font.family: Theme.displayFont font.pixelSize: 25 font.bold: true font.letterSpacing: 2 elide: Text.ElideRight } Text { text: (panel.userName || "NODE") + " // " + (panel.zoneName || "LOCAL") color: Theme.accent font.family: Theme.microFont font.pixelSize: 9 font.letterSpacing: 1.4 elide: Text.ElideRight } Text { text: (panel.localIp || "--") + " // " + (panel.tailnetIp || "NO TAILNET") color: Theme.muted font.family: Theme.microFont font.pixelSize: 9 font.letterSpacing: 1.4 elide: Text.ElideRight } } // Rule between identity and clock, the same hairline the dense bar // puts between its host name and node readout. Rectangle { // The layout owns x/y/width/height: the 8px inset that was // `height: parent.height - 8` becomes margins, and a bare `width: 2` // would be overridden. Layout.preferredWidth: 2 Layout.fillHeight: true Layout.topMargin: 4 Layout.bottomMargin: 4 color: Theme.hair } Column { id: clock // Both columns share the width evenly, as before. `width: 210` here was // dead — a layout assigns width — and `Layout.alignment` is ignored // while an item fills. Layout.fillWidth: true Layout.fillHeight: true spacing: 2 Text { width: parent.width text: panel.timeText(panel.now) horizontalAlignment: Text.AlignRight color: Theme.text font.family: Theme.displayFont font.pixelSize: 18 font.bold: true font.letterSpacing: 1 } Text { width: parent.width text: panel.dateText(panel.now) horizontalAlignment: Text.AlignRight color: Theme.accent font.family: Theme.microFont font.pixelSize: 11 } Text { width: parent.width text: panel.zoneAbbrev + " // " + panel.utcOffset horizontalAlignment: Text.AlignRight color: Theme.muted font.family: Theme.microFont font.pixelSize: 9 font.letterSpacing: 0.7 } } } }