feat(quickshell): cap the rail's open chamfers and trace between them
A chamfer with no neighbour behind it now carries the corner it removed, put back OUTSIDE the panel as a detached accent triangle. capGap is the perpendicular distance from the cut, hence the per-axis shift of capGap over root 2: the cap moves along the cut's normal, not along an axis. The trace joining two caps belongs to the RAIL, not the panel — the run it draws is the gap BETWEEN two panels, which no panel can see. HyprChromeBar filters the row down to panels (a slack Item has no `rightChamfer`, so spacers drop out, and dropping them is exactly what makes a trace span them), then joins each right cap to the next left cap: straight, one 45° step at the midpoint of the gap, straight. It meets the middle of each cap's outward face rather than its tip. TestPanel is a staging slot between two spacers. It counts seconds since load, which is the cheapest proof the panel is live, and standing alone it exercises a cap and a trace at both ends — something a rail of butted panels never does. Restructures the module in the same commit, since the moves and the edits above land in the same files: folders are PascalCase, the bar and its widgets moved under Widgets/Bar, and DebugWindow takes the HyprChrome Theme instead of the legacy one. The two singletons are identical today, so that is not a visual fix — it is a palette edit reaching the debug stage in future. Rename detection needs -M40% to follow HyprChromeBar, which grew past the default similarity threshold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U19X5LGTxtq4pb4jdNVivv
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.HyprChrome.Theme
|
||||
import qs.HyprChrome.Widgets.Bar.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user