feat(quickshell): add hyprchrome bar with collapsible panels
A second shell chrome under dotfiles/quickshell/hyprchrome, built around a BarPanel that carries TWO renderings of its data: the default children are the expanded detail view, `summary` the terse one shown while collapsed. Both stay bound to the same sources, so the densities cannot disagree, and the panel cross-fades between them while its height animates. Panels: HostPanel (hostname, user, timezone, clock), VitalsPanel (CPU load and temperature, memory, GPU load and temperature, all metered), TrayPanel (system tray, self-sizing). HyprChromeBar pins them to DP-2 and owns `expanded` for the whole rail — SUPER A, via GlobalShortcut "chrome". GPU busy comes off sysfs rather than the node_exporter scrape VitalsData already does: the hwmon collector carries the card's temps, power and clocks but not its utilisation. The bar's height binds to each panel's `targetHeight` — where it will settle, not where the animation currently is — because the exclusive zone is window-sized by default, and binding to the animated height relayouts every tiled window on the output twelve times per toggle. The zone follows the target immediately so the desktop reflows once, at the start; the surface itself shrinks only after the panels finish, or it would clip them mid-animation. DebugWindow stages a widget in the middle of the secondary monitor (SUPER CTRL D), masked so only the staged widget takes pointer input. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.widgets.theme
|
||||
|
||||
// Debug stage: a bare, chrome-less staging area in the middle of ONE monitor
|
||||
// (the secondary by default), used to look at a widget in isolation before it
|
||||
// has a home in the bar or a launcher.
|
||||
//
|
||||
// DebugWindow {
|
||||
// VitalBar { width: 220; value: 0.4 }
|
||||
// }
|
||||
//
|
||||
// Children are reparented into the centred slot, which sizes itself to them —
|
||||
// so they must carry their own size (implicit or explicit). Do NOT anchor a
|
||||
// child to the slot (`anchors.fill: parent`): the slot measures its children,
|
||||
// so that is a binding loop.
|
||||
//
|
||||
// Nothing is drawn around them — no panel, no background, no dim: whatever is
|
||||
// staged is exactly what appears. Only the staged widgets take pointer input
|
||||
// (`mask`), so the rest of the monitor stays clickable, and the window never
|
||||
// takes keyboard focus. Toggle: SUPER CTRL D.
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
// Monitor to stage on. Falls back to the LAST connected screen when the
|
||||
// name matches nothing, so a single-monitor session still gets a stage.
|
||||
property string screenName: "HDMI-A-1"
|
||||
property bool active: true
|
||||
|
||||
default property alias content: slot.data
|
||||
|
||||
// Quickshell.screens is a QML list, not a JS array — no .find() on it.
|
||||
readonly property var targetScreen: {
|
||||
const screens = Quickshell.screens;
|
||||
if (screens.length === 0)
|
||||
return null;
|
||||
for (let i = 0; i < screens.length; i++) {
|
||||
if (screens[i].name === root.screenName)
|
||||
return screens[i];
|
||||
}
|
||||
return screens[screens.length - 1];
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
root.active = !root.active;
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "debug"
|
||||
description: "Toggle the debug widget stage"
|
||||
onPressed: root.toggle()
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: win
|
||||
|
||||
screen: root.targetScreen
|
||||
visible: root.active && root.targetScreen !== null
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
// A HUD, not a modal: never steal the keyboard from the focused window.
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
// Ignore the dense bar's exclusive zone so "centred" means the centre of
|
||||
// the monitor, not the centre of what is left below the bar.
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Everything outside the staged widgets is click-through.
|
||||
mask: Region {
|
||||
item: slot
|
||||
}
|
||||
|
||||
Item {
|
||||
id: slot
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: Math.max(childrenRect.width, placeholder.visible ? placeholder.implicitWidth : 0)
|
||||
height: Math.max(childrenRect.height, placeholder.visible ? placeholder.implicitHeight : 0)
|
||||
}
|
||||
|
||||
// Sits beside the slot, not inside it, so it never counts itself. Without
|
||||
// it an unsized child looks identical to a broken window.
|
||||
Text {
|
||||
id: placeholder
|
||||
|
||||
visible: slot.children.length === 0
|
||||
anchors.centerIn: parent
|
||||
text: "NO WIDGETS STAGED"
|
||||
color: Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 8
|
||||
font.letterSpacing: 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user