The expanded body is a RowLayout, so the divider's `y`/`width`/`height`, the clock column's `width: 210`, and the user/zone text's `y: 32` were all being discarded silently — a layout assigns its children's geometry, and a Column positions its own. Only the anchors among these ever warned. The divider's 8px inset becomes Layout margins, and `Layout.alignment` goes from the clock column, where it is ignored while the item fills. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
221 lines
7.2 KiB
QML
221 lines
7.2 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 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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
height: 48
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|