Compare commits
18
Commits
master
..
15e081e52d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15e081e52d | ||
|
|
abccf536f6 | ||
|
|
61da7748af | ||
|
|
bbe35dd72e | ||
|
|
c69aa4fea2 | ||
|
|
e7c30fd39d | ||
|
|
15698d3a78 | ||
|
|
7eb7b948e8 | ||
|
|
7268221a51 | ||
|
|
e38a8403ac | ||
|
|
6406e06330 | ||
|
|
9b6b799a5d | ||
|
|
5ecc73f084 | ||
|
|
fdc00a8574 | ||
|
|
b16cc93ff6 | ||
|
|
01bc169180 | ||
|
|
54896033c6 | ||
|
|
22fe8ab778 |
@@ -14,3 +14,6 @@ keys.txt
|
||||
|
||||
# local env (PATH etc.)
|
||||
.env
|
||||
|
||||
# local visual-verification output
|
||||
.artifacts/
|
||||
|
||||
@@ -427,93 +427,6 @@ another way in.
|
||||
(via the `/mnt/jupiter` samba mount) before the first switch if you want
|
||||
it preserved instead of starting clean.
|
||||
|
||||
### Obsidian vaults (jupiter CouchDB + mars bridge)
|
||||
|
||||
CouchDB itself is fully declarative (`services/dev/obsidian-livesync.nix`), but
|
||||
three things are runtime state it cannot own.
|
||||
|
||||
**1. Each vault's database is created by the plugin.** Point Self-hosted
|
||||
LiveSync at `https://notes.mgaction.town` (URI field) with the database name in
|
||||
its own field — *not* as a path on the URI. Turn on End-to-End Encryption and
|
||||
Obfuscate Properties **before the first sync**; both are remote-format
|
||||
decisions and changing them later means converting or rebuilding the database.
|
||||
The passphrase lives in the HomeLab Proton Pass vault, never in sops — it is
|
||||
what keeps a publicly reachable database from being a readable one.
|
||||
|
||||
Database names must start with a lowercase letter (`a-z0-9_$()+-` after that).
|
||||
An illegal name is rejected by neptun's matcher rather than CouchDB, and shows
|
||||
up in Obsidian as a connection failure with **no error message at all**.
|
||||
|
||||
**2. luna's vault credentials on mars.** `hosts/mars/secrets.nix` needs two
|
||||
values before mars will activate: `couchdb_luna_password` and
|
||||
`obsidian_luna_passphrase`.
|
||||
|
||||
```
|
||||
sops --set '["couchdb_luna_password"] "<password>"' secrets/mars.yaml
|
||||
sops --set '["obsidian_luna_passphrase"] "<passphrase>"' secrets/mars.yaml
|
||||
```
|
||||
|
||||
Keep both alphanumeric. sops substitutes into already-rendered JSON, so a `"`
|
||||
or `\` in either produces an invalid `config.json`; the bridge logs
|
||||
`Could not parse configuration!` and then runs on with **zero peers** instead
|
||||
of exiting, which looks exactly like a bridge that is simply idle.
|
||||
|
||||
As set up today these are the `obsidian` admin password and the same
|
||||
passphrase as the personal vault, which means mars — the box running an
|
||||
autonomous agent — can decrypt and read every vault database. Optional
|
||||
hardening, either half independently:
|
||||
|
||||
```
|
||||
# password comes straight out of sops; never echo it
|
||||
LUNA_PW=$(sops --decrypt --extract '["couchdb_luna_password"]' secrets/mars.yaml)
|
||||
ADMIN=obsidian # prompts for the admin password
|
||||
curl -u "$ADMIN" -X PUT http://jupiter.orbit.sol:5984/_users/org.couchdb.user:luna \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"name\":\"luna\",\"type\":\"user\",\"roles\":[],\"password\":\"$LUNA_PW\"}"
|
||||
curl -u "$ADMIN" -X PUT http://jupiter.orbit.sol:5984/luna_wiki/_security \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"admins":{"names":[],"roles":[]},"members":{"names":["luna"],"roles":[]}}'
|
||||
unset LUNA_PW
|
||||
```
|
||||
|
||||
then set `username` in `hosts/mars/livesync-bridge.nix` to `luna` and put that
|
||||
account's password in `couchdb_luna_password`. Run it against jupiter over the
|
||||
tailnet — `/_users` is blocked on the public vhost on purpose. A vault-specific
|
||||
passphrase is the other half, changed in the plugin and mirrored into sops.
|
||||
|
||||
**3. The database name must match.** `database` in
|
||||
`hosts/mars/livesync-bridge.nix` has to be exactly the name entered in the
|
||||
plugin. A mismatch does not error — with an admin credential PouchDB simply
|
||||
creates the misnamed database and replicates an empty vault into it.
|
||||
|
||||
Order matters: set the vault up from Obsidian first so the database exists and
|
||||
carries the plugin's own tweaks, then deploy mars. Afterwards:
|
||||
|
||||
```
|
||||
systemctl status livesync-bridge # on mars
|
||||
cat /var/lib/livesync-bridge/health.json # per-peer ok/backendUp/detail
|
||||
ls /var/lib/livesync-bridge/vault # her notes, as real markdown
|
||||
```
|
||||
|
||||
The vault is mounted into the agent container at `/opt/data/vault`, inside
|
||||
`HERMES_WRITE_SAFE_ROOT`, so luna can write as well as read.
|
||||
|
||||
A note luna writes reaches CouchDB as soon as the bridge sees it, but whether
|
||||
it then reaches your devices depends on that vault's **Sync Mode** in the
|
||||
plugin. Only "LiveSync (real-time)" pulls continuously; the periodic/on-save
|
||||
presets need their timer or a manual **Replicate**. A file that appears only
|
||||
after clicking Replicate is the client waiting, not the bridge failing — the
|
||||
database already had it. Check the bridge's own side in the journal:
|
||||
|
||||
```
|
||||
journalctl -u livesync-bridge | grep -- '--> luna-remote'
|
||||
```
|
||||
|
||||
⚠️ **Verify her writes actually land before trusting this.** Upstream has three
|
||||
open issues on the storage→CouchDB direction (#50, #23, #46) and all fail
|
||||
silently — the log reports the upload and the database never updates. Create a
|
||||
note as luna, confirm it appears on a phone, and re-check after any input bump.
|
||||
|
||||
### mercury (Raspberry Pi 3B+)
|
||||
|
||||
- `./deploy flash mercury /dev/sdX` writes the dedicated age key to the root
|
||||
|
||||
@@ -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,14 +27,19 @@ 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 1–7 remain available on `SUPER CTRL 1–7` 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.
|
||||
- `assets/` — SVG icons referenced via `file://${Quickshell.shellDir}/assets/...`.
|
||||
|
||||
**Styling**: No shared theme/tokens file yet — colors (`#FFD063` accent, `#0F1012`/`#292C30` backgrounds, `#EEEEEE` text) and metrics are hardcoded per-component. When touching visuals, grep for the existing hex color across `widgets/` to keep new elements consistent rather than introducing new values.
|
||||
**Styling**: All colors and font families come from the `Theme` singleton
|
||||
(`widgets/theme/Theme.qml`, `import qs.widgets.theme`) — there are no color or
|
||||
font literals left anywhere under `widgets/`. Add a token there rather than
|
||||
hardcoding a value; alpha variants of the two main colors go through
|
||||
`Theme.textAlpha(a)` / `Theme.accentAlpha(a)` instead of a hand-written
|
||||
`Qt.rgba(...)`. Metrics (sizes, spacing) are still per-component.
|
||||
|
||||
**Component base pattern**: `BarWidget.qml` (`widgets/bar/modules/BarWidget.qml`) is a `WrapperMouseArea` + `WrapperRectangle` combo providing hover-triggered border highlight (`Behavior on border.color` animation) and `Layout.fillWidth`. Bar modules extend it via `default property alias content` rather than duplicating the hover/border chrome.
|
||||
|
||||
**Fonts**: Clock/Date use the `Digital-7 Mono` font family — expected to be installed system-wide (see sibling `~/.dots/fonts/digital_7`), not bundled in this repo.
|
||||
**Fonts**: Two families, both via `Theme`. `Theme.displayFont` / `Theme.readoutFont` (an alias of it) are `DepartureMono Nerd Font`, installed by `services/desktop/desktop-apps.nix`; `Theme.microFont` is `DejaVu Sans Mono`. The shell no longer uses `Digital-7 Mono`, which was never packaged and depended on a manual `~/.dots/fonts/digital_7` install.
|
||||
|
||||
@@ -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 0–100 °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,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
hyprctl dispatch 'hl.dsp.global ("quickshell:launcher7")'
|
||||
hyprctl dispatch 'hl.dsp.global ("quickshell:launcher8")'
|
||||
|
||||
@@ -1,21 +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 {
|
||||
// Left sidebar in the Slant (V6) style — toggle with SUPER CTRL S.
|
||||
SideBar {}
|
||||
BarBottom {}
|
||||
// Dense multi-monitor status rail; visual core is headlessly renderable.
|
||||
HyprChromeBar {}
|
||||
|
||||
// App launcher variants — toggled via Hyprland global shortcuts
|
||||
// (SUPER CTRL 1/2/3). Try each and keep the one you like.
|
||||
// App launcher variants — 1–11; variant 8 remains the primary HUD.
|
||||
LauncherStack {} // 1 — left vertical list
|
||||
LauncherGrid {} // 2 — centered icon grid
|
||||
LauncherSpotlight {} // 3 — top-center command bar
|
||||
@@ -23,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,27 @@
|
||||
import QtQuick
|
||||
import qs.widgets.bar
|
||||
|
||||
DenseBarContent {
|
||||
width: 1920
|
||||
height: 164
|
||||
|
||||
now: new Date(2026, 7, 27, 21, 47, 0)
|
||||
hostName: "TERRA"
|
||||
telemetryReady: true
|
||||
ratesReady: true
|
||||
cpuFraction: 0.62
|
||||
cpuThreads: 16
|
||||
memoryFraction: 0.78
|
||||
memoryUsedText: "24.9G"
|
||||
temperatureCelsius: 54
|
||||
networkInterface: "enp7s0"
|
||||
networkRxText: "842.6M/S"
|
||||
networkTxText: "116.2M/S"
|
||||
storageFraction: 0.69
|
||||
storageFreeText: "1.82T FREE"
|
||||
workspaceIds: [1, 2, 3, 4]
|
||||
activeWorkspaceId: 1
|
||||
trayCount: 3
|
||||
audioFraction: 0.50
|
||||
audioMuted: false
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: 720
|
||||
implicitHeight: 120
|
||||
|
||||
readonly property color voidColor: "#0a0a0a"
|
||||
readonly property color inkColor: "#dedede"
|
||||
readonly property color mutedColor: "#858585"
|
||||
readonly property color accentColor: "#e8722a"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.voidColor
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.voidColor
|
||||
strokeColor: Qt.rgba(0.87, 0.87, 0.87, 0.28)
|
||||
strokeWidth: 1
|
||||
startX: 1
|
||||
startY: 1
|
||||
PathLine { x: root.width - 14; y: 1 }
|
||||
PathLine { x: root.width - 1; y: 14 }
|
||||
PathLine { x: root.width - 1; y: root.height - 9 }
|
||||
PathLine { x: root.width - 9; y: root.height - 1 }
|
||||
PathLine { x: 17; y: root.height - 1 }
|
||||
PathLine { x: 1; y: root.height - 17 }
|
||||
PathLine { x: 1; y: 1 }
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.accentColor
|
||||
strokeWidth: 0
|
||||
startX: 1
|
||||
startY: 1
|
||||
PathLine { x: 92; y: 1 }
|
||||
PathLine { x: 92; y: 3 }
|
||||
PathLine { x: 1; y: 3 }
|
||||
PathLine { x: 1; y: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: 14
|
||||
y: 12
|
||||
width: 29
|
||||
height: 14
|
||||
color: root.accentColor
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "001"
|
||||
color: root.voidColor
|
||||
font.pixelSize: 8
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 52
|
||||
y: 13
|
||||
text: "HEADLESS RENDER ARRAY"
|
||||
color: root.inkColor
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 9
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.4
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: 14
|
||||
y: 34
|
||||
width: root.width - 28
|
||||
height: 1
|
||||
color: root.inkColor
|
||||
opacity: 0.18
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 16
|
||||
y: 48
|
||||
text: "ORBITAL"
|
||||
color: root.inkColor
|
||||
font.family: "sans-serif-condensed"
|
||||
font.pixelSize: 28
|
||||
font.bold: true
|
||||
font.letterSpacing: 2
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 18
|
||||
y: 80
|
||||
text: "QML // GRABTOIMAGE // SOFTWARE RHI"
|
||||
color: root.accentColor
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 7
|
||||
font.letterSpacing: 1.4
|
||||
}
|
||||
|
||||
Row {
|
||||
x: 250
|
||||
y: 56
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: 16
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
width: 20
|
||||
height: 12
|
||||
color: index < 11 ? root.accentColor : Qt.rgba(0.87, 0.87, 0.87, 0.06)
|
||||
border.width: 1
|
||||
border.color: index < 11 ? root.accentColor : Qt.rgba(0.87, 0.87, 0.87, 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 250
|
||||
y: 78
|
||||
text: "RENDER PIPELINE"
|
||||
color: root.mutedColor
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 7
|
||||
font.letterSpacing: 1.1
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 585
|
||||
y: 77
|
||||
text: "68.75%"
|
||||
color: root.inkColor
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 15
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: 14
|
||||
y: root.height - 9
|
||||
width: root.width - 28
|
||||
height: 3
|
||||
color: root.accentColor
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
@@ -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,80 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import qs.widgets.launcher
|
||||
|
||||
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 ? "#FFF3C0" : "#FFD063"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 250
|
||||
}
|
||||
}
|
||||
|
||||
// Gentle "listening" pulse while the dock is open.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#FFFFFF"
|
||||
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,80 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
import qs.widgets.launcher
|
||||
|
||||
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 ? "#FFF3C0" : "#FFD063"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 250
|
||||
}
|
||||
}
|
||||
|
||||
// Gentle "listening" pulse while the console is open.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#FFFFFF"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
import qs.widgets.vitals
|
||||
|
||||
// Production/layer-shell adapter. All visual composition lives in the
|
||||
// Item-rooted DenseBarContent so the exact bar can be rendered headlessly.
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property real volume: sink?.audio?.volume ?? 0
|
||||
readonly property bool muted: sink?.audio?.muted ?? false
|
||||
|
||||
readonly property var workspaceIds: Hyprland.workspaces.values
|
||||
.filter(workspace => workspace.id > 0)
|
||||
.map(workspace => workspace.id)
|
||||
readonly property int activeWorkspaceId: Hyprland.focusedWorkspace?.id ?? 1
|
||||
readonly property var rootDisk: vitals.disks.length > 0 ? vitals.disks[0] : null
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [root.sink]
|
||||
}
|
||||
|
||||
VitalsData {
|
||||
id: vitals
|
||||
active: true
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: window
|
||||
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
color: "transparent"
|
||||
implicitHeight: 164
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
DenseBarContent {
|
||||
anchors.fill: parent
|
||||
|
||||
hostName: vitals.host || "LOCAL"
|
||||
telemetryReady: vitals.ready && !vitals.failed
|
||||
ratesReady: vitals.ratesReady && !vitals.failed
|
||||
cpuFraction: vitals.cpu
|
||||
cpuThreads: vitals.cpuThreads
|
||||
memoryFraction: vitals.memTotal > 0 ? vitals.memUsed / vitals.memTotal : 0
|
||||
memoryUsedText: vitals.fmtBytes(vitals.memUsed)
|
||||
temperatureCelsius: vitals.cpuTemp
|
||||
networkInterface: vitals.netIface || "NET"
|
||||
networkRxText: vitals.fmtRate(vitals.netRx)
|
||||
networkTxText: vitals.fmtRate(vitals.netTx)
|
||||
storageFraction: root.rootDisk && root.rootDisk.size > 0
|
||||
? root.rootDisk.used / root.rootDisk.size : 0
|
||||
storageFreeText: root.rootDisk
|
||||
? vitals.fmtBytes(root.rootDisk.size - root.rootDisk.used) + " FREE"
|
||||
: "-- FREE"
|
||||
workspaceIds: root.workspaceIds.length > 0 ? root.workspaceIds : [1, 2, 3, 4]
|
||||
activeWorkspaceId: root.activeWorkspaceId
|
||||
trayCount: SystemTray.items.values.length
|
||||
audioFraction: Math.max(0, Math.min(1, root.volume))
|
||||
audioMuted: root.muted
|
||||
|
||||
onWorkspaceActivated: workspaceId => {
|
||||
const workspace = Hyprland.workspaces.values.find(item => item.id === workspaceId);
|
||||
if (workspace)
|
||||
workspace.activate();
|
||||
else
|
||||
Hyprland.dispatch("workspace " + workspaceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
// Renderable visual core for the desktop telemetry rail. Runtime services stay
|
||||
// in DenseBar.qml so this Item can be exercised without Wayland or Hyprland.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: 1920
|
||||
implicitHeight: 164
|
||||
|
||||
readonly property bool compact: width <= 1400
|
||||
|
||||
property bool autoClock: true
|
||||
property date now: new Date()
|
||||
property string hostName: "LOCAL"
|
||||
property bool telemetryReady: false
|
||||
property bool ratesReady: false
|
||||
property real cpuFraction: 0
|
||||
property int cpuThreads: 0
|
||||
property real memoryFraction: 0
|
||||
property string memoryUsedText: "--"
|
||||
property real temperatureCelsius: NaN
|
||||
property string networkInterface: "NET"
|
||||
property string networkRxText: "--"
|
||||
property string networkTxText: "--"
|
||||
property real storageFraction: 0
|
||||
property string storageFreeText: "-- FREE"
|
||||
property var workspaceIds: [1, 2, 3, 4]
|
||||
property int activeWorkspaceId: 1
|
||||
property int trayCount: 0
|
||||
property real audioFraction: 0
|
||||
property bool audioMuted: false
|
||||
|
||||
signal workspaceActivated(int workspaceId)
|
||||
|
||||
function pct(value, ready) {
|
||||
return ready ? Math.round(Math.max(0, Math.min(1, value)) * 100) : 0;
|
||||
}
|
||||
|
||||
function two(value) {
|
||||
return value < 10 ? "0" + value : String(value);
|
||||
}
|
||||
|
||||
function timeText(value) {
|
||||
return root.two(value.getHours()) + ":" + root.two(value.getMinutes());
|
||||
}
|
||||
|
||||
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()] + " // " + root.two(value.getDate()) + " " + months[value.getMonth()];
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 30000
|
||||
running: root.autoClock
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.now = new Date()
|
||||
}
|
||||
|
||||
// Faint drafting grid; no gradient and deliberately subordinate to data.
|
||||
Repeater {
|
||||
model: Math.ceil(root.width / 40)
|
||||
Rectangle {
|
||||
required property int index
|
||||
x: index * 40
|
||||
width: 1
|
||||
height: root.height
|
||||
color: Theme.text
|
||||
opacity: 0.018
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Math.ceil(root.height / 40)
|
||||
Rectangle {
|
||||
required property int index
|
||||
y: index * 40
|
||||
width: root.width
|
||||
height: 1
|
||||
color: Theme.text
|
||||
opacity: 0.018
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: array
|
||||
x: 12
|
||||
y: 8
|
||||
width: root.width - 24
|
||||
height: 92
|
||||
|
||||
readonly property real identityWidth: root.compact ? 250 : 320
|
||||
readonly property real stateWidth: root.compact ? 300 : 350
|
||||
readonly property real flexWidth: Math.max(250, (width - identityWidth - stateWidth - 32) / 2)
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
StatusBarPanel {
|
||||
width: array.identityWidth
|
||||
height: array.height
|
||||
panelId: "001"
|
||||
title: "COMMAND LAYER"
|
||||
|
||||
Text {
|
||||
x: 12
|
||||
y: 34
|
||||
text: root.hostName.toUpperCase()
|
||||
color: Theme.text
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: root.compact ? 20 : 25
|
||||
font.bold: true
|
||||
font.letterSpacing: 2
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 92
|
||||
}
|
||||
|
||||
Text {
|
||||
x: 13
|
||||
y: 63
|
||||
text: "STATUS ARRAY // " + (root.telemetryReady ? "LIVE" : "STANDBY")
|
||||
color: Theme.accent
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 7
|
||||
font.letterSpacing: 1.4
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: parent.width - 75
|
||||
y: 36
|
||||
width: 1
|
||||
height: 43
|
||||
color: Theme.hair
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: parent.width - 62
|
||||
y: 42
|
||||
width: 5
|
||||
height: 5
|
||||
color: Theme.accent
|
||||
opacity: root.telemetryReady ? 1 : 0.35
|
||||
}
|
||||
|
||||
Text {
|
||||
x: parent.width - 50
|
||||
y: 38
|
||||
text: root.telemetryReady ? "ONLINE" : "LOCAL"
|
||||
color: Theme.accent
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 7
|
||||
}
|
||||
|
||||
Text {
|
||||
x: parent.width - 63
|
||||
y: 57
|
||||
text: "NODE // " + (root.hostName || "--").toUpperCase().slice(0, 7)
|
||||
color: Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 6
|
||||
}
|
||||
|
||||
Shape {
|
||||
x: parent.width - 63
|
||||
y: 71
|
||||
width: 48
|
||||
height: 10
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Theme.accent
|
||||
strokeWidth: 1
|
||||
startX: 0; startY: 6
|
||||
PathLine { x: 11; y: 6 }
|
||||
PathLine { x: 16; y: 1 }
|
||||
PathLine { x: 22; y: 9 }
|
||||
PathLine { x: 27; y: 6 }
|
||||
PathLine { x: 48; y: 6 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusBarPanel {
|
||||
width: array.flexWidth
|
||||
height: array.height
|
||||
panelId: "02"
|
||||
title: "RESOURCE LATTICE"
|
||||
meta: root.telemetryReady ? "REALTIME" : "FALLBACK"
|
||||
|
||||
Row {
|
||||
x: 11
|
||||
y: 32
|
||||
width: parent.width - 22
|
||||
height: 51
|
||||
spacing: root.compact ? 7 : 12
|
||||
|
||||
MetricBlock {
|
||||
width: (parent.width - parent.spacing * (root.compact ? 1 : 2)) / (root.compact ? 2 : 3)
|
||||
label: "CPU"
|
||||
value: root.cpuFraction
|
||||
ready: root.ratesReady
|
||||
readout: root.ratesReady ? root.pct(root.cpuFraction, true) + "%" : "--"
|
||||
detailLeft: "CORE:" + (root.cpuThreads || "--")
|
||||
detailRight: root.ratesReady ? "BUSY" : "PRIME"
|
||||
}
|
||||
|
||||
MetricBlock {
|
||||
width: (parent.width - parent.spacing * (root.compact ? 1 : 2)) / (root.compact ? 2 : 3)
|
||||
label: "MEM"
|
||||
value: root.memoryFraction
|
||||
ready: root.telemetryReady
|
||||
readout: root.telemetryReady ? root.pct(root.memoryFraction, true) + "%" : "--"
|
||||
detailLeft: "ALLOC"
|
||||
detailRight: root.memoryUsedText
|
||||
}
|
||||
|
||||
MetricBlock {
|
||||
visible: !root.compact
|
||||
width: (parent.width - parent.spacing * 2) / 3
|
||||
label: "THERM"
|
||||
value: isFinite(root.temperatureCelsius) ? root.temperatureCelsius / 100 : 0
|
||||
ready: isFinite(root.temperatureCelsius)
|
||||
readout: isFinite(root.temperatureCelsius) ? Math.round(root.temperatureCelsius) + "°C" : "--"
|
||||
detailLeft: "ZONE:01"
|
||||
detailRight: isFinite(root.temperatureCelsius) && root.temperatureCelsius >= 85 ? "HOT" : "NOMINAL"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusBarPanel {
|
||||
width: array.flexWidth
|
||||
height: array.height
|
||||
panelId: "03"
|
||||
title: "CARRIER UPLINK"
|
||||
meta: root.networkInterface.toUpperCase()
|
||||
|
||||
NetworkTrace {
|
||||
x: 12
|
||||
y: 43
|
||||
width: parent.width - (root.compact ? 117 : 145)
|
||||
height: 33
|
||||
level: root.ratesReady ? root.cpuFraction : 0.25
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 14
|
||||
y: 34
|
||||
width: root.compact ? 92 : 116
|
||||
spacing: 1
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.networkRxText
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: Theme.accent
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: root.compact ? 11 : 13
|
||||
font.bold: true
|
||||
elide: Text.ElideLeft
|
||||
}
|
||||
MicroText { width: parent.width; text: "RX // DOWN"; horizontalAlignment: Text.AlignRight }
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.networkTxText
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: Theme.text
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: root.compact ? 11 : 13
|
||||
font.bold: true
|
||||
elide: Text.ElideLeft
|
||||
}
|
||||
MicroText { width: parent.width; text: "TX // UP"; horizontalAlignment: Text.AlignRight }
|
||||
}
|
||||
}
|
||||
|
||||
StatusBarPanel {
|
||||
width: array.stateWidth
|
||||
height: array.height
|
||||
panelId: "04"
|
||||
title: "SYSTEM STATE"
|
||||
|
||||
Row {
|
||||
x: 10
|
||||
y: 35
|
||||
spacing: 6
|
||||
|
||||
StateCell { code: "N"; active: root.ratesReady; label: "NET" }
|
||||
StateCell { code: root.audioMuted ? "M" : "A"; active: !root.audioMuted; label: "AUD" }
|
||||
StateCell { visible: !root.compact; code: String(root.trayCount); active: root.trayCount > 0; label: "TRAY" }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: root.compact ? 101 : 151
|
||||
y: 33
|
||||
width: 1
|
||||
height: 46
|
||||
color: Theme.hair
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
y: 34
|
||||
width: root.compact ? 91 : 82
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.timeText(root.now)
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: Theme.text
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: root.compact ? 20 : 22
|
||||
font.bold: true
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.dateText(root.now)
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: Theme.accent
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 6
|
||||
elide: Text.ElideLeft
|
||||
}
|
||||
MicroText { width: parent.width; text: "LOCAL TIME"; horizontalAlignment: Text.AlignRight }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusBarPanel {
|
||||
id: rail
|
||||
x: 12
|
||||
y: 108
|
||||
width: root.width - 24
|
||||
height: 34
|
||||
showHeader: false
|
||||
chamfer: 10
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
|
||||
RailSection {
|
||||
width: root.compact ? 252 : 310
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "06 // DESKTOP"
|
||||
color: Theme.accent
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 7
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
Repeater {
|
||||
model: root.workspaceIds.slice(0, root.compact ? 4 : 6)
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
width: 24
|
||||
height: 17
|
||||
color: Number(modelData) === root.activeWorkspaceId ? Theme.accentAlpha(0.18) : "transparent"
|
||||
border.width: 1
|
||||
border.color: Number(modelData) === root.activeWorkspaceId ? Theme.accent : Theme.hair
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.two(Number(parent.modelData))
|
||||
color: Number(parent.modelData) === root.activeWorkspaceId ? Theme.accent : Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 7
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.workspaceActivated(Number(parent.modelData))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RailSection {
|
||||
width: root.compact ? 260 : 410
|
||||
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: "PROCESS" }
|
||||
Text {
|
||||
x: 76
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "COMPOSITOR // NODE_EXPORTER // SHELL"
|
||||
color: root.telemetryReady ? Theme.text : Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 7
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 84
|
||||
}
|
||||
}
|
||||
|
||||
RailSection {
|
||||
width: root.compact ? 300 : 385
|
||||
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: "VOL // ROOT" }
|
||||
Text {
|
||||
x: 93
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.storageFreeText
|
||||
color: Theme.text
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 8
|
||||
}
|
||||
SegmentMeter {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.compact ? 95 : 170
|
||||
height: 7
|
||||
segments: root.compact ? 10 : 16
|
||||
value: root.storageFraction
|
||||
ready: root.telemetryReady
|
||||
}
|
||||
}
|
||||
|
||||
RailSection {
|
||||
visible: !root.compact
|
||||
width: 350
|
||||
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: "AUDIO BUS" }
|
||||
Text {
|
||||
x: 89
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.audioMuted ? "MUTED" : Math.round(root.audioFraction * 100) + "%"
|
||||
color: root.audioMuted ? Theme.muted : Theme.accent
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 8
|
||||
}
|
||||
SegmentMeter {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 155
|
||||
height: 7
|
||||
segments: 16
|
||||
value: root.audioFraction
|
||||
ready: true
|
||||
}
|
||||
}
|
||||
|
||||
RailSection {
|
||||
width: Math.max(150, rail.width - (root.compact ? 812 : 1455) - 20)
|
||||
borderVisible: false
|
||||
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: root.compact ? "SYS // " + root.timeText(root.now) : "EVENTS" }
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.telemetryReady ? "WARN:00 // ERR:00" : "TELEMETRY WAIT"
|
||||
color: root.telemetryReady ? Theme.muted : Theme.accent
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 7
|
||||
elide: Text.ElideLeft
|
||||
width: parent.width - 64
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom routing trace and caution hatch.
|
||||
Shape {
|
||||
x: 12
|
||||
y: 150
|
||||
width: root.width - 24
|
||||
height: 12
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Theme.accentAlpha(0.55)
|
||||
strokeWidth: 1
|
||||
startX: 0; startY: 1
|
||||
PathLine { x: 220; y: 1 }
|
||||
PathLine { x: 232; y: 11 }
|
||||
PathLine { x: 330; y: 11 }
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
x: 360
|
||||
y: 151
|
||||
width: root.width - 372
|
||||
height: 3
|
||||
spacing: 5
|
||||
clip: true
|
||||
Repeater {
|
||||
model: Math.ceil(parent.width / 10)
|
||||
Rectangle {
|
||||
required property int index
|
||||
width: 5
|
||||
height: 3
|
||||
color: index % 2 === 0 ? Theme.accent : "transparent"
|
||||
opacity: 0.58
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component MicroText: Text {
|
||||
color: Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 6
|
||||
font.letterSpacing: 0.7
|
||||
}
|
||||
|
||||
component RailSection: Item {
|
||||
property bool borderVisible: true
|
||||
height: rail.height
|
||||
Rectangle {
|
||||
visible: parent.borderVisible
|
||||
anchors.right: parent.right
|
||||
width: 1
|
||||
height: parent.height
|
||||
color: Theme.hair
|
||||
opacity: 0.65
|
||||
}
|
||||
}
|
||||
|
||||
component MetricBlock: Item {
|
||||
id: metric
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
property bool ready: false
|
||||
property string readout: "--"
|
||||
property string detailLeft: ""
|
||||
property string detailRight: ""
|
||||
height: 51
|
||||
|
||||
MicroText { x: 0; y: 1; text: metric.label; font.pixelSize: 7 }
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
y: -3
|
||||
text: metric.readout
|
||||
color: Theme.text
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: root.compact ? 14 : 17
|
||||
font.bold: true
|
||||
}
|
||||
SegmentMeter {
|
||||
x: 0; y: 19
|
||||
width: parent.width
|
||||
height: 9
|
||||
segments: 10
|
||||
value: metric.value
|
||||
ready: metric.ready
|
||||
}
|
||||
MicroText { x: 0; y: 34; text: metric.detailLeft }
|
||||
MicroText { anchors.right: parent.right; y: 34; text: metric.detailRight; horizontalAlignment: Text.AlignRight }
|
||||
}
|
||||
|
||||
component SegmentMeter: Row {
|
||||
id: meter
|
||||
property int segments: 10
|
||||
property real value: 0
|
||||
property bool ready: false
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: meter.segments
|
||||
Rectangle {
|
||||
required property int index
|
||||
width: Math.max(2, (meter.width - (meter.segments - 1) * meter.spacing) / meter.segments)
|
||||
height: meter.height
|
||||
readonly property bool on: meter.ready && index < Math.round(Math.max(0, Math.min(1, meter.value)) * meter.segments)
|
||||
color: on ? Theme.accent : Theme.textAlpha(0.06)
|
||||
border.width: 1
|
||||
border.color: on ? Theme.accent : Theme.textAlpha(0.18)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component StateCell: Item {
|
||||
id: state
|
||||
property string code: "--"
|
||||
property string label: ""
|
||||
property bool active: false
|
||||
width: root.compact ? 39 : 43
|
||||
height: 42
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
ShapePath {
|
||||
fillColor: state.active ? Theme.accentAlpha(0.18) : "transparent"
|
||||
strokeColor: state.active ? Theme.accent : Theme.hair
|
||||
strokeWidth: 1
|
||||
startX: 1; startY: 1
|
||||
PathLine { x: parent.width - 7; y: 1 }
|
||||
PathLine { x: parent.width - 1; y: 7 }
|
||||
PathLine { x: parent.width - 1; y: parent.height - 1 }
|
||||
PathLine { x: 7; y: parent.height - 1 }
|
||||
PathLine { x: 1; y: parent.height - 7 }
|
||||
PathLine { x: 1; y: 1 }
|
||||
}
|
||||
}
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 7
|
||||
text: state.code
|
||||
color: state.active ? Theme.accent : Theme.text
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
MicroText { anchors.horizontalCenter: parent.horizontalCenter; y: 27; text: state.label }
|
||||
}
|
||||
|
||||
component RadarGauge: Item {
|
||||
id: radar
|
||||
property real size: 58
|
||||
property real level: 0
|
||||
width: size
|
||||
height: size
|
||||
|
||||
Repeater {
|
||||
model: [1, 0.72, 0.36]
|
||||
Rectangle {
|
||||
required property int index
|
||||
required property var modelData
|
||||
anchors.centerIn: parent
|
||||
width: radar.size * Number(modelData)
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: index === 0 ? Theme.accent : Theme.hair
|
||||
}
|
||||
}
|
||||
Rectangle { x: 0; y: parent.height / 2; width: parent.width; height: 1; color: Theme.hair }
|
||||
Rectangle { x: parent.width / 2; y: 0; width: 1; height: parent.height; color: Theme.hair }
|
||||
Rectangle { x: 12; y: 18; width: 4; height: 4; color: Theme.accent }
|
||||
Rectangle { x: parent.width - 14; y: parent.height - 20; width: 4; height: 4; color: Theme.accent }
|
||||
Rectangle { x: parent.width / 2; y: parent.height - 11; width: 4; height: 4; color: Theme.accent }
|
||||
MicroText { anchors.right: parent.right; y: 3; text: "R:" + Math.round(radar.level * 99); color: Theme.accent }
|
||||
}
|
||||
|
||||
component NetworkTrace: Item {
|
||||
id: trace
|
||||
property real level: 0
|
||||
Rectangle { x: 0; y: parent.height - 1; width: parent.width; height: 1; color: Theme.hair }
|
||||
Rectangle { x: 0; y: 0; width: 1; height: parent.height; color: Theme.hair }
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Theme.accent
|
||||
strokeWidth: 1.2
|
||||
startX: 0; startY: trace.height * 0.65
|
||||
PathLine { x: trace.width * 0.10; y: trace.height * 0.65 }
|
||||
PathLine { x: trace.width * 0.15; y: trace.height * (0.25 + trace.level * 0.15) }
|
||||
PathLine { x: trace.width * 0.21; y: trace.height * 0.78 }
|
||||
PathLine { x: trace.width * 0.31; y: trace.height * 0.56 }
|
||||
PathLine { x: trace.width * 0.43; y: trace.height * 0.62 }
|
||||
PathLine { x: trace.width * 0.49; y: trace.height * 0.18 }
|
||||
PathLine { x: trace.width * 0.57; y: trace.height * 0.82 }
|
||||
PathLine { x: trace.width * 0.68; y: trace.height * 0.58 }
|
||||
PathLine { x: trace.width * 0.79; y: trace.height * 0.62 }
|
||||
PathLine { x: trace.width * 0.85; y: trace.height * 0.34 }
|
||||
PathLine { x: trace.width * 0.91; y: trace.height * 0.75 }
|
||||
PathLine { x: trace.width; y: trace.height * 0.60 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
// Chamfered panel chrome for the dense status rail: outline, corner accent
|
||||
// lines, and the optional header strip (id chip / title / meta / tick marks).
|
||||
// Content is supplied as children by the call site.
|
||||
//
|
||||
// Colors and fonts both come from the Theme singleton.
|
||||
Item {
|
||||
id: panel
|
||||
|
||||
property string panelId: ""
|
||||
property string title: ""
|
||||
property string meta: ""
|
||||
property bool showHeader: true
|
||||
property int chamfer: 13
|
||||
property int offsetY: 2
|
||||
property int accentLineThickness: 3
|
||||
|
||||
Shape {
|
||||
id: panelShape
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.hair
|
||||
strokeWidth: 1
|
||||
startX: 0; startY: panel.offsetY
|
||||
PathLine { x: panelShape.width - panel.chamfer; y: panel.offsetY }
|
||||
PathLine { x: panelShape.width; y: panel.chamfer }
|
||||
PathLine { x: panelShape.width; y: panelShape.height }
|
||||
PathLine { x: panel.chamfer; y: panelShape.height }
|
||||
PathLine { x: 0; y: panelShape.height - panel.chamfer }
|
||||
PathLine { x: 0; y: panel.offsetY }
|
||||
}
|
||||
|
||||
// Upper left accent line
|
||||
ShapePath {
|
||||
fillColor: Theme.accent
|
||||
strokeWidth: 0
|
||||
startX: 0; startY: 0
|
||||
PathLine { x: Math.min(49, panelShape.width / 3); y: 0 }
|
||||
PathLine { x: Math.min(49, panelShape.width / 3); 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 - Math.min(49, panelShape.width / 3); y: panelShape.height }
|
||||
PathLine { x: panelShape.width - Math.min(49, panelShape.width / 3); y: panelShape.height - panel.accentLineThickness }
|
||||
PathLine { x: panelShape.width; y: panelShape.height - panel.accentLineThickness }
|
||||
PathLine { x: panelShape.width; y: panelShape.height }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: panel.showHeader
|
||||
x: 1; y: 22
|
||||
width: parent.width - 2
|
||||
height: 1
|
||||
color: Theme.text
|
||||
opacity: 0.12
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: panel.showHeader
|
||||
x: 5; y: 7
|
||||
width: panel.panelId.length > 2 ? 29 : 24
|
||||
height: 11
|
||||
color: Theme.accent
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: panel.panelId
|
||||
color: Theme.surface
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 8
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: panel.showHeader
|
||||
x: 40; y: 7
|
||||
width: parent.width - 105
|
||||
text: panel.title
|
||||
color: Theme.text
|
||||
font.family: Theme.displayFont
|
||||
font.pixelSize: 9
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.1
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Inlined rather than reusing DenseBarContent's MicroText, which is an
|
||||
// inline component and therefore not visible from another file.
|
||||
Text {
|
||||
visible: panel.showHeader && panel.meta.length > 0
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
y: 6
|
||||
text: panel.meta
|
||||
width: Math.min(80, parent.width / 4)
|
||||
color: Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 6
|
||||
font.letterSpacing: 0.7
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: panel.showHeader
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
y: 14
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: 5
|
||||
Rectangle { required property int index; width: 4; height: 2; color: Theme.accent }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
Shape {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
strokeWidth: 0
|
||||
|
||||
startX: 0
|
||||
|
||||
@@ -5,6 +5,7 @@ import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
|
||||
import qs.widgets.decoration
|
||||
import qs.widgets.theme
|
||||
|
||||
WrapperItem {
|
||||
RowLayout {
|
||||
@@ -24,7 +25,7 @@ WrapperItem {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#0F1012"
|
||||
fillColor: Theme.surface
|
||||
|
||||
startX: 8
|
||||
startY: 0
|
||||
@@ -52,7 +53,7 @@ WrapperItem {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#0F1012"
|
||||
fillColor: Theme.surface
|
||||
|
||||
startX: 16
|
||||
startY: 0
|
||||
@@ -83,7 +84,7 @@ WrapperItem {
|
||||
startY: 0
|
||||
|
||||
strokeWidth: 0
|
||||
fillColor: "#0F1012"
|
||||
fillColor: Theme.surface
|
||||
|
||||
PathLine {
|
||||
x: shape.width
|
||||
|
||||
@@ -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" : "A–Z / 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 } } }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.widgets.theme
|
||||
|
||||
// Variant 4 — "Console": a full-width command deck that unfolds down out of
|
||||
// the top bar, with a horizontal carousel of app cards. While open it drives
|
||||
@@ -99,7 +100,7 @@ Scope {
|
||||
anchors.right: parent.right
|
||||
|
||||
clip: true
|
||||
color: "#0F1012"
|
||||
color: Theme.surface
|
||||
|
||||
height: root.active ? win.openHeight : 0
|
||||
|
||||
@@ -116,7 +117,7 @@ Scope {
|
||||
|
||||
// Accent lid — mirrors the top bar strip, reads as its extension.
|
||||
Rectangle {
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
implicitHeight: 4
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -132,8 +133,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: ">_"
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 22
|
||||
font.bold: true
|
||||
}
|
||||
@@ -147,7 +148,7 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: "#EEEEEE"
|
||||
color: Theme.text
|
||||
font.pointSize: 16
|
||||
clip: true
|
||||
|
||||
@@ -182,7 +183,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "launch application…"
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -190,14 +191,14 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length + " apps"
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 15
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: "#292C30"
|
||||
color: Theme.raised
|
||||
implicitHeight: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -236,9 +237,9 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: card.selected ? "#292C30" : "transparent"
|
||||
color: card.selected ? Theme.raised : "transparent"
|
||||
border.width: 1
|
||||
border.color: card.selected ? "#FFD063" : "#292C30"
|
||||
border.color: card.selected ? Theme.accent : Theme.raised
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
@@ -261,7 +262,7 @@ Scope {
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: card.modelData.name
|
||||
color: card.selected ? "#FFD063" : "#EEEEEE"
|
||||
color: card.selected ? Theme.accent : Theme.text
|
||||
font.pointSize: 9
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
@@ -277,8 +278,8 @@ Scope {
|
||||
anchors.centerIn: parent
|
||||
visible: model.apps.length === 0
|
||||
text: "no matches"
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
// Variant 7 — a copy of the "Slant" (V6) launcher, plus a small floating
|
||||
// power panel docked to its right with Shutdown / Reboot buttons.
|
||||
@@ -77,7 +78,7 @@ Scope {
|
||||
// Dim backdrop — click to dismiss.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#0A0A0C"
|
||||
color: Theme.surface
|
||||
opacity: 0.55
|
||||
|
||||
MouseArea {
|
||||
@@ -107,8 +108,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.accent
|
||||
strokeWidth: 2
|
||||
|
||||
startX: frame.chamfer
|
||||
@@ -153,7 +154,7 @@ Scope {
|
||||
// run out to the top and left edges to meet the straight borders.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: 0
|
||||
startY: frame.chamfer
|
||||
@@ -179,7 +180,7 @@ Scope {
|
||||
// bottom and right edges.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height - frame.chamfer
|
||||
@@ -205,7 +206,7 @@ Scope {
|
||||
// detached from the panel by the width of the cut.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height
|
||||
@@ -228,7 +229,7 @@ Scope {
|
||||
// slanted end pieces on both arms.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
// inner, right end of the bottom arm
|
||||
startX: panelShape.width / 3
|
||||
@@ -277,7 +278,7 @@ Scope {
|
||||
// slanted end pieces on both arms.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
// inner, bottom of the right arm
|
||||
startX: panelShape.width
|
||||
@@ -345,7 +346,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -367,7 +368,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 15
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -389,7 +390,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 24
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -420,8 +421,8 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 20
|
||||
font.letterSpacing: 1
|
||||
clip: true
|
||||
@@ -457,7 +458,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "run"
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -465,8 +466,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 18
|
||||
}
|
||||
}
|
||||
@@ -489,7 +490,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: divider.slant
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -512,7 +513,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 0
|
||||
startY: divider.height
|
||||
PathLine {
|
||||
@@ -535,7 +536,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: divider.width
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -597,7 +598,7 @@ Scope {
|
||||
// Body
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#22262C"
|
||||
fillColor: Theme.raised
|
||||
startX: appRow.shear
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -620,7 +621,7 @@ Scope {
|
||||
// Left accent edge
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: appRow.shear
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -655,7 +656,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.name
|
||||
color: appRow.selected ? "#FFD063" : "#EEEEEE"
|
||||
color: appRow.selected ? Theme.accent : Theme.text
|
||||
font.pointSize: 12
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -663,7 +664,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.genericName || ""
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font.pointSize: 10
|
||||
elide: Text.ElideRight
|
||||
Layout.maximumWidth: 220
|
||||
@@ -694,8 +695,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: "#FFD063"
|
||||
strokeColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
strokeColor: Theme.accent
|
||||
strokeWidth: 2
|
||||
|
||||
startX: 0
|
||||
@@ -732,9 +733,9 @@ Scope {
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "POWER"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 11
|
||||
color: "#0F1012"
|
||||
color: Theme.surface
|
||||
}
|
||||
|
||||
Repeater {
|
||||
@@ -762,8 +763,8 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 1
|
||||
strokeColor: powerButton.containsMouse ? "#FFD063" : "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
strokeColor: powerButton.containsMouse ? Theme.accent : Theme.muted
|
||||
fillColor: Theme.raised
|
||||
|
||||
startX: powerButton.chamfer
|
||||
startY: 0
|
||||
@@ -785,8 +786,8 @@ Scope {
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: powerButton.modelData.label
|
||||
color: powerButton.containsMouse ? "#FFD063" : "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: powerButton.containsMouse ? Theme.accent : Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.widgets.theme
|
||||
|
||||
// Variant 5 — "Dock": the Console concept mirrored to the bottom edge. A
|
||||
// full-width deck rises up out of the bottom bar, which energizes via
|
||||
@@ -98,7 +99,7 @@ Scope {
|
||||
anchors.right: parent.right
|
||||
|
||||
clip: true
|
||||
color: "#0F1012"
|
||||
color: Theme.surface
|
||||
|
||||
height: root.active ? win.openHeight : 0
|
||||
|
||||
@@ -153,9 +154,9 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: card.selected ? "#292C30" : "transparent"
|
||||
color: card.selected ? Theme.raised : "transparent"
|
||||
border.width: 1
|
||||
border.color: card.selected ? "#FFD063" : "#292C30"
|
||||
border.color: card.selected ? Theme.accent : Theme.raised
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
@@ -178,7 +179,7 @@ Scope {
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: card.modelData.name
|
||||
color: card.selected ? "#FFD063" : "#EEEEEE"
|
||||
color: card.selected ? Theme.accent : Theme.text
|
||||
font.pointSize: 9
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
@@ -194,14 +195,14 @@ Scope {
|
||||
anchors.centerIn: parent
|
||||
visible: model.apps.length === 0
|
||||
text: "no matches"
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: "#292C30"
|
||||
color: Theme.raised
|
||||
implicitHeight: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -217,8 +218,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: ">_"
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 22
|
||||
font.bold: true
|
||||
}
|
||||
@@ -232,7 +233,7 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: "#EEEEEE"
|
||||
color: Theme.text
|
||||
font.pointSize: 16
|
||||
clip: true
|
||||
|
||||
@@ -267,7 +268,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "launch application…"
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -275,15 +276,15 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length + " apps"
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 15
|
||||
}
|
||||
}
|
||||
|
||||
// Accent lid — mirrors the bottom bar strip.
|
||||
Rectangle {
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
implicitHeight: 4
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.widgets.theme
|
||||
|
||||
// Variant 2 — "Grid": centered app-drawer with a dim backdrop and icon tiles.
|
||||
Scope {
|
||||
@@ -78,7 +79,7 @@ Scope {
|
||||
// Dim backdrop — click anywhere outside to dismiss.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#0A0A0C"
|
||||
color: Theme.surface
|
||||
opacity: 0.55
|
||||
|
||||
MouseArea {
|
||||
@@ -93,9 +94,9 @@ Scope {
|
||||
width: 760
|
||||
height: 560
|
||||
|
||||
color: "#0F1012"
|
||||
color: Theme.surface
|
||||
border.width: 1
|
||||
border.color: "#FFD063"
|
||||
border.color: Theme.accent
|
||||
opacity: 0.98
|
||||
|
||||
// Accent corner brackets for the cyberpunk frame.
|
||||
@@ -104,14 +105,14 @@ Scope {
|
||||
anchors.left: parent.left
|
||||
width: 5
|
||||
height: 28
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
}
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: 28
|
||||
height: 5
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -126,8 +127,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: "APPS"
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 22
|
||||
font.letterSpacing: 3
|
||||
}
|
||||
@@ -138,9 +139,9 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#292C30"
|
||||
color: Theme.raised
|
||||
border.width: 1
|
||||
border.color: input.activeFocus ? "#FFD063" : "#7A7B7D"
|
||||
border.color: input.activeFocus ? Theme.accent : Theme.muted
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
@@ -155,7 +156,7 @@ Scope {
|
||||
anchors.rightMargin: 12
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: "#EEEEEE"
|
||||
color: Theme.text
|
||||
font.pointSize: 13
|
||||
clip: true
|
||||
|
||||
@@ -198,7 +199,7 @@ Scope {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: input.text.length === 0
|
||||
text: "Type to search…"
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -207,7 +208,7 @@ Scope {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
implicitHeight: 3
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -242,9 +243,9 @@ Scope {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
color: tile.selected ? "#292C30" : "transparent"
|
||||
color: tile.selected ? Theme.raised : "transparent"
|
||||
border.width: 1
|
||||
border.color: tile.selected ? "#FFD063" : "transparent"
|
||||
border.color: tile.selected ? Theme.accent : "transparent"
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
@@ -266,7 +267,7 @@ Scope {
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: tile.modelData.name
|
||||
color: tile.selected ? "#FFD063" : "#EEEEEE"
|
||||
color: tile.selected ? Theme.accent : Theme.text
|
||||
font.pointSize: 10
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
|
||||
@@ -7,6 +7,7 @@ import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
// Variant 6 — "Slant": breaks up the rectangular layout with angled geometry —
|
||||
// a chamfered panel, a slanted header divider and sheared row highlights.
|
||||
@@ -77,7 +78,7 @@ Scope {
|
||||
// Dim backdrop — click to dismiss.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#0A0A0C"
|
||||
color: Theme.surface
|
||||
opacity: 0.55
|
||||
|
||||
MouseArea {
|
||||
@@ -107,8 +108,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.accent
|
||||
strokeWidth: 2
|
||||
|
||||
startX: frame.chamfer
|
||||
@@ -153,7 +154,7 @@ Scope {
|
||||
// run out to the top and left edges to meet the straight borders.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: 0
|
||||
startY: frame.chamfer
|
||||
@@ -179,7 +180,7 @@ Scope {
|
||||
// bottom and right edges.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height - frame.chamfer
|
||||
@@ -205,7 +206,7 @@ Scope {
|
||||
// detached from the panel by the width of the cut.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height
|
||||
@@ -228,7 +229,7 @@ Scope {
|
||||
// slanted end pieces on both arms.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
// inner, right end of the bottom arm
|
||||
startX: panelShape.width / 3
|
||||
@@ -277,7 +278,7 @@ Scope {
|
||||
// slanted end pieces on both arms.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
// inner, bottom of the right arm
|
||||
startX: panelShape.width
|
||||
@@ -345,7 +346,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -367,7 +368,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 15
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -389,7 +390,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 24
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -420,8 +421,8 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 20
|
||||
font.letterSpacing: 1
|
||||
clip: true
|
||||
@@ -457,7 +458,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "run"
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -465,8 +466,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 18
|
||||
}
|
||||
}
|
||||
@@ -489,7 +490,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: divider.slant
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -512,7 +513,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 0
|
||||
startY: divider.height
|
||||
PathLine {
|
||||
@@ -535,7 +536,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: divider.width
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -597,7 +598,7 @@ Scope {
|
||||
// Body
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#22262C"
|
||||
fillColor: Theme.raised
|
||||
startX: appRow.shear
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -620,7 +621,7 @@ Scope {
|
||||
// Left accent edge
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: appRow.shear
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -655,7 +656,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.name
|
||||
color: appRow.selected ? "#FFD063" : "#EEEEEE"
|
||||
color: appRow.selected ? Theme.accent : Theme.text
|
||||
font.pointSize: 12
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -663,7 +664,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.genericName || ""
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font.pointSize: 10
|
||||
elide: Text.ElideRight
|
||||
Layout.maximumWidth: 220
|
||||
|
||||
@@ -6,6 +6,7 @@ import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.widgets.theme
|
||||
|
||||
// Variant 3 — "Spotlight": top-center command bar with a compact result list.
|
||||
Scope {
|
||||
@@ -91,9 +92,9 @@ Scope {
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 60
|
||||
color: "#0F1012"
|
||||
color: Theme.surface
|
||||
border.width: 1
|
||||
border.color: "#FFD063"
|
||||
border.color: Theme.accent
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
@@ -103,8 +104,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: ">"
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 26
|
||||
font.bold: true
|
||||
}
|
||||
@@ -118,7 +119,7 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: "#EEEEEE"
|
||||
color: Theme.text
|
||||
font.pointSize: 17
|
||||
clip: true
|
||||
|
||||
@@ -153,7 +154,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "run application"
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -161,8 +162,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length + " ▸"
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
@@ -172,7 +173,7 @@ Scope {
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 3
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
visible: model.apps.length > 0
|
||||
}
|
||||
|
||||
@@ -181,11 +182,11 @@ Scope {
|
||||
Layout.fillWidth: true
|
||||
// Cap the visible height to maxRows; scroll beyond that.
|
||||
implicitHeight: Math.min(model.apps.length, win.maxRows) * 44 + 2
|
||||
color: "#0F1012"
|
||||
color: Theme.surface
|
||||
opacity: 0.97
|
||||
visible: model.apps.length > 0
|
||||
border.width: 1
|
||||
border.color: "#292C30"
|
||||
border.color: Theme.raised
|
||||
|
||||
ListView {
|
||||
id: list
|
||||
@@ -215,14 +216,14 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: entry.selected ? "#1A1C1F" : "transparent"
|
||||
color: entry.selected ? Theme.selection : "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 3
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
visible: entry.selected
|
||||
}
|
||||
|
||||
@@ -239,7 +240,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: entry.modelData.name
|
||||
color: entry.selected ? "#FFD063" : "#EEEEEE"
|
||||
color: entry.selected ? Theme.accent : Theme.text
|
||||
font.pointSize: 12
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -247,7 +248,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: entry.modelData.genericName || ""
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font.pointSize: 10
|
||||
elide: Text.ElideRight
|
||||
Layout.maximumWidth: 200
|
||||
|
||||
@@ -6,6 +6,7 @@ import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.widgets.theme
|
||||
|
||||
// Variant 1 — "Stack": left-anchored vertical list launcher.
|
||||
// Framed with the top/bottom accent strips used by the main Bar.
|
||||
@@ -82,7 +83,7 @@ Scope {
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
implicitHeight: 5
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -93,8 +94,8 @@ Scope {
|
||||
|
||||
margin: 12
|
||||
border.width: 1
|
||||
border.color: "#FFD063"
|
||||
color: "#0F1012"
|
||||
border.color: Theme.accent
|
||||
color: Theme.surface
|
||||
opacity: 0.95
|
||||
|
||||
ColumnLayout {
|
||||
@@ -107,7 +108,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: "▚"
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
font.pointSize: 14
|
||||
font.bold: true
|
||||
}
|
||||
@@ -121,8 +122,8 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 16
|
||||
clip: true
|
||||
|
||||
@@ -158,7 +159,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "search"
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -166,14 +167,14 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 14
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
implicitHeight: 2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -207,7 +208,7 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: appRow.selected ? "#292C30" : "transparent"
|
||||
color: appRow.selected ? Theme.raised : "transparent"
|
||||
|
||||
// Accent bar on the selected row.
|
||||
Rectangle {
|
||||
@@ -215,7 +216,7 @@ Scope {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 3
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
visible: appRow.selected
|
||||
}
|
||||
|
||||
@@ -236,7 +237,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.name
|
||||
color: appRow.selected ? "#FFD063" : "#EEEEEE"
|
||||
color: appRow.selected ? Theme.accent : Theme.text
|
||||
font.pointSize: 12
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -245,7 +246,7 @@ Scope {
|
||||
Text {
|
||||
visible: text.length > 0
|
||||
text: appRow.modelData.genericName || appRow.modelData.comment || ""
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font.pointSize: 9
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -259,7 +260,7 @@ Scope {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
implicitHeight: 5
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Quickshell.Services.Notifications
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.widgets.theme
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
@@ -12,7 +13,7 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
implicitHeight: 5
|
||||
|
||||
Layout.fillWidth: true
|
||||
@@ -23,8 +24,8 @@ ColumnLayout {
|
||||
|
||||
margin: 12
|
||||
border.width: 1
|
||||
border.color: "#FFD063"
|
||||
color: "#0F1012"
|
||||
border.color: Theme.accent
|
||||
color: Theme.surface
|
||||
opacity: .9
|
||||
|
||||
ColumnLayout {
|
||||
@@ -45,7 +46,7 @@ ColumnLayout {
|
||||
|
||||
Text {
|
||||
text: root.modelData.summary
|
||||
color: "#EEEEEE"
|
||||
color: Theme.text
|
||||
font.pointSize: 14
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
@@ -57,7 +58,7 @@ ColumnLayout {
|
||||
id: dismissButton
|
||||
|
||||
text: "×"
|
||||
color: "#FFD063"
|
||||
color: Theme.accent
|
||||
font.pointSize: 18
|
||||
|
||||
MouseArea {
|
||||
@@ -71,7 +72,7 @@ ColumnLayout {
|
||||
Text {
|
||||
visible: root.modelData.body !== ""
|
||||
text: root.modelData.body
|
||||
color: "#EEEEEE"
|
||||
color: Theme.text
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
@@ -92,9 +93,9 @@ ColumnLayout {
|
||||
|
||||
required property NotificationAction modelData
|
||||
|
||||
color: "#292C30"
|
||||
color: Theme.raised
|
||||
border.width: 1
|
||||
border.color: "#FFD063"
|
||||
border.color: Theme.accent
|
||||
implicitHeight: 28
|
||||
implicitWidth: actionText.implicitWidth + 16
|
||||
|
||||
@@ -102,7 +103,7 @@ ColumnLayout {
|
||||
id: actionText
|
||||
anchors.centerIn: parent
|
||||
text: actionButton.modelData.text
|
||||
color: "#EEEEEE"
|
||||
color: Theme.text
|
||||
font.pointSize: 12
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.widgets.theme
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
@@ -68,7 +69,7 @@ Scope {
|
||||
anchors.bottomMargin: 4
|
||||
|
||||
text: Math.round(Math.min(root.volume, root.maxVolume) * 100) + "%"
|
||||
color: !root.muted && root.volume > 1 ? "#FF6B4A" : "#FFD063"
|
||||
color: !root.muted && root.volume > 1 ? Theme.hot : Theme.accent
|
||||
font.pointSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
@@ -84,7 +85,7 @@ Scope {
|
||||
anchors.centerIn: parent
|
||||
implicitHeight: 6
|
||||
width: parent.width * Math.min(root.volume, root.maxVolume) / root.maxVolume
|
||||
color: "#FF6B4A"
|
||||
color: Theme.hot
|
||||
visible: !root.muted && root.volume > 1
|
||||
|
||||
Behavior on width {
|
||||
@@ -98,7 +99,7 @@ Scope {
|
||||
anchors.centerIn: parent
|
||||
implicitHeight: 6
|
||||
width: parent.width * Math.min(root.volume, 1) / root.maxVolume
|
||||
color: root.muted ? "#7A7B7D" : "#FFD063"
|
||||
color: root.muted ? Theme.muted : Theme.accent
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
|
||||
@@ -1,476 +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
|
||||
|
||||
// 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: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
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: "#FFD063"
|
||||
|
||||
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: "#FFD063"
|
||||
|
||||
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: "#FFD063"
|
||||
|
||||
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: "#FFD063"
|
||||
|
||||
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 — Digital-7, hh over mm
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Qt.formatDateTime(clock.date, "hh\nmm")
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 22
|
||||
font.letterSpacing: 1
|
||||
color: "#EEEEEE"
|
||||
|
||||
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: "Digital-7 Mono"
|
||||
font.pointSize: 13
|
||||
color: "#FFD063"
|
||||
}
|
||||
|
||||
// Slanted divider
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 4
|
||||
|
||||
Shape {
|
||||
id: divider
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
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: "Digital-7 Mono"
|
||||
font.pointSize: 10
|
||||
color: "#7A7B7D"
|
||||
}
|
||||
|
||||
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: "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
|
||||
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: "#FF6B4A"
|
||||
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: "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
|
||||
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 ? "#7A7B7D" : "#FFD063"
|
||||
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: "Digital-7 Mono"
|
||||
font.pointSize: 14
|
||||
color: root.muted ? "#7A7B7D" : "#EEEEEE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@ import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
// The right bar: a compact utility strip for the SystemTray icons, styled
|
||||
// in the same family as the Launcher (V6 "Slant") and SideBar — accent
|
||||
// color, chamfered corners, Digital-7 Mono, the slash-trio motif — but with
|
||||
// color, chamfered corners, the readout font, the slash-trio motif — but with
|
||||
// its own plain, evenly-chamfered panel rather than their ornate
|
||||
// bracket-and-cap silhouette, since this is a secondary/utility bar rather
|
||||
// than a hero surface.
|
||||
@@ -54,8 +55,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.accent
|
||||
strokeWidth: 2
|
||||
|
||||
startX: frame.chamfer
|
||||
@@ -114,7 +115,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine { x: 10; y: 0 }
|
||||
@@ -124,7 +125,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 15
|
||||
startY: 0
|
||||
PathLine { x: 19; y: 0 }
|
||||
@@ -134,7 +135,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 24
|
||||
startY: 0
|
||||
PathLine { x: 28; y: 0 }
|
||||
@@ -155,7 +156,7 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 8
|
||||
startY: 0
|
||||
PathLine { x: divider.width; y: 0 }
|
||||
@@ -169,9 +170,9 @@ Scope {
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "TRAY"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 12
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
}
|
||||
|
||||
ListView {
|
||||
@@ -201,17 +202,17 @@ Scope {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: SystemTray.items.values.length === 0
|
||||
text: "--"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 14
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: SystemTray.items.values.length
|
||||
font.family: "Digital-7 Mono"
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 16
|
||||
color: "#EEEEEE"
|
||||
color: Theme.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
// A single tray icon, chamfered to match the Slant (launcher V6 / SideBar)
|
||||
// visual language instead of a plain rectangular hit target.
|
||||
@@ -35,8 +36,8 @@ MouseArea {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 1
|
||||
strokeColor: root.containsMouse ? "#FFD063" : "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
strokeColor: root.containsMouse ? Theme.accent : Theme.muted
|
||||
fillColor: Theme.raised
|
||||
|
||||
startX: root.chamfer
|
||||
startY: 0
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
// Horizontal meter in the Slant language: a chamfered track whose fill is a
|
||||
// clipped copy of the SAME hexagon, so empty and full always share one
|
||||
@@ -26,8 +27,8 @@ Item {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 1
|
||||
strokeColor: bar.unknown ? "#3A3D42" : "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
strokeColor: bar.unknown ? Theme.disabled : Theme.muted
|
||||
fillColor: Theme.raised
|
||||
|
||||
startX: bar.chamfer
|
||||
startY: 0
|
||||
@@ -72,7 +73,7 @@ Item {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: bar.hot ? "#FF6B4A" : "#FFD063"
|
||||
fillColor: bar.hot ? Theme.hot : Theme.accent
|
||||
|
||||
Behavior on fillColor {
|
||||
ColorAnimation {
|
||||
|
||||
@@ -6,6 +6,7 @@ import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
// Host vitals HUD — the "Slant" language (chamfered panel, trapezoid bevels,
|
||||
// outward corner chunks, floating cap, slanted dividers) carried over from the
|
||||
@@ -59,7 +60,7 @@ Scope {
|
||||
// Dim backdrop — click anywhere to dismiss.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#0A0A0C"
|
||||
color: Theme.surface
|
||||
opacity: 0.5
|
||||
|
||||
MouseArea {
|
||||
@@ -102,8 +103,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.accent
|
||||
strokeWidth: 2
|
||||
|
||||
startX: frame.chamfer
|
||||
@@ -121,7 +122,7 @@ Scope {
|
||||
// Thick top-left bevel accent.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: 0
|
||||
startY: frame.chamfer
|
||||
@@ -134,7 +135,7 @@ Scope {
|
||||
// Thick bottom-right bevel accent.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height - frame.chamfer
|
||||
@@ -147,7 +148,7 @@ Scope {
|
||||
// Floating triangle cap in the bottom-right notch.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height
|
||||
@@ -159,7 +160,7 @@ Scope {
|
||||
// Heavy outward chunk wrapping the bottom-left corner.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: panelShape.width / 3
|
||||
startY: panelShape.height
|
||||
@@ -176,7 +177,7 @@ Scope {
|
||||
// Heavy outward chunk wrapping the top-right corner.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height / 3
|
||||
@@ -217,7 +218,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine { x: 10; y: 0 }
|
||||
@@ -227,7 +228,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 15
|
||||
startY: 0
|
||||
PathLine { x: 19; y: 0 }
|
||||
@@ -237,7 +238,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 24
|
||||
startY: 0
|
||||
PathLine { x: 28; y: 0 }
|
||||
@@ -249,8 +250,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: (vitals.host || "vitals").toUpperCase()
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 20
|
||||
font.letterSpacing: 2
|
||||
}
|
||||
@@ -261,15 +262,15 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: "UP"
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 11
|
||||
}
|
||||
|
||||
Text {
|
||||
text: vitals.fmtUptime(vitals.uptime)
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
@@ -281,8 +282,8 @@ Scope {
|
||||
Layout.fillWidth: true
|
||||
visible: vitals.failed
|
||||
text: "NODE_EXPORTER UNREACHABLE ON :" + vitals.port
|
||||
color: "#FF6B4A"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.hot
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 13
|
||||
}
|
||||
|
||||
@@ -353,7 +354,7 @@ Scope {
|
||||
Text {
|
||||
Layout.preferredWidth: 96
|
||||
text: diskRow.modelData.mount
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font.pointSize: 9
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
@@ -369,8 +370,8 @@ Scope {
|
||||
Layout.preferredWidth: 48
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: Math.round(diskRow.frac * 100) + "%"
|
||||
color: diskRow.frac >= 0.9 ? "#FF6B4A" : "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: diskRow.frac >= 0.9 ? Theme.hot : Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 13
|
||||
}
|
||||
|
||||
@@ -378,8 +379,8 @@ Scope {
|
||||
Layout.preferredWidth: 104
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: vitals.fmtBytes(diskRow.modelData.size - diskRow.modelData.used) + " FREE"
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 11
|
||||
}
|
||||
}
|
||||
@@ -399,21 +400,21 @@ Scope {
|
||||
Text {
|
||||
Layout.preferredWidth: 96
|
||||
text: vitals.netIface || "NET"
|
||||
color: "#7A7B7D"
|
||||
color: Theme.muted
|
||||
font.pointSize: 9
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "RX"
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 11
|
||||
}
|
||||
|
||||
Text {
|
||||
text: vitals.fmtRate(vitals.netRx)
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 14
|
||||
}
|
||||
|
||||
@@ -423,15 +424,15 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: "TX"
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 11
|
||||
}
|
||||
|
||||
Text {
|
||||
text: vitals.fmtRate(vitals.netTx)
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 14
|
||||
|
||||
// Right-align the TX readout against the panel edge the
|
||||
@@ -469,8 +470,8 @@ Scope {
|
||||
Text {
|
||||
Layout.preferredWidth: 46
|
||||
text: metric.label
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 15
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
@@ -486,8 +487,8 @@ Scope {
|
||||
Layout.preferredWidth: 54
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: metric.readout
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
@@ -499,8 +500,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: metric.detail
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 11
|
||||
}
|
||||
|
||||
@@ -511,8 +512,8 @@ Scope {
|
||||
Text {
|
||||
visible: metric.aside.length > 0
|
||||
text: metric.aside
|
||||
color: metric.asideHot ? "#FF6B4A" : "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
color: metric.asideHot ? Theme.hot : Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
font.pointSize: 11
|
||||
}
|
||||
}
|
||||
@@ -530,7 +531,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: "#FFD063"
|
||||
fillColor: Theme.accent
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine { x: divider.width; y: 0 }
|
||||
|
||||
Generated
+18
-35
@@ -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": {
|
||||
@@ -176,22 +176,6 @@
|
||||
"url": "https://git.mgaction.town/darman/hypr-chrome.git"
|
||||
}
|
||||
},
|
||||
"livesync-bridge": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1787571662,
|
||||
"narHash": "sha256-btLnQNbFzCPaSVcY9rtiPdYeXrZjoK9AYvfA9+ovsIc=",
|
||||
"owner": "vrtmrz",
|
||||
"repo": "livesync-bridge",
|
||||
"rev": "c3760beaa0851214da4860903445d7f6420ca025",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "vrtmrz",
|
||||
"repo": "livesync-bridge",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"media-manager": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@@ -286,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": {
|
||||
@@ -307,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": {
|
||||
@@ -370,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": {
|
||||
@@ -386,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": {
|
||||
@@ -416,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": {
|
||||
@@ -506,7 +490,6 @@
|
||||
"disko": "disko",
|
||||
"home-manager": "home-manager",
|
||||
"hypr-chrome": "hypr-chrome",
|
||||
"livesync-bridge": "livesync-bridge",
|
||||
"mediamanager-nix": "mediamanager-nix",
|
||||
"nix-flatpak": "nix-flatpak",
|
||||
"nixos-anywhere": "nixos-anywhere",
|
||||
|
||||
@@ -31,18 +31,6 @@
|
||||
url = "github:strangeglyph/mediamanager-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
# livesync-bridge — headless CouchDB <-> filesystem sync for Obsidian
|
||||
# LiveSync, used on mars to give luna a real directory of markdown
|
||||
# (hosts/mars/livesync-bridge.nix). Not a flake and not in nixpkgs, so it
|
||||
# comes in as plain source pinned by flake.lock; the service copies it out
|
||||
# and runs it under deno. Pinning matters more than usual here — this is a
|
||||
# small third-party project with open bugs on the storage->couchdb path,
|
||||
# so an unreviewed bump could quietly change how the agent's notes are
|
||||
# written back.
|
||||
livesync-bridge = {
|
||||
url = "github:vrtmrz/livesync-bridge";
|
||||
flake = false;
|
||||
};
|
||||
authentik-nix.url = "github:nix-community/authentik-nix";
|
||||
nix-flatpak.url = "github:gmodena/nix-flatpak";
|
||||
# Own Hyprland plugin (border + title bar), public repo, fetched over
|
||||
@@ -564,5 +552,147 @@
|
||||
machine.crash()
|
||||
'';
|
||||
};
|
||||
|
||||
# `nix develop` — hot-reload loop for dotfiles/quickshell.
|
||||
#
|
||||
# hosts/terra/home.nix ships the shell via `xdg.configFile."quickshell"`,
|
||||
# which COPIES the tree into the store, so ~/.config/quickshell is a
|
||||
# read-only symlink into /nix/store and every QML tweak costs a
|
||||
# nixos-rebuild. quickshell DOES hot-reload on file save — but only for
|
||||
# the files it is watching, which are those frozen store copies. Pointing
|
||||
# it at the working tree with `qs -p` restores edit-save-see, no rebuild.
|
||||
#
|
||||
# quickshell keys instance identity on the CONFIG PATH, so a working-tree
|
||||
# instance and the store-backed one are two different instances that would
|
||||
# both map layer-shell bars onto every output. Hence a swap, not a second
|
||||
# instance — and the swap starts dev FIRST, killing the packaged shell
|
||||
# only once dev is confirmed up, so a QML error in the working tree leaves
|
||||
# you on your normal bar instead of no bar at all.
|
||||
#
|
||||
# Every kill is scoped to one config (`qs kill` = default only, `qs kill
|
||||
# -p` = that path only). A blanket kill would also take out unrelated
|
||||
# quickshell instances — pkgs/rishot.nix is one.
|
||||
#
|
||||
# Deliberately NOT wired to direnv (no .envrc in this repo): programs.direnv
|
||||
# is enabled for this user, so a `use flake` would swap the running desktop
|
||||
# shell on every `cd` into the checkout, including over ssh.
|
||||
devShells.${system}.default =
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
# Same nixpkgs terra's home.nix takes pkgs.quickshell from, so the dev
|
||||
# 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.
|
||||
preamble = ''
|
||||
root="$(${git} rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
cfg="$root/dotfiles/quickshell"
|
||||
if [ ! -f "$cfg/shell.qml" ]; then
|
||||
echo "no shell.qml under $cfg — run this from the homelab checkout" >&2
|
||||
exit 1
|
||||
fi
|
||||
# `qs list` exits 0 whether or not it found anything, and only emits
|
||||
# json when it did — so "json came back" is the liveness test.
|
||||
running() { ${qs} list -p "$1" -j 2>/dev/null | grep -q '"id"'; }
|
||||
prod_running() { ${qs} list -j 2>/dev/null | grep -q '"id"'; }
|
||||
'';
|
||||
|
||||
qs-dev = pkgs.writeShellScriptBin "qs-dev" ''
|
||||
set -uo pipefail
|
||||
${preamble}
|
||||
|
||||
if [ -z "''${WAYLAND_DISPLAY:-}" ]; then
|
||||
echo "qs-dev: no WAYLAND_DISPLAY — refusing to swap the desktop shell" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if running "$cfg"; then
|
||||
echo "qs-dev: already running from $cfg"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
${qs} -d -p "$cfg"
|
||||
|
||||
# Confirm it came up before touching the packaged shell.
|
||||
for _ in $(seq 1 50); do
|
||||
running "$cfg" && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if ! running "$cfg"; then
|
||||
echo "qs-dev: dev shell failed to start — packaged shell left alone" >&2
|
||||
echo "qs-dev: run 'qs -p $cfg' in the foreground to see the QML error" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${qs} kill || true
|
||||
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}
|
||||
|
||||
running "$cfg" && ${qs} kill -p "$cfg" || true
|
||||
prod_running || ${qs} -d
|
||||
echo "qs-prod: back on ~/.config/quickshell"
|
||||
'';
|
||||
in
|
||||
pkgs.mkShell {
|
||||
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,
|
||||
# replacing the shell that set the trap, so the restore would
|
||||
# never run and you'd be left on the dev instance. Non-interactive
|
||||
# use gets the explicit `nix develop -c qs-dev` instead.
|
||||
# - WAYLAND_DISPLAY, so entering the shell over ssh cannot kill the
|
||||
# desktop's bar and leave nothing in its place.
|
||||
# - a sentinel, so a nested `nix develop` does not swap (and then
|
||||
# restore) a second time.
|
||||
shellHook = ''
|
||||
if [[ $- == *i* ]] && [ -n "''${WAYLAND_DISPLAY:-}" ] && [ -z "''${HOMELAB_QS_DEV:-}" ]; then
|
||||
export HOMELAB_QS_DEV=1
|
||||
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"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
../../services/media/seerr.nix
|
||||
../../services/media/immich.nix
|
||||
../../services/dev/gitea.nix
|
||||
../../services/dev/obsidian-livesync.nix
|
||||
];
|
||||
|
||||
# sabnzbd's unrar dependency is unfree; scope the allowance to just that
|
||||
|
||||
@@ -69,21 +69,4 @@
|
||||
sops.secrets.sabnzbd_eweka_username.owner = "sabnzbd";
|
||||
sops.secrets.sabnzbd_eweka_password.owner = "sabnzbd";
|
||||
|
||||
# CouchDB admin account for Obsidian LiveSync
|
||||
# (services/dev/obsidian-livesync.nix). Rendered into an [admins] ini
|
||||
# fragment rather than passed as services.couchdb.adminPass, which would put
|
||||
# the plaintext in the world-readable store.
|
||||
#
|
||||
# owner = couchdb on BOTH: couchdb re-reads its ini chain as its own
|
||||
# User=/Group= after systemd drops privileges, and sops defaults to
|
||||
# root:root 0400 — without this it comes up with no admin configured, which
|
||||
# under require_valid_user means every request 401s.
|
||||
sops.secrets.couchdb_admin_password.owner = "couchdb";
|
||||
sops.templates."couchdb-admins.ini" = {
|
||||
owner = "couchdb";
|
||||
content = ''
|
||||
[admins]
|
||||
obsidian = ${config.sops.placeholder.couchdb_admin_password}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
./disk-config.nix # disko: OS-disk partitions + filesystems
|
||||
./secrets.nix # sops-nix: samba/tailscale/hermes secrets
|
||||
./hermes-agent.nix
|
||||
./livesync-bridge.nix
|
||||
../../common.nix # shared base: user / ssh / nix / firewall
|
||||
../../services/containers.nix
|
||||
../../services/vpn/tailscale.nix
|
||||
|
||||
@@ -278,13 +278,6 @@ in
|
||||
"${hermesHome}:/opt/data"
|
||||
"${dropboxDir}:/opt/data/dropbox"
|
||||
|
||||
# luna's Obsidian vault, kept in sync with CouchDB on jupiter by
|
||||
# livesync-bridge.nix. Under /opt/data so it lands inside
|
||||
# HERMES_WRITE_SAFE_ROOT and she can write notes, not just read them —
|
||||
# same reasoning as the dropbox above. The bridge runs as this very
|
||||
# uid/gid, so no ownership fixup is needed on either side.
|
||||
"/var/lib/livesync-bridge/vault:/opt/data/vault"
|
||||
|
||||
# git/tea for luna: the image doesn't ship `tea` (and shouldn't be
|
||||
# trusted to have a known-good `git` either), so both come from this
|
||||
# host's Nix store instead — mounted read-only at fixed PATH-visible
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
{ config, pkgs, inputs, ... }:
|
||||
|
||||
# livesync-bridge (vrtmrz) — mirrors an Obsidian LiveSync vault out of CouchDB
|
||||
# on jupiter (services/dev/obsidian-livesync.nix) into a real directory of
|
||||
# markdown here, so luna can read and write the vault as files. Obsidian itself
|
||||
# is an Electron GUI with no headless mode, and an agent wants files anyway.
|
||||
#
|
||||
# ⚠️ THE WRITE-BACK PATH IS THE RISKY ONE. Upstream has three open, unanswered
|
||||
# issues on storage->couchdb — #50 (Jun 2026, writes detected and logged as
|
||||
# uploaded, database never updated), #23 (only lowercase filenames transmitted
|
||||
# from storage), #46 (silent stall on files over ~30KB). All fail QUIETLY: the
|
||||
# log says success and the note never arrives. So do not treat this directory
|
||||
# as durable storage for anything luna cannot regenerate, and check that her
|
||||
# edits actually reach your devices before trusting it. (E2EE itself is fine —
|
||||
# PeerCouchDB.ts hard-errors if a passphrase is missing for an encrypted
|
||||
# remote, so it is a deliberate code path. The one issue claiming E2EE breaks
|
||||
# bridging, #12, is a single unreproduced report with no maintainer reply.)
|
||||
#
|
||||
#
|
||||
# EXPECTED NOISE ON FIRST SYNC: a stack trace per historically-deleted file —
|
||||
# NotFound: ... remove '<vault>/Welcome.md' at PeerStorage.delete
|
||||
# CouchDB keeps deletion tombstones, and the bridge replays them against a
|
||||
# directory where the file never existed. PeerStorage.ts:33-40 catches it,
|
||||
# logs, and returns false, so nothing is wrong; it only LOOKS fatal because
|
||||
# main.ts pins the logger to LOG_LEVEL_DEBUG, which prints exception dumps
|
||||
# that are otherwise verbose-level. It stops once the initial catch-up ends.
|
||||
# Talks to CouchDB over the TAILNET (jupiter.orbit.sol:5984), not through
|
||||
# neptun: mars is a tailnet node, so the public vhost, its TLS and its path
|
||||
# allowlist are all irrelevant here.
|
||||
let
|
||||
stateDir = "/var/lib/livesync-bridge";
|
||||
appDir = "${stateDir}/app";
|
||||
vaultDir = "${stateDir}/vault";
|
||||
|
||||
# The same uid/gid the hermes-agent container runs as (hermes-agent.nix).
|
||||
# Deliberate: the bridge and luna both read and write these files, and
|
||||
# sharing one uid removes any dependence on the container's umask. Two
|
||||
# different uids in a shared group only works while every file stays
|
||||
# group-writable, and a single 0644 file dropped by the agent would stall
|
||||
# sync on that path with nothing but a permission error in the log.
|
||||
hermesUid = 986;
|
||||
|
||||
# Which vault. `group` is what pairs the two peers — both must match or the
|
||||
# bridge starts cleanly and simply never syncs anything.
|
||||
#
|
||||
# ⚠️ `database` must be the name entered in the Obsidian plugin for luna's
|
||||
# vault. Get it wrong and nothing errors: the credential below is CouchDB's
|
||||
# admin, so PouchDB CREATES the misnamed database and replicates an empty
|
||||
# vault into it quite happily.
|
||||
peerGroup = "luna";
|
||||
database = "luna_wiki";
|
||||
in
|
||||
{
|
||||
# hermes-agent.nix declares the GROUP (gid 983) but no user: the container
|
||||
# brings its own uid and needs no host account. The bridge does need one to
|
||||
# run as, so the matching user is declared here.
|
||||
users.users.hermes = {
|
||||
uid = hermesUid;
|
||||
group = "hermes";
|
||||
isSystemUser = true;
|
||||
home = stateDir;
|
||||
description = "Hermes agent uid, shared with the livesync-bridge service";
|
||||
};
|
||||
|
||||
# Created here rather than by the service so they exist before anything
|
||||
# tries to use them:
|
||||
# - vaultDir before podman-hermes-agent starts, because a bind-mount
|
||||
# source that does not exist is created by podman as root:root and the
|
||||
# bridge then cannot write into its own vault;
|
||||
# - appDir because WorkingDirectory applies to ExecStartPre as well, so a
|
||||
# missing one fails the unit before preStart ever gets to create it.
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${vaultDir} 0770 hermes hermes -"
|
||||
"d ${appDir} 0750 hermes hermes -"
|
||||
"d ${stateDir}/deno 0750 hermes hermes -"
|
||||
];
|
||||
|
||||
# The bridge's peer config, rendered by sops because it carries three
|
||||
# secrets inline (CouchDB password + both passphrases) and the file format
|
||||
# has no include mechanism.
|
||||
#
|
||||
# ⚠️ sops substitutes placeholders into the ALREADY-RENDERED json, so a
|
||||
# secret containing a double quote or a backslash produces an invalid config
|
||||
# and the bridge logs "Could not parse configuration!" and then sits there
|
||||
# with zero peers — it does not exit. Keep all three values alphanumeric.
|
||||
sops.templates."livesync-bridge.json" = {
|
||||
owner = "hermes";
|
||||
content = builtins.toJSON {
|
||||
peers = [
|
||||
{
|
||||
type = "couchdb";
|
||||
name = "luna-remote";
|
||||
group = peerGroup;
|
||||
url = "http://jupiter.orbit.sol:5984";
|
||||
inherit database;
|
||||
username = "obsidian";
|
||||
password = config.sops.placeholder.couchdb_luna_password;
|
||||
passphrase = config.sops.placeholder.obsidian_luna_passphrase;
|
||||
# The plugin derives path obfuscation from the same passphrase it
|
||||
# uses for content, so this is the same secret. Split into its own
|
||||
# field because the bridge takes them separately — if paths come
|
||||
# back as garbage while contents decode fine, this is the field that
|
||||
# is wrong.
|
||||
obfuscatePassphrase = config.sops.placeholder.obsidian_luna_passphrase;
|
||||
# Reads the chunking tweaks the plugin stored in the remote, instead
|
||||
# of guessing sizes that then disagree with every other client.
|
||||
useRemoteTweaks = true;
|
||||
baseDir = "";
|
||||
}
|
||||
{
|
||||
type = "storage";
|
||||
name = "luna-vault";
|
||||
group = peerGroup;
|
||||
baseDir = vaultDir;
|
||||
# Catch up on anything that changed while the service was down.
|
||||
scanOfflineChanges = true;
|
||||
useChokidar = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.livesync-bridge = {
|
||||
description = "Obsidian LiveSync bridge (CouchDB <-> ${vaultDir})";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network-online.target" "tailscaled.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
|
||||
environment = {
|
||||
# Persistent module + npm cache. Without a fixed DENO_DIR the service
|
||||
# re-downloads its whole dependency tree on every start.
|
||||
DENO_DIR = "${stateDir}/deno";
|
||||
# main.ts reads this instead of ./dat/config.json, which keeps the
|
||||
# secret out of the copied source tree entirely.
|
||||
LSB_CONFIG = config.sops.templates."livesync-bridge.json".path;
|
||||
LSB_HEALTH_FILE = "${stateDir}/health.json";
|
||||
HOME = stateDir;
|
||||
};
|
||||
|
||||
# Copy the pinned source out of the store and install its locked deps.
|
||||
# It cannot run from /nix/store directly: deno.jsonc sets
|
||||
# `nodeModulesDir: manual` with byonm, so `deno install` must write a
|
||||
# node_modules/ next to the sources.
|
||||
#
|
||||
# The copy target is a FIXED path on purpose. Deno keys localStorage —
|
||||
# which is where the bridge records per-file sync state (Peer.ts:119) — by
|
||||
# the main module's origin, and stores it under
|
||||
# DENO_DIR/location_data/<sha of that origin>. VERIFIED by running the same
|
||||
# source from two paths against one DENO_DIR: two separate origin dirs
|
||||
# appear. Running straight from /nix/store would therefore change the
|
||||
# origin on every input bump and silently reset the bridge to a full
|
||||
# rescan of both peers.
|
||||
#
|
||||
# Guarded by a stamp file so this is a no-op on ordinary restarts; only a
|
||||
# flake input bump pays for the re-install (which needs network).
|
||||
preStart = ''
|
||||
set -eu
|
||||
stamp=${stateDir}/.src
|
||||
if [ "$(cat "$stamp" 2>/dev/null || true)" != "${inputs.livesync-bridge}" ]; then
|
||||
# Contents only — appDir is this unit's WorkingDirectory, and
|
||||
# deleting the cwd out from under deno breaks the install below.
|
||||
find ${appDir} -mindepth 1 -delete
|
||||
cp -r ${inputs.livesync-bridge}/. ${appDir}/
|
||||
chmod -R u+w ${appDir}
|
||||
${pkgs.deno}/bin/deno install --frozen
|
||||
printf '%s' "${inputs.livesync-bridge}" > "$stamp"
|
||||
fi
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
User = "hermes";
|
||||
Group = "hermes";
|
||||
StateDirectory = "livesync-bridge";
|
||||
WorkingDirectory = appDir;
|
||||
# `deno task run` is `deno run -A main.ts`; invoked directly so the
|
||||
# task runner is not in the supervision path.
|
||||
ExecStart = "${pkgs.deno}/bin/deno run -A main.ts";
|
||||
# main.ts installs an unhandledrejection guard, but a genuinely dead
|
||||
# process should still come back rather than trip the start limit.
|
||||
Restart = "always";
|
||||
RestartSec = 30;
|
||||
# Group-writable output, so the two identities stay interchangeable if
|
||||
# the uid sharing above is ever unpicked.
|
||||
UMask = "0007";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -65,36 +65,4 @@
|
||||
# so they're visible inside the container at /opt/data/....
|
||||
# restartUnits re-provisions both on rotation, without a full mars deploy.
|
||||
sops.secrets.gitea_luna_token.restartUnits = [ "hermes-agent-prepare-dirs.service" ];
|
||||
|
||||
# livesync-bridge (livesync-bridge.nix) — luna's Obsidian vault, mirrored
|
||||
# out of CouchDB on jupiter. Both values are consumed by the rendered
|
||||
# config.json rather than read directly, so the sops default of root:root
|
||||
# 0400 is correct here; only the TEMPLATE needs an owner (set where it is
|
||||
# defined, next to the vault path it references).
|
||||
#
|
||||
# couchdb_luna_password holds jupiter's `obsidian` ADMIN password — the same
|
||||
# value as secrets/jupiter.yaml's couchdb_admin_password — and
|
||||
# obsidian_luna_passphrase is the same passphrase as the personal vault.
|
||||
# That is a deliberate choice to reuse what already existed, but it is worth
|
||||
# being clear about what it costs: mars can decrypt and read EVERY vault
|
||||
# database, not just luna's, and mars is the box running an autonomous
|
||||
# agent. The two are independent to fix, cheapest first:
|
||||
#
|
||||
# 1. A vault-specific passphrase (re-encrypts luna's remote database, but
|
||||
# leaves the personal vault's contents unreadable from here).
|
||||
# 2. A CouchDB account scoped to luna's database via _security (three curl
|
||||
# calls, in README -> "Obsidian vaults"), which also stops mars from
|
||||
# reaching the other databases at all.
|
||||
#
|
||||
# Neither is required for the bridge to work; both shrink the blast radius
|
||||
# if mars is ever compromised.
|
||||
sops.secrets.couchdb_luna_password = { };
|
||||
|
||||
# The E2EE passphrase for luna's vault, as entered in the Obsidian plugin.
|
||||
# Vault passphrases otherwise never leave the clients (see the note in
|
||||
# services/dev/obsidian-livesync.nix) — this one has to be here because mars
|
||||
# IS a client: it decrypts in order to write real markdown to disk. Path
|
||||
# obfuscation uses the same passphrase in the plugin, so the bridge's
|
||||
# separate obfuscatePassphrase field is fed from this one value.
|
||||
sops.secrets.obsidian_luna_passphrase = { };
|
||||
}
|
||||
|
||||
@@ -111,62 +111,6 @@
|
||||
reverse_proxy http://jupiter.orbit.sol:2283
|
||||
'';
|
||||
|
||||
# ---- Obsidian LiveSync (CouchDB on jupiter) ----
|
||||
# Obsidian's mobile apps refuse cleartext HTTP and *.jupiter.sol cannot hold
|
||||
# a publicly trusted cert, so the vault database is published here instead of
|
||||
# staying on the LAN. That means a credentialed database on the open
|
||||
# internet; two things keep it sane:
|
||||
#
|
||||
# 1. The plugin's end-to-end encryption, switched on BEFORE the first sync.
|
||||
# jupiter then stores only ciphertext, so a breach here is not a leak of
|
||||
# the notes themselves.
|
||||
# 2. This allowlist. CouchDB serves far more than the replication API —
|
||||
# Fauxton (/_utils), /_all_dbs, and /_node/_local/_config, the last of
|
||||
# which REWRITES the server's config given admin credentials. Only the
|
||||
# paths the plugin actually speaks are proxied; everything else is
|
||||
# answered here and never reaches jupiter. Use the tailnet for the rest:
|
||||
# `curl http://jupiter.orbit.sol:5984/_utils/`.
|
||||
#
|
||||
# ONE DATABASE PER VAULT, and the matcher keys off CouchDB's own naming rule
|
||||
# rather than listing them: every system endpoint begins with `_`, and a
|
||||
# user-creatable database never can (CouchDB requires a lowercase letter
|
||||
# first). So adding a vault needs no edit here. `_session` is the single
|
||||
# underscore path let through, for cookie auth.
|
||||
#
|
||||
# The flip side of not listing them: a mistyped but otherwise LEGAL database
|
||||
# name is proxied through and reaches CouchDB, which answers a real 404 the
|
||||
# plugin can report. An ILLEGAL one — anything starting with a capital or an
|
||||
# underscore — fails the matcher instead and gets caddy's 404, which carries
|
||||
# no CORS headers and surfaces in Obsidian as a connection failure with no
|
||||
# error message at all. If a new vault refuses to connect and the plugin
|
||||
# says nothing, check the database name is lowercase first.
|
||||
#
|
||||
# Never point two vaults at one database: LiveSync merges them into a single
|
||||
# file tree, which is not cleanly reversible.
|
||||
#
|
||||
# Known consequence: LiveSync's "Check database configuration" panel reads
|
||||
# /_node/_local/_config and so reports the server as unconfigured from
|
||||
# outside. Expected — that config is declarative in
|
||||
# services/dev/obsidian-livesync.nix and is not the plugin's to patch.
|
||||
#
|
||||
# `flush_interval -1` is required, not tuning: replication rides a
|
||||
# continuous _changes feed, which caddy would otherwise buffer — sync then
|
||||
# stalls until the buffer fills (same reason vpn.mgaction.town sets it).
|
||||
#
|
||||
# No netcup edge-firewall change: this rides the 443 the other vhosts
|
||||
# already use, unlike gitea's :2222.
|
||||
services.caddy.virtualHosts."notes.mgaction.town".extraConfig = ''
|
||||
@livesync path_regexp ^/(_session|[a-z][a-z0-9_$()+-]*)?(/.*)?$
|
||||
handle @livesync {
|
||||
reverse_proxy http://jupiter.orbit.sol:5984 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
handle {
|
||||
respond 404
|
||||
}
|
||||
'';
|
||||
|
||||
# ---- Hermes dashboard ----
|
||||
# Authentik-gated (hosts/mars/hermes-agent.nix has the OIDC config and the
|
||||
# "create the Authentik app" instructions — moved here from jupiter).
|
||||
|
||||
@@ -54,7 +54,6 @@ in
|
||||
pkgs.hyprcursor
|
||||
pkgs.bibata-cursors
|
||||
pkgs.papirus-icon-theme
|
||||
tome
|
||||
];
|
||||
|
||||
xdg.desktopEntries.btop = {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -15,7 +15,6 @@ sabnzbd_nzb_key: ENC[AES256_GCM,data:DNVenqhJ7wf5Ng0XRA1gJN95e+90e6D9NImOSHJv/Us
|
||||
sabnzbd_eweka_username: ENC[AES256_GCM,data:eLsTZoM8T8fAlGaXWlDaoQ==,iv:eawyGhN7+d6UfBIbI3y1qgq+MYBGrXP6VfAkSOK6llA=,tag:ELOfQGHU5NOxZFhKOKf8LA==,type:str]
|
||||
sabnzbd_eweka_password: ENC[AES256_GCM,data:Mt3ZHAe2wzacCQq3x9Uy8WxjrVNad1SmU6sl8ZgrkMLymfq2eP4JzO/uPdD33A==,iv:PnFT95Zxqz4QBpPF5PRloKpoa15AU7Ef/Owwy+iDotw=,tag:/uRX00RzHLJN3gws5Qz8SA==,type:str]
|
||||
gitea_hermes_webhook_secret: ENC[AES256_GCM,data:Q8e+mj05MJI7CEJwRonpOmQphAZ0CfnZFoGxrDSSiyHoH3BNhqBU5gBmzuu+6NK9OS33kN+JnFvrwCeEzVxooA==,iv:mdsKOMD5B0Jzh1YRmRh71P8Io9RFtI6aqAky5x+WxOQ=,tag:v3LKz5a8ayl7WAzIPbwj6Q==,type:str]
|
||||
couchdb_admin_password: ENC[AES256_GCM,data:QHkCFUwLbQdd5yKETI5qAz4CkEfsPcl2iCU8F9mX3PA=,iv:2dPNKjjoXEgm7wfC6MlhQTMvAXSNDtaXnjWl2ldl4fk=,tag:1ZltqH94Q5/6GbXAnaIgdg==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
@@ -36,7 +35,7 @@ sops:
|
||||
CzjSDQZTcseEXZNwuzZcfB5Mvq0BQvjOj7lGuxzuE4qwWkdJWGfVLQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
|
||||
lastmodified: "2026-08-25T20:39:37Z"
|
||||
mac: ENC[AES256_GCM,data:Z59BCw8gETfddXqul4LXrq6V3LBJA1itF7A1VNUERwK4NfaUGwWUhbl9h7YF/srzgtg9yGjbFB/f5kwmT3k/TWTG+C0M/4KOyTVs4y5UvB9gI4g8awYbtnFDPRAcqqcxoMD0sgapgVcNh48KWv76ndF6UGn+QfWn9eF4KBP+ZzQ=,iv:57myu1aTMMSLTz+1ldwxdusnzh8cyPwrLiEIx3rLS9w=,tag:isthpdOydD4ZNoFZyflViw==,type:str]
|
||||
lastmodified: "2026-08-23T03:16:52Z"
|
||||
mac: ENC[AES256_GCM,data:uQcOxORIWugK43LpQLI7JEjH6oGooseKCQQt0d+n43i7o23JGdUN5Wy/iD7GqmtVVZod02gl1ohEXV+kpvgetFpAO5NZu76HUVPFgaLOx+2LjrR1pNpC+52Iqlx52uypwby9eDvnC01jLFHu2l13NGBrLM3JQGmEXF57phzM/Q4=,iv:H7o3gdx/1GmZ1FRm7z97TNmiVpm6YFCEk0Puw4ZETDs=,tag:bsz3bcK2z3szrwpo55bSzQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
|
||||
+2
-4
@@ -6,8 +6,6 @@ telegram_bot_token: ENC[AES256_GCM,data:WX+KFtoqFodkoWNwd7EXUrUJakZ9oaMZgg4OnCeL
|
||||
hermes_dashboard_oidc_client_secret: ENC[AES256_GCM,data:IMPNTPMKO+b7eyV4hyGfnvH1/i+W4IPDNjncoyB1oIV8WaB6nOJn0sSEuTUCKB94K+Y7bsVQU0zpbKdIYOdGqgmPzwMCsScxMt4SewTmiiqWxv6SQFf4EzMxgXqjMvH8PWDzLcI2C2tI/KcVS251iqRViOTFe1/tkm+mV8sJmEI=,iv:F/rOUDmJZoGPS9fObAni5ntyOqbbhMWDPdHGLTexwlA=,tag:ALf98DmB0JziGspZMiLCiw==,type:str]
|
||||
gitea_luna_token: ENC[AES256_GCM,data:0ypW9oVFs1mXYPhPareMFRdkSYcvHSCm+fQOd7/76lJEXi217r9dmg==,iv:j3TPm/iLk6pB6CmDePFBOlnhxWSbmLKvOhz06SM1T7k=,tag:ydErvC2mZ1RRnwNffiHkkg==,type:str]
|
||||
gitea_hermes_webhook_secret: ENC[AES256_GCM,data:lV78H0xAehPxusSO/QruOYkt7fkMJrW+ScZL4UWYvgnBGn/D+1XHYPyHCqe2sEEWSlIaAgWMMoZzoVJ1Z1NFVQ==,iv:GmTZxoH2iiL/vTVgPfziXIFYD+Rl3cbh9hqXvWps+iw=,tag:jtXEUOVFfKrpTRK7S9PZMA==,type:str]
|
||||
couchdb_luna_password: ENC[AES256_GCM,data:V91is2h7UskI1rtwMzQyduNXoDPTYNwa4sw9K9WU+wE=,iv:k976ImKR19+CvGOVsHsrqMaFFtSQxVi6zABCJuQ5AWE=,tag:W2SN4SET/+o4+Wt6BOJ1LA==,type:str]
|
||||
obsidian_luna_passphrase: ENC[AES256_GCM,data:fqHtP3g4J40ddYL9lzCixrisdC/DEJJermE=,iv:FU6BGNcBnyP8Rz3dNBk0+K0aAQTDW6/0aVaFm1rBFkk=,tag:Mg1aKqh++2rmU8ROaVIgDw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
@@ -28,7 +26,7 @@ sops:
|
||||
oyJ7PS3lW+PxH5AZkeeU7gXO/pz2oDku0aDOds7kaD3n0+qSWicQ+Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1eapjg6tdrr0fuvmgs3q3nlvnjkaxez298qynqqqxt0lpcv0lrsyq7ayxjk
|
||||
lastmodified: "2026-08-25T21:51:24Z"
|
||||
mac: ENC[AES256_GCM,data:dz249hf3w8Tn0JStFOhhpdCZFMx2yxmNABx1CbeIQ/JlICAU82e4fg8AzJQY9EMOEs3Zx6L61yieljD4A/HLip5rDVlAXqqLeklW60eb7BHuSO79YfFgot+rS05g8WqFkRJYWzmkXhMbErCI133n72XEdeqMUU/m0djOUZlVRLs=,iv:d7G6TJRLfmXvQ2BUG9Hi83lOB9anhmb9qneLeVSCBhc=,tag:F6X/0z00PjiYum5kf8YApA==,type:str]
|
||||
lastmodified: "2026-08-23T05:55:49Z"
|
||||
mac: ENC[AES256_GCM,data:a3vCmrQMCS25tNWrzTeiGmOHf4Fn356PO3uNa2HvS21EBCKTc6YWBj9KmpORdz+6t03JJe/4eiGdghGaLhRr+JXyQnaT54gSV+FhC3dH6blind746XN3h+Z9rxiva6apvcAGUZ9k01Js5IXN9efEMhcI6w0U4oVuVqtvShvg8A8=,iv:9kF3cJ1vyy2H3eH10DVCYmWeXv2MH4AFDiF8cOajlw4=,tag:zhombLVpL8M1TUtYur/gYQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
{ config, ... }:
|
||||
|
||||
# CouchDB, tuned as the backend for Obsidian Self-hosted LiveSync
|
||||
# (vrtmrz/obsidian-livesync). The plugin replicates the vault into CouchDB
|
||||
# chunk-by-chunk over PouchDB's replication protocol, so this is a plain
|
||||
# CouchDB 3 node — nothing Obsidian-specific runs here.
|
||||
#
|
||||
# Published PUBLICLY as https://notes.mgaction.town via neptun's caddy (see
|
||||
# hosts/neptun/configuration.nix), because Obsidian's mobile apps refuse
|
||||
# cleartext HTTP and jupiter's *.jupiter.sol names cannot get a real cert.
|
||||
# That makes the settings below security-relevant, not cosmetic:
|
||||
#
|
||||
# - `require_valid_user` in BOTH [chttpd] and [chttpd_auth]: without it
|
||||
# CouchDB answers unauthenticated GETs on the open internet.
|
||||
# - neptun's vhost allowlists only the endpoints the plugin uses, so Fauxton
|
||||
# (/_utils) and the cluster/config endpoints are not reachable from
|
||||
# outside at all — reach them over the tailnet instead.
|
||||
# - Turn ON end-to-end encryption in the plugin (Settings → Remote Database
|
||||
# → End-to-End Encryption, plus "Obfuscate Properties", which covers the
|
||||
# paths and timestamps that E2EE alone leaves readable). Then this server
|
||||
# only ever holds ciphertext, which is what makes a publicly-reachable
|
||||
# credentialed database an acceptable trade rather than a bad one.
|
||||
#
|
||||
# Its passphrase is a SEPARATE secret from couchdb_admin_password below —
|
||||
# deliberately, and it must stay that way. The couchdb password
|
||||
# authenticates to this server and is stored here (hashed) and in
|
||||
# secrets/jupiter.yaml; the E2EE passphrase never leaves the Obsidian
|
||||
# clients and CouchDB has no idea it exists. Reusing one string for both
|
||||
# hands whoever obtains that credential the decryption key as well, which
|
||||
# is precisely the failure E2EE is here to prevent. The passphrase is
|
||||
# therefore NOT in sops (nothing on this host consumes it) — it lives in
|
||||
# the HomeLab Proton Pass vault, with the deploy credentials.
|
||||
#
|
||||
# Losing it costs the remote database, not the notes: wipe it and
|
||||
# re-initialize from a device that still holds the plaintext vault.
|
||||
{
|
||||
services.couchdb = {
|
||||
enable = true;
|
||||
|
||||
# Listens on all interfaces, same reasoning as immich: :5984 is NOT opened
|
||||
# in the firewall, so it is reachable over tailscale0 (trusted in
|
||||
# common.nix) and localhost only. That is the path neptun's caddy takes.
|
||||
bindAddress = "0.0.0.0";
|
||||
port = 5984;
|
||||
|
||||
# The vault database is the ONLY copy of the notes once LiveSync is the
|
||||
# source of truth, so it belongs on the array, not the 29G eMMC. All three
|
||||
# of these default under /var/lib/couchdb and have to move together —
|
||||
# configFile especially, since CouchDB writes to it at runtime (below).
|
||||
databaseDir = "/mnt/data/AppData/couchdb";
|
||||
viewIndexDir = "/mnt/data/AppData/couchdb";
|
||||
configFile = "/mnt/data/AppData/couchdb/local.ini";
|
||||
|
||||
# The admin password, as an [admins] ini fragment from sops.
|
||||
# services.couchdb.adminPass would render it into the world-readable
|
||||
# store; extraConfigFiles is the module's own documented hook for this
|
||||
# (hosts/jupiter/secrets.nix renders the template).
|
||||
#
|
||||
# ⚠️ CouchDB hashes a plaintext admin password at startup and persists the
|
||||
# hash to the LAST, writable file in its ini chain — local.ini above,
|
||||
# which then takes precedence over this fragment. So changing the sops
|
||||
# value alone does NOT rotate the password: delete the `[admins]` line
|
||||
# from /mnt/data/AppData/couchdb/local.ini and restart as well.
|
||||
extraConfigFiles = [ config.sops.templates."couchdb-admins.ini".path ];
|
||||
|
||||
# Values taken from LiveSync's own CouchDB setup documentation; the plugin
|
||||
# refuses to replicate (or silently truncates) without them.
|
||||
extraConfig = {
|
||||
couchdb = {
|
||||
# Creates _users/_replicator on first boot instead of leaving the node
|
||||
# in the un-set-up state where every request 500s.
|
||||
single_node = "true";
|
||||
# LiveSync splits notes into chunks, but a big pasted image still
|
||||
# arrives as one document. 8MB (the default) is too small.
|
||||
max_document_size = "50000000";
|
||||
};
|
||||
|
||||
chttpd = {
|
||||
require_valid_user = "true";
|
||||
max_http_request_size = "4294967296";
|
||||
enable_cors = "true";
|
||||
};
|
||||
|
||||
chttpd_auth = {
|
||||
require_valid_user = "true";
|
||||
authentication_redirect = "/_utils/session.html";
|
||||
};
|
||||
|
||||
httpd = {
|
||||
# Makes CouchDB answer 401 with a WWW-Authenticate challenge rather
|
||||
# than a bare 401 body — the plugin's basic-auth flow depends on it.
|
||||
"WWW-Authenticate" = ''Basic realm="couchdb"'';
|
||||
enable_cors = "true";
|
||||
};
|
||||
|
||||
# Obsidian is an Electron/Capacitor app, so its requests carry these
|
||||
# non-http origins. Without them desktop and mobile both fail CORS
|
||||
# preflight and the plugin reports a bare "cannot connect".
|
||||
cors = {
|
||||
credentials = "true";
|
||||
origins = "app://obsidian.md,capacitor://localhost,http://localhost";
|
||||
headers = "accept, authorization, content-type, origin, referer";
|
||||
methods = "GET, PUT, POST, HEAD, DELETE";
|
||||
max_age = "3600";
|
||||
};
|
||||
|
||||
# The module points [log] file at /var/log/couchdb.log, which nothing
|
||||
# rotates — on a 29G eMMC an info-level log of every replication request
|
||||
# is a slow disk-fill. stderr hands it to journald's capped storage
|
||||
# instead (the file setting is then ignored).
|
||||
log = {
|
||||
writer = "stderr";
|
||||
level = "warning";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# /mnt/data/AppData is drwx--x--- darman:users, so the couchdb user needs
|
||||
# group "users" just to traverse into its own database dir. The dir itself
|
||||
# is created couchdb:couchdb by the module's tmpfiles rule.
|
||||
users.users.couchdb.extraGroups = [ "users" ];
|
||||
|
||||
# databaseDir is outside /var/lib, so systemd derives no mount dependency
|
||||
# from it. Without this CouchDB starts with the array missing, creates an
|
||||
# empty database on the eMMC, and LiveSync sees a remote vault that lost
|
||||
# every note — which it would then happily replicate back to the clients.
|
||||
systemd.services.couchdb.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM nixos/nix:2.30.2
|
||||
|
||||
ARG NIXPKGS_REV=e5bdc4a41d4c072fe1e3787eaa0320a384741d44
|
||||
ARG QSMCP_REV=76b1b008f0352064121af9334065012e31d80fc2
|
||||
|
||||
ENV NIX_CONFIG="experimental-features = nix-command flakes" \
|
||||
PATH="/nix/var/nix/profiles/default/bin:${PATH}"
|
||||
|
||||
RUN nix profile install --profile /nix/var/nix/profiles/default \
|
||||
"github:NixOS/nixpkgs/${NIXPKGS_REV}#quickshell" \
|
||||
"github:NixOS/nixpkgs/${NIXPKGS_REV}#nodejs_22"
|
||||
|
||||
RUN mkdir -p /opt/qsmcp \
|
||||
&& git init /opt/qsmcp \
|
||||
&& git -C /opt/qsmcp remote add origin https://github.com/fedsfarm/qsmcp.git \
|
||||
&& git -C /opt/qsmcp fetch --depth 1 origin "${QSMCP_REV}" \
|
||||
&& git -C /opt/qsmcp checkout --detach FETCH_HEAD \
|
||||
&& test "$(git -C /opt/qsmcp rev-parse HEAD)" = "${QSMCP_REV}" \
|
||||
&& cd /opt/qsmcp \
|
||||
&& npm ci --omit=dev \
|
||||
&& rm -rf /opt/qsmcp/.git
|
||||
|
||||
# qsmcp's CLI hard-codes a 1568px safety cap. Desktop bar previews need their
|
||||
# true target width so responsive layout and text collisions are testable.
|
||||
RUN node -e 'const fs=require("fs"); const p="/opt/qsmcp/src/index.ts"; const s=fs.readFileSync(p,"utf8"); const old="max_dimension: 1568"; if (!s.includes(old)) throw new Error("qsmcp max_dimension source changed"); fs.writeFileSync(p,s.replace(old,"max_dimension: 4096"))'
|
||||
|
||||
ENV QSMCP_SHELL_ROOT=/workspace/quickshell \
|
||||
QSMCP_QS_BIN=/nix/var/nix/profiles/default/bin/qs \
|
||||
QT_QUICK_BACKEND=software
|
||||
|
||||
COPY entrypoint.sh /usr/local/bin/quickshell-preview
|
||||
RUN chmod +x /usr/local/bin/quickshell-preview
|
||||
|
||||
RUN mkdir -p /workspace/quickshell
|
||||
WORKDIR /workspace/quickshell
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/quickshell-preview"]
|
||||
@@ -0,0 +1,39 @@
|
||||
# Headless Quickshell previews
|
||||
|
||||
This directory provides a reproducible Docker wrapper around
|
||||
[qsmcp](https://github.com/fedsfarm/qsmcp). It renders a Quickshell `Item` to a
|
||||
PNG with Qt's `grabToImage()` without connecting to the host Wayland session.
|
||||
|
||||
Both nixpkgs and qsmcp are pinned in `Dockerfile`. The runner uses
|
||||
`docker create` plus `docker cp` rather than bind mounts, so it also works with
|
||||
rootless/remote daemons whose mount namespace cannot see the checkout.
|
||||
|
||||
## Smoke test
|
||||
|
||||
Start Docker, then run from the repository root:
|
||||
|
||||
```sh
|
||||
./tools/quickshell-preview/render.sh
|
||||
```
|
||||
|
||||
The default output is:
|
||||
|
||||
```text
|
||||
/tmp/homelab-quickshell-preview/smoke.png
|
||||
```
|
||||
|
||||
## Render another Item-rooted component
|
||||
|
||||
```sh
|
||||
./tools/quickshell-preview/render.sh \
|
||||
tests/DenseBarHeadless.qml \
|
||||
.artifacts/quickshell-preview/dense-bar-1920.png \
|
||||
1920 164
|
||||
```
|
||||
|
||||
The component path is relative to `dotfiles/quickshell`. Keep production
|
||||
`PanelWindow` wrappers thin and put their visual content in an `Item` component
|
||||
so it can be rendered through this offscreen path.
|
||||
|
||||
`PanelWindow`-rooted previews require a nested compositor and are intentionally
|
||||
outside this first smoke-test image.
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p /tmp/qsmcp-runtime /tmp/qsmcp-home /output
|
||||
chmod 700 /tmp/qsmcp-runtime
|
||||
|
||||
export HOME=/tmp/qsmcp-home
|
||||
export XDG_RUNTIME_DIR=/tmp/qsmcp-runtime
|
||||
|
||||
exec /nix/var/nix/profiles/default/bin/node \
|
||||
/opt/qsmcp/src/index.ts render "$@"
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)"
|
||||
tool_dir="$repo_root/tools/quickshell-preview"
|
||||
shell_root="$repo_root/dotfiles/quickshell"
|
||||
image="homelab-quickshell-preview:qsmcp-76b1b008"
|
||||
|
||||
component="${1:-tests/HeadlessSmoke.qml}"
|
||||
output="${2:-${TMPDIR:-/tmp}/homelab-quickshell-preview/smoke.png}"
|
||||
width="${3:-720}"
|
||||
height="${4:-120}"
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "error: Docker-compatible daemon is unavailable" >&2
|
||||
exit 69
|
||||
fi
|
||||
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output="$PWD/$output" ;;
|
||||
esac
|
||||
|
||||
output_dir="$(dirname "$output")"
|
||||
output_name="$(basename "$output")"
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
container_id=""
|
||||
cleanup() {
|
||||
if [[ -n "$container_id" ]]; then
|
||||
docker rm --force "$container_id" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
docker build --tag "$image" --file "$tool_dir/Dockerfile" "$tool_dir"
|
||||
|
||||
container_id="$(docker create \
|
||||
--network none \
|
||||
--tmpfs /tmp:rw,noexec,nosuid,size=256m \
|
||||
"$image" \
|
||||
"$component" \
|
||||
--width "$width" \
|
||||
--height "$height" \
|
||||
--dpr 1 \
|
||||
--padding 0 \
|
||||
--background solid \
|
||||
--bg '#0a0a0a' \
|
||||
--out "/output/$output_name")"
|
||||
|
||||
# docker cp streams files through the API, so this works with rootless and
|
||||
# remote daemons whose filesystem cannot see the client's repository path.
|
||||
docker cp "$shell_root/." "$container_id:/workspace/quickshell"
|
||||
docker start --attach "$container_id"
|
||||
docker cp "$container_id:/output/$output_name" "$output"
|
||||
|
||||
test -s "$output"
|
||||
printf 'rendered=%s\n' "$output"
|
||||
Reference in New Issue
Block a user