Split the lock branding source into its own component

This commit is contained in:
2026-08-21 15:08:38 -04:00
parent 076561c428
commit 78a9da3deb
5 changed files with 74 additions and 89 deletions
+27 -29
View File
@@ -95,36 +95,33 @@ under `Apps` instead of below `System`. A `before:` that names an unknown id is
ignored, and a row without one keeps its file order.
`plugins/blob.lock/` clones `omarchy.lock` to put `branding/screensaver.txt` on
the lock screen. Stock `LockView.qml` draws a blurred wallpaper, the password
field, and a fingerprint hint - there is no branding element and no config key
for one, so the only way to get the art behind the password prompt is a fork.
`Service.qml` gains a watched `FileView` on the branding file and passes the
text to both `LockView` instances (the real lock surface and the theme
preview); `LockView.qml` gains one `Text` above the input field. The field
itself keeps the position and size it has upstream. `Text.Fit` scales the art
down to whatever room is left above the field, so a wider or taller
`screensaver.txt` cannot run off the screen, and an unreadable or missing file
leaves the lock unbranded rather than broken.
the lock screen and to restyle the password field like the AGS widgets. Stock
`LockView.qml` draws a blurred wallpaper, the field, and a fingerprint hint -
there is no branding element and no config key for one, so a fork is the only
way in.
The password field is restyled to match the AGS widgets: a 2px border and
square corners in place of the shell's 3px rounded outline, an
`alpha(background, 0.6)` fill matching `.qs-tile`, and the AGS border pair -
`alpha(color4, 0.5)` while the field is empty, going to a solid `accent` once
there is something in it, with `urgent` on a failed attempt. Only the geometry
and the alphas are fixed; the colors come from the active theme, so the field
follows a theme change the way the AGS widgets do. The `[lock]` tokens in a
theme's `shell.toml` no longer reach the border or the fill.
`color4` needs reading from disk. The `Color` singleton resolves the palette
down to `foreground`, `background`, `accent`, `urgent`, and `muted`, and uses
`color4` only as the fallback when a theme declares no separate `accent` - so
the raw slot the AGS stylesheet borders with is not exposed anywhere. The
service parses it out of the active theme's `colors.toml` with the same regex
`Color.loadColors` uses. Stock `Color` reads that file once at startup and
takes runtime theme switches over IPC instead, but
The fork is three small changes. `BrandingSource.qml` is new and holds both
file reads: the branding text, and the `color4` slot parsed out of the active
theme's `colors.toml`. `Color` resolves a palette down to `foreground`,
`background`, `accent`, `urgent`, and `muted` and never exposes `color4`, which
is the color AGS borders with. Stock `Color` reads that file once at startup
and takes theme switches over IPC, but
`~/.local/state/omarchy/current/theme` is a real directory rewritten in place
rather than a swapped symlink, so watching the file is enough here. A palette
with no `color4` falls back to the accent.
rather than a swapped symlink, so watching the file is enough here.
`Service.qml` gains only that component and two bindings on each of its two
`LockView` instances (the lock surface and the theme preview). `LockView.qml`
gains a `Text` above the field, and the field's own skin.
The styling follows `ags/style.css`: a 2px border and square corners in place
of the shell's 3px rounded outline, an `alpha(background, 0.6)` fill matching
`.qs-tile`, and the AGS border pair - `alpha(color4, 0.5)` while the field is
empty, going to a solid `accent` once there is something in it, and `urgent` on
a failed attempt. Only the geometry and the alphas are fixed; the colors come
from the active theme, so the field follows a theme change the way the AGS
widgets do. The `[lock]` tokens in a theme's `shell.toml` no longer reach the
border or the fill. `Text.Fit` scales the branding into whatever room is left
above the field, so a wider or taller `screensaver.txt` cannot run off the
screen, and a missing file leaves the lock unbranded rather than broken.
Unlike the other clones this one is load-bearing for security. The lock is a
`service` plugin, so it is enabled by its id appearing in `plugins[]` and the
@@ -140,7 +137,8 @@ removed through `omarchy plugin remove`.
Re-copy `LockView.qml` and `Service.qml` from
`/usr/share/omarchy/shell/plugins/lock/` after an Omarchy update that touches
the lock, then re-apply the branding property, the `FileView`, and the `Text`.
the lock, then re-apply the two blocks above. `BrandingSource.qml` is wholly
ours and carries over untouched.
`plugins/blob.bar/` replaces the whole bar so the clock cannot be dragged out
of the center. Omarchy 4 puts a drag-to-reorder handler on every bar module and
@@ -0,0 +1,41 @@
import QtQuick
import Quickshell
import Quickshell.Io
Item {
id: root
readonly property string home: Quickshell.env("HOME")
readonly property string brandingPath: home + "/.config/omarchy/branding/screensaver.txt"
readonly property string palettePath: home + "/.local/state/omarchy/current/theme/colors.toml"
property string brandingText: ""
property string paletteColor4: ""
function readPaletteColor4(raw) {
var lines = String(raw || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var match = lines[i].match(/^\s*color4\s*=\s*["']?(#[0-9A-Fa-f]{6})/)
if (match) return match[1]
}
return ""
}
FileView {
path: root.brandingPath
watchChanges: true
printErrors: false
onLoaded: root.brandingText = text()
onLoadFailed: root.brandingText = ""
onFileChanged: reload()
}
FileView {
path: root.palettePath
watchChanges: true
printErrors: false
onLoaded: root.paletteColor4 = root.readPaletteColor4(text())
onLoadFailed: root.paletteColor4 = ""
onFileChanged: reload()
}
}
-16
View File
@@ -22,8 +22,6 @@ Item {
readonly property string placeholderText: "Enter Password"
readonly property int fieldWidth: 381
readonly property int fieldHeight: 67
// AGS draws every surface with a 2px border and square corners; see the
// 2px/border-radius: 0 pairs throughout ags/style.css.
readonly property int outlineThickness: 2
readonly property int fieldRadius: 0
readonly property int fieldFontSize: Math.round(Style.font.heading * 1.125)
@@ -37,22 +35,12 @@ Item {
readonly property real passwordDotScale: dotMetrics.advanceWidth > 0
? Math.min(1, (passwordInput.width - 4) / dotMetrics.advanceWidth)
: 1
// Gap between the branding and the password field, and the share of the
// screen the art may claim. Text.Fit needs a bounded width and height to
// scale into, so both are given explicitly below.
readonly property int brandingGap: Style.space(48)
readonly property int brandingMaxWidth: 1100
readonly property int brandingMaxFontSize: Math.round(Style.font.heading * 1.5)
readonly property bool showPasswordCursor: inputEnabled && !authenticatingPassword && failureMessage.length === 0
readonly property bool errorState: failureMessage.length > 0
// The AGS border pair: containers rest on alpha(@color4, 0.5) and go to a
// solid @accent once in use. An empty field reads as resting and a filled
// one as active. The spec is built here rather than read from the [lock]
// theme tokens, since those carry the shell's own heavier look; the colors
// themselves still come from the active theme, so this follows a theme
// change exactly like the AGS widgets do. color4 falls back to the accent
// for a palette that does not define one.
readonly property bool inputActive: passwordText.length > 0 || authenticatingPassword
readonly property color inputRestingBorder: paletteColor4.length > 0 ? paletteColor4 : Color.accent
readonly property color inputBorderColor: errorState
@@ -142,10 +130,6 @@ Item {
onPositionChanged: root.wakeRequested()
}
// Sits above the password field rather than replacing anything, so the
// input keeps the position and size it has upstream. Text.Fit shrinks the
// art to whatever room is left above the field, which keeps a tall or wide
// screensaver.txt from running off the top of a small screen.
Text {
id: branding
anchors.horizontalCenter: parent.horizontalCenter
+6 -41
View File
@@ -15,8 +15,6 @@ Item {
readonly property string stateHome: home + "/.local/state"
readonly property string userName: Quickshell.env("USER") || Quickshell.env("LOGNAME")
readonly property string currentBackgroundLink: stateHome + "/omarchy/current/background"
readonly property string brandingPath: home + "/.config/omarchy/branding/screensaver.txt"
readonly property string currentThemePath: stateHome + "/omarchy/current/theme"
property bool lockRequested: false
property bool pendingSessionLock: false
@@ -31,8 +29,6 @@ Item {
property int failedAttempts: 0
property string backgroundPath: ""
property int backgroundVersion: 0
property string brandingText: ""
property string paletteColor4: ""
property string lastEvent: "init"
property string lastEventAt: ""
property bool strandedLock: false
@@ -274,8 +270,8 @@ Item {
anchors.fill: parent
backgroundPath: root.backgroundPath
backgroundVersion: root.backgroundVersion
brandingText: root.brandingText
paletteColor4: root.paletteColor4
brandingText: brandingSource.brandingText
paletteColor4: brandingSource.paletteColor4
fingerprintConfigured: root.fingerprintConfigured
authenticatingPassword: root.authenticatingPassword
failureMessage: root.failureMessage
@@ -306,8 +302,8 @@ Item {
anchors.fill: parent
backgroundPath: root.backgroundPath
backgroundVersion: root.backgroundVersion
brandingText: root.brandingText
paletteColor4: root.paletteColor4
brandingText: brandingSource.brandingText
paletteColor4: brandingSource.paletteColor4
fingerprintConfigured: root.fingerprintConfigured
authenticatingPassword: false
failureMessage: ""
@@ -489,39 +485,8 @@ Item {
else armBlankTimer()
}
// The Color singleton exposes only foreground, background, accent, urgent,
// and muted, so the raw color4 slot the AGS stylesheet borders with has to
// be read here. Same file and same shape Color itself parses; the current
// theme directory is a real directory rewritten in place on every theme
// change, so watching the file is enough to follow a theme switch.
function readPaletteColor4(raw) {
var lines = String(raw || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var match = lines[i].match(/^\s*color4\s*=\s*["']?(#[0-9A-Fa-f]{6})/)
if (match) return match[1]
}
return ""
}
FileView {
path: root.currentThemePath + "/colors.toml"
watchChanges: true
printErrors: false
onLoaded: root.paletteColor4 = root.readPaletteColor4(text())
onLoadFailed: root.paletteColor4 = ""
onFileChanged: reload()
}
// The same file the screensaver paints. Watched so an edit through
// `omarchy branding screensaver text` shows up on the next lock without a
// shell restart. A missing file simply leaves the lock screen unbranded.
FileView {
path: root.brandingPath
watchChanges: true
printErrors: false
onLoaded: root.brandingText = text()
onLoadFailed: root.brandingText = ""
onFileChanged: reload()
BrandingSource {
id: brandingSource
}
FileView {
-3
View File
@@ -63,9 +63,6 @@ function parseMenuJsonc(raw) {
return out
}
// A row may name a sibling's id in `before:` to sit ahead of it. User rows are
// appended after every default row, so without this a new top-level entry can
// only land at the bottom of the root menu.
function applyBeforeHints(items, itemOrder) {
var order = itemOrder.slice()