Compare commits
3
Commits
b16cc93ff6
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45e08e35a2 | ||
|
|
a8a1cffa3e | ||
|
|
15ae1cf608 |
@@ -14,6 +14,3 @@ keys.txt
|
||||
|
||||
# local env (PATH etc.)
|
||||
.env
|
||||
|
||||
# local visual-verification output
|
||||
.artifacts/
|
||||
|
||||
@@ -427,6 +427,93 @@ 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
|
||||
|
||||
@@ -33,13 +33,8 @@ Quickshell hot-reloads QML on file save when already running, so for most edits
|
||||
- `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**: 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.
|
||||
**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.
|
||||
|
||||
**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**: 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.
|
||||
**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.
|
||||
|
||||
@@ -10,9 +10,6 @@ import qs.widgets.systray
|
||||
import qs.widgets.vitals
|
||||
|
||||
Scope {
|
||||
// Dense multi-monitor status rail; visual core is headlessly renderable.
|
||||
DenseBar {}
|
||||
|
||||
// Left sidebar in the Slant (V6) style — toggle with SUPER CTRL S.
|
||||
SideBar {}
|
||||
BarBottom {}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import qs.widgets.launcher
|
||||
import qs.widgets.theme
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
@@ -43,7 +42,7 @@ Scope {
|
||||
implicitHeight: 12
|
||||
|
||||
// Brightens when the Dock launcher (variant 5) opens.
|
||||
color: LauncherState.dockOpen ? Theme.accentSoft : Theme.accent
|
||||
color: LauncherState.dockOpen ? "#FFF3C0" : "#FFD063"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
@@ -54,7 +53,7 @@ Scope {
|
||||
// Gentle "listening" pulse while the dock is open.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.highlight
|
||||
color: "#FFFFFF"
|
||||
opacity: 0
|
||||
visible: LauncherState.dockOpen
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
import qs.widgets.launcher
|
||||
import qs.widgets.theme
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
@@ -43,7 +42,7 @@ Scope {
|
||||
implicitHeight: 6
|
||||
|
||||
// Brightens when the Console launcher (variant 4) opens.
|
||||
color: LauncherState.consoleOpen ? Theme.accentSoft : Theme.accent
|
||||
color: LauncherState.consoleOpen ? "#FFF3C0" : "#FFD063"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
@@ -54,7 +53,7 @@ Scope {
|
||||
// Gentle "listening" pulse while the console is open.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.highlight
|
||||
color: "#FFFFFF"
|
||||
opacity: 0
|
||||
visible: LauncherState.consoleOpen
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,690 +0,0 @@
|
||||
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 radarWidth: root.compact ? 130 : 164
|
||||
readonly property real stateWidth: root.compact ? 210 : 250
|
||||
readonly property real flexWidth: Math.max(250, (width - identityWidth - radarWidth - stateWidth - 32) / 2)
|
||||
|
||||
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.radarWidth
|
||||
height: array.height
|
||||
panelId: "03"
|
||||
title: "SCAN"
|
||||
|
||||
RadarGauge {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 28
|
||||
size: 57
|
||||
level: root.cpuFraction
|
||||
}
|
||||
}
|
||||
|
||||
StatusBarPanel {
|
||||
width: array.flexWidth
|
||||
height: array.height
|
||||
panelId: "04"
|
||||
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: "05"
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
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,11 +1,10 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
Shape {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
strokeWidth: 0
|
||||
|
||||
startX: 0
|
||||
|
||||
@@ -5,7 +5,6 @@ import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
|
||||
import qs.widgets.decoration
|
||||
import qs.widgets.theme
|
||||
|
||||
WrapperItem {
|
||||
RowLayout {
|
||||
@@ -25,7 +24,7 @@ WrapperItem {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.surface
|
||||
fillColor: "#0F1012"
|
||||
|
||||
startX: 8
|
||||
startY: 0
|
||||
@@ -53,7 +52,7 @@ WrapperItem {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.surface
|
||||
fillColor: "#0F1012"
|
||||
|
||||
startX: 16
|
||||
startY: 0
|
||||
@@ -84,7 +83,7 @@ WrapperItem {
|
||||
startY: 0
|
||||
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.surface
|
||||
fillColor: "#0F1012"
|
||||
|
||||
PathLine {
|
||||
x: shape.width
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -100,7 +99,7 @@ Scope {
|
||||
anchors.right: parent.right
|
||||
|
||||
clip: true
|
||||
color: Theme.surface
|
||||
color: "#0F1012"
|
||||
|
||||
height: root.active ? win.openHeight : 0
|
||||
|
||||
@@ -117,7 +116,7 @@ Scope {
|
||||
|
||||
// Accent lid — mirrors the top bar strip, reads as its extension.
|
||||
Rectangle {
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
implicitHeight: 4
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -133,8 +132,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: ">_"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 22
|
||||
font.bold: true
|
||||
}
|
||||
@@ -148,7 +147,7 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: Theme.text
|
||||
color: "#EEEEEE"
|
||||
font.pointSize: 16
|
||||
clip: true
|
||||
|
||||
@@ -183,7 +182,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "launch application…"
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -191,14 +190,14 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length + " apps"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 15
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Theme.raised
|
||||
color: "#292C30"
|
||||
implicitHeight: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -237,9 +236,9 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: card.selected ? Theme.raised : "transparent"
|
||||
color: card.selected ? "#292C30" : "transparent"
|
||||
border.width: 1
|
||||
border.color: card.selected ? Theme.accent : Theme.raised
|
||||
border.color: card.selected ? "#FFD063" : "#292C30"
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
@@ -262,7 +261,7 @@ Scope {
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: card.modelData.name
|
||||
color: card.selected ? Theme.accent : Theme.text
|
||||
color: card.selected ? "#FFD063" : "#EEEEEE"
|
||||
font.pointSize: 9
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
@@ -278,8 +277,8 @@ Scope {
|
||||
anchors.centerIn: parent
|
||||
visible: model.apps.length === 0
|
||||
text: "no matches"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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.
|
||||
@@ -78,7 +77,7 @@ Scope {
|
||||
// Dim backdrop — click to dismiss.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.surface
|
||||
color: "#0A0A0C"
|
||||
opacity: 0.55
|
||||
|
||||
MouseArea {
|
||||
@@ -108,8 +107,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.accent
|
||||
fillColor: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
strokeWidth: 2
|
||||
|
||||
startX: frame.chamfer
|
||||
@@ -154,7 +153,7 @@ Scope {
|
||||
// run out to the top and left edges to meet the straight borders.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: 0
|
||||
startY: frame.chamfer
|
||||
@@ -180,7 +179,7 @@ Scope {
|
||||
// bottom and right edges.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height - frame.chamfer
|
||||
@@ -206,7 +205,7 @@ Scope {
|
||||
// detached from the panel by the width of the cut.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height
|
||||
@@ -229,7 +228,7 @@ Scope {
|
||||
// slanted end pieces on both arms.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
// inner, right end of the bottom arm
|
||||
startX: panelShape.width / 3
|
||||
@@ -278,7 +277,7 @@ Scope {
|
||||
// slanted end pieces on both arms.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
// inner, bottom of the right arm
|
||||
startX: panelShape.width
|
||||
@@ -346,7 +345,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -368,7 +367,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 15
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -390,7 +389,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 24
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -421,8 +420,8 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 20
|
||||
font.letterSpacing: 1
|
||||
clip: true
|
||||
@@ -458,7 +457,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "run"
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -466,8 +465,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 18
|
||||
}
|
||||
}
|
||||
@@ -490,7 +489,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: divider.slant
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -513,7 +512,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 0
|
||||
startY: divider.height
|
||||
PathLine {
|
||||
@@ -536,7 +535,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: divider.width
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -598,7 +597,7 @@ Scope {
|
||||
// Body
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.raised
|
||||
fillColor: "#22262C"
|
||||
startX: appRow.shear
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -621,7 +620,7 @@ Scope {
|
||||
// Left accent edge
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: appRow.shear
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -656,7 +655,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.name
|
||||
color: appRow.selected ? Theme.accent : Theme.text
|
||||
color: appRow.selected ? "#FFD063" : "#EEEEEE"
|
||||
font.pointSize: 12
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -664,7 +663,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.genericName || ""
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font.pointSize: 10
|
||||
elide: Text.ElideRight
|
||||
Layout.maximumWidth: 220
|
||||
@@ -695,8 +694,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.accent
|
||||
strokeColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
strokeColor: "#FFD063"
|
||||
strokeWidth: 2
|
||||
|
||||
startX: 0
|
||||
@@ -733,9 +732,9 @@ Scope {
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "POWER"
|
||||
font.family: Theme.readoutFont
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 11
|
||||
color: Theme.surface
|
||||
color: "#0F1012"
|
||||
}
|
||||
|
||||
Repeater {
|
||||
@@ -763,8 +762,8 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 1
|
||||
strokeColor: powerButton.containsMouse ? Theme.accent : Theme.muted
|
||||
fillColor: Theme.raised
|
||||
strokeColor: powerButton.containsMouse ? "#FFD063" : "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
|
||||
startX: powerButton.chamfer
|
||||
startY: 0
|
||||
@@ -786,8 +785,8 @@ Scope {
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: powerButton.modelData.label
|
||||
color: powerButton.containsMouse ? Theme.accent : Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
color: powerButton.containsMouse ? "#FFD063" : "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -99,7 +98,7 @@ Scope {
|
||||
anchors.right: parent.right
|
||||
|
||||
clip: true
|
||||
color: Theme.surface
|
||||
color: "#0F1012"
|
||||
|
||||
height: root.active ? win.openHeight : 0
|
||||
|
||||
@@ -154,9 +153,9 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: card.selected ? Theme.raised : "transparent"
|
||||
color: card.selected ? "#292C30" : "transparent"
|
||||
border.width: 1
|
||||
border.color: card.selected ? Theme.accent : Theme.raised
|
||||
border.color: card.selected ? "#FFD063" : "#292C30"
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
@@ -179,7 +178,7 @@ Scope {
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: card.modelData.name
|
||||
color: card.selected ? Theme.accent : Theme.text
|
||||
color: card.selected ? "#FFD063" : "#EEEEEE"
|
||||
font.pointSize: 9
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
@@ -195,14 +194,14 @@ Scope {
|
||||
anchors.centerIn: parent
|
||||
visible: model.apps.length === 0
|
||||
text: "no matches"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Theme.raised
|
||||
color: "#292C30"
|
||||
implicitHeight: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -218,8 +217,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: ">_"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 22
|
||||
font.bold: true
|
||||
}
|
||||
@@ -233,7 +232,7 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: Theme.text
|
||||
color: "#EEEEEE"
|
||||
font.pointSize: 16
|
||||
clip: true
|
||||
|
||||
@@ -268,7 +267,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "launch application…"
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -276,15 +275,15 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length + " apps"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 15
|
||||
}
|
||||
}
|
||||
|
||||
// Accent lid — mirrors the bottom bar strip.
|
||||
Rectangle {
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
implicitHeight: 4
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 {
|
||||
@@ -79,7 +78,7 @@ Scope {
|
||||
// Dim backdrop — click anywhere outside to dismiss.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.surface
|
||||
color: "#0A0A0C"
|
||||
opacity: 0.55
|
||||
|
||||
MouseArea {
|
||||
@@ -94,9 +93,9 @@ Scope {
|
||||
width: 760
|
||||
height: 560
|
||||
|
||||
color: Theme.surface
|
||||
color: "#0F1012"
|
||||
border.width: 1
|
||||
border.color: Theme.accent
|
||||
border.color: "#FFD063"
|
||||
opacity: 0.98
|
||||
|
||||
// Accent corner brackets for the cyberpunk frame.
|
||||
@@ -105,14 +104,14 @@ Scope {
|
||||
anchors.left: parent.left
|
||||
width: 5
|
||||
height: 28
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
}
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: 28
|
||||
height: 5
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -127,8 +126,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: "APPS"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 22
|
||||
font.letterSpacing: 3
|
||||
}
|
||||
@@ -139,9 +138,9 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.raised
|
||||
color: "#292C30"
|
||||
border.width: 1
|
||||
border.color: input.activeFocus ? Theme.accent : Theme.muted
|
||||
border.color: input.activeFocus ? "#FFD063" : "#7A7B7D"
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
@@ -156,7 +155,7 @@ Scope {
|
||||
anchors.rightMargin: 12
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: Theme.text
|
||||
color: "#EEEEEE"
|
||||
font.pointSize: 13
|
||||
clip: true
|
||||
|
||||
@@ -199,7 +198,7 @@ Scope {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: input.text.length === 0
|
||||
text: "Type to search…"
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -208,7 +207,7 @@ Scope {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
implicitHeight: 3
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -243,9 +242,9 @@ Scope {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
color: tile.selected ? Theme.raised : "transparent"
|
||||
color: tile.selected ? "#292C30" : "transparent"
|
||||
border.width: 1
|
||||
border.color: tile.selected ? Theme.accent : "transparent"
|
||||
border.color: tile.selected ? "#FFD063" : "transparent"
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
@@ -267,7 +266,7 @@ Scope {
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: tile.modelData.name
|
||||
color: tile.selected ? Theme.accent : Theme.text
|
||||
color: tile.selected ? "#FFD063" : "#EEEEEE"
|
||||
font.pointSize: 10
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
|
||||
@@ -7,7 +7,6 @@ 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.
|
||||
@@ -78,7 +77,7 @@ Scope {
|
||||
// Dim backdrop — click to dismiss.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.surface
|
||||
color: "#0A0A0C"
|
||||
opacity: 0.55
|
||||
|
||||
MouseArea {
|
||||
@@ -108,8 +107,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.accent
|
||||
fillColor: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
strokeWidth: 2
|
||||
|
||||
startX: frame.chamfer
|
||||
@@ -154,7 +153,7 @@ Scope {
|
||||
// run out to the top and left edges to meet the straight borders.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: 0
|
||||
startY: frame.chamfer
|
||||
@@ -180,7 +179,7 @@ Scope {
|
||||
// bottom and right edges.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height - frame.chamfer
|
||||
@@ -206,7 +205,7 @@ Scope {
|
||||
// detached from the panel by the width of the cut.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height
|
||||
@@ -229,7 +228,7 @@ Scope {
|
||||
// slanted end pieces on both arms.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
// inner, right end of the bottom arm
|
||||
startX: panelShape.width / 3
|
||||
@@ -278,7 +277,7 @@ Scope {
|
||||
// slanted end pieces on both arms.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
// inner, bottom of the right arm
|
||||
startX: panelShape.width
|
||||
@@ -346,7 +345,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -368,7 +367,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 15
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -390,7 +389,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 24
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -421,8 +420,8 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 20
|
||||
font.letterSpacing: 1
|
||||
clip: true
|
||||
@@ -458,7 +457,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "run"
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -466,8 +465,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 18
|
||||
}
|
||||
}
|
||||
@@ -490,7 +489,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: divider.slant
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -513,7 +512,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 0
|
||||
startY: divider.height
|
||||
PathLine {
|
||||
@@ -536,7 +535,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: divider.width
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -598,7 +597,7 @@ Scope {
|
||||
// Body
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.raised
|
||||
fillColor: "#22262C"
|
||||
startX: appRow.shear
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -621,7 +620,7 @@ Scope {
|
||||
// Left accent edge
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: appRow.shear
|
||||
startY: 0
|
||||
PathLine {
|
||||
@@ -656,7 +655,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.name
|
||||
color: appRow.selected ? Theme.accent : Theme.text
|
||||
color: appRow.selected ? "#FFD063" : "#EEEEEE"
|
||||
font.pointSize: 12
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -664,7 +663,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.genericName || ""
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font.pointSize: 10
|
||||
elide: Text.ElideRight
|
||||
Layout.maximumWidth: 220
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 {
|
||||
@@ -92,9 +91,9 @@ Scope {
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 60
|
||||
color: Theme.surface
|
||||
color: "#0F1012"
|
||||
border.width: 1
|
||||
border.color: Theme.accent
|
||||
border.color: "#FFD063"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
@@ -104,8 +103,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: ">"
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 26
|
||||
font.bold: true
|
||||
}
|
||||
@@ -119,7 +118,7 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: Theme.text
|
||||
color: "#EEEEEE"
|
||||
font.pointSize: 17
|
||||
clip: true
|
||||
|
||||
@@ -154,7 +153,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "run application"
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -162,8 +161,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length + " ▸"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
@@ -173,7 +172,7 @@ Scope {
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 3
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
visible: model.apps.length > 0
|
||||
}
|
||||
|
||||
@@ -182,11 +181,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: Theme.surface
|
||||
color: "#0F1012"
|
||||
opacity: 0.97
|
||||
visible: model.apps.length > 0
|
||||
border.width: 1
|
||||
border.color: Theme.raised
|
||||
border.color: "#292C30"
|
||||
|
||||
ListView {
|
||||
id: list
|
||||
@@ -216,14 +215,14 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: entry.selected ? Theme.selection : "transparent"
|
||||
color: entry.selected ? "#1A1C1F" : "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 3
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
visible: entry.selected
|
||||
}
|
||||
|
||||
@@ -240,7 +239,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: entry.modelData.name
|
||||
color: entry.selected ? Theme.accent : Theme.text
|
||||
color: entry.selected ? "#FFD063" : "#EEEEEE"
|
||||
font.pointSize: 12
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -248,7 +247,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: entry.modelData.genericName || ""
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font.pointSize: 10
|
||||
elide: Text.ElideRight
|
||||
Layout.maximumWidth: 200
|
||||
|
||||
@@ -6,7 +6,6 @@ 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.
|
||||
@@ -83,7 +82,7 @@ Scope {
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
implicitHeight: 5
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -94,8 +93,8 @@ Scope {
|
||||
|
||||
margin: 12
|
||||
border.width: 1
|
||||
border.color: Theme.accent
|
||||
color: Theme.surface
|
||||
border.color: "#FFD063"
|
||||
color: "#0F1012"
|
||||
opacity: 0.95
|
||||
|
||||
ColumnLayout {
|
||||
@@ -108,7 +107,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: "▚"
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
font.pointSize: 14
|
||||
font.bold: true
|
||||
}
|
||||
@@ -122,8 +121,8 @@ Scope {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 16
|
||||
clip: true
|
||||
|
||||
@@ -159,7 +158,7 @@ Scope {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text.length === 0
|
||||
text: "search"
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
@@ -167,14 +166,14 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: model.apps.length
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 14
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
implicitHeight: 2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -208,7 +207,7 @@ Scope {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: appRow.selected ? Theme.raised : "transparent"
|
||||
color: appRow.selected ? "#292C30" : "transparent"
|
||||
|
||||
// Accent bar on the selected row.
|
||||
Rectangle {
|
||||
@@ -216,7 +215,7 @@ Scope {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 3
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
visible: appRow.selected
|
||||
}
|
||||
|
||||
@@ -237,7 +236,7 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: appRow.modelData.name
|
||||
color: appRow.selected ? Theme.accent : Theme.text
|
||||
color: appRow.selected ? "#FFD063" : "#EEEEEE"
|
||||
font.pointSize: 12
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -246,7 +245,7 @@ Scope {
|
||||
Text {
|
||||
visible: text.length > 0
|
||||
text: appRow.modelData.genericName || appRow.modelData.comment || ""
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font.pointSize: 9
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -260,7 +259,7 @@ Scope {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
implicitHeight: 5
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import Quickshell.Services.Notifications
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.widgets.theme
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
@@ -13,7 +12,7 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
implicitHeight: 5
|
||||
|
||||
Layout.fillWidth: true
|
||||
@@ -24,8 +23,8 @@ ColumnLayout {
|
||||
|
||||
margin: 12
|
||||
border.width: 1
|
||||
border.color: Theme.accent
|
||||
color: Theme.surface
|
||||
border.color: "#FFD063"
|
||||
color: "#0F1012"
|
||||
opacity: .9
|
||||
|
||||
ColumnLayout {
|
||||
@@ -46,7 +45,7 @@ ColumnLayout {
|
||||
|
||||
Text {
|
||||
text: root.modelData.summary
|
||||
color: Theme.text
|
||||
color: "#EEEEEE"
|
||||
font.pointSize: 14
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
@@ -58,7 +57,7 @@ ColumnLayout {
|
||||
id: dismissButton
|
||||
|
||||
text: "×"
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
font.pointSize: 18
|
||||
|
||||
MouseArea {
|
||||
@@ -72,7 +71,7 @@ ColumnLayout {
|
||||
Text {
|
||||
visible: root.modelData.body !== ""
|
||||
text: root.modelData.body
|
||||
color: Theme.text
|
||||
color: "#EEEEEE"
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
@@ -93,9 +92,9 @@ ColumnLayout {
|
||||
|
||||
required property NotificationAction modelData
|
||||
|
||||
color: Theme.raised
|
||||
color: "#292C30"
|
||||
border.width: 1
|
||||
border.color: Theme.accent
|
||||
border.color: "#FFD063"
|
||||
implicitHeight: 28
|
||||
implicitWidth: actionText.implicitWidth + 16
|
||||
|
||||
@@ -103,7 +102,7 @@ ColumnLayout {
|
||||
id: actionText
|
||||
anchors.centerIn: parent
|
||||
text: actionButton.modelData.text
|
||||
color: Theme.text
|
||||
color: "#EEEEEE"
|
||||
font.pointSize: 12
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.widgets.theme
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
@@ -69,7 +68,7 @@ Scope {
|
||||
anchors.bottomMargin: 4
|
||||
|
||||
text: Math.round(Math.min(root.volume, root.maxVolume) * 100) + "%"
|
||||
color: !root.muted && root.volume > 1 ? Theme.hot : Theme.accent
|
||||
color: !root.muted && root.volume > 1 ? "#FF6B4A" : "#FFD063"
|
||||
font.pointSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
@@ -85,7 +84,7 @@ Scope {
|
||||
anchors.centerIn: parent
|
||||
implicitHeight: 6
|
||||
width: parent.width * Math.min(root.volume, root.maxVolume) / root.maxVolume
|
||||
color: Theme.hot
|
||||
color: "#FF6B4A"
|
||||
visible: !root.muted && root.volume > 1
|
||||
|
||||
Behavior on width {
|
||||
@@ -99,7 +98,7 @@ Scope {
|
||||
anchors.centerIn: parent
|
||||
implicitHeight: 6
|
||||
width: parent.width * Math.min(root.volume, 1) / root.maxVolume
|
||||
color: root.muted ? Theme.muted : Theme.accent
|
||||
color: root.muted ? "#7A7B7D" : "#FFD063"
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
|
||||
@@ -8,7 +8,6 @@ import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import qs.widgets.theme
|
||||
|
||||
// A vertical sidebar carrying the "Slant" (launcher V6) visual language —
|
||||
// chamfered panel, thick trapezoid bevel accents, an outward bracket, a
|
||||
@@ -82,8 +81,8 @@ Scope {
|
||||
// Panel: big chamfers on top-right & bottom-left, small bevels
|
||||
// on top-left & bottom-right (mirror of the left-anchored panel).
|
||||
ShapePath {
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.accent
|
||||
fillColor: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
strokeWidth: 2
|
||||
|
||||
startX: panelShape.width - frame.chamfer
|
||||
@@ -125,7 +124,7 @@ Scope {
|
||||
// Thick top-right bevel accent (trapezoid to the edges).
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: panelShape.width
|
||||
startY: frame.chamfer
|
||||
@@ -150,7 +149,7 @@ Scope {
|
||||
// Thick bottom-left bevel accent.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: 0
|
||||
startY: panelShape.height - frame.chamfer
|
||||
@@ -175,7 +174,7 @@ Scope {
|
||||
// Floating triangle cap in the bottom-left notch.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: 0
|
||||
startY: panelShape.height
|
||||
@@ -198,7 +197,7 @@ Scope {
|
||||
// corner following the small chamfer.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: 0
|
||||
startY: frame.armSide
|
||||
@@ -246,15 +245,15 @@ Scope {
|
||||
anchors.rightMargin: 8
|
||||
spacing: 10
|
||||
|
||||
// Clock — readout font, hh over mm
|
||||
// Clock — Digital-7, hh over mm
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Qt.formatDateTime(clock.date, "hh\nmm")
|
||||
font.family: Theme.readoutFont
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 22
|
||||
font.letterSpacing: 1
|
||||
color: Theme.text
|
||||
color: "#EEEEEE"
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
@@ -267,9 +266,9 @@ Scope {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Qt.formatDateTime(clock.date, "dd\nMMM").toUpperCase()
|
||||
font.family: Theme.readoutFont
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 13
|
||||
color: Theme.accent
|
||||
color: "#FFD063"
|
||||
}
|
||||
|
||||
// Slanted divider
|
||||
@@ -283,7 +282,7 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 8
|
||||
startY: 0
|
||||
PathLine { x: divider.width; y: 0 }
|
||||
@@ -302,9 +301,9 @@ Scope {
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "VOL"
|
||||
font.family: Theme.readoutFont
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 10
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -337,8 +336,8 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 1
|
||||
strokeColor: Theme.muted
|
||||
fillColor: Theme.raised
|
||||
strokeColor: "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
|
||||
startX: seg.chamfer
|
||||
startY: 0
|
||||
@@ -376,7 +375,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.hot
|
||||
fillColor: "#FF6B4A"
|
||||
startX: segFill.chamfer
|
||||
startY: 0
|
||||
PathLine { x: segFill.width; y: 0 }
|
||||
@@ -408,8 +407,8 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 1
|
||||
strokeColor: Theme.muted
|
||||
fillColor: Theme.raised
|
||||
strokeColor: "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
|
||||
startX: volMeter.chamfer
|
||||
startY: 0
|
||||
@@ -449,7 +448,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: root.muted ? Theme.muted : Theme.accent
|
||||
fillColor: root.muted ? "#7A7B7D" : "#FFD063"
|
||||
startX: vol.chamfer
|
||||
startY: 0
|
||||
PathLine { x: vol.width; y: 0 }
|
||||
@@ -467,9 +466,9 @@ Scope {
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.muted ? "--" : Math.round(root.volume * 100)
|
||||
font.family: Theme.readoutFont
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 14
|
||||
color: root.muted ? Theme.muted : Theme.text
|
||||
color: root.muted ? "#7A7B7D" : "#EEEEEE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ 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, the readout font, the slash-trio motif — but with
|
||||
// color, chamfered corners, Digital-7 Mono, 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.
|
||||
@@ -55,8 +54,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.accent
|
||||
fillColor: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
strokeWidth: 2
|
||||
|
||||
startX: frame.chamfer
|
||||
@@ -115,7 +114,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine { x: 10; y: 0 }
|
||||
@@ -125,7 +124,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 15
|
||||
startY: 0
|
||||
PathLine { x: 19; y: 0 }
|
||||
@@ -135,7 +134,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 24
|
||||
startY: 0
|
||||
PathLine { x: 28; y: 0 }
|
||||
@@ -156,7 +155,7 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 8
|
||||
startY: 0
|
||||
PathLine { x: divider.width; y: 0 }
|
||||
@@ -170,9 +169,9 @@ Scope {
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "TRAY"
|
||||
font.family: Theme.readoutFont
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 12
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
}
|
||||
|
||||
ListView {
|
||||
@@ -202,17 +201,17 @@ Scope {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: SystemTray.items.values.length === 0
|
||||
text: "--"
|
||||
font.family: Theme.readoutFont
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 14
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: SystemTray.items.values.length
|
||||
font.family: Theme.readoutFont
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 16
|
||||
color: Theme.text
|
||||
color: "#EEEEEE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ 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.
|
||||
@@ -36,8 +35,8 @@ MouseArea {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 1
|
||||
strokeColor: root.containsMouse ? Theme.accent : Theme.muted
|
||||
fillColor: Theme.raised
|
||||
strokeColor: root.containsMouse ? "#FFD063" : "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
|
||||
startX: root.chamfer
|
||||
startY: 0
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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,6 +1,5 @@
|
||||
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
|
||||
@@ -27,8 +26,8 @@ Item {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 1
|
||||
strokeColor: bar.unknown ? Theme.disabled : Theme.muted
|
||||
fillColor: Theme.raised
|
||||
strokeColor: bar.unknown ? "#3A3D42" : "#7A7B7D"
|
||||
fillColor: "#292C30"
|
||||
|
||||
startX: bar.chamfer
|
||||
startY: 0
|
||||
@@ -73,7 +72,7 @@ Item {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: bar.hot ? Theme.hot : Theme.accent
|
||||
fillColor: bar.hot ? "#FF6B4A" : "#FFD063"
|
||||
|
||||
Behavior on fillColor {
|
||||
ColorAnimation {
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -60,7 +59,7 @@ Scope {
|
||||
// Dim backdrop — click anywhere to dismiss.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.surface
|
||||
color: "#0A0A0C"
|
||||
opacity: 0.5
|
||||
|
||||
MouseArea {
|
||||
@@ -103,8 +102,8 @@ Scope {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.surface
|
||||
strokeColor: Theme.accent
|
||||
fillColor: "#0F1012"
|
||||
strokeColor: "#FFD063"
|
||||
strokeWidth: 2
|
||||
|
||||
startX: frame.chamfer
|
||||
@@ -122,7 +121,7 @@ Scope {
|
||||
// Thick top-left bevel accent.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: 0
|
||||
startY: frame.chamfer
|
||||
@@ -135,7 +134,7 @@ Scope {
|
||||
// Thick bottom-right bevel accent.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height - frame.chamfer
|
||||
@@ -148,7 +147,7 @@ Scope {
|
||||
// Floating triangle cap in the bottom-right notch.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height
|
||||
@@ -160,7 +159,7 @@ Scope {
|
||||
// Heavy outward chunk wrapping the bottom-left corner.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: panelShape.width / 3
|
||||
startY: panelShape.height
|
||||
@@ -177,7 +176,7 @@ Scope {
|
||||
// Heavy outward chunk wrapping the top-right corner.
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
|
||||
startX: panelShape.width
|
||||
startY: panelShape.height / 3
|
||||
@@ -218,7 +217,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine { x: 10; y: 0 }
|
||||
@@ -228,7 +227,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 15
|
||||
startY: 0
|
||||
PathLine { x: 19; y: 0 }
|
||||
@@ -238,7 +237,7 @@ Scope {
|
||||
}
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 24
|
||||
startY: 0
|
||||
PathLine { x: 28; y: 0 }
|
||||
@@ -250,8 +249,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: (vitals.host || "vitals").toUpperCase()
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 20
|
||||
font.letterSpacing: 2
|
||||
}
|
||||
@@ -262,15 +261,15 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: "UP"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 11
|
||||
}
|
||||
|
||||
Text {
|
||||
text: vitals.fmtUptime(vitals.uptime)
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
@@ -282,8 +281,8 @@ Scope {
|
||||
Layout.fillWidth: true
|
||||
visible: vitals.failed
|
||||
text: "NODE_EXPORTER UNREACHABLE ON :" + vitals.port
|
||||
color: Theme.hot
|
||||
font.family: Theme.readoutFont
|
||||
color: "#FF6B4A"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 13
|
||||
}
|
||||
|
||||
@@ -354,7 +353,7 @@ Scope {
|
||||
Text {
|
||||
Layout.preferredWidth: 96
|
||||
text: diskRow.modelData.mount
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font.pointSize: 9
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
@@ -370,8 +369,8 @@ Scope {
|
||||
Layout.preferredWidth: 48
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: Math.round(diskRow.frac * 100) + "%"
|
||||
color: diskRow.frac >= 0.9 ? Theme.hot : Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
color: diskRow.frac >= 0.9 ? "#FF6B4A" : "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 13
|
||||
}
|
||||
|
||||
@@ -379,8 +378,8 @@ Scope {
|
||||
Layout.preferredWidth: 104
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: vitals.fmtBytes(diskRow.modelData.size - diskRow.modelData.used) + " FREE"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 11
|
||||
}
|
||||
}
|
||||
@@ -400,21 +399,21 @@ Scope {
|
||||
Text {
|
||||
Layout.preferredWidth: 96
|
||||
text: vitals.netIface || "NET"
|
||||
color: Theme.muted
|
||||
color: "#7A7B7D"
|
||||
font.pointSize: 9
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "RX"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 11
|
||||
}
|
||||
|
||||
Text {
|
||||
text: vitals.fmtRate(vitals.netRx)
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 14
|
||||
}
|
||||
|
||||
@@ -424,15 +423,15 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: "TX"
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 11
|
||||
}
|
||||
|
||||
Text {
|
||||
text: vitals.fmtRate(vitals.netTx)
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 14
|
||||
|
||||
// Right-align the TX readout against the panel edge the
|
||||
@@ -470,8 +469,8 @@ Scope {
|
||||
Text {
|
||||
Layout.preferredWidth: 46
|
||||
text: metric.label
|
||||
color: Theme.accent
|
||||
font.family: Theme.readoutFont
|
||||
color: "#FFD063"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 15
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
@@ -487,8 +486,8 @@ Scope {
|
||||
Layout.preferredWidth: 54
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: metric.readout
|
||||
color: Theme.text
|
||||
font.family: Theme.readoutFont
|
||||
color: "#EEEEEE"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 16
|
||||
}
|
||||
}
|
||||
@@ -500,8 +499,8 @@ Scope {
|
||||
|
||||
Text {
|
||||
text: metric.detail
|
||||
color: Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 11
|
||||
}
|
||||
|
||||
@@ -512,8 +511,8 @@ Scope {
|
||||
Text {
|
||||
visible: metric.aside.length > 0
|
||||
text: metric.aside
|
||||
color: metric.asideHot ? Theme.hot : Theme.muted
|
||||
font.family: Theme.readoutFont
|
||||
color: metric.asideHot ? "#FF6B4A" : "#7A7B7D"
|
||||
font.family: "Digital-7 Mono"
|
||||
font.pointSize: 11
|
||||
}
|
||||
}
|
||||
@@ -531,7 +530,7 @@ Scope {
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: Theme.accent
|
||||
fillColor: "#FFD063"
|
||||
startX: 6
|
||||
startY: 0
|
||||
PathLine { x: divider.width; y: 0 }
|
||||
|
||||
Generated
+17
@@ -176,6 +176,22 @@
|
||||
"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": {
|
||||
@@ -490,6 +506,7 @@
|
||||
"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,6 +31,18 @@
|
||||
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
|
||||
@@ -552,113 +564,5 @@
|
||||
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";
|
||||
|
||||
# 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-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 ];
|
||||
|
||||
# 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
|
||||
qs-dev && trap qs-prod EXIT
|
||||
fi
|
||||
echo "homelab devshell — qs-dev (working tree) / qs-prod (packaged); exit restores"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
../../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,4 +69,21 @@
|
||||
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,6 +8,7 @@
|
||||
./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,6 +278,13 @@ 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
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
{ 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,4 +65,36 @@
|
||||
# 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,6 +111,62 @@
|
||||
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).
|
||||
|
||||
@@ -15,6 +15,7 @@ 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: |
|
||||
@@ -35,7 +36,7 @@ sops:
|
||||
CzjSDQZTcseEXZNwuzZcfB5Mvq0BQvjOj7lGuxzuE4qwWkdJWGfVLQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
|
||||
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]
|
||||
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]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
|
||||
+4
-2
@@ -6,6 +6,8 @@ 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: |
|
||||
@@ -26,7 +28,7 @@ sops:
|
||||
oyJ7PS3lW+PxH5AZkeeU7gXO/pz2oDku0aDOds7kaD3n0+qSWicQ+Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1eapjg6tdrr0fuvmgs3q3nlvnjkaxez298qynqqqxt0lpcv0lrsyq7ayxjk
|
||||
lastmodified: "2026-08-23T05:55:49Z"
|
||||
mac: ENC[AES256_GCM,data:a3vCmrQMCS25tNWrzTeiGmOHf4Fn356PO3uNa2HvS21EBCKTc6YWBj9KmpORdz+6t03JJe/4eiGdghGaLhRr+JXyQnaT54gSV+FhC3dH6blind746XN3h+Z9rxiva6apvcAGUZ9k01Js5IXN9efEMhcI6w0U4oVuVqtvShvg8A8=,iv:9kF3cJ1vyy2H3eH10DVCYmWeXv2MH4AFDiF8cOajlw4=,tag:zhombLVpL8M1TUtYur/gYQ==,type:str]
|
||||
lastmodified: "2026-08-25T21:51:24Z"
|
||||
mac: ENC[AES256_GCM,data:dz249hf3w8Tn0JStFOhhpdCZFMx2yxmNABx1CbeIQ/JlICAU82e4fg8AzJQY9EMOEs3Zx6L61yieljD4A/HLip5rDVlAXqqLeklW60eb7BHuSO79YfFgot+rS05g8WqFkRJYWzmkXhMbErCI133n72XEdeqMUU/m0djOUZlVRLs=,iv:d7G6TJRLfmXvQ2BUG9Hi83lOB9anhmb9qneLeVSCBhc=,tag:F6X/0z00PjiYum5kf8YApA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
{ 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" ];
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
# 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"]
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/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 "$@"
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/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