Compare commits

...
Author SHA1 Message Date
darman 15e081e52d Merge remote-tracking branch 'origin/feat/quickshell-dense-bar' into feat/quickshell-dense-bar 2026-08-30 21:18:05 +02:00
luna abccf536f6 [verified] feat(quickshell): add cyber dock launcher 2026-08-30 16:54:19 +00:00
darmanandClaude Opus 5 61da7748af chore(terra): unload hypr-chrome, round corners at 25
Comments out both the plugin and its `plugin.hyprchrome` settings — Hyprland
rejects plugin config for a plugin that is not loaded, so the two have to go
together. The flake input stays, so re-enabling is two uncommented lines.

With the window frames gone, decoration rounding goes 10 -> 25 and
rounding_power 2.0 -> 1.0 (previously unset). Both were applied live first via
`hyprctl eval 'hl.config{...}'`; plain `hyprctl keyword` is refused by the lua
config's non-legacy parser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
2026-08-29 04:13:54 +02:00
darmanandClaude Opus 5 bbe35dd72e feat(quickshell): join adjacent panels into a continuous rail
At spacing 0 a row of panels read as separate tiles, each closing its own
silhouette. BarPanel can now drop either cut corner — rightChamfer (top-right)
and leftChamfer (bottom-left), the only two the shape cuts — so an edge that a
neighbour butts against runs square into it. The bar turns off the host's right
and the vitals' left; the tray keeps both, since the spacer between them is not
a panel and that edge is free.

A side with its chamfer off also draws a connector: the shared edge restroked
in accent at outlineWidth + 2, so the join reads as a deliberate seam rather
than two outlines that happen to touch. The outline's own width becomes a
property so the connector can be defined against it instead of as a second
literal.

The lower-right accent strip takes accentLineWidth, the same slug-derived width
the upper-left one already used, instead of its own Math.min(49, width / 3).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
2026-08-29 04:13:43 +02:00
darmanandClaude Opus 5 c69aa4fea2 fix(quickshell): stop the bar twitching when a collapse finishes
Two causes, both outside the panel animation itself.

bodyBottom and bodyEndPadding are animated reals, so their sum spends the tail
of every transition on a fraction. A layout rounds that up, then drops a pixel
the moment the animation lands on its exact value — a 1px hop after the motion
has visibly finished. implicitHeight and targetHeight now round.

The rest was the surface. Hyprland animates layer-surface resizes (animations
enabled, `layers` left at its default), and the deferred shrink put that resize
exactly where the panel motion ended. The bar is now sized once to the expanded
rail via the new BarPanel.expandedHeight and never resizes; only exclusiveZone
tracks the state, so the desktop still reflows once per toggle, at the start.
That retires barHeight, the content-height handler and the 340ms shrink timer.

A surface that stays tall would swallow clicks across the screen while the rail
is collapsed, so input is masked to the panel row.

Instrumenting BarPanel per frame ruled the panels themselves out first: slug
and summary hold the same absolute y through an entire collapse.

The bar and scrim also get their own layer namespaces. Nothing depends on them
yet; they are the handle for a layerrule that would exempt the rail from
compositor animations without catching the launchers, which share the default
"quickshell" namespace and do want their fade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
2026-08-29 03:44:40 +02:00
darmanandClaude Opus 5 e7c30fd39d feat(quickshell): show local and tailnet address in HostPanel
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
2026-08-29 03:30:35 +02:00
darmanandClaude Opus 5 15698d3a78 feat(devshell): stream quickshell warnings and errors in nix develop
qs-log follows the working-tree instance's log filtered to WARN|ERROR (-a for
everything), and the shellHook starts it in the background once qs-dev is up,
taking it down again in the exit trap alongside qs-prod. A binding loop or a
failed binding is a WARN, and easy to miss when it scrolls past unwatched.

It starts at the end of the log rather than replaying the backlog, and
re-attaches in a loop: `qs log -f` ends when the instance it attached to exits,
and the dev shell outlives individual instances.

Also corrects the hot-reload note added in 7268221, which was wrong on both
counts. Tested against the running shell: `touch` never reloads (mtime is not a
content change) and inode-replacing edits like `sed -i` are picked up fine. The
reliable check is whether `qs log` shows a "Reloading configuration..." line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
2026-08-29 03:25:26 +02:00
darmanandClaude Opus 5 7eb7b948e8 feat(quickshell): dim the desktop behind the expanded hyprchrome bar
A full-screen scrim carrying the dense bar's drafting grid, shown while the
rail is expanded and faded out with it.

It sits on the TOP layer while the bar moves to OVERLAY. Both on one layer
would stack by surface creation order, which is not something to rely on; a
layer apart makes "above windows, below the bar" a guarantee. The mask is an
empty Region, so the scrim takes no clicks and reserves nothing.

The grid is the dense bar's, at twice the spacing and with registration
crosses on every other intersection. Its 0.018 opacity was tuned against a
near-black panel and is invisible over a scrim on lit windows, so grid and
crosses are both properties rather than constants, and the lines take a
desaturated accent derived from the palette instead of plain text colour.

Cross geometry rounds with Math.floor on both the mark's offset and the bars
inside it. anchors.*Center halves the box unfloored, which put an even-sized
mark half a pixel off the 1px rule it registers against. crossThickness is in
steps for the same reason: 1 -> 1px, 2 -> 3px, 3 -> 5px, since only an odd
width straddles a rule symmetrically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
2026-08-29 03:25:14 +02:00
darmanandClaude Opus 5 7268221a51 fix(quickshell): drop layout-overridden geometry in HostPanel
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
2026-08-29 02:41:39 +02:00
darmanandClaude Opus 5 e38a8403ac 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
2026-08-29 02:38:00 +02:00
darman 6406e06330 Auto stash before merge of "feat/quickshell-dense-bar" and "origin/feat/quickshell-dense-bar" 2026-08-28 23:03:36 +02:00
luna 9b6b799a5d [verified] feat(quickshell): add orbit launcher variant 2026-08-28 20:58:37 +00:00
luna 5ecc73f084 [verified] feat(quickshell): add blade launcher variant 2026-08-28 20:48:23 +00:00
luna fdc00a8574 [verified] feat(quickshell): add dense application launcher 2026-08-28 07:35:06 +00:00
36 changed files with 3979 additions and 704 deletions
+2 -2
View File
@@ -14,7 +14,7 @@ qs -p . # run this directory explicitly regardless of symlink
qs -n # exit immediately if another instance is already running (use to avoid duplicate shells while iterating)
```
Quickshell hot-reloads QML on file save when already running, so for most edits just save and check the running instance rather than restarting `qs`. There is no separate build/lint/test tooling in this repo — verification is visual/behavioral via the running shell. `qmlls` (QML language server) is configured via `.qmlls.ini` for editor diagnostics.
Quickshell hot-reloads QML on file save when already running, so for most edits just save and check the running instance rather than restarting `qs`. It watches file CONTENT: `touch` alone never reloads (mtime is not a change), while any real edit does, including inode-replacing ones (`sed -i`, `perl -i`). A save that is not picked up leaves the shell rendering the previous config with no error — `qs log` shows a `Reloading configuration...` line for every save it saw, so that is the check. There is no separate build/lint/test tooling in this repo — verification is visual/behavioral via the running shell. `qmlls` (QML language server) is configured via `.qmlls.ini` for editor diagnostics.
## Architecture
@@ -27,7 +27,7 @@ Quickshell hot-reloads QML on file save when already running, so for most edits
**Directory layout**:
- `widgets/bar/``Bar.qml` is the main sidebar (right-anchored, full height) hosting the module stack (date, clock, tray, decorative dividers); `BarTop.qml`/`BarBottom.qml` are thin accent-colored strips anchored to the top/bottom edges.
- `widgets/bar/modules/` — individual bar widgets (`Clock`, `Date`, `Tray`/`TrayItem`, `Volume`) built on the shared `BarWidget` base component.
- `widgets/launcher/`app launcher panel (`Launcher``LauncherPanel``SearchBar`/`TextField`), currently a WIP skeleton (search box has no backing logic yet, `Launcher.qml` is instantiated with `visible: false`).
- `widgets/launcher/`shared `AppModel` search/execution plus eight launcher variants. `ApplicationLauncher` (variant 8 and the primary `SUPER` launcher) keeps its visual core in the headlessly renderable `ApplicationLauncherContent`; variants 17 remain available on `SUPER CTRL 17` for comparison.
- `widgets/decoration/` — reusable QtQuick `Shape`-based visual accents (angled panel edges, slashes) used to give bar panels their non-rectangular look. `Dummy.qml` is a placeholder/test rectangle.
- `widgets/input/` — thin wrappers around `QtQuick.Controls` inputs (currently just `TextField`).
- `widgets/layout/``HorizontalStack`/`VerticalStack`: `RowLayout`/`ColumnLayout` wrappers that expose `default property alias content` for terser call sites, with a trailing filler `Item` that soaks up remaining space.
@@ -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
}
}
}
@@ -0,0 +1,61 @@
pragma Singleton
import Quickshell
import QtQuick
// Single source of truth for the shell's palette and font families.
//
// The shell previously ran two unrelated palettes: an amber one (#FFD063) used
// by the launchers, sidebar, systray, vitals and notifications, and an orange
// one (#e8722a) that only the dense bar had, tokenized as per-file properties.
// This unifies on the ORANGE values under the AMBER naming scheme.
//
// `surface` deliberately takes the dense bar's void (#0a0a0a) rather than the
// old panel background (#0F1012), which also absorbs the near-identical
// #0A0A0C scrim.
Singleton {
// ---- core ----
readonly property color accent: "#e8722a" // was #FFD063 (amber) / #e8722a (bar)
readonly property color text: "#dedede" // was #EEEEEE / #dedede
readonly property color muted: "#858585" // was #7A7B7D / #858585
readonly property color surface: "#0a0a0a" // was #0F1012 + #0A0A0C + #0a0a0a
readonly property color hot: "#ff6b4a" // alert/hot; no bar equivalent, kept
// ---- supporting darks ----
// A three-step ramp above `surface`. `raised` also absorbs #22262C, which
// differed from #292C30 by an imperceptible amount across two call sites.
readonly property color selection: "#1a1c1f" // selected row fill
readonly property color raised: "#292c30" // raised surface / border
readonly property color disabled: "#3a3d42" // unknown / disabled stroke
// ---- accents ----
// The pale "flash" the top/bottom bars show while a launcher is open. Was a
// hand-picked #FFF3C0 against amber; derived here so it tracks `accent`.
// 55% toward white reproduces the original amber relationship closely
// (#FFD063 -> #FFE9B8 vs the hand-picked #FFF3C0).
readonly property color accentSoft: Qt.tint(accent, Qt.rgba(1, 1, 1, 0.55))
readonly property color highlight: "#ffffff"
// ---- fonts ----
// Two faces. `readoutFont` is an alias rather than a second literal so the
// two roles cannot silently drift apart; point it at a different family if
// the readouts should ever diverge from the headings again.
//
// Installed by services/desktop/desktop-apps.nix (nerd-fonts.departure-mono).
// The former readout face, Digital-7 Mono, was never packaged — it relied on
// a manual ~/.dots/fonts/digital_7 install, so dropping it also removes an
// undeclared external dependency.
readonly property string displayFont: "DepartureMono Nerd Font" // headings, large values
readonly property string readoutFont: displayFont // seven-segment readouts: launchers, sidebar, systray, vitals
readonly property string microFont: "DejaVu Sans Mono" // dense bar micro labels
// ---- derived alpha variants ----
// The dense bar hand-encoded these as Qt.rgba(0.87,0.87,0.87,a) = text and
// Qt.rgba(0.91,0.45,0.16,a) = accent. Expressed as functions so the
// relationship survives a palette change.
function textAlpha(a) { return Qt.rgba(text.r, text.g, text.b, a); }
function accentAlpha(a) { return Qt.rgba(accent.r, accent.g, accent.b, a); }
// Hairline rule / panel outline: text at 28%.
readonly property color hair: textAlpha(0.28)
}
@@ -0,0 +1,163 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Wayland
import QtQuick
import qs.hyprchrome.theme
// Full-screen scrim behind the bar: dims the desktop and lays the dense bar's
// drafting grid over it while the rail is expanded.
//
// Sits on the TOP layer while the bar itself is on OVERLAY. Two surfaces on the
// same layer stack by creation order, which is not something to rely on; one
// layer apart is a guarantee — above ordinary windows, below the bar.
//
// It reserves nothing and takes no input: the mask is an empty Region, so
// clicks land on whatever is underneath rather than on the scrim.
PanelWindow {
id: backdrop
property bool active: true
property real dim: 0.55
property int gridSpacing: 120
// The dense bar drew this grid at 0.018 against its own near-black panel.
// Over a 55% scrim on top of lit windows that is invisible, so it is a
// knob rather than a constant.
property real gridOpacity: 0.1
// The accent with its saturation pulled back: warm enough to read as part
// of the palette, not so loud that a full-screen grid competes with the
// bar. Derived rather than a literal so it tracks a palette change.
// Registration crosses sit on every other intersection of the grid.
property color crossColor: Theme.muted
property real crossOpacity: 0.35
property int crossSize: 20
// Thickness in STEPS, not pixels: 1 -> 1px, 2 -> 3px, 3 -> 5px. Only odd
// widths can straddle a 1px rule symmetrically, so an even pixel count
// would push every mark half a pixel off the grid it registers against.
property int crossThickness: 2
readonly property int crossWeight: Math.max(1, backdrop.crossThickness) * 2 - 1
property color gridColor: Qt.hsla(Theme.accent.hslHue,
Theme.accent.hslSaturation * 0.45,
Theme.accent.hslLightness,
1)
visible: scrim.opacity > 0
WlrLayershell.namespace: "hyprchrome-scrim"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
color: "transparent"
readonly property int crossColumns: Math.ceil(backdrop.width / (backdrop.gridSpacing * 2)) + 1
readonly property int crossRows: Math.ceil(backdrop.height / (backdrop.gridSpacing * 2)) + 1
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {}
Item {
id: scrim
anchors.fill: parent
opacity: backdrop.active ? 1 : 0
// Matched to the bar's own collapse so the scrim and the panels resolve
// together rather than one trailing the other.
Behavior on opacity {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Rectangle {
anchors.fill: parent
color: Theme.surface
opacity: backdrop.dim
}
// Faint drafting grid; no gradient and deliberately subordinate to
// whatever is showing through it.
Repeater {
model: Math.ceil(scrim.width / backdrop.gridSpacing)
Rectangle {
required property int index
x: index * backdrop.gridSpacing
width: 1
height: scrim.height
color: backdrop.gridColor
opacity: backdrop.gridOpacity
}
}
Repeater {
model: Math.ceil(scrim.height / backdrop.gridSpacing)
Rectangle {
required property int index
y: index * backdrop.gridSpacing
width: scrim.width
height: 1
color: backdrop.gridColor
opacity: backdrop.gridOpacity
}
}
// Register marks on every other line, so they land on a 2x grid rather
// than on every crossing — sparse enough to read as drafting registration
// rather than as texture.
Repeater {
model: backdrop.crossColumns * backdrop.crossRows
Item {
required property int index
readonly property int column: index % backdrop.crossColumns
readonly property int row: Math.floor(index / backdrop.crossColumns)
// Marks on every other rule in both directions, so they line up
// in columns as well as rows. Both offsets are whole multiples of
// gridSpacing, so every mark lands on a real intersection.
//
// Math.floor, not /2: the item offset and the bars inside it must
// round the same way, or an even crossSize sits half a pixel off the
// rule it marks.
x: column * backdrop.gridSpacing * 2 - Math.floor(backdrop.crossSize / 2)
y: row * backdrop.gridSpacing * 2 - Math.floor(backdrop.crossSize / 2)
width: backdrop.crossSize
height: backdrop.crossSize
opacity: backdrop.crossOpacity
Rectangle {
// Placed with the same Math.floor the item's own offset uses.
// anchors.verticalCenter halves the height unfloored, so an even
// crossSize put the 1px bar half a pixel off the rule it marks.
y: Math.floor(backdrop.crossSize / 2) - Math.floor(backdrop.crossWeight / 2)
width: parent.width
height: backdrop.crossWeight
color: backdrop.crossColor
}
Rectangle {
x: Math.floor(backdrop.crossSize / 2) - Math.floor(backdrop.crossWeight / 2)
width: backdrop.crossWeight
height: parent.height
color: backdrop.crossColor
}
}
}
}
}
@@ -0,0 +1,143 @@
import QtQuick
import QtQuick.Shapes
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.hyprchrome.widgets
import qs.hyprchrome.widgets.host
import qs.hyprchrome.widgets.vitals
import qs.hyprchrome.widgets.tray
Scope {
id: root
// Monitor the rail lives on. Falls back to the FIRST connected screen when
// the name matches nothing, so the bar still appears on a single-monitor
// session or after a cable swap (DebugWindow falls back to the last one
// instead — it wants the secondary).
property string screenName: "DP-2"
// 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[0];
}
// Density is a property of the BAR: every panel follows it, so the whole rail
// expands and collapses as one. Panels keep their own animation; only the
// decision is centralised here.
property bool expanded: true
function toggle() {
root.expanded = !root.expanded;
}
// SUPER A — see hosts/terra/home/hyprland.nix.
GlobalShortcut {
name: "chrome"
description: "Expand or collapse the hyprchrome bar"
onPressed: root.toggle()
}
// Scrim first: it is a layer below the bar, so stacking does not depend on
// creation order, but keeping the declaration order the same as the visual
// order costs nothing.
ChromeBackdrop {
screen: root.targetScreen
active: root.expanded
}
PanelWindow {
id: window
screen: root.targetScreen
visible: root.targetScreen !== null
// One layer above the scrim, so the stacking is guaranteed rather than
// dependent on surface creation order. See ChromeBackdrop.
// Own namespace so a layerrule can exempt the rail from Hyprland's layer
// animation without also catching the launchers, which share the default
// "quickshell" namespace and do want their fade.
WlrLayershell.namespace: "hyprchrome-bar"
WlrLayershell.layer: WlrLayer.Overlay
property int margin: 12
// The surface never resizes: it is always tall enough for the expanded
// rail, and only the exclusive zone tracks the current state. Resizing a
// layer surface makes Hyprland animate the change, which showed up as the
// panels twitching a pixel or two the moment the collapse finished.
//
// The zone still follows the target height, so tiled windows reflow once
// per toggle, at the start, and slide while the panels animate.
readonly property real expandedContent: Math.max(hostPanel.expandedHeight, vitalsPanel.expandedHeight, trayPanel.expandedHeight) + window.margin * 2
readonly property real contentHeight: Math.max(hostPanel.targetHeight, vitalsPanel.targetHeight, trayPanel.targetHeight) + window.margin * 2
implicitHeight: Math.round(window.expandedContent)
exclusionMode: ExclusionMode.Normal
exclusiveZone: Math.round(window.contentHeight)
// Only the panels take input. Without this the surface would keep eating
// clicks across its full height while the rail is collapsed.
mask: Region {
item: panelRow
}
anchors { top: true; left: true; right: true; }
color: "transparent"
RowLayout {
id: panelRow
x: window.margin
y: window.margin
width: window.width - window.margin * 2
spacing: 0
HostPanel {
id: hostPanel
expanded: root.expanded
// Vitals butts against its right edge; the left end of the row is free.
rightChamfer: false
toggleOnClick: false
Layout.preferredWidth: 350
Layout.fillHeight: true
}
VitalsPanel {
id: vitalsPanel
expanded: root.expanded
// Host on the left, the spacer on the right — and a spacer is not a
// panel, so that edge keeps its cut.
leftChamfer: false
toggleOnClick: false
Layout.preferredWidth: 500
Layout.fillHeight: true
}
// Slack lives between the left group and the tray, so the tray sits
// flush right whatever the other panels measure.
Item { Layout.fillWidth: true }
TrayPanel {
id: trayPanel
expanded: root.expanded
toggleOnClick: false
Layout.fillHeight: true
}
}
}
}
@@ -0,0 +1,39 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.hyprchrome.theme
import qs.hyprchrome.widgets.panels
// Staging widget for the debug window: a BarPanel carrying filler copy in both
// of the panel's densities — the full block when expanded, one elided line when
// collapsed. Both read the same `text`, so the two modes cannot disagree.
//
// Give it a width; the height follows whichever body is showing.
BarPanel {
id: lorem
property string text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since 1966, when designers at Letraset and James Mosley, the librarian at St Bride Printing Library in London, took a 1914 Cicero translation and scrambled it to make dummy text for Letraset's Body Type sheets. It has survived not only many decades, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised thanks to these sheets and more recently with desktop publishing software like Aldus PageMaker and Microsoft Word including versions of Lorem Ipsum."
title: "LOREM IPSUM"
// Collapsed: one line, cut off where the panel ends.
summary: Text {
width: parent.width
text: lorem.text
color: Theme.muted
font.family: Theme.displayFont
font.pixelSize: 12
maximumLineCount: 1
elide: Text.ElideRight
}
// Expanded: the whole thing, wrapped.
Text {
width: parent.width
text: lorem.text
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 12
wrapMode: Text.WordWrap
}
}
@@ -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.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
}
}
}
}
@@ -0,0 +1,430 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
import qs.hyprchrome.theme
// Chamfered panel chrome for the dense status rail: outline, corner accent
// lines, header strip (id chip / title / meta / tick marks) and a collapsing
// body.
//
// The body holds TWO renderings of the same data: the default children are the
// expanded detail view, `summary` the terse one shown while collapsed. Both
// stay instantiated and bound to the same sources — two renderings of one
// truth, not two truths — and the panel cross-fades between them while its
// height animates to whichever is showing.
//
// BarPanel {
// panelId: "02"
// summary: Text { text: "CPU 43%" }
// MetricRow { /* the full view */ }
// }
//
// Expanded, the detail view sits under the header rule. Collapsed, the summary
// moves up ONTO the header line, starting just right of the slug chip and
// centred in the strip, so the whole panel becomes a single line. The slug
// in both modes; title, meta and tick deco fade out with the detail body.
//
// Each slot keeps its own fixed geometry — only the panel's height animates,
// and the body is clipped — so neither rendering reflows while the other one
// is fading. Slot children are measured (`childrenRect`), so they must carry
// their own size and must NOT anchor to the slot.
//
// Colors and fonts both come from the Theme singleton.
Item {
id: panel
property string panelId: ""
property string title: ""
property string meta: ""
property bool expanded: true
property int chamfer: 13
property int offsetY: 2
property int accentLineThickness: 3
readonly property int headerHeight: 28
// Breathing room between the header rule and the detail view. The
// collapsed summary is unaffected — it sits on the header line itself.
property int headerGap: 8
// Left offset of the header row, and the inset the upper-left accent line
// is sized against.
readonly property int headerPadding: 8
// Upper-left accent line, sized to the slug chip it underlines. Lives on
// the panel rather than on the ShapePath: a PathLine does not see its
// ShapePath's own properties by bare name (they resolve through the
// component scope, not the parent object).
// Unclamped: what the header chrome WANTS to span. Everything that has to
// stay independent of the panel's final width reads this one — a width
// that clamps against panel.width cannot also decide it.
readonly property real headerContentWidth: slugChip.width + panel.headerPadding * 2
readonly property real accentLineWidth: Math.min(panel.width, panel.headerContentWidth)
// Narrowest the panel can be before the title runs into the meta text and
// tick deco. A panel that sizes itself to its content (the tray) has to
// take this as a floor; none of it depends on panel.width, so it can.
readonly property real headerMinWidth: panel.headerContentWidth + panelTitle.width
+ headerRow.spacing + headerEnd.width + 12 + panel.headerPadding
property int padding: 12
// Floor for the animated height, so a collapsed strip with a short (or
// empty) summary still reads as a panel rather than a hairline.
property int minimumHeight: 38
// Self-toggling is a convenience for staging a panel on its own. A host
// that drives `expanded` for a whole group turns it off: assigning to a
// bound property from a click would destroy that binding for good.
property bool toggleOnClick: true
default property alias content: detail.data
property alias summary: brief.data
// Where the summary sits when collapsed: just past the slug chip, and
// centred in the strip the panel collapses to, which is sized to the
// summary itself (or the height floor, whichever is taller).
readonly property real briefLeft: panel.headerContentWidth
readonly property real collapsedHeight: Math.max(panel.minimumHeight, brief.height + panel.headerPadding * 2)
readonly property real briefTop: Math.round((panel.collapsedHeight - brief.height) / 2)
// Bottom of the visible body, and the gap kept below it. The states bind
// these to whichever rendering is showing and the transitions animate them,
// so they are plain properties rather than ternaries on implicitHeight —
// an animation cannot drive a binding.
property real bodyBottom: detail.y + detail.height
property real bodyEndPadding: panel.padding
// Rounded: bodyBottom and bodyEndPadding are animated reals, so the sum
// spends the tail of every transition on a fraction. A layout rounds that
// UP, then drops a pixel the moment the animation lands on its exact
// value — a 1px hop after the motion has visibly finished.
implicitHeight: Math.round(Math.max(panel.minimumHeight, panel.bodyBottom + panel.bodyEndPadding))
// Where the panel settles in each state, skipping the values the transition
// passes through: a host sizes its surface and its exclusive zone from these
// rather than from the animated height, so the desktop is relaid out once per
// toggle instead of once per animation frame.
// Height of the expanded body regardless of the current state — what a
// host needs to size a surface that must not resize when panels collapse.
readonly property real expandedHeight: Math.round(Math.max(panel.minimumHeight,
detail.y + detail.height + panel.padding))
readonly property real targetHeight: panel.expanded
? panel.expandedHeight
: Math.round(Math.max(panel.minimumHeight, panel.collapsedHeight))
// The two chamfer cuts (top-right at y=chamfer, bottom-left at
// height-chamfer) cross once the panel is shorter than twice the chamfer,
// which turns the outline inside out for the last frames of a collapse.
readonly property real activeChamfer: Math.max(2, Math.min(panel.chamfer, panel.height / 2 - 1))
// The silhouette has exactly two cut corners: top-right and bottom-left.
// Turn one off where another panel butts against that side, so a row laid
// out with no spacing reads as one continuous strip instead of a line of
// separate tiles. A cut corner costs its side nothing when disabled — the
// edge simply runs square into the neighbour.
property bool rightChamfer: true // top-right cut
property bool leftChamfer: true // bottom-left cut
readonly property real rightCut: panel.rightChamfer ? panel.activeChamfer : 0
readonly property real leftCut: panel.leftChamfer ? panel.activeChamfer : 0
// The seam where two panels meet is drawn a step heavier and in accent, so
// a chamfer-less join reads as a deliberate connector rather than as two
// outlines that happen to touch.
property int outlineWidth: 1
readonly property int connectorWidth: panel.outlineWidth + 2
Shape {
id: panelShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
// Main panel shape
ShapePath {
fillColor: Theme.surface
strokeColor: Theme.hair
strokeWidth: panel.outlineWidth
startX: 0; startY: panel.offsetY
PathLine { x: panelShape.width - panel.rightCut; y: panel.offsetY }
PathLine { x: panelShape.width; y: panel.rightChamfer ? panel.activeChamfer : panel.offsetY }
PathLine { x: panelShape.width; y: panelShape.height }
PathLine { x: panel.leftCut; y: panelShape.height }
PathLine { x: 0; y: panelShape.height - panel.leftCut }
PathLine { x: 0; y: panel.offsetY }
}
// Connecting edges — drawn only on a side whose chamfer is off, which is
// exactly where a neighbour butts against this panel.
ShapePath {
fillColor: "transparent"
strokeColor: Theme.accent
strokeWidth: panel.rightChamfer ? 0 : panel.connectorWidth
startX: panelShape.width; startY: panel.offsetY
PathLine { x: panelShape.width; y: panelShape.height }
}
ShapePath {
fillColor: "transparent"
strokeColor: Theme.accent
strokeWidth: panel.leftChamfer ? 0 : panel.connectorWidth
startX: 0; startY: panel.offsetY
PathLine { x: 0; y: panelShape.height }
}
// Upper left accent line
ShapePath {
fillColor: Theme.accent
strokeWidth: 0
startX: 0; startY: 0
PathLine { x: panel.accentLineWidth; y: 0 }
PathLine { x: panel.accentLineWidth; y: panel.accentLineThickness }
PathLine { x: 0; y: panel.accentLineThickness }
PathLine { x: 0; y: 0 }
}
// Lower right accent line
ShapePath {
fillColor: Theme.accent
strokeWidth: 0
startX: panelShape.width; startY: panelShape.height
PathLine { x: panelShape.width - panel.accentLineWidth; y: panelShape.height }
PathLine { x: panelShape.width - panel.accentLineWidth; y: panelShape.height - panel.accentLineThickness }
PathLine { x: panelShape.width; y: panelShape.height - panel.accentLineThickness }
PathLine { x: panelShape.width; y: panelShape.height }
}
}
// Header
Row {
id: headerRow
x: panel.headerPadding
// Centred in the header band rather than pinned, so the chip keeps
// clear of the rule when the slug font changes size.
y: Math.round((panel.headerHeight - height) / 2)
// Puts the title where the accent line ends: chip + headerPadding on
// both sides of it.
spacing: panel.headerPadding
// Header slug — the one piece that survives a collapse, so the strip
// still says which panel it is.
Rectangle {
id: slugChip
width: panelSlugText.implicitWidth + 6
height: panelSlugText.implicitHeight + 3
color: Theme.accent
Text {
id: panelSlugText
anchors.centerIn: parent
text: panel.panelId
color: Theme.surface
font.family: Theme.microFont
font.pixelSize: 9
font.bold: true
}
}
// Header title
Item {
id: panelTitle
width: panelTitleText.implicitWidth + 6
height: panelTitleText.implicitHeight + 3
Text {
id: panelTitleText
anchors.centerIn: parent
text: panel.title
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 11
font.bold: true
font.letterSpacing: 1.1
elide: Text.ElideRight
}
}
}
// Everything else in the header fades as one, so a collapse is a single
// coordinated move rather than four independently timed ones.
Item {
id: headerExtras
anchors.fill: parent
// Header separator
Rectangle {
x: 1; y: panel.headerHeight
width: parent.width - 2
height: 1
color: Theme.text
opacity: 0.12
}
// Header end deco
Row {
id: headerEnd
anchors.right: parent.right
anchors.rightMargin: 12
y: 12
spacing: 4
Text {
id: metaText
visible: panel.meta.length > 0
text: panel.meta
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 8
font.letterSpacing: 0.7
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
}
// Same height as the meta text, so the Row aligning both by their
// tops also aligns them by their bottoms — no wrapper needed.
Row {
spacing: 2
Repeater {
model: 5
Rectangle { required property int index; width: 4; height: metaText.implicitHeight; color: Theme.accent }
}
}
}
}
// Body. Spans the panel and clips, because mid-collapse the panel is
// already shorter than the detail view that is still fading out.
Item {
id: bodyClip
anchors.fill: parent
clip: true
Item {
id: detail
x: panel.padding
y: panel.headerHeight + panel.headerGap
width: Math.max(0, panel.width - panel.padding * 2)
height: childrenRect.height
visible: opacity > 0
}
Item {
id: brief
x: panel.briefLeft
y: panel.briefTop
width: Math.max(0, panel.width - panel.briefLeft - panel.padding)
height: childrenRect.height
opacity: 0
visible: opacity > 0
}
}
// Click anywhere on the panel to switch densities. Sits above the body, so
// interactive content in a slot would need its own handler on top of this.
MouseArea {
anchors.fill: parent
onClicked: {
if (panel.toggleOnClick)
panel.expanded = !panel.expanded;
}
}
states: [
State {
name: "expanded"
when: panel.expanded
PropertyChanges {
target: panel
bodyBottom: detail.y + detail.height
bodyEndPadding: panel.padding
}
PropertyChanges { target: detail; opacity: 1 }
PropertyChanges { target: brief; opacity: 0 }
PropertyChanges { target: panelTitle; opacity: 1 }
PropertyChanges { target: headerExtras; opacity: 1 }
},
State {
name: "collapsed"
when: !panel.expanded
// Symmetric about the slug's midline: the same gap the summary has
// above it is kept below, so the strip reads as one line.
PropertyChanges {
target: panel
bodyBottom: brief.y + brief.height
bodyEndPadding: panel.collapsedHeight - brief.y - brief.height
}
PropertyChanges { target: detail; opacity: 0 }
PropertyChanges { target: brief; opacity: 1 }
PropertyChanges { target: panelTitle; opacity: 0 }
PropertyChanges { target: headerExtras; opacity: 0 }
}
]
// Out fast, resize, in late. Fading both bodies on the same clock would
// show them at half opacity on top of each other in the middle frames.
transitions: [
Transition {
to: "collapsed"
ParallelAnimation {
NumberAnimation {
targets: [detail, panelTitle, headerExtras]
property: "opacity"
duration: 90
easing.type: Easing.OutCubic
}
NumberAnimation {
target: panel
properties: "bodyBottom,bodyEndPadding"
duration: 200
easing.type: Easing.OutCubic
}
SequentialAnimation {
PauseAnimation { duration: 110 }
NumberAnimation {
target: brief
property: "opacity"
duration: 120
easing.type: Easing.OutCubic
}
}
}
},
Transition {
to: "expanded"
ParallelAnimation {
NumberAnimation {
target: brief
property: "opacity"
duration: 90
easing.type: Easing.OutCubic
}
NumberAnimation {
target: panel
properties: "bodyBottom,bodyEndPadding"
duration: 200
easing.type: Easing.OutCubic
}
SequentialAnimation {
PauseAnimation { duration: 110 }
NumberAnimation {
targets: [detail, panelTitle, headerExtras]
property: "opacity"
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
]
}
@@ -0,0 +1,72 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.SystemTray
import QtQuick
import QtQuick.Shapes
import qs.hyprchrome.theme
// One tray item as a chamfered cell. Declares `modelData` required so it can be
// a Repeater delegate directly, without an Item wrapper in between.
//
// Left click activates, right click opens the item's own menu — anchored under
// the cell rather than at the window edge, since this rail sits along the top.
MouseArea {
id: cell
required property SystemTrayItem modelData
property int iconSize: 18
property int chamfer: 4
implicitWidth: cell.iconSize + 8
implicitHeight: cell.iconSize + 8
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: event => {
if (event.button === Qt.LeftButton) {
cell.modelData.activate();
} else if (cell.modelData.hasMenu) {
const window = cell.QsWindow?.window;
if (window) {
const anchor = cell.mapToItem(null, 0, cell.height);
cell.modelData.display(window, anchor.x, anchor.y);
}
}
event.accepted = true;
}
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: 1
strokeColor: cell.containsMouse ? Theme.accent : Theme.textAlpha(0.18)
fillColor: cell.containsMouse ? Theme.selection : Theme.textAlpha(0.06)
startX: cell.chamfer
startY: 0
PathLine { x: cell.width; y: 0 }
PathLine { x: cell.width; y: cell.height - cell.chamfer }
PathLine { x: cell.width - cell.chamfer; y: cell.height }
PathLine { x: 0; y: cell.height }
PathLine { x: 0; y: cell.chamfer }
PathLine { x: cell.chamfer; y: 0 }
Behavior on strokeColor { ColorAnimation { duration: 150 } }
}
}
Image {
anchors.centerIn: parent
source: cell.modelData.icon
width: cell.iconSize
height: cell.iconSize
fillMode: Image.PreserveAspectFit
smooth: true
}
}
@@ -0,0 +1,80 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.SystemTray
import QtQuick
import qs.hyprchrome.theme
import qs.hyprchrome.widgets.panels
// System tray in the hyprchrome panel chrome, in both densities: the same
// items, drawn large enough to hit when expanded and shrunk onto the header
// line when collapsed.
//
// Unlike the other panels this one sizes itself horizontally — the item count
// is whatever the session happens to be running — so a layout can just give it
// `Layout.fillHeight` and let its implicit width stand.
BarPanel {
id: panel
panelId: "TRY"
title: "SYSTEM TRAY"
meta: panel.itemCount + (panel.itemCount === 1 ? " ITEM" : " ITEMS")
readonly property int itemCount: SystemTray.items.values.length
implicitWidth: Math.max(panel.headerMinWidth,
panel.briefLeft + brief.implicitWidth + panel.padding,
panel.padding * 2 + icons.implicitWidth)
// Collapsed: the same icons, small, on the header line.
summary: Row {
id: brief
spacing: 5
Repeater {
model: SystemTray.items
TrayIcon {
iconSize: 13
}
}
Text {
visible: panel.itemCount === 0
text: "NO ITEMS"
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 9
font.letterSpacing: 1.2
height: 21
verticalAlignment: Text.AlignVCenter
}
}
// Expanded: full-size cells.
Row {
id: icons
spacing: 8
Repeater {
model: SystemTray.items
TrayIcon {
iconSize: 18
}
}
Text {
visible: panel.itemCount === 0
text: "NO ITEMS"
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 9
font.letterSpacing: 1.2
height: 26
verticalAlignment: Text.AlignVCenter
}
}
}
@@ -0,0 +1,60 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Io
import QtQuick
// amdgpu utilisation, straight off sysfs.
//
// node_exporter's hwmon collector carries the card's temperatures, power and
// clocks — which is where VitalsData gets them — but not its busy percentage,
// so this is the one vital that cannot come from the same scrape.
//
// The card number is globbed rather than pinned: it is card1 on terra today,
// but it depends on probe order and moves when a GPU is added or removed.
Scope {
id: root
// Poll only while something is showing it, like VitalsData.
property bool active: false
property int interval: 2000
property real value: 0 // 0..1 busy
property bool ready: false
onActiveChanged: {
if (!root.active)
root.ready = false;
}
Process {
id: probe
command: ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | head -n1"]
stdout: StdioCollector {
onStreamFinished: {
const busy = parseInt(this.text.trim(), 10);
if (isFinite(busy)) {
root.value = Math.max(0, Math.min(1, busy / 100));
root.ready = true;
} else {
// No amdgpu (or no permission) — leave the meter blank
// rather than pinning it at zero, which would read as idle.
root.ready = false;
}
}
}
}
Timer {
interval: root.interval
running: root.active
repeat: true
triggeredOnStart: true
onTriggered: {
if (!probe.running)
probe.running = true;
}
}
}
@@ -0,0 +1,40 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.hyprchrome.theme
// Segmented horizontal meter: a row of cells lit up to `value`.
//
// A copy of the dense bar's meter rather than a reuse of it — that one is an
// inline component inside DenseBarContent.qml and so is not visible from any
// other file (the same reason BarPanel inlines its own MicroText).
Row {
id: meter
property int segments: 16
property real value: 0 // 0..1
property bool ready: false
property real warn: 0.85 // fraction at which the lit cells go hot
readonly property real fraction: Math.max(0, Math.min(1, meter.value))
readonly property bool hot: meter.ready && meter.fraction >= meter.warn
readonly property color litColor: meter.hot ? Theme.hot : Theme.accent
spacing: 2
Repeater {
model: meter.segments
Rectangle {
required property int index
readonly property bool lit: meter.ready && index < Math.round(meter.fraction * meter.segments)
width: Math.max(2, (meter.width - (meter.segments - 1) * meter.spacing) / meter.segments)
height: meter.height
color: lit ? meter.litColor : Theme.textAlpha(0.06)
border.width: 1
border.color: lit ? meter.litColor : Theme.textAlpha(0.18)
}
}
}
@@ -0,0 +1,58 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.hyprchrome.theme
// One metric of the expanded vitals panel: label, meter, readout on a line.
//
// The label and readout columns are fixed so the meters of stacked rows line
// up on both edges regardless of how long any one readout gets.
Item {
id: row
property string label: ""
property real value: 0 // 0..1
property string readout: "--"
property bool ready: false
property real warn: 0.85
property int labelWidth: 52
property int readoutWidth: 46
readonly property bool hot: row.ready && row.value >= row.warn
implicitHeight: 11
Text {
anchors.verticalCenter: parent.verticalCenter
width: row.labelWidth
text: row.label
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 10
font.letterSpacing: 0.7
elide: Text.ElideRight
}
SegmentMeter {
x: row.labelWidth
anchors.verticalCenter: parent.verticalCenter
width: Math.max(0, row.width - row.labelWidth - row.readoutWidth)
height: 9
value: row.value
ready: row.ready
warn: row.warn
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: row.readoutWidth
text: row.readout
color: row.hot ? Theme.hot : Theme.text
font.family: Theme.displayFont
font.pixelSize: 10
font.bold: true
horizontalAlignment: Text.AlignRight
}
}
@@ -0,0 +1,142 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.hyprchrome.theme
import qs.hyprchrome.widgets.panels
import qs.widgets.vitals
// Host vitals in the hyprchrome panel chrome, in both of BarPanel's densities.
//
// Expanded: CPU load and temperature, memory usage, GPU load and temperature,
// each as a segmented meter with its readout.
// Collapsed: the same five numbers as percentages behind Nerd Font glyphs.
//
// Both bodies read the same properties below, so the two densities cannot
// disagree — they are one set of numbers rendered twice.
//
// The scrape comes from the shared VitalsData (node_exporter over loopback);
// GPU busy is the one reading that scrape does not carry, so it comes off
// sysfs through GpuBusy.
BarPanel {
id: panel
panelId: "MON"
title: "RESOURCE MONITOR"
meta: vitals.failed ? "OFFLINE" : vitals.ready ? "REALTIME" : "PRIMING"
// Poll only while the panel exists on screen; both sources idle otherwise.
property bool polling: true
// Temperatures are metered against a 0100 °C span so a bar means the same
// thing on every row.
readonly property real tempCeiling: 100
readonly property real memoryFraction: vitals.memTotal > 0 ? vitals.memUsed / vitals.memTotal : 0
readonly property bool cpuTempReady: isFinite(vitals.cpuTemp)
readonly property bool gpuTempReady: isFinite(vitals.gpuTemp)
function pct(value, ready) {
return ready ? Math.round(Math.max(0, Math.min(1, value)) * 100) + "%" : "--";
}
function tempFraction(celsius) {
return isFinite(celsius) ? Math.max(0, Math.min(1, celsius / panel.tempCeiling)) : 0;
}
VitalsData {
id: vitals
active: panel.polling
}
GpuBusy {
id: gpu
active: panel.polling
}
// Collapsed: glyph + percentage, in the same order as the rows below.
// Codepoints are Nerd Fonts v3 — oct-cpu, fa-thermometer-half,
// md-memory, md-expansion-card-variant — all present in DepartureMono.
summary: RowLayout {
spacing: 12
Item { Layout.fillWidth: true }
Readout { icon: "CPU:"; value: panel.pct(vitals.cpu, vitals.ratesReady) }
Readout { icon: "CPU Temp:"; value: vitals.fmtTemp(vitals.cpuTemp) }
Readout { icon: "Mem:"; value: panel.pct(panel.memoryFraction, vitals.ready) }
Readout { icon: "GPU:"; value: panel.pct(gpu.value, gpu.ready) }
Readout { icon: "GPU Temp:"; value: vitals.fmtTemp(vitals.gpuTemp) }
Item { Layout.fillWidth: true }
}
// Expanded: the same five, metered.
Column {
width: parent.width
spacing: 5
VitalRow {
width: parent.width
label: "CPU"
value: vitals.cpu
ready: vitals.ratesReady && !vitals.failed
readout: panel.pct(vitals.cpu, vitals.ratesReady)
}
VitalRow {
width: parent.width
label: "CPU TMP"
value: panel.tempFraction(vitals.cpuTemp)
ready: panel.cpuTempReady
readout: vitals.fmtTemp(vitals.cpuTemp)
warn: 0.85
}
VitalRow {
width: parent.width
label: "MEM"
value: panel.memoryFraction
ready: vitals.ready && !vitals.failed
readout: panel.pct(panel.memoryFraction, vitals.ready)
}
VitalRow {
width: parent.width
label: "GPU"
value: gpu.value
ready: gpu.ready
readout: panel.pct(gpu.value, gpu.ready)
}
VitalRow {
width: parent.width
label: "GPU TMP"
value: panel.tempFraction(vitals.gpuTemp)
ready: panel.gpuTempReady
readout: vitals.fmtTemp(vitals.gpuTemp)
warn: 0.85
}
}
component Readout: Row {
property string icon: ""
property string value: "--"
spacing: 4
Text {
text: parent.icon
color: Theme.accent
font.family: Theme.displayFont
font.pixelSize: 12
}
Text {
text: parent.value
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 12
font.bold: true
}
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
hyprctl dispatch 'hl.dsp.global ("quickshell:launcher7")'
hyprctl dispatch 'hl.dsp.global ("quickshell:launcher8")'
+48 -8
View File
@@ -1,24 +1,26 @@
//@ pragma UseQApplication
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.widgets.bar
import qs.widgets.launcher
import qs.widgets.notifications
import qs.widgets.osd
import qs.widgets.sidebar
import qs.widgets.systray
import qs.widgets.theme
import qs.widgets.vitals
import qs.hyprchrome
import qs.hyprchrome.widgets
import qs.hyprchrome.widgets.debug
import qs.hyprchrome.widgets.host
import qs.hyprchrome.widgets.vitals
Scope {
// Dense multi-monitor status rail; visual core is headlessly renderable.
DenseBar {}
HyprChromeBar {}
// Left sidebar in the Slant (V6) style — toggle with SUPER CTRL S.
SideBar {}
BarBottom {}
// App launcher variants — toggled via Hyprland global shortcuts
// (SUPER CTRL 1/2/3). Try each and keep the one you like.
// App launcher variants — 111; variant 8 remains the primary HUD.
LauncherStack {} // 1 — left vertical list
LauncherGrid {} // 2 — centered icon grid
LauncherSpotlight {} // 3 — top-center command bar
@@ -26,10 +28,48 @@ Scope {
LauncherDock {} // 5 — deck rising from the bottom bar
LauncherSlant {} // 6 — angular / sheared panel
LauncherCorner {} // 7 — Slant (V6) copy + floating power panel (shutdown/reboot)
ApplicationLauncher {} // 8 — dense HUD command index (primary)
BladeLauncher {} // 9 — asymmetric blade matrix
OrbitLauncher {} // 10 — radial targeting arena
CyberDock {} // 11 — cyberpunk bottom cartridge dock
Notifications {}
VolumeOsd {}
// Host vitals HUD — toggle with SUPER CTRL V.
Vitals {}
// Widget staging area, centered on the secondary monitor (HDMI-A-1),
// toggled with SUPER CTRL D. Swap the children below for whatever widget
// is being worked on; they must carry their own size (see DebugWindow).
// DebugWindow {
// id: debugStage
// // VitalsPanel has no implicit width — the slot measures its children, so
// // each staged panel states its own. Height follows the mode it is in.
// // Click a panel to collapse or expand it.
// Column {
// spacing: 12
// padding: 12
// HostPanel {
// width: 560
// }
// HostPanel {
// width: 560
// expanded: false
// }
// VitalsPanel {
// width: 560
// }
// VitalsPanel {
// width: 560
// expanded: false
// }
// }
// }
}
@@ -0,0 +1,62 @@
import QtQuick
import qs.widgets.launcher
ApplicationLauncherContent {
width: 1080
height: 620
query: "term"
selectedIndex: 1
showIcons: false
apps: [
{
name: "Alacritty",
genericName: "Terminal Emulator",
comment: "GPU accelerated command interface",
icon: "utilities-terminal",
categories: ["System", "TerminalEmulator"]
},
{
name: "Ghostty",
genericName: "Terminal",
comment: "Fast native terminal for local operations",
icon: "com.mitchellh.ghostty",
categories: ["System", "TerminalEmulator"]
},
{
name: "Kitty",
genericName: "Terminal Emulator",
comment: "GPU based terminal with multiplexing",
icon: "kitty",
categories: ["System", "TerminalEmulator"]
},
{
name: "Vivaldi",
genericName: "Web Browser",
comment: "Access network and web applications",
icon: "vivaldi",
categories: ["Network", "WebBrowser"]
},
{
name: "Cosmic Files",
genericName: "File Manager",
comment: "Browse and manage local storage",
icon: "com.system76.CosmicFiles",
categories: ["System", "FileManager"]
},
{
name: "Obsidian",
genericName: "Knowledge Base",
comment: "Local-first markdown workspace",
icon: "obsidian",
categories: ["Office"]
},
{
name: "Steam",
genericName: "Game Platform",
comment: "Launch and manage games",
icon: "steam",
categories: ["Game"]
}
]
}
@@ -0,0 +1,20 @@
import QtQuick
import qs.widgets.launcher
BladeLauncherContent {
width: 1120
height: 640
query: "dev"
selectedIndex: 2
showIcons: false
apps: [
{ name: "Visual Studio Code", genericName: "Code Editor", comment: "Edit and debug software projects", icon: "visual-studio-code", categories: ["Development"] },
{ name: "GitKraken", genericName: "Git Client", comment: "Inspect branches and repository history", icon: "gitkraken", categories: ["Development"] },
{ name: "OpenCode", genericName: "AI Development", comment: "Agentic terminal coding environment", icon: "utilities-terminal", categories: ["Development", "Utility"] },
{ name: "Alacritty", genericName: "Terminal Emulator", comment: "GPU accelerated command interface", icon: "utilities-terminal", categories: ["System"] },
{ name: "Vivaldi", genericName: "Web Browser", comment: "Access network and web applications", icon: "vivaldi", categories: ["Network"] },
{ name: "Obsidian", genericName: "Knowledge Base", comment: "Local-first markdown workspace", icon: "obsidian", categories: ["Office"] },
{ name: "Blender", genericName: "3D Creation", comment: "Model, animate and render 3D scenes", icon: "blender", categories: ["Graphics"] },
{ name: "Steam", genericName: "Game Platform", comment: "Launch and manage games", icon: "steam", categories: ["Game"] }
]
}
@@ -0,0 +1,21 @@
import QtQuick
import qs.widgets.launcher
CyberDockContent {
width: 1440
height: 272
query: ""
selectedIndex: 3
showIcons: false
apps: [
{ name: "Alacritty", genericName: "Terminal", comment: "GPU accelerated command interface", icon: "utilities-terminal" },
{ name: "Vivaldi", genericName: "Browser", comment: "Access web applications", icon: "vivaldi" },
{ name: "Obsidian", genericName: "Knowledge", comment: "Local-first markdown workspace", icon: "obsidian" },
{ name: "Cosmic Files", genericName: "Files", comment: "Browse local storage", icon: "com.system76.CosmicFiles" },
{ name: "Jellyfin", genericName: "Media", comment: "Stream the homelab library", icon: "jellyfin-media-player" },
{ name: "Spotify", genericName: "Music", comment: "Browse and play music", icon: "spotify" },
{ name: "Steam", genericName: "Games", comment: "Launch and manage games", icon: "steam" },
{ name: "Blender", genericName: "3D", comment: "Model and render scenes", icon: "blender" },
{ name: "Visual Studio Code", genericName: "Editor", comment: "Edit and debug projects", icon: "visual-studio-code" }
]
}
@@ -0,0 +1,20 @@
import QtQuick
import qs.widgets.launcher
OrbitLauncherContent {
width: 1120
height: 660
query: "media"
selectedIndex: 3
showIcons: false
apps: [
{ name: "Vivaldi", genericName: "Web Browser", comment: "Access network and web applications", icon: "vivaldi" },
{ name: "Obsidian", genericName: "Knowledge Base", comment: "Local-first markdown workspace", icon: "obsidian" },
{ name: "Blender", genericName: "3D Creation", comment: "Model, animate and render 3D scenes", icon: "blender" },
{ name: "Jellyfin Media Player", genericName: "Media Player", comment: "Stream media from the homelab library", icon: "jellyfin-media-player" },
{ name: "Spotify", genericName: "Music Player", comment: "Browse and play music", icon: "spotify" },
{ name: "Steam", genericName: "Game Platform", comment: "Launch and manage games", icon: "steam" },
{ name: "Cosmic Files", genericName: "File Manager", comment: "Browse and manage local storage", icon: "com.system76.CosmicFiles" },
{ name: "Alacritty", genericName: "Terminal Emulator", comment: "GPU accelerated command interface", icon: "utilities-terminal" }
]
}
@@ -1,81 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.widgets.launcher
import qs.widgets.theme
Scope {
id: root
Variants {
model: Quickshell.screens
PanelWindow {
required property var modelData
screen: modelData
color: "transparent"
anchors {
bottom: true
left: true
right: true
}
implicitHeight: wrapper.implicitHeight
WrapperRectangle {
id: wrapper
color: "transparent"
anchors.fill: parent
leftMargin: 12
rightMargin: 12
bottomMargin: 12
Rectangle {
implicitHeight: 12
// Brightens when the Dock launcher (variant 5) opens.
color: LauncherState.dockOpen ? Theme.accentSoft : Theme.accent
Behavior on color {
ColorAnimation {
duration: 250
}
}
// Gentle "listening" pulse while the dock is open.
Rectangle {
anchors.fill: parent
color: Theme.highlight
opacity: 0
visible: LauncherState.dockOpen
SequentialAnimation on opacity {
running: LauncherState.dockOpen
loops: Animation.Infinite
NumberAnimation {
to: 0.4
duration: 700
easing.type: Easing.InOutSine
}
NumberAnimation {
to: 0.0
duration: 700
easing.type: Easing.InOutSine
}
}
}
}
}
}
}
}
@@ -1,81 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import qs.widgets.launcher
import qs.widgets.theme
Scope {
id: root
Variants {
model: Quickshell.screens
PanelWindow {
id: topBar
required property var modelData
screen: modelData
color: "transparent"
anchors {
top: true
left: true
right: true
}
implicitHeight: wrapper.implicitHeight
WrapperRectangle {
id: wrapper
color: "transparent"
anchors.fill: parent
margin: 12
bottomMargin: 0
Rectangle {
implicitHeight: 6
// Brightens when the Console launcher (variant 4) opens.
color: LauncherState.consoleOpen ? Theme.accentSoft : Theme.accent
Behavior on color {
ColorAnimation {
duration: 250
}
}
// Gentle "listening" pulse while the console is open.
Rectangle {
anchors.fill: parent
color: Theme.highlight
opacity: 0
visible: LauncherState.consoleOpen
SequentialAnimation on opacity {
running: LauncherState.consoleOpen
loops: Animation.Infinite
NumberAnimation {
to: 0.4
duration: 700
easing.type: Easing.InOutSine
}
NumberAnimation {
to: 0.0
duration: 700
easing.type: Easing.InOutSine
}
}
}
}
}
}
}
}
@@ -96,9 +96,8 @@ Item {
height: 92
readonly property real identityWidth: root.compact ? 250 : 320
readonly property real radarWidth: root.compact ? 130 : 164
readonly property real stateWidth: root.compact ? 210 : 250
readonly property real flexWidth: Math.max(250, (width - identityWidth - radarWidth - stateWidth - 32) / 2)
readonly property real stateWidth: root.compact ? 300 : 350
readonly property real flexWidth: Math.max(250, (width - identityWidth - stateWidth - 32) / 2)
Row {
anchors.fill: parent
@@ -234,24 +233,10 @@ Item {
}
}
StatusBarPanel {
width: array.radarWidth
height: array.height
panelId: "03"
title: "SCAN"
RadarGauge {
anchors.horizontalCenter: parent.horizontalCenter
y: 28
size: 57
level: root.cpuFraction
}
}
StatusBarPanel {
width: array.flexWidth
height: array.height
panelId: "04"
panelId: "03"
title: "CARRIER UPLINK"
meta: root.networkInterface.toUpperCase()
@@ -298,7 +283,7 @@ Item {
StatusBarPanel {
width: array.stateWidth
height: array.height
panelId: "05"
panelId: "04"
title: "SYSTEM STATE"
Row {
@@ -0,0 +1,101 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import QtQuick
import qs.widgets.theme
// Primary application launcher (variant 8). The full-screen layer-shell adapter
// owns focus, DesktopEntries and execution; ApplicationLauncherContent remains
// an Item so the complete visual state can be rendered headlessly.
Scope {
id: root
property bool active: false
function toggle() { root.active = !root.active; }
GlobalShortcut {
name: "launcher8"
description: "Toggle dense application command index"
onPressed: root.toggle()
}
PanelWindow {
id: win
visible: root.active
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
color: Theme.textAlpha(0)
anchors {
top: true
left: true
right: true
bottom: true
}
property int selectedIndex: 0
function clampSelection(index) {
if (model.apps.length === 0)
return 0;
return Math.max(0, Math.min(model.apps.length - 1, index));
}
function move(delta) {
const count = model.apps.length;
if (count === 0)
return;
selectedIndex = ((selectedIndex + delta) % count + count) % count;
}
function launch(index) {
if (model.launch(index))
root.active = false;
}
onVisibleChanged: {
if (visible) {
content.clearSearch();
selectedIndex = 0;
content.focusSearch();
}
}
AppModel {
id: model
search: content.query
}
Rectangle {
anchors.fill: parent
color: Theme.surface
opacity: 0.72
MouseArea {
anchors.fill: parent
onClicked: root.active = false
}
}
ApplicationLauncherContent {
id: content
anchors.centerIn: parent
width: 1080
height: 620
scale: Math.min(1, (parent.width - 64) / width, (parent.height - 64) / height)
transformOrigin: Item.Center
apps: model.apps
selectedIndex: win.selectedIndex
onSelectionRequested: index => win.selectedIndex = win.clampSelection(index)
onMoveRequested: delta => win.move(delta)
onLaunchRequested: index => win.launch(index)
onDismissRequested: root.active = false
}
}
}
@@ -0,0 +1,602 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import qs.widgets.bar
import qs.widgets.theme
// Headlessly renderable visual core for the primary application launcher.
// Runtime concerns (DesktopEntries, layer shell, launching) stay in
// ApplicationLauncher.qml; this component only renders state and emits intent.
Item {
id: root
property var apps: []
property int selectedIndex: 0
property bool showIcons: true
property alias query: searchInput.text
readonly property var selectedApp: apps.length > 0 && selectedIndex >= 0 && selectedIndex < apps.length
? apps[selectedIndex] : null
signal selectionRequested(int index)
signal moveRequested(int delta)
signal launchRequested(int index)
signal dismissRequested
function focusSearch() { searchInput.forceActiveFocus(); }
function clearSearch() { searchInput.text = ""; }
implicitWidth: 1080
implicitHeight: 620
component MicroText: Text {
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 7
font.letterSpacing: 0.9
elide: Text.ElideRight
}
component KeyHint: Row {
property string keyText: ""
property string actionText: ""
spacing: 6
Rectangle {
width: keyLabel.implicitWidth + 10
height: 18
color: Theme.accentAlpha(0.14)
border.width: 1
border.color: Theme.accent
Text {
id: keyLabel
anchors.centerIn: parent
text: parent.parent.keyText
color: Theme.accent
font.family: Theme.microFont
font.pixelSize: 7
font.bold: true
}
}
MicroText {
anchors.verticalCenter: parent.verticalCenter
text: parent.actionText
}
}
StatusBarPanel {
anchors.fill: parent
panelId: "008"
title: "APPLICATION COMMAND INDEX"
meta: root.apps.length + " TARGETS"
chamfer: 18
// Query module.
StatusBarPanel {
id: queryPanel
x: 18
y: 34
width: 670
height: 76
panelId: "QRY"
title: "SEARCH VECTOR"
meta: "FUZZY / PREFIX"
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 31
height: 34
spacing: 12
Text {
text: ">_"
color: Theme.accent
font.family: Theme.displayFont
font.pixelSize: 20
font.bold: true
}
TextInput {
id: searchInput
Layout.fillWidth: true
Layout.fillHeight: true
verticalAlignment: TextInput.AlignVCenter
color: Theme.text
selectionColor: Theme.accent
selectedTextColor: Theme.surface
font.family: Theme.displayFont
font.pixelSize: 18
font.letterSpacing: 1
clip: true
onTextChanged: root.selectionRequested(0)
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Down:
case Qt.Key_Tab:
root.moveRequested(1);
event.accepted = true;
break;
case Qt.Key_Up:
case Qt.Key_Backtab:
root.moveRequested(-1);
event.accepted = true;
break;
case Qt.Key_PageDown:
root.moveRequested(5);
event.accepted = true;
break;
case Qt.Key_PageUp:
root.moveRequested(-5);
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
root.launchRequested(root.selectedIndex);
event.accepted = true;
break;
case Qt.Key_Escape:
root.dismissRequested();
event.accepted = true;
break;
}
}
Text {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
visible: searchInput.text.length === 0
text: "TYPE APPLICATION DESIGNATION"
color: Theme.muted
font: searchInput.font
}
}
Rectangle {
Layout.preferredWidth: 84
Layout.preferredHeight: 22
color: Theme.accentAlpha(0.12)
border.width: 1
border.color: Theme.hair
Text {
anchors.centerIn: parent
text: "LIVE INDEX"
color: Theme.accent
font.family: Theme.microFont
font.pixelSize: 7
font.bold: true
font.letterSpacing: 0.8
}
}
}
}
// Search result table.
StatusBarPanel {
id: resultPanel
x: 18
y: 118
width: 670
height: 424
panelId: "IDX"
title: "APPLICATION TABLE"
meta: root.query.length > 0 ? "FILTER ACTIVE" : "AZ / LOCAL"
ListView {
id: appList
x: 9
y: 29
width: parent.width - 18
height: parent.height - 38
clip: true
spacing: 3
model: root.apps
currentIndex: root.selectedIndex
boundsBehavior: Flickable.StopAtBounds
onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain)
delegate: MouseArea {
id: row
required property int index
required property var modelData
width: ListView.view.width
height: 48
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
readonly property bool selected: index === root.selectedIndex
onEntered: root.selectionRequested(index)
onClicked: root.launchRequested(index)
Shape {
id: rowShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: row.selected ? Theme.selection : Theme.textAlpha(0.025)
strokeColor: row.selected ? Theme.accent : Theme.textAlpha(0.10)
strokeWidth: 1
startX: 9; startY: 0
PathLine { x: rowShape.width; y: 0 }
PathLine { x: rowShape.width - 9; y: rowShape.height }
PathLine { x: 0; y: rowShape.height }
PathLine { x: 9; y: 0 }
}
ShapePath {
fillColor: row.selected ? Theme.accent : Theme.textAlpha(0.12)
strokeWidth: 0
startX: 9; startY: 0
PathLine { x: 13; y: 0 }
PathLine { x: 4; y: rowShape.height }
PathLine { x: 0; y: rowShape.height }
PathLine { x: 9; y: 0 }
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 18
anchors.rightMargin: 18
spacing: 12
Text {
Layout.preferredWidth: 28
text: String(row.index + 1).padStart(2, "0")
color: row.selected ? Theme.accent : Theme.muted
font.family: Theme.microFont
font.pixelSize: 8
font.bold: row.selected
}
Rectangle {
Layout.preferredWidth: 34
Layout.preferredHeight: 34
color: row.selected ? Theme.accentAlpha(0.14) : Theme.textAlpha(0.035)
border.width: 1
border.color: row.selected ? Theme.accent : Theme.hair
IconImage {
visible: root.showIcons
anchors.centerIn: parent
implicitSize: 24
source: root.showIcons
? Quickshell.iconPath(row.modelData.icon || "application-x-executable", "application-x-executable")
: ""
}
Text {
visible: !root.showIcons
anchors.centerIn: parent
text: String(row.modelData.name || "?").charAt(0).toUpperCase()
color: row.selected ? Theme.accent : Theme.text
font.family: Theme.displayFont
font.pixelSize: 14
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Text {
Layout.fillWidth: true
text: row.modelData.name || "UNKNOWN TARGET"
color: row.selected ? Theme.accent : Theme.text
font.family: Theme.displayFont
font.pixelSize: 11
font.bold: row.selected
font.letterSpacing: 0.5
elide: Text.ElideRight
}
MicroText {
Layout.fillWidth: true
text: row.modelData.genericName || row.modelData.comment || "DESKTOP APPLICATION"
}
}
MicroText {
Layout.preferredWidth: 100
horizontalAlignment: Text.AlignRight
text: row.index === root.selectedIndex ? "LOCKED" : "AVAILABLE"
color: row.selected ? Theme.accent : Theme.muted
}
Rectangle {
Layout.preferredWidth: 38
Layout.preferredHeight: 6
color: Theme.textAlpha(0.05)
border.width: 1
border.color: Theme.hair
Rectangle {
width: row.selected ? parent.width : Math.max(5, parent.width * (1 - row.index / Math.max(1, root.apps.length)))
height: parent.height
color: row.selected ? Theme.accent : Theme.textAlpha(0.20)
}
}
}
}
Text {
anchors.centerIn: parent
visible: root.apps.length === 0
text: "NO EXECUTABLE TARGETS MATCH QUERY"
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 9
font.letterSpacing: 1.2
}
}
}
// Selected application inspector.
StatusBarPanel {
id: inspector
x: 700
y: 34
width: 362
height: 508
panelId: "SEL"
title: "TARGET INSPECTOR"
meta: root.selectedApp ? "LOCKED" : "NO TARGET"
Item {
id: reticle
anchors.horizontalCenter: parent.horizontalCenter
y: 39
width: 126
height: 126
Rectangle {
anchors.centerIn: parent
width: 112
height: 112
radius: 56
color: Theme.textAlpha(0.015)
border.width: 1
border.color: Theme.accentAlpha(0.52)
}
Rectangle {
anchors.centerIn: parent
width: 84
height: 84
radius: 42
color: Theme.textAlpha(0)
border.width: 1
border.color: Theme.hair
}
Rectangle { anchors.centerIn: parent; width: 126; height: 1; color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 1; height: 126; color: Theme.hair }
Rectangle {
anchors.centerIn: parent
width: 60
height: 60
color: Theme.surface
border.width: 1
border.color: Theme.accent
IconImage {
visible: root.showIcons
anchors.centerIn: parent
implicitSize: 44
source: root.showIcons && root.selectedApp
? Quickshell.iconPath(root.selectedApp.icon || "application-x-executable", "application-x-executable")
: ""
}
Text {
visible: !root.showIcons
anchors.centerIn: parent
text: root.selectedApp ? String(root.selectedApp.name || "?").charAt(0).toUpperCase() : "?"
color: Theme.accent
font.family: Theme.displayFont
font.pixelSize: 28
font.bold: true
}
}
Repeater {
model: 4
Rectangle {
required property int index
width: 5; height: 5
x: index % 2 === 0 ? 8 : reticle.width - 13
y: index < 2 ? 8 : reticle.height - 13
color: Theme.accent
}
}
}
Text {
anchors.top: reticle.bottom
anchors.topMargin: 14
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 34
horizontalAlignment: Text.AlignHCenter
text: root.selectedApp ? root.selectedApp.name : "NO TARGET"
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 19
font.bold: true
font.letterSpacing: 1
elide: Text.ElideRight
}
MicroText {
id: genericLabel
anchors.top: reticle.bottom
anchors.topMargin: 42
x: 18
width: parent.width - 36
horizontalAlignment: Text.AlignHCenter
text: root.selectedApp ? (root.selectedApp.genericName || "DESKTOP APPLICATION") : "AWAITING SEARCH VECTOR"
color: Theme.accent
}
Rectangle {
x: 18
anchors.top: genericLabel.bottom
anchors.topMargin: 12
width: parent.width - 36
height: 1
color: Theme.hair
}
MicroText {
id: description
x: 22
anchors.top: genericLabel.bottom
anchors.topMargin: 27
width: parent.width - 44
height: 42
text: root.selectedApp ? (root.selectedApp.comment || "No application description supplied by desktop entry.") : "Enter a designation to acquire an executable target."
wrapMode: Text.Wrap
elide: Text.ElideRight
maximumLineCount: 3
horizontalAlignment: Text.AlignHCenter
}
GridLayout {
id: telemetryGrid
x: 18
anchors.top: description.bottom
anchors.topMargin: 15
width: parent.width - 36
columns: 2
rowSpacing: 7
columnSpacing: 7
Repeater {
model: [
{ label: "ENTRY", value: "DESKTOP" },
{ label: "MODE", value: "DETACHED" },
{ label: "AUTH", value: "LOCAL USER" },
{ label: "INDEX", value: String(Math.max(0, root.selectedIndex + 1)).padStart(3, "0") }
]
Rectangle {
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: 46
color: Theme.textAlpha(0.025)
border.width: 1
border.color: Theme.hair
MicroText { x: 8; y: 7; width: parent.width - 16; text: modelData.label }
Text {
x: 8; y: 22; width: parent.width - 16
text: modelData.value
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 9
font.bold: true
font.letterSpacing: 0.6
elide: Text.ElideRight
}
}
}
}
Item {
x: 18
anchors.bottom: executeTile.top
anchors.bottomMargin: 12
width: parent.width - 36
height: 18
Row {
spacing: 5
Repeater {
model: 24
Rectangle {
required property int index
width: 7; height: 3
y: index % 3 === 0 ? 0 : 4
color: index < 16 ? Theme.accent : Theme.hair
transform: Rotation { angle: -35 }
}
}
}
}
MouseArea {
id: executeTile
x: 18
anchors.bottom: parent.bottom
anchors.bottomMargin: 14
width: parent.width - 36
height: 42
enabled: root.selectedApp !== null
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.launchRequested(root.selectedIndex)
Rectangle {
anchors.fill: parent
color: executeTile.containsMouse ? Theme.accent : Theme.accentAlpha(0.16)
border.width: 1
border.color: Theme.accent
Text {
anchors.centerIn: parent
text: root.selectedApp ? "EXECUTE SELECTED TARGET" : "NO TARGET ACQUIRED"
color: executeTile.containsMouse ? Theme.surface : Theme.accent
font.family: Theme.displayFont
font.pixelSize: 10
font.bold: true
font.letterSpacing: 1.2
}
}
}
}
// Dense command footer.
StatusBarPanel {
x: 18
y: 550
width: 1044
height: 52
showHeader: false
chamfer: 9
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 18
MicroText { text: "INPUT CHANNEL // KEYBOARD"; color: Theme.accent }
Rectangle { Layout.preferredWidth: 1; Layout.preferredHeight: 22; color: Theme.hair }
KeyHint { keyText: "↑ ↓"; actionText: "SELECT" }
KeyHint { keyText: "ENTER"; actionText: "EXECUTE" }
KeyHint { keyText: "ESC"; actionText: "ABORT" }
Item { Layout.fillWidth: true }
MicroText { text: "QRY:" + (root.query.length > 0 ? "ACTIVE" : "IDLE") }
MicroText { text: "CRC // A7F2" }
Rectangle {
Layout.preferredWidth: 76
Layout.preferredHeight: 4
color: Theme.accent
}
}
}
}
}
@@ -0,0 +1,80 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import QtQuick
import qs.widgets.theme
// Variant 9 — asymmetric Blade application launcher.
Scope {
id: root
property bool active: false
function toggle() { root.active = !root.active; }
GlobalShortcut {
name: "launcher9"
description: "Toggle app launcher (Blade matrix)"
onPressed: root.toggle()
}
PanelWindow {
id: win
visible: root.active
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
color: Theme.textAlpha(0)
anchors { top: true; left: true; right: true; bottom: true }
property int selectedIndex: 0
function clampSelection(index) {
return model.apps.length === 0 ? 0 : Math.max(0, Math.min(model.apps.length - 1, index));
}
function move(delta) {
const count = model.apps.length;
if (count === 0)
return;
selectedIndex = ((selectedIndex + delta) % count + count) % count;
}
function launch(index) {
if (model.launch(index))
root.active = false;
}
onVisibleChanged: {
if (visible) {
content.clearSearch();
selectedIndex = 0;
content.focusSearch();
}
}
AppModel { id: model; search: content.query }
Rectangle {
anchors.fill: parent
color: Theme.surface
opacity: 0.78
MouseArea { anchors.fill: parent; onClicked: root.active = false }
}
BladeLauncherContent {
id: content
anchors.centerIn: parent
width: 1120
height: 640
scale: Math.min(1, (parent.width - 56) / width, (parent.height - 56) / height)
transformOrigin: Item.Center
apps: model.apps
selectedIndex: win.selectedIndex
onSelectionRequested: index => win.selectedIndex = win.clampSelection(index)
onMoveRequested: delta => win.move(delta)
onLaunchRequested: index => win.launch(index)
onDismissRequested: root.active = false
}
}
}
@@ -0,0 +1,473 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import qs.widgets.theme
// Variant 9 visual core: asymmetric "Blade" composition. It shares the shell
// Theme and AppModel contract, but intentionally does not reuse StatusBarPanel
// or the nested-panel structure of ApplicationLauncherContent.
Item {
id: root
property var apps: []
property int selectedIndex: 0
property bool showIcons: true
property alias query: commandInput.text
readonly property var selectedApp: apps.length > 0 && selectedIndex >= 0 && selectedIndex < apps.length
? apps[selectedIndex] : null
signal selectionRequested(int index)
signal moveRequested(int delta)
signal launchRequested(int index)
signal dismissRequested
function focusSearch() { commandInput.forceActiveFocus(); }
function clearSearch() { commandInput.text = ""; }
implicitWidth: 1120
implicitHeight: 640
component MicroText: Text {
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 7
font.letterSpacing: 0.9
elide: Text.ElideRight
}
Rectangle { anchors.fill: parent; color: Theme.surface }
// Sparse circuit traces behind the active surfaces.
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.textAlpha(0)
strokeColor: Theme.accentAlpha(0.24)
strokeWidth: 1
startX: 24; startY: 92
PathLine { x: 115; y: 92 }
PathLine { x: 138; y: 69 }
PathLine { x: 480; y: 69 }
PathLine { x: 494; y: 55 }
PathLine { x: 840; y: 55 }
}
ShapePath {
fillColor: Theme.textAlpha(0)
strokeColor: Theme.hair
strokeWidth: 1
startX: 115; startY: 570
PathLine { x: 146; y: 601 }
PathLine { x: 775; y: 601 }
PathLine { x: 801; y: 575 }
PathLine { x: 1095; y: 575 }
}
}
// Top command mast an open angular frame rather than panel chrome.
Shape {
id: mast
x: 26; y: 18
width: parent.width - 52; height: 72
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.textAlpha(0.018)
strokeColor: Theme.hair
strokeWidth: 1
startX: 0; startY: 17
PathLine { x: 17; y: 0 }
PathLine { x: mast.width - 110; y: 0 }
PathLine { x: mast.width - 88; y: 22 }
PathLine { x: mast.width; y: 22 }
PathLine { x: mast.width; y: mast.height }
PathLine { x: 0; y: mast.height }
PathLine { x: 0; y: 17 }
}
ShapePath {
fillColor: Theme.accent
strokeWidth: 0
startX: 0; startY: 17
PathLine { x: 17; y: 0 }
PathLine { x: 122; y: 0 }
PathLine { x: 116; y: 4 }
PathLine { x: 20; y: 4 }
PathLine { x: 4; y: 20 }
PathLine { x: 4; y: mast.height }
PathLine { x: 0; y: mast.height }
PathLine { x: 0; y: 17 }
}
}
Text {
x: 54; y: 31
text: "BLADE // EXECUTION MATRIX"
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 16
font.bold: true
font.letterSpacing: 1.4
}
MicroText { x: 56; y: 57; text: "09 / APPLICATION ROUTER / LOCAL DESKTOP ENTRIES"; color: Theme.accent }
Row {
anchors.right: parent.right
anchors.rightMargin: 48
y: 37
spacing: 4
Repeater {
model: 16
Rectangle {
required property int index
width: 9; height: index % 4 === 0 ? 16 : 8
y: index % 4 === 0 ? 0 : 8
color: index < Math.min(16, root.apps.length + 6) ? Theme.accent : Theme.hair
transform: Rotation { angle: -28 }
}
}
}
// Left category rotor and mode spine.
Item {
id: rotorZone
x: 26; y: 108
width: 188; height: 444
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.textAlpha(0.018)
strokeColor: Theme.hair
strokeWidth: 1
startX: 0; startY: 0
PathLine { x: 154; y: 0 }
PathLine { x: 188; y: 34 }
PathLine { x: 188; y: 408 }
PathLine { x: 152; y: 444 }
PathLine { x: 0; y: 444 }
PathLine { x: 0; y: 0 }
}
ShapePath {
fillColor: Theme.accent
strokeWidth: 0
startX: 0; startY: 0
PathLine { x: 7; y: 0 }
PathLine { x: 7; y: 444 }
PathLine { x: 0; y: 444 }
PathLine { x: 0; y: 0 }
}
}
Text {
x: 20; y: 18
text: "VECTOR"
color: Theme.accent
font.family: Theme.displayFont
font.pixelSize: 11
font.bold: true
font.letterSpacing: 1.4
}
MicroText { x: 20; y: 38; text: "CATEGORY BUS" }
Item {
id: rotor
anchors.horizontalCenter: parent.horizontalCenter
y: 70; width: 126; height: 126
Rectangle { anchors.centerIn: parent; width: 116; height: 116; radius: 58; color: Theme.textAlpha(0); border.width: 1; border.color: Theme.accentAlpha(0.55) }
Rectangle { anchors.centerIn: parent; width: 82; height: 82; radius: 41; color: Theme.textAlpha(0); border.width: 1; border.color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 126; height: 1; color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 1; height: 126; color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 32; height: 32; color: Theme.accentAlpha(0.18); border.width: 1; border.color: Theme.accent; transform: Rotation { angle: 45 } }
Text { anchors.centerIn: parent; text: "A"; color: Theme.accent; font.family: Theme.displayFont; font.pixelSize: 16; font.bold: true }
Repeater {
model: 4
Rectangle {
required property int index
width: 6; height: 6; radius: 3
x: [16, 101, 101, 16][index]
y: [16, 16, 101, 101][index]
color: Theme.accent
}
}
}
Column {
x: 18; y: 218
width: parent.width - 38
spacing: 7
Repeater {
model: ["ALL TARGETS", "SYSTEM", "DEVELOP", "NETWORK", "MEDIA"]
Rectangle {
required property int index
required property string modelData
width: parent.width - index * 5
height: 31
x: index * 5
color: index === 0 ? Theme.accentAlpha(0.14) : Theme.textAlpha(0.025)
border.width: 1
border.color: index === 0 ? Theme.accent : Theme.hair
Text {
x: 9; anchors.verticalCenter: parent.verticalCenter
text: String(index).padStart(2, "0") + " " + modelData
color: index === 0 ? Theme.accent : Theme.muted
font.family: Theme.microFont
font.pixelSize: 7
font.bold: index === 0
font.letterSpacing: 0.7
}
}
}
}
MicroText { x: 18; anchors.bottom: parent.bottom; anchors.bottomMargin: 14; text: "BUS // UNFILTERED"; color: Theme.accent }
}
// Central command spine.
Item {
id: commandZone
x: 230; y: 108
width: 594; height: 486
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.textAlpha(0.012)
strokeColor: Theme.hair
strokeWidth: 1
startX: 20; startY: 0
PathLine { x: commandZone.width; y: 0 }
PathLine { x: commandZone.width - 20; y: commandZone.height }
PathLine { x: 0; y: commandZone.height }
PathLine { x: 20; y: 0 }
}
}
// Search blade.
Shape {
id: searchBlade
x: 12; y: 12; width: parent.width - 32; height: 58
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.accentAlpha(0.10)
strokeColor: Theme.accent
strokeWidth: 1
startX: 18; startY: 0
PathLine { x: searchBlade.width; y: 0 }
PathLine { x: searchBlade.width - 18; y: searchBlade.height }
PathLine { x: 0; y: searchBlade.height }
PathLine { x: 18; y: 0 }
}
}
Text { x: 32; y: 28; text: ">"; color: Theme.accent; font.family: Theme.displayFont; font.pixelSize: 20; font.bold: true }
TextInput {
id: commandInput
x: 62; y: 22; width: 430; height: 38
verticalAlignment: TextInput.AlignVCenter
color: Theme.text
selectionColor: Theme.accent
selectedTextColor: Theme.surface
font.family: Theme.displayFont
font.pixelSize: 17
font.letterSpacing: 1
clip: true
onTextChanged: root.selectionRequested(0)
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Down: case Qt.Key_Tab: root.moveRequested(1); event.accepted = true; break;
case Qt.Key_Up: case Qt.Key_Backtab: root.moveRequested(-1); event.accepted = true; break;
case Qt.Key_PageDown: root.moveRequested(5); event.accepted = true; break;
case Qt.Key_PageUp: root.moveRequested(-5); event.accepted = true; break;
case Qt.Key_Return: case Qt.Key_Enter: root.launchRequested(root.selectedIndex); event.accepted = true; break;
case Qt.Key_Escape: root.dismissRequested(); event.accepted = true; break;
}
}
Text { anchors.fill: parent; verticalAlignment: Text.AlignVCenter; visible: commandInput.text.length === 0; text: "ACQUIRE TARGET"; color: Theme.muted; font: commandInput.font }
}
MicroText { anchors.right: parent.right; anchors.rightMargin: 38; y: 35; text: root.apps.length + " HIT"; color: Theme.accent }
ListView {
id: bladeList
x: 10; y: 84; width: parent.width - 26; height: 382
model: root.apps
currentIndex: root.selectedIndex
clip: true
spacing: 4
boundsBehavior: Flickable.StopAtBounds
onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain)
delegate: MouseArea {
id: blade
required property int index
required property var modelData
readonly property bool selected: index === root.selectedIndex
width: ListView.view.width - (index % 2 === 0 ? 0 : 16)
height: 43
x: index % 2 === 0 ? 0 : 16
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.selectionRequested(index)
onClicked: root.launchRequested(index)
Shape {
id: bladeShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: blade.selected ? Theme.selection : Theme.textAlpha(0.025)
strokeColor: blade.selected ? Theme.accent : Theme.textAlpha(0.12)
strokeWidth: 1
startX: 16; startY: 0
PathLine { x: bladeShape.width; y: 0 }
PathLine { x: bladeShape.width - 24; y: bladeShape.height }
PathLine { x: 0; y: bladeShape.height }
PathLine { x: 16; y: 0 }
}
ShapePath {
fillColor: blade.selected ? Theme.accent : Theme.textAlpha(0.12)
strokeWidth: 0
startX: 16; startY: 0
PathLine { x: 22; y: 0 }
PathLine { x: 6; y: bladeShape.height }
PathLine { x: 0; y: bladeShape.height }
PathLine { x: 16; y: 0 }
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 26
anchors.rightMargin: 28
spacing: 10
Text { Layout.preferredWidth: 26; text: String(blade.index + 1).padStart(2, "0"); color: blade.selected ? Theme.accent : Theme.muted; font.family: Theme.microFont; font.pixelSize: 7 }
Rectangle {
Layout.preferredWidth: 28; Layout.preferredHeight: 28
color: blade.selected ? Theme.accentAlpha(0.14) : Theme.textAlpha(0.025)
border.width: 1; border.color: blade.selected ? Theme.accent : Theme.hair
IconImage { visible: root.showIcons; anchors.centerIn: parent; implicitSize: 20; source: root.showIcons ? Quickshell.iconPath(blade.modelData.icon || "application-x-executable", "application-x-executable") : "" }
Text { visible: !root.showIcons; anchors.centerIn: parent; text: String(blade.modelData.name || "?").charAt(0).toUpperCase(); color: blade.selected ? Theme.accent : Theme.text; font.family: Theme.displayFont; font.pixelSize: 11; font.bold: true }
}
Text { Layout.fillWidth: true; text: blade.modelData.name || "UNKNOWN"; color: blade.selected ? Theme.accent : Theme.text; font.family: Theme.displayFont; font.pixelSize: 10; font.bold: blade.selected; elide: Text.ElideRight }
MicroText { Layout.preferredWidth: 125; horizontalAlignment: Text.AlignRight; text: blade.modelData.genericName || "APPLICATION" }
Rectangle { Layout.preferredWidth: blade.selected ? 32 : 12; Layout.preferredHeight: 3; color: blade.selected ? Theme.accent : Theme.hair }
}
}
Text { anchors.centerIn: parent; visible: root.apps.length === 0; text: "NO TARGET VECTOR"; color: Theme.muted; font.family: Theme.microFont; font.pixelSize: 9; font.letterSpacing: 1.2 }
}
}
// Right dossier: open brackets and data lines, deliberately not a panel.
Item {
id: dossier
x: 842; y: 108
width: 252; height: 444
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.textAlpha(0)
strokeColor: Theme.accent
strokeWidth: 2
startX: 36; startY: 0
PathLine { x: 0; y: 0 }
PathLine { x: 0; y: 86 }
}
ShapePath {
fillColor: Theme.textAlpha(0)
strokeColor: Theme.accent
strokeWidth: 2
startX: dossier.width - 36; startY: dossier.height
PathLine { x: dossier.width; y: dossier.height }
PathLine { x: dossier.width; y: dossier.height - 86 }
}
ShapePath {
fillColor: Theme.textAlpha(0)
strokeColor: Theme.hair
strokeWidth: 1
startX: 20; startY: 20
PathLine { x: dossier.width; y: 20 }
PathLine { x: dossier.width; y: dossier.height - 20 }
PathLine { x: 0; y: dossier.height - 20 }
PathLine { x: 0; y: 104 }
}
}
MicroText { x: 18; y: 12; text: "ACTIVE DOSSIER"; color: Theme.accent }
Text {
x: 18; y: 45; width: parent.width - 36
text: root.selectedApp ? root.selectedApp.name : "NO TARGET"
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 18
font.bold: true
font.letterSpacing: 0.8
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
}
MicroText { x: 18; y: 94; width: parent.width - 36; text: root.selectedApp ? (root.selectedApp.genericName || "DESKTOP APPLICATION") : "AWAITING ACQUISITION"; color: Theme.accent }
Rectangle { x: 18; y: 119; width: parent.width - 36; height: 1; color: Theme.hair }
MicroText { x: 18; y: 137; width: parent.width - 36; height: 52; wrapMode: Text.Wrap; maximumLineCount: 4; text: root.selectedApp ? (root.selectedApp.comment || "No metadata supplied by desktop entry.") : "Enter a search vector and select an executable target." }
Column {
x: 18; y: 210; width: parent.width - 36; spacing: 8
Repeater {
model: [
{ k: "INDEX", v: String(Math.max(0, root.selectedIndex + 1)).padStart(3, "0") },
{ k: "TYPE", v: "DESKTOP ENTRY" },
{ k: "ROUTE", v: "DETACHED" },
{ k: "STATE", v: root.selectedApp ? "ARMED" : "IDLE" }
]
Item {
required property var modelData
width: parent.width; height: 27
Rectangle { x: 0; y: parent.height - 1; width: parent.width; height: 1; color: Theme.hair }
MicroText { x: 0; anchors.verticalCenter: parent.verticalCenter; text: modelData.k }
Text { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: modelData.v; color: modelData.k === "STATE" ? Theme.accent : Theme.text; font.family: Theme.displayFont; font.pixelSize: 8; font.bold: true; font.letterSpacing: 0.5 }
}
}
}
MouseArea {
id: fire
x: 18; anchors.bottom: parent.bottom; anchors.bottomMargin: 34
width: parent.width - 36; height: 44
enabled: root.selectedApp !== null
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.launchRequested(root.selectedIndex)
Shape {
id: fireShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: fire.containsMouse ? Theme.accent : Theme.accentAlpha(0.16)
strokeColor: Theme.accent
strokeWidth: 1
startX: 14; startY: 0
PathLine { x: fireShape.width; y: 0 }
PathLine { x: fireShape.width - 14; y: fireShape.height }
PathLine { x: 0; y: fireShape.height }
PathLine { x: 14; y: 0 }
}
}
Text { anchors.centerIn: parent; text: "LAUNCH VECTOR"; color: fire.containsMouse ? Theme.surface : Theme.accent; font.family: Theme.displayFont; font.pixelSize: 10; font.bold: true; font.letterSpacing: 1.2 }
}
}
// Bottom caution lane and key map.
Row {
x: 28; y: 612; spacing: 4
Repeater { model: 30; Rectangle { required property int index; width: 12; height: 4; color: index % 3 === 0 ? Theme.accent : Theme.hair; transform: Rotation { angle: -32 } } }
}
MicroText { x: 420; y: 606; text: "↑↓ SELECT // ENTER LAUNCH // ESC ABORT // PGUP/PGDN STEP"; color: Theme.accent }
MicroText { anchors.right: parent.right; anchors.rightMargin: 28; y: 606; text: "BLADE-09 / CRC A7F2" }
}
@@ -0,0 +1,88 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import QtQuick
import qs.widgets.theme
// Variant 11 cyberpunk bottom dock inspired by V5's rising carousel.
Scope {
id: root
property bool active: false
function toggle() { root.active = !root.active; }
onActiveChanged: {
LauncherState.dockOpen = root.active;
if (root.active)
win.prepareOpen();
}
GlobalShortcut {
name: "launcher11"
description: "Toggle app launcher (Cyber Dock)"
onPressed: root.toggle()
}
PanelWindow {
id: win
visible: root.active || deck.y < height
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: root.active ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
color: Theme.textAlpha(0)
anchors { bottom: true; left: true; right: true }
margins { bottom: 10; left: 12; right: 12 }
implicitHeight: 290
property int selectedIndex: 0
function prepareOpen() {
deck.clearSearch();
selectedIndex = 0;
deck.focusSearch();
}
function clampSelection(index) {
return model.apps.length === 0 ? 0 : Math.max(0, Math.min(model.apps.length - 1, index));
}
function move(delta) {
const count = model.apps.length;
if (count === 0)
return;
selectedIndex = ((selectedIndex + delta) % count + count) % count;
}
function launch(index) {
if (model.launch(index))
root.active = false;
}
AppModel { id: model; search: deck.query }
CyberDockContent {
id: deck
width: 1440
height: 272
x: (parent.width - width) / 2
y: root.active ? parent.height - height : parent.height + 4
scale: Math.min(1, (parent.width - 16) / width)
transformOrigin: Item.Bottom
apps: model.apps
selectedIndex: win.selectedIndex
Behavior on y {
NumberAnimation {
duration: 240
easing.type: root.active ? Easing.OutCubic : Easing.InCubic
}
}
onSelectionRequested: index => win.selectedIndex = win.clampSelection(index)
onMoveRequested: delta => win.move(delta)
onLaunchRequested: index => win.launch(index)
onDismissRequested: root.active = false
}
}
}
@@ -0,0 +1,300 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import qs.widgets.theme
// Headlessly renderable visual core for variant 11. Inspired by V5's bottom
// deck and horizontal carousel, but owns a denser industrial cyberpunk chassis.
Item {
id: root
property var apps: []
property int selectedIndex: 0
property bool showIcons: true
property alias query: dockInput.text
readonly property var selectedApp: apps.length > 0 && selectedIndex >= 0 && selectedIndex < apps.length
? apps[selectedIndex] : null
signal selectionRequested(int index)
signal moveRequested(int delta)
signal launchRequested(int index)
signal dismissRequested
function focusSearch() { dockInput.forceActiveFocus(); }
function clearSearch() { dockInput.text = ""; }
implicitWidth: 1440
implicitHeight: 272
component MicroText: Text {
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 7
font.letterSpacing: 0.9
elide: Text.ElideRight
}
Rectangle { anchors.fill: parent; color: Theme.textAlpha(0) }
// Main dock chassis with clipped upper corners and heavier lower edge.
Shape {
id: chassis
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.surface
strokeColor: Theme.hair
strokeWidth: 1
startX: 28; startY: 0
PathLine { x: chassis.width - 28; y: 0 }
PathLine { x: chassis.width; y: 28 }
PathLine { x: chassis.width; y: chassis.height }
PathLine { x: 0; y: chassis.height }
PathLine { x: 0; y: 28 }
PathLine { x: 28; y: 0 }
}
ShapePath {
fillColor: Theme.accent
strokeWidth: 0
startX: 28; startY: 0
PathLine { x: 250; y: 0 }
PathLine { x: 244; y: 4 }
PathLine { x: 32; y: 4 }
PathLine { x: 4; y: 32 }
PathLine { x: 4; y: 96 }
PathLine { x: 0; y: 96 }
PathLine { x: 0; y: 28 }
PathLine { x: 28; y: 0 }
}
ShapePath {
fillColor: Theme.accent
strokeWidth: 0
startX: 0; startY: chassis.height - 6
PathLine { x: chassis.width; y: chassis.height - 6 }
PathLine { x: chassis.width; y: chassis.height }
PathLine { x: 0; y: chassis.height }
PathLine { x: 0; y: chassis.height - 6 }
}
}
// Top caution seam.
Row {
x: 270; y: 1; spacing: 5
Repeater {
model: 58
Rectangle {
required property int index
width: 10; height: 3
color: index % 4 === 0 ? Theme.accent : Theme.hair
transform: Rotation { angle: -32 }
}
}
}
// Left system coupler.
Item {
id: coupler
x: 18; y: 18
width: 156; height: 184
Rectangle { anchors.centerIn: parent; width: 112; height: 112; radius: 56; color: Theme.textAlpha(0.012); border.width: 1; border.color: Theme.accentAlpha(0.55) }
Rectangle { anchors.centerIn: parent; width: 78; height: 78; radius: 39; color: Theme.textAlpha(0); border.width: 1; border.color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 118; height: 1; color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 1; height: 118; color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 38; height: 38; color: Theme.accentAlpha(0.16); border.width: 1; border.color: Theme.accent; transform: Rotation { angle: 45 } }
Text { anchors.centerIn: parent; text: "11"; color: Theme.accent; font.family: Theme.displayFont; font.pixelSize: 13; font.bold: true }
MicroText { anchors.horizontalCenter: parent.horizontalCenter; anchors.bottom: parent.bottom; anchors.bottomMargin: 15; text: "DOCK BUS"; color: Theme.accent }
}
Text {
x: 24; y: 16
text: "CYBER//DOCK"
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 9
font.bold: true
font.letterSpacing: 1
}
// Horizontal cartridge conveyor.
ListView {
id: cartridgeList
x: 178; y: 20
width: root.width - 356
height: 180
orientation: ListView.Horizontal
model: root.apps
currentIndex: root.selectedIndex
spacing: 8
clip: true
boundsBehavior: Flickable.StopAtBounds
onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain)
delegate: MouseArea {
id: cartridge
required property int index
required property var modelData
readonly property bool selected: index === root.selectedIndex
width: selected ? 128 : 108
height: selected ? 176 : 148
y: selected ? 0 : 24
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.selectionRequested(index)
onClicked: root.launchRequested(index)
Behavior on y { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } }
Behavior on height { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } }
Shape {
id: cartridgeShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: cartridge.selected ? Theme.selection : Theme.textAlpha(0.022)
strokeColor: cartridge.selected ? Theme.accent : Theme.hair
strokeWidth: cartridge.selected ? 2 : 1
startX: 12; startY: 0
PathLine { x: cartridgeShape.width - 8; y: 0 }
PathLine { x: cartridgeShape.width; y: 8 }
PathLine { x: cartridgeShape.width; y: cartridgeShape.height - 14 }
PathLine { x: cartridgeShape.width - 14; y: cartridgeShape.height }
PathLine { x: 0; y: cartridgeShape.height }
PathLine { x: 0; y: 12 }
PathLine { x: 12; y: 0 }
}
ShapePath {
fillColor: cartridge.selected ? Theme.accent : Theme.textAlpha(0.12)
strokeWidth: 0
startX: 0; startY: cartridgeShape.height - 6
PathLine { x: cartridgeShape.width - 14; y: cartridgeShape.height - 6 }
PathLine { x: cartridgeShape.width - 20; y: cartridgeShape.height }
PathLine { x: 0; y: cartridgeShape.height }
PathLine { x: 0; y: cartridgeShape.height - 6 }
}
}
MicroText { x: 9; y: 8; text: "C" + String(cartridge.index + 1).padStart(2, "0"); color: cartridge.selected ? Theme.accent : Theme.muted }
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
y: cartridge.selected ? 30 : 27
width: cartridge.selected ? 68 : 54
height: width
color: cartridge.selected ? Theme.accentAlpha(0.12) : Theme.textAlpha(0.018)
border.width: 1
border.color: cartridge.selected ? Theme.accent : Theme.hair
IconImage { visible: root.showIcons; anchors.centerIn: parent; implicitSize: cartridge.selected ? 48 : 38; source: root.showIcons ? Quickshell.iconPath(cartridge.modelData.icon || "application-x-executable", "application-x-executable") : "" }
Text { visible: !root.showIcons; anchors.centerIn: parent; text: String(cartridge.modelData.name || "?").charAt(0).toUpperCase(); color: cartridge.selected ? Theme.accent : Theme.text; font.family: Theme.displayFont; font.pixelSize: cartridge.selected ? 23 : 18; font.bold: true }
}
Text {
x: 8; anchors.bottom: generic.top; anchors.bottomMargin: 4
width: parent.width - 16
text: cartridge.modelData.name || "UNKNOWN"
color: cartridge.selected ? Theme.accent : Theme.text
font.family: Theme.displayFont
font.pixelSize: cartridge.selected ? 10 : 9
font.bold: cartridge.selected
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
MicroText {
id: generic
x: 8; anchors.bottom: parent.bottom; anchors.bottomMargin: 13
width: parent.width - 16
text: cartridge.modelData.genericName || "APPLICATION"
horizontalAlignment: Text.AlignHCenter
}
}
Text { anchors.centerIn: parent; visible: root.apps.length === 0; text: "NO CARTRIDGES MATCH QUERY"; color: Theme.muted; font.family: Theme.microFont; font.pixelSize: 9; font.letterSpacing: 1.2 }
}
// Right telemetry clamp.
Item {
id: clamp
anchors.right: parent.right
anchors.rightMargin: 18
y: 20; width: 150; height: 180
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.textAlpha(0.018)
strokeColor: Theme.hair
strokeWidth: 1
startX: 0; startY: 0
PathLine { x: 128; y: 0 }
PathLine { x: 150; y: 22 }
PathLine { x: 150; y: 180 }
PathLine { x: 0; y: 180 }
PathLine { x: 0; y: 0 }
}
}
MicroText { x: 12; y: 12; text: "ACTIVE SLOT"; color: Theme.accent }
Text { x: 12; y: 35; width: parent.width - 24; text: String(Math.max(0, root.selectedIndex + 1)).padStart(3, "0"); color: Theme.text; font.family: Theme.displayFont; font.pixelSize: 28; font.bold: true }
MicroText { x: 12; y: 75; width: parent.width - 24; text: root.selectedApp ? root.selectedApp.name : "NO TARGET"; color: Theme.text }
MicroText { x: 12; y: 96; width: parent.width - 24; text: root.apps.length + " AVAILABLE" }
Row { x: 12; y: 120; spacing: 3; Repeater { model: 12; Rectangle { required property int index; width: 7; height: 4; color: index < Math.min(12, root.apps.length) ? Theme.accent : Theme.hair } } }
MicroText { x: 12; anchors.bottom: parent.bottom; anchors.bottomMargin: 12; text: "BUS // ARMED"; color: Theme.accent }
}
// Bottom command spine.
Shape {
id: commandSpine
x: 176; y: 211
width: root.width - 352; height: 48
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.accentAlpha(0.10)
strokeColor: Theme.accent
strokeWidth: 1
startX: 14; startY: 0
PathLine { x: commandSpine.width; y: 0 }
PathLine { x: commandSpine.width - 14; y: commandSpine.height }
PathLine { x: 0; y: commandSpine.height }
PathLine { x: 14; y: 0 }
}
}
Text { x: 196; y: 221; text: ">_"; color: Theme.accent; font.family: Theme.displayFont; font.pixelSize: 17; font.bold: true }
TextInput {
id: dockInput
x: 230; y: 219; width: root.width - 650; height: 34
verticalAlignment: TextInput.AlignVCenter
color: Theme.text
selectionColor: Theme.accent
selectedTextColor: Theme.surface
font.family: Theme.displayFont
font.pixelSize: 14
font.letterSpacing: 1
clip: true
onTextChanged: root.selectionRequested(0)
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Right: case Qt.Key_Tab: root.moveRequested(1); event.accepted = true; break;
case Qt.Key_Left: case Qt.Key_Backtab: root.moveRequested(-1); event.accepted = true; break;
case Qt.Key_PageDown: root.moveRequested(5); event.accepted = true; break;
case Qt.Key_PageUp: root.moveRequested(-5); event.accepted = true; break;
case Qt.Key_Return: case Qt.Key_Enter: root.launchRequested(root.selectedIndex); event.accepted = true; break;
case Qt.Key_Escape: root.dismissRequested(); event.accepted = true; break;
}
}
Text { anchors.fill: parent; verticalAlignment: Text.AlignVCenter; visible: dockInput.text.length === 0; text: "FILTER DOCK CARTRIDGES"; color: Theme.muted; font: dockInput.font }
}
MicroText { x: root.width - 492; y: 228; text: "← → SELECT // ENTER LAUNCH // ESC RETRACT"; color: Theme.accent }
// Lower rail hardware.
Row {
x: 18; anchors.bottom: parent.bottom; anchors.bottomMargin: 1; spacing: 4
Repeater { model: 70; Rectangle { required property int index; width: 10; height: 4; color: index % 5 === 0 ? Theme.accent : Theme.hair; transform: Rotation { angle: -32 } } }
}
}
@@ -0,0 +1,79 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import QtQuick
import qs.widgets.theme
// Variant 10 radial Orbit application launcher.
Scope {
id: root
property bool active: false
function toggle() { root.active = !root.active; }
GlobalShortcut {
name: "launcher10"
description: "Toggle app launcher (Orbit targeting)"
onPressed: root.toggle()
}
PanelWindow {
id: win
visible: root.active
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
color: Theme.textAlpha(0)
anchors { top: true; left: true; right: true; bottom: true }
property int selectedIndex: 0
function clampSelection(index) {
return model.apps.length === 0 ? 0 : Math.max(0, Math.min(model.apps.length - 1, index));
}
function move(delta) {
const count = model.apps.length;
if (count === 0)
return;
selectedIndex = ((selectedIndex + delta) % count + count) % count;
}
function launch(index) {
if (model.launch(index))
root.active = false;
}
onVisibleChanged: {
if (visible) {
content.clearSearch();
selectedIndex = 0;
content.focusSearch();
}
}
AppModel { id: model; search: content.query }
Rectangle {
anchors.fill: parent
color: Theme.surface
opacity: 0.80
MouseArea { anchors.fill: parent; onClicked: root.active = false }
}
OrbitLauncherContent {
id: content
anchors.centerIn: parent
width: 1120
height: 660
scale: Math.min(1, (parent.width - 56) / width, (parent.height - 56) / height)
transformOrigin: Item.Center
apps: model.apps
selectedIndex: win.selectedIndex
onSelectionRequested: index => win.selectedIndex = win.clampSelection(index)
onMoveRequested: delta => win.move(delta)
onLaunchRequested: index => win.launch(index)
onDismissRequested: root.active = false
}
}
}
@@ -0,0 +1,338 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import qs.widgets.theme
// Variant 10 visual core: radial application targeting arena plus execution
// tape. Shares Theme/AppModel contracts but no launcher panel primitives.
Item {
id: root
property var apps: []
property int selectedIndex: 0
property bool showIcons: true
property alias query: orbitInput.text
readonly property var selectedApp: apps.length > 0 && selectedIndex >= 0 && selectedIndex < apps.length
? apps[selectedIndex] : null
readonly property int orbitCount: Math.min(8, apps.length)
signal selectionRequested(int index)
signal moveRequested(int delta)
signal launchRequested(int index)
signal dismissRequested
function focusSearch() { orbitInput.forceActiveFocus(); }
function clearSearch() { orbitInput.text = ""; }
implicitWidth: 1120
implicitHeight: 660
component MicroText: Text {
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 7
font.letterSpacing: 0.9
elide: Text.ElideRight
}
Rectangle { anchors.fill: parent; color: Theme.surface }
// Perimeter bracket frame, deliberately open on all four sides.
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.textAlpha(0)
strokeColor: Theme.accent
strokeWidth: 2
startX: 24; startY: 92
PathLine { x: 24; y: 24 }
PathLine { x: 128; y: 24 }
}
ShapePath {
fillColor: Theme.textAlpha(0)
strokeColor: Theme.accent
strokeWidth: 2
startX: root.width - 128; startY: 24
PathLine { x: root.width - 24; y: 24 }
PathLine { x: root.width - 24; y: 92 }
}
ShapePath {
fillColor: Theme.textAlpha(0)
strokeColor: Theme.accent
strokeWidth: 2
startX: 24; startY: root.height - 92
PathLine { x: 24; y: root.height - 24 }
PathLine { x: 128; y: root.height - 24 }
}
ShapePath {
fillColor: Theme.textAlpha(0)
strokeColor: Theme.accent
strokeWidth: 2
startX: root.width - 128; startY: root.height - 24
PathLine { x: root.width - 24; y: root.height - 24 }
PathLine { x: root.width - 24; y: root.height - 92 }
}
}
Text {
x: 44; y: 38
text: "ORBIT // APPLICATION TARGETING"
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 17
font.bold: true
font.letterSpacing: 1.5
}
MicroText { x: 46; y: 64; text: "10 / RADIAL INDEX / EXECUTION CONTROL"; color: Theme.accent }
MicroText { anchors.right: parent.right; anchors.rightMargin: 44; y: 48; text: root.apps.length + " TRACKED OBJECTS" }
// Radial targeting arena.
Item {
id: arena
x: 36; y: 92
width: 716; height: 500
// Axis traces.
Rectangle { anchors.centerIn: parent; width: parent.width - 28; height: 1; color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 1; height: parent.height - 16; color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 414; height: 414; radius: 207; color: Theme.textAlpha(0.008); border.width: 1; border.color: Theme.accentAlpha(0.50) }
Rectangle { anchors.centerIn: parent; width: 326; height: 326; radius: 163; color: Theme.textAlpha(0); border.width: 1; border.color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 220; height: 220; radius: 110; color: Theme.textAlpha(0); border.width: 1; border.color: Theme.accentAlpha(0.28) }
// Cardinal labels.
MicroText { anchors.horizontalCenter: parent.horizontalCenter; y: 8; text: "N // INDEX 00" }
MicroText { anchors.horizontalCenter: parent.horizontalCenter; anchors.bottom: parent.bottom; anchors.bottomMargin: 5; text: "S // COMMAND BUS" }
MicroText { anchors.verticalCenter: parent.verticalCenter; x: 8; text: "W"; color: Theme.accent }
MicroText { anchors.verticalCenter: parent.verticalCenter; anchors.right: parent.right; anchors.rightMargin: 8; text: "E"; color: Theme.accent }
// App nodes distributed around the outer orbit.
Repeater {
model: root.orbitCount
MouseArea {
id: node
required property int index
readonly property var app: root.apps[index]
readonly property real angle: -Math.PI / 2 + index * (2 * Math.PI / Math.max(1, root.orbitCount))
readonly property bool selected: index === root.selectedIndex
width: selected ? 92 : 74
height: selected ? 52 : 44
x: arena.width / 2 + Math.cos(angle) * 205 - width / 2
y: arena.height / 2 + Math.sin(angle) * 205 - height / 2
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.selectionRequested(index)
onClicked: root.launchRequested(index)
Shape {
id: nodeShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: node.selected ? Theme.selection : Theme.surface
strokeColor: node.selected ? Theme.accent : Theme.hair
strokeWidth: node.selected ? 2 : 1
startX: 10; startY: 0
PathLine { x: nodeShape.width; y: 0 }
PathLine { x: nodeShape.width - 10; y: nodeShape.height }
PathLine { x: 0; y: nodeShape.height }
PathLine { x: 10; y: 0 }
}
}
Rectangle {
x: 7; anchors.verticalCenter: parent.verticalCenter
width: 27; height: 27
color: node.selected ? Theme.accentAlpha(0.18) : Theme.textAlpha(0.025)
border.width: 1
border.color: node.selected ? Theme.accent : Theme.hair
IconImage { visible: root.showIcons; anchors.centerIn: parent; implicitSize: 20; source: root.showIcons ? Quickshell.iconPath(node.app.icon || "application-x-executable", "application-x-executable") : "" }
Text { visible: !root.showIcons; anchors.centerIn: parent; text: String(node.app.name || "?").charAt(0).toUpperCase(); color: node.selected ? Theme.accent : Theme.text; font.family: Theme.displayFont; font.pixelSize: 11; font.bold: true }
}
Text {
x: 40; anchors.verticalCenter: parent.verticalCenter
width: parent.width - 48
text: node.app.name || "UNKNOWN"
color: node.selected ? Theme.accent : Theme.text
font.family: Theme.displayFont
font.pixelSize: 8
font.bold: node.selected
elide: Text.ElideRight
}
}
}
// Core target, not a conventional card.
Item {
id: core
anchors.centerIn: parent
width: 196; height: 196
Rectangle { anchors.centerIn: parent; width: 186; height: 186; radius: 93; color: Theme.surface; border.width: 2; border.color: Theme.accent }
Rectangle { anchors.centerIn: parent; width: 156; height: 156; radius: 78; color: Theme.accentAlpha(0.04); border.width: 1; border.color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 118; height: 118; color: Theme.textAlpha(0.012); border.width: 1; border.color: Theme.accentAlpha(0.42); transform: Rotation { angle: 45 } }
Rectangle { anchors.centerIn: parent; width: 196; height: 1; color: Theme.hair }
Rectangle { anchors.centerIn: parent; width: 1; height: 196; color: Theme.hair }
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
y: 31; width: 58; height: 58
color: Theme.surface
border.width: 1; border.color: Theme.accent
IconImage { visible: root.showIcons; anchors.centerIn: parent; implicitSize: 42; source: root.showIcons && root.selectedApp ? Quickshell.iconPath(root.selectedApp.icon || "application-x-executable", "application-x-executable") : "" }
Text { visible: !root.showIcons; anchors.centerIn: parent; text: root.selectedApp ? String(root.selectedApp.name || "?").charAt(0).toUpperCase() : "?"; color: Theme.accent; font.family: Theme.displayFont; font.pixelSize: 26; font.bold: true }
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
y: 100; width: 156
text: root.selectedApp ? root.selectedApp.name : "NO TARGET"
color: Theme.text
font.family: Theme.displayFont
font.pixelSize: 11
font.bold: true
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
MicroText { anchors.horizontalCenter: parent.horizontalCenter; y: 124; width: 150; horizontalAlignment: Text.AlignHCenter; text: root.selectedApp ? (root.selectedApp.genericName || "APPLICATION") : "AWAITING LOCK"; color: Theme.accent }
MicroText { anchors.horizontalCenter: parent.horizontalCenter; y: 146; text: "LOCK // " + String(Math.max(0, root.selectedIndex + 1)).padStart(3, "0") }
}
// Search rail crosses the bottom of the arena.
Shape {
id: queryRail
x: 98; anchors.bottom: parent.bottom; anchors.bottomMargin: 24
width: 520; height: 48
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.accentAlpha(0.10)
strokeColor: Theme.accent
strokeWidth: 1
startX: 16; startY: 0
PathLine { x: queryRail.width; y: 0 }
PathLine { x: queryRail.width - 16; y: queryRail.height }
PathLine { x: 0; y: queryRail.height }
PathLine { x: 16; y: 0 }
}
}
Text { x: 118; anchors.bottom: parent.bottom; anchors.bottomMargin: 33; text: "⌕"; color: Theme.accent; font.family: Theme.displayFont; font.pixelSize: 19; font.bold: true }
TextInput {
id: orbitInput
x: 152; anchors.bottom: parent.bottom; anchors.bottomMargin: 31
width: 385; height: 32
verticalAlignment: TextInput.AlignVCenter
color: Theme.text
selectionColor: Theme.accent
selectedTextColor: Theme.surface
font.family: Theme.displayFont
font.pixelSize: 15
font.letterSpacing: 1
clip: true
onTextChanged: root.selectionRequested(0)
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Right: case Qt.Key_Down: case Qt.Key_Tab: root.moveRequested(1); event.accepted = true; break;
case Qt.Key_Left: case Qt.Key_Up: case Qt.Key_Backtab: root.moveRequested(-1); event.accepted = true; break;
case Qt.Key_PageDown: root.moveRequested(5); event.accepted = true; break;
case Qt.Key_PageUp: root.moveRequested(-5); event.accepted = true; break;
case Qt.Key_Return: case Qt.Key_Enter: root.launchRequested(root.selectedIndex); event.accepted = true; break;
case Qt.Key_Escape: root.dismissRequested(); event.accepted = true; break;
}
}
Text { anchors.fill: parent; verticalAlignment: Text.AlignVCenter; visible: orbitInput.text.length === 0; text: "SCAN APPLICATION INDEX"; color: Theme.muted; font: orbitInput.font }
}
}
// Right execution tape.
Item {
id: tape
x: 778; y: 92
width: 308; height: 500
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.textAlpha(0.014)
strokeColor: Theme.hair
strokeWidth: 1
startX: 0; startY: 0
PathLine { x: tape.width - 28; y: 0 }
PathLine { x: tape.width; y: 28 }
PathLine { x: tape.width; y: tape.height }
PathLine { x: 28; y: tape.height }
PathLine { x: 0; y: tape.height - 28 }
PathLine { x: 0; y: 0 }
}
ShapePath {
fillColor: Theme.accent
strokeWidth: 0
startX: 0; startY: 0
PathLine { x: 82; y: 0 }
PathLine { x: 82; y: 4 }
PathLine { x: 0; y: 4 }
PathLine { x: 0; y: 0 }
}
}
Text { x: 16; y: 15; text: "EXECUTION TAPE"; color: Theme.accent; font.family: Theme.displayFont; font.pixelSize: 10; font.bold: true; font.letterSpacing: 1.2 }
MicroText { anchors.right: parent.right; anchors.rightMargin: 17; y: 17; text: root.apps.length + " ROWS" }
Rectangle { x: 14; y: 40; width: parent.width - 28; height: 1; color: Theme.hair }
ListView {
id: tapeList
x: 12; y: 51; width: parent.width - 24; height: 350
model: root.apps
currentIndex: root.selectedIndex
spacing: 3
clip: true
boundsBehavior: Flickable.StopAtBounds
onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain)
delegate: MouseArea {
id: tapeRow
required property int index
required property var modelData
readonly property bool selected: index === root.selectedIndex
width: ListView.view.width
height: 37
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.selectionRequested(index)
onClicked: root.launchRequested(index)
Rectangle { anchors.fill: parent; color: tapeRow.selected ? Theme.selection : Theme.textAlpha(0.018); border.width: 1; border.color: tapeRow.selected ? Theme.accent : Theme.textAlpha(0.09) }
Rectangle { x: 0; width: tapeRow.selected ? 6 : 2; height: parent.height; color: tapeRow.selected ? Theme.accent : Theme.hair }
Text { x: 13; anchors.verticalCenter: parent.verticalCenter; width: 24; text: String(tapeRow.index + 1).padStart(2, "0"); color: tapeRow.selected ? Theme.accent : Theme.muted; font.family: Theme.microFont; font.pixelSize: 7 }
Text { x: 43; anchors.verticalCenter: parent.verticalCenter; width: parent.width - 116; text: tapeRow.modelData.name || "UNKNOWN"; color: tapeRow.selected ? Theme.accent : Theme.text; font.family: Theme.displayFont; font.pixelSize: 9; font.bold: tapeRow.selected; elide: Text.ElideRight }
MicroText { anchors.right: parent.right; anchors.rightMargin: 10; anchors.verticalCenter: parent.verticalCenter; text: tapeRow.selected ? "LOCK" : "PASS"; color: tapeRow.selected ? Theme.accent : Theme.muted }
}
}
MicroText { x: 16; y: 417; width: parent.width - 32; height: 28; wrapMode: Text.Wrap; maximumLineCount: 2; text: root.selectedApp ? (root.selectedApp.comment || "No target metadata supplied.") : "No target acquired." }
MouseArea {
id: execute
x: 14; anchors.bottom: parent.bottom; anchors.bottomMargin: 15
width: parent.width - 28; height: 42
enabled: root.selectedApp !== null
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.launchRequested(root.selectedIndex)
Rectangle { anchors.fill: parent; color: execute.containsMouse ? Theme.accent : Theme.accentAlpha(0.16); border.width: 1; border.color: Theme.accent }
Text { anchors.centerIn: parent; text: "COMMIT ORBITAL LAUNCH"; color: execute.containsMouse ? Theme.surface : Theme.accent; font.family: Theme.displayFont; font.pixelSize: 9; font.bold: true; font.letterSpacing: 1.1 }
}
}
Row {
x: 42; y: 622; spacing: 4
Repeater { model: 28; Rectangle { required property int index; width: 11; height: 3; color: index % 4 === 0 ? Theme.accent : Theme.hair; transform: Rotation { angle: -30 } } }
}
MicroText { x: 390; y: 617; text: "←↑↓→ TRACK // ENTER COMMIT // ESC RELEASE"; color: Theme.accent }
MicroText { anchors.right: parent.right; anchors.rightMargin: 40; y: 617; text: "ORBIT-10 // CRC 9C31" }
}
@@ -1,477 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Pipewire
import Quickshell.Wayland
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import qs.widgets.theme
// A vertical sidebar carrying the "Slant" (launcher V6) visual language
// chamfered panel, thick trapezoid bevel accents, an outward bracket, a
// floating triangle cap and a slanted divider. Right-anchored, mirrored.
Scope {
id: root
property bool active: true
function toggle() {
root.active = !root.active;
}
GlobalShortcut {
name: "sidebar"
description: "Toggle the Slant sidebar"
onPressed: root.toggle()
}
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property real volume: sink?.audio?.volume ?? 0
readonly property bool muted: sink?.audio?.muted ?? false
readonly property real maxVolume: 1.5
PwObjectTracker {
objects: [root.sink]
}
PanelWindow {
id: win
visible: root.active
color: "transparent"
anchors {
top: true
right: true
bottom: true
}
margins {
top: 8
}
// Frame width plus the gutter the outward bracket grows into.
readonly property int pad: 10
implicitWidth: 72 + 2 * pad
Item {
id: frame
// Inset so the outward bracket has room in the gutter.
anchors.fill: parent
anchors.margins: win.pad
readonly property int chamfer: 18 // big cut corners (top-right, bottom-left)
readonly property int smallChamfer: 7 // small bevel (top-left, bottom-right)
readonly property int bevel: 4 // inward thickness of the trapezoid accents
readonly property int chunkThick: 7 // outward thickness of the bracket
readonly property int chunkSlant: 10 // slant of the bracket end pieces
readonly property int capSize: 10 // floating triangle cap
readonly property int armSide: 58 // bracket arm down the left edge
readonly property int armTop: 34 // bracket arm along the top edge
Shape {
id: panelShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
// Panel: big chamfers on top-right & bottom-left, small bevels
// on top-left & bottom-right (mirror of the left-anchored panel).
ShapePath {
fillColor: Theme.surface
strokeColor: Theme.accent
strokeWidth: 2
startX: panelShape.width - frame.chamfer
startY: 0
PathLine {
x: frame.smallChamfer
y: 0
}
PathLine {
x: 0
y: frame.smallChamfer
}
PathLine {
x: 0
y: panelShape.height - frame.chamfer
}
PathLine {
x: frame.chamfer
y: panelShape.height
}
PathLine {
x: panelShape.width - frame.smallChamfer
y: panelShape.height
}
PathLine {
x: panelShape.width
y: panelShape.height - frame.smallChamfer
}
PathLine {
x: panelShape.width
y: frame.chamfer
}
PathLine {
x: panelShape.width - frame.chamfer
y: 0
}
}
// Thick top-right bevel accent (trapezoid to the edges).
ShapePath {
strokeWidth: 0
fillColor: Theme.accent
startX: panelShape.width
startY: frame.chamfer
PathLine {
x: panelShape.width - frame.chamfer
y: 0
}
PathLine {
x: panelShape.width - frame.chamfer - 2 * frame.bevel
y: 0
}
PathLine {
x: panelShape.width
y: frame.chamfer + 2 * frame.bevel
}
PathLine {
x: panelShape.width
y: frame.chamfer
}
}
// Thick bottom-left bevel accent.
ShapePath {
strokeWidth: 0
fillColor: Theme.accent
startX: 0
startY: panelShape.height - frame.chamfer
PathLine {
x: frame.chamfer
y: panelShape.height
}
PathLine {
x: frame.chamfer + 2 * frame.bevel
y: panelShape.height
}
PathLine {
x: 0
y: panelShape.height - frame.chamfer - 2 * frame.bevel
}
PathLine {
x: 0
y: panelShape.height - frame.chamfer
}
}
// Floating triangle cap in the bottom-left notch.
ShapePath {
strokeWidth: 0
fillColor: Theme.accent
startX: 0
startY: panelShape.height
PathLine {
x: 0
y: panelShape.height - frame.capSize
}
PathLine {
x: frame.capSize
y: panelShape.height
}
PathLine {
x: 0
y: panelShape.height
}
}
// Outward bracket wrapping the top-left corner: down the left
// edge and along the top edge, with slanted ends and a beveled
// corner following the small chamfer.
ShapePath {
strokeWidth: 0
fillColor: Theme.accent
startX: 0
startY: frame.armSide
PathLine {
x: -frame.chunkThick
y: frame.armSide - frame.chunkSlant
}
PathLine {
x: -frame.chunkThick
y: frame.smallChamfer
}
PathLine {
x: frame.smallChamfer
y: -frame.chunkThick
}
PathLine {
x: frame.armTop - frame.chunkSlant
y: -frame.chunkThick
}
PathLine {
x: frame.armTop
y: 0
}
PathLine {
x: frame.smallChamfer
y: 0
}
PathLine {
x: 0
y: frame.smallChamfer
}
PathLine {
x: 0
y: frame.armSide
}
}
}
// Content
ColumnLayout {
anchors.fill: parent
anchors.topMargin: 16
anchors.bottomMargin: 16
anchors.leftMargin: 10
anchors.rightMargin: 8
spacing: 10
// Clock readout font, hh over mm
Text {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: Qt.formatDateTime(clock.date, "hh\nmm")
font.family: Theme.readoutFont
font.pointSize: 22
font.letterSpacing: 1
color: Theme.text
SystemClock {
id: clock
precision: SystemClock.Minutes
}
}
// Date
Text {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: Qt.formatDateTime(clock.date, "dd\nMMM").toUpperCase()
font.family: Theme.readoutFont
font.pointSize: 13
color: Theme.accent
}
// Slanted divider
Item {
Layout.fillWidth: true
implicitHeight: 4
Shape {
id: divider
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: 0
fillColor: Theme.accent
startX: 8
startY: 0
PathLine { x: divider.width; y: 0 }
PathLine { x: divider.width - 8; y: divider.height }
PathLine { x: 0; y: divider.height }
PathLine { x: 8; y: 0 }
}
}
}
Item {
Layout.fillHeight: true
}
// Volume vertical meter with a sheared fill and a % readout
Text {
Layout.alignment: Qt.AlignHCenter
text: "VOL"
font.family: Theme.readoutFont
font.pointSize: 10
color: Theme.muted
}
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 3
// Overflow three floating slanted segments for volume
// pushed above 100%, lit with the same overload color as
// the volume OSD.
Repeater {
model: 3
delegate: Item {
id: seg
required property int index
readonly property real segStart: 1 + (2 - index) / 3 * (root.maxVolume - 1)
readonly property real segEnd: 1 + (3 - index) / 3 * (root.maxVolume - 1)
readonly property real frac: root.muted ? 0 : Math.max(0, Math.min(1, (root.volume - segStart) / (segEnd - segStart)))
readonly property int chamfer: 4
Layout.alignment: Qt.AlignHCenter
implicitWidth: 16
implicitHeight: 12
// Empty track, chamfered to match the main meter.
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: 1
strokeColor: Theme.muted
fillColor: Theme.raised
startX: seg.chamfer
startY: 0
PathLine { x: seg.width; y: 0 }
PathLine { x: seg.width; y: seg.height - seg.chamfer }
PathLine { x: seg.width - seg.chamfer; y: seg.height }
PathLine { x: 0; y: seg.height }
PathLine { x: 0; y: seg.chamfer }
PathLine { x: seg.chamfer; y: 0 }
}
}
// Overload fill reveals a fixed copy of the same
// chamfered hexagon from the bottom, so filled and
// empty states always share one silhouette.
Item {
id: segFillClip
anchors.left: parent.left
anchors.leftMargin: 2
anchors.right: parent.right
anchors.rightMargin: 2
anchors.bottom: parent.bottom
anchors.bottomMargin: 2
clip: true
height: seg.frac * (seg.height - 4)
Shape {
id: segFill
width: seg.width - 4
height: seg.height - 4
y: segFillClip.height - height
preferredRendererType: Shape.CurveRenderer
readonly property int chamfer: seg.chamfer - 2
ShapePath {
strokeWidth: 0
fillColor: Theme.hot
startX: segFill.chamfer
startY: 0
PathLine { x: segFill.width; y: 0 }
PathLine { x: segFill.width; y: segFill.height - segFill.chamfer }
PathLine { x: segFill.width - segFill.chamfer; y: segFill.height }
PathLine { x: 0; y: segFill.height }
PathLine { x: 0; y: segFill.chamfer }
PathLine { x: segFill.chamfer; y: 0 }
}
}
}
}
}
Item {
id: volMeter
Layout.alignment: Qt.AlignHCenter
implicitWidth: 16
implicitHeight: 96
readonly property int chamfer: 6
// Track chamfered top-left/bottom-right to match the
// panel's slant, so the sheared fill sits in a shape that
// agrees with it rather than a plain rectangle.
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: 1
strokeColor: Theme.muted
fillColor: Theme.raised
startX: volMeter.chamfer
startY: 0
PathLine { x: volMeter.width; y: 0 }
PathLine { x: volMeter.width; y: volMeter.height - volMeter.chamfer }
PathLine { x: volMeter.width - volMeter.chamfer; y: volMeter.height }
PathLine { x: 0; y: volMeter.height }
PathLine { x: 0; y: volMeter.chamfer }
PathLine { x: volMeter.chamfer; y: 0 }
}
}
// Accent fill reveals a fixed copy of the same
// chamfered hexagon as the track from the bottom, so
// the fill and background always share one silhouette.
Item {
id: fillClip
anchors.left: parent.left
anchors.leftMargin: 2
anchors.right: parent.right
anchors.rightMargin: 2
anchors.bottom: parent.bottom
anchors.bottomMargin: 2
clip: true
readonly property real frac: root.muted ? 0 : Math.min(root.volume, 1)
height: frac * (volMeter.height - 4)
Shape {
id: vol
width: volMeter.width - 4
height: volMeter.height - 4
y: fillClip.height - height
preferredRendererType: Shape.CurveRenderer
readonly property int chamfer: volMeter.chamfer - 2
ShapePath {
strokeWidth: 0
fillColor: root.muted ? Theme.muted : Theme.accent
startX: vol.chamfer
startY: 0
PathLine { x: vol.width; y: 0 }
PathLine { x: vol.width; y: vol.height - vol.chamfer }
PathLine { x: vol.width - vol.chamfer; y: vol.height }
PathLine { x: 0; y: vol.height }
PathLine { x: 0; y: vol.chamfer }
PathLine { x: vol.chamfer; y: 0 }
}
}
}
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: root.muted ? "--" : Math.round(root.volume * 100)
font.family: Theme.readoutFont
font.pointSize: 14
color: root.muted ? Theme.muted : Theme.text
}
}
}
}
}
Generated
+18 -18
View File
@@ -14,11 +14,11 @@
"uv2nix": "uv2nix"
},
"locked": {
"lastModified": 1786986906,
"narHash": "sha256-DJ1oU9szQJNdEM0dysh4NnKOB1HwOKtNukrUYKpawVs=",
"lastModified": 1787577519,
"narHash": "sha256-YNAXQTgR26RJiX2vtYjk6OtBu2jMWeq4qUN6sUrt6Lc=",
"owner": "nix-community",
"repo": "authentik-nix",
"rev": "afdb2eeca1e0b38fabb93c4a8944be73d3581268",
"rev": "30c37930450d7a5fefa8ffec613f037fc75c3071",
"type": "github"
},
"original": {
@@ -270,11 +270,11 @@
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1787124618,
"narHash": "sha256-aKf1k2hvYgaxP9oxDPRiv9npEJLODC9eKxk7nR69lzQ=",
"lastModified": 1787728766,
"narHash": "sha256-g2oZlrBU3AI2ubCiY/UyE9ALTIDueTcF//QP3vaY9IQ=",
"owner": "nix-community",
"repo": "nixos-anywhere",
"rev": "ad8fa24e11eef167fd72d49fafefa3f840312d71",
"rev": "6b77f26ec4538ced04bf1d02f374b0ec02e9c27e",
"type": "github"
},
"original": {
@@ -291,11 +291,11 @@
"nixos-unstable": "nixos-unstable"
},
"locked": {
"lastModified": 1787222173,
"narHash": "sha256-acp6QJnWVnLvnanC59CMkiDC/i0ZhdFKiB7prru9SHw=",
"lastModified": 1787826771,
"narHash": "sha256-gWkyr3I/cg4SHWGkAoUo24+BQaqoi+S0T2JxDjsA+pw=",
"owner": "nix-community",
"repo": "nixos-images",
"rev": "e17386d9193d6d5a90f1b4b6a8a5cd2620d34b56",
"rev": "f6714acc84ce92df7286c89a571ad1e946057a5b",
"type": "github"
},
"original": {
@@ -354,11 +354,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1787209939,
"narHash": "sha256-WvvHR4kSQLAbtouMC/ruZ5UpLwlUcY3K4FAllMN+yGk=",
"lastModified": 1787814960,
"narHash": "sha256-PYZq1qzCJXC2zGI0mH07vrZBsw6DRBAOX0jN1pPtqOQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "391b592eb44808b3bd0cb80bb71b63a5a118b8bb",
"rev": "c27cdad491a991b11ed731760aa2ef8db0cb0410",
"type": "github"
},
"original": {
@@ -370,11 +370,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1787204541,
"narHash": "sha256-OURZPknrTjQrlNyxPdqzyqmU/81Wes1CUP/Ft1Rv/YI=",
"lastModified": 1787753485,
"narHash": "sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5880666fd9eb563038431edb35c2d0aa595884e6",
"rev": "062346a6d85bc4b49dfaa61c986e9c5be21217d1",
"type": "github"
},
"original": {
@@ -400,11 +400,11 @@
]
},
"locked": {
"lastModified": 1785730568,
"narHash": "sha256-NjSPsgjJ7MSpBtTkUcmNhRe6AFZ96+zsca2M8YuQi8Y=",
"lastModified": 1786936886,
"narHash": "sha256-8SFyOdmcG6Nsh/JzlH812lTI/v+ur06fpQhj/acNn8k=",
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"rev": "90fde00db3687922d39d95fc591475fd0bbbcd72",
"rev": "90ffdeee1a4929b231913df067448cd9803d3e07",
"type": "github"
},
"original": {
+36 -2
View File
@@ -583,6 +583,7 @@
# instance is the identical build to the packaged one.
qs = "${nixpkgs.legacyPackages.${system}.quickshell}/bin/qs";
git = "${nixpkgs.legacyPackages.${system}.git}/bin/git";
grep = "${nixpkgs.legacyPackages.${system}.gnugrep}/bin/grep";
# Resolved at RUN time, not build time: the entire point is to run the
# working tree, and `self` here is only a store snapshot of it.
@@ -631,6 +632,33 @@
echo "qs-dev: live on $cfg edits there now hot-reload"
'';
# qs log -f prints everything the instance logs; WARN and ERROR are the
# two that mean something is wrong with the QML in front of you. A
# binding loop or a failed binding is a WARN and easy to miss when it
# scrolls past inside a reload's worth of chatter.
qs-log = pkgs.writeShellScriptBin "qs-log" ''
set -uo pipefail
${preamble}
filter='WARN|ERROR'
case "''${1:-}" in
-a|--all) filter='.' ;;
esac
# -t 1: `qs log -f` replays the whole backlog first, which would dump
# every historical warning into the terminal on shell entry.
#
# `qs log -f` ends when the instance it attached to exits, and the dev
# shell outlives individual instances — a QML error kills one, `qs-dev`
# starts another. Re-attach instead of going quiet for the session.
while :; do
if running "$cfg"; then
${qs} log -p "$cfg" -t 1 -f 2>/dev/null | ${grep} --line-buffered -E "$filter" >&2
fi
sleep 1
done
'';
qs-prod = pkgs.writeShellScriptBin "qs-prod" ''
set -uo pipefail
${preamble}
@@ -641,7 +669,7 @@
'';
in
pkgs.mkShell {
packages = [ pkgs.quickshell qs-dev qs-prod ];
packages = [ pkgs.quickshell qs-dev qs-prod qs-log ];
# Swap on entry, swap back on exit. Three guards:
# - interactive only ($- has i). `nix develop --command X` EXECs X,
@@ -655,9 +683,15 @@
shellHook = ''
if [[ $- == *i* ]] && [ -n "''${WAYLAND_DISPLAY:-}" ] && [ -z "''${HOMELAB_QS_DEV:-}" ]; then
export HOMELAB_QS_DEV=1
qs-dev && trap qs-prod EXIT
if qs-dev; then
# Stream the dev instance's warnings and errors into this
# terminal, and take the follower down with the shell.
qs-log & HOMELAB_QS_LOG=$!
trap 'kill "$HOMELAB_QS_LOG" 2>/dev/null; qs-prod' EXIT
fi
fi
echo "homelab devshell qs-dev (working tree) / qs-prod (packaged); exit restores"
echo "homelab devshell quickshell WARN/ERROR stream here; qs-log -a for everything"
'';
};
};
-1
View File
@@ -54,7 +54,6 @@ in
pkgs.hyprcursor
pkgs.bibata-cursors
pkgs.papirus-icon-theme
tome
];
xdg.desktopEntries.btop = {
+26 -14
View File
@@ -36,7 +36,7 @@ let
# here rather than at runtime, since hyprpaper has no built-in "random"
# mode; re-pick and rebuild (or swap in real per-monitor selection) when
# this stops being a placeholder.
wallpaper = "/mnt/hdd_01/data/Pictures/Wallhaven/wallhaven-4yjyd4.png";
wallpaper = "/mnt/hdd_01/data/Pictures/Wallhaven/wallhaven-ym81rl.png";
# Dispatchers → the new hl.dsp.* API (signatures verified against hyprland
# 0.55's src/config/lua/bindings/LuaBindingsDispatchers.cpp).
@@ -89,7 +89,8 @@ in
wayland.windowManager.hyprland = {
enable = true;
plugins = [ inputs.hypr-chrome.packages.${pkgs.stdenv.hostPlatform.system}.default ];
# Unloaded for now; re-add together with the plugin.hyprchrome settings below.
# plugins = [ inputs.hypr-chrome.packages.${pkgs.stdenv.hostPlatform.system}.default ];
settings = {
# ---- colours (from colors.conf) ----
fg_color = { _var = "rgba(eeeeeeff)"; };
@@ -131,7 +132,8 @@ in
decoration = {
dim_special = 0.3;
rounding = 10;
rounding = 25;
rounding_power = 1.0;
blur = {
enabled = true;
special = true; # blur behind the special workspace
@@ -158,16 +160,18 @@ in
allow_workspace_cycles = true;
};
plugin.hyprchrome = {
enabled = true;
glow_size = 12;
glow_strength = 0.85;
shadow_size = 24;
shadow_color = lua "bg_color";
shadow_offset = lua "{ 4, 8 }";
outline_size = lua "2";
outline_color = lua "fg_color";
};
# Unloaded for now — Hyprland rejects plugin config for a plugin that is
# not loaded, so this stays commented until it goes back in `plugins` above.
# plugin.hyprchrome = {
# enabled = true;
# glow_size = 12;
# glow_strength = 0.85;
# shadow_size = 24;
# shadow_color = lua "bg_color";
# shadow_offset = lua "{ 4, 8 }";
# outline_size = lua "2";
# outline_color = lua "fg_color";
# };
};
# ---- animations ----
@@ -198,14 +202,22 @@ in
# quickshell app-launcher variants (evaluating — pick one)
]
++ (map (n: bind "SUPER + CTRL + ${toString n}" (dsp.global "quickshell:launcher${toString n}")) (lib.range 1 7))
++ (map (n: bind "SUPER + CTRL + ${toString n}" (dsp.global "quickshell:launcher${toString n}")) (lib.range 1 9))
++ [
# variant 10 (CTRL+0 is already the quickshell restart binding below)
(bind "SUPER + CTRL + SHIFT + 0" (dsp.global "quickshell:launcher10"))
# variant 11 cyber dock
(bind "SUPER + CTRL + SHIFT + D" (dsp.global "quickshell:launcher11"))
# restart quickshell (also starts it if not running)
(bind "SUPER + CTRL + 0" (dsp.exec "qs kill; sleep 0.3; qs"))
# toggle the Slant sidebar
(bind "SUPER + CTRL + S" (dsp.global "quickshell:sidebar"))
# toggle the host vitals HUD
(bind "SUPER + CTRL + V" (dsp.global "quickshell:vitals"))
# toggle the debug widget stage (centred on the secondary monitor)
(bind "SUPER + CTRL + D" (dsp.global "quickshell:debug"))
# expand / collapse the hyprchrome bar as a whole
(bind "SUPER + A" (dsp.global "quickshell:chrome"))
(bind "SUPER + B" (dsp.exec "vivaldi"))
(bind "SUPER + E" (dsp.exec "cosmic-files"))