A third line under user // timezone, muted so the identity block stays a descending three tiers. Unlike the name, user and zone — none of which can change under a running shell — addresses can, so this polls on a 30s timer instead of joining the startup one-shot. The local one is read off the interface holding the default route with tailscale0 excluded: as an exit node tailscale0 holds that route itself, and the panel would show the tailnet address on both sides. Drops the expanded body's `height: 48`. A third line has to grow the panel, and BarPanel measures the body to decide how tall it is, so a fixed height would have clipped the new row instead. The bar follows through targetHeight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
267 lines
9.1 KiB
QML
267 lines
9.1 KiB
QML
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
|
|
}
|
|
}
|
|
}
|
|
}
|