aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 12:35:49 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 12:35:49 +0200
commit5e3de7cbf39b54114244acbde998364b5b49ea24 (patch)
tree75b7edb80af24303e9d2deb4998c4911511eb3f2
downloadsddm-theme-udt-5e3de7cbf39b54114244acbde998364b5b49ea24.tar.gz
sddm-theme-udt-5e3de7cbf39b54114244acbde998364b5b49ea24.zip
docs: add sddm-theme-udt design spec
-rw-r--r--docs/superpowers/specs/2026-09-17-sddm-theme-udt-design.md290
1 files changed, 290 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-09-17-sddm-theme-udt-design.md b/docs/superpowers/specs/2026-09-17-sddm-theme-udt-design.md
new file mode 100644
index 0000000..0d9cff5
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-17-sddm-theme-udt-design.md
@@ -0,0 +1,290 @@
+# sddm-theme-udt, Design
+
+Date: 2026-09-17
+Status: approved, not yet implemented
+
+## Goal
+
+An SDDM login theme that belongs to the unified-desktop-theme (UDT) family: the
+same palette, the same wallpaper, the same rounded accent-bordered visual
+language as rofi, waybar, conky and the quickshell desktop. It tracks the
+wallpaper set from the `appearance` quickshell without needing a root re-run
+after every wallpaper change, and it keeps the UDT rule that a colour is written
+down once.
+
+The theme is its own repository, `sddm-theme-udt`, wired into UDT through
+`bin/udt-accent` and `install.sh`. It targets the SDDM on this machine: 0.21.0,
+Qt 6.11, theme API 2.0.
+
+## Problem
+
+SDDM is the last unconverted consumer. Everything else on the desktop reads the
+UDT palette and follows the wallpaper accent, so the login screen is the one
+place that still looks like a different system.
+
+The hard part is not drawing a login box. It is that SDDM runs its greeter as
+the unprivileged `sddm` user, while the palette and the wallpaper belong to the
+login user and change over time:
+
+- The greeter's copy of the theme is root-installed and cannot be rewritten by
+ the user on every wallpaper change.
+- `~/.cache/udt` is unreachable to `sddm`: `/home/<user>/.cache` is mode 0700,
+ so even a world-readable file inside it cannot be opened. The wallpaper itself
+ is reachable (`~/Pictures/...` is 0755), the palette is not.
+- The greeter is a privileged boundary. It must not execute anything the login
+ user can rewrite.
+
+So the design needs a channel that is live, world-readable, and data-only.
+
+## Decisions
+
+1. **Live tracking through `theme.conf.user`.** SDDM merges a `theme.conf.user`
+ file next to `theme.conf` and exposes the result as the QML `config` object
+ (`config.stringValue(key)`, SDDM 0.20+). Root installs
+ `/usr/share/sddm/themes/udt/theme.conf.user` as a symlink to a generated INI
+ in the user's home. The greeter follows the symlink and reads fresh values on
+ every start, so a wallpaper change reaches the login screen with no root
+ action. Rejected: baking the palette at install time (stale after every
+ wallpaper change); reading a generated QML palette at runtime (executes
+ user-writable code inside the greeter); reading JSON through
+ `XMLHttpRequest` (Qt 6 disables local file reads without
+ `QML_XHR_ALLOW_FILE_READ=1`, verified by probe, and depends on an env var
+ SDDM does not set).
+
+2. **One palette source, one writer.** `udt-accent` already regenerates every
+ accent-bearing output after each wallpaper change and already reads the
+ installed `palette.rasi`. It gains one more output,
+ `~/.local/share/udt/sddm-theme.conf`, written from the same palette dict that
+ feeds `udt-palette.qml`. No second writer, no new role list to keep in sync.
+ Rejected: a `udt-palette` target too (two writers to one file, the exact
+ drift UDT warns about).
+
+3. **The generated file lives in `~/.local/share/udt/`, not `~/.cache/udt/`.**
+ `~/.cache` is 0700 and blocks the greeter, `~/.local/share` is 0755. Putting
+ it there needs no permission change to any existing directory. Rejected:
+ chmod `~/.cache` to 0711 (touches every cached file's exposure for one theme);
+ a root-owned `/var/cache/udt` chowned to the user (a second root action just
+ to hold one file).
+
+4. **Wallpaper is read from its real path, not through a symlink in the cache.**
+ `udt-accent` records `os.path.realpath(image)` in the INI. The path is under
+ `~/Pictures/...`, which the greeter can read. Rejected: `~/.cache/udt/wpaper`
+ (unreachable behind 0700) and `~/.config/wallp/wall_h` (readable, but a
+ second source of truth for the same value).
+
+5. **The theme directory is copied, not symlinked into place.** The deployable
+ files live under `theme/` in this repository. Root copies that tree into
+ `/usr/share/sddm/themes/udt/`. Only the INI is user-writable; every QML file
+ the greeter executes is root-owned. Rejected: symlinking the repository into
+ `/usr/share/sddm` (live edits, but lets user-writable QML run in the
+ greeter).
+
+6. **GUI on one screen, wallpaper on all.** SDDM's root item spans the united
+ geometry of every screen; `screenModel` exposes each screen's `name`,
+ `geometry` and a `primary` index. The wallpaper is drawn once per screen via
+ a `Repeater`, the login card is centered inside the geometry of the selected
+ screen. The selected screen is `uiScreen` (an output name) falling back to
+ `screenModel.primary`. Rejected: mirroring the UI on every screen (what the
+ stock themes do, and what the user explicitly did not want).
+
+7. **Idle behaviour mirrors Breeze.** After `fadeoutMs` (default 60000) with no
+ input the card fades out, leaving only the wallpaper; any key, click or mouse
+ move brings it back. Rejected: keep the card always visible (the stock
+ behaviour the user asked to move away from); a screensaver-style timeout
+ (SDDM already blanks the display separately).
+
+8. **Fonts: Noto Sans only.** It is installed system-wide
+ (`/usr/share/fonts/TTF`) and visible to the greeter. Inconsolata Nerd Font
+ lives in `~/.fonts` and is not visible to the `sddm` user, so the theme does
+ not reference it. Rejected: bundling the font in `theme/fonts/` and loading
+ it with `FontLoader` (adds a licensed binary to the repo for at most the
+ clock); installing it system-wide in the root block (a system change for one
+ glyph run). Revisit only if the login clock looks wrong in Noto Sans.
+
+9. **Colours keep the Catppuccin-compat names** the rest of UDT emits (`base`,
+ `mantle`, `surface0`..`surface2`, `text`, `subtext0`, `overlay0`, `accent`,
+ ...) so the SDDM INI, `palette.rasi` and `udt-palette.qml` describe one
+ palette under one set of names. Rejected: fresh role names for SDDM (a second
+ vocabulary for the same colours).
+
+## Architecture
+
+```
+wallpaper change
+ wallp -> udt-accent <image>
+ extracts + snaps accent, then writes
+ ~/.local/share/udt/sddm-theme.conf (0644, world-readable)
+
+root install (once, printed by UDT's install.sh)
+ /usr/share/sddm/themes/udt/ root-owned theme code
+ Main.qml
+ components/{LoginCard,PowerBar,Backdrop}.qml
+ icons/*.svg
+ theme.conf static defaults + fallbacks
+ metadata.desktop
+ theme.conf.user -> ~/.local/share/udt/sddm-theme.conf
+ /etc/sddm.conf.d/udt.conf [Theme] Current=udt
+
+greeter start (as sddm)
+ reads theme.conf + theme.conf.user into `config`
+ reads config.background directly
+ selects a screen from screenModel
+```
+
+Data flow is one-directional and needs no daemon: `udt-accent` writes a file,
+SDDM reads it at the next greeter launch. Re-running the root copy is only
+needed when the theme code itself changes.
+
+### Why `theme.conf.user`
+
+The greeter parses `theme.conf.user` with `QSettings` (INI), so it is data, not
+code. Nothing the login user can write is ever interpreted as QML. The split
+between root-owned `theme.conf` (defaults, fallbacks) and user-owned
+`theme.conf.user` (live values) is exactly SDDM's documented override mechanism,
+not a trick.
+
+### Palette to UI mapping
+
+`udt-accent` writes under `[General]`:
+
+| key | meaning | role |
+| --- | --- | --- |
+| `type` | `image` | constant |
+| `background` | absolute wallpaper path | `os.path.realpath(image)` |
+| `accent` | snapped wallpaper accent | `ACCENTS[name]` |
+| `base` | scrim + card ground | `bg` |
+| `mantle` | card ground, deeper | `bg_alt` |
+| `crust` | deepest ground | `bg_deep` |
+| `surface0` | inputs | `surface` |
+| `surface1` | hover / borders | `surface_alt` |
+| `surface2` | raised | `surface_high` |
+| `text` | primary text | `fg` |
+| `subtext0` | secondary text | `fg_dim` |
+| `subtext1` | brighter secondary | `fg_bright` |
+| `overlay0` | placeholders | `fg_faint` |
+| `overlay1` | dividers | `mid_low` |
+| `overlay2` | raised dividers | `mid_high` |
+| `red` | login failure | `critical` |
+| `yellow` | caps lock hint | `warning` |
+| `green` | success | `success` |
+| `teal` | info | `info` |
+| `blue`, `lavender` | session/host chrome | `border_active`, `accent` |
+
+`theme.conf` repeats the same keys with fixed Macchiato values so the theme
+parses and renders before any wallpaper has ever been set.
+
+## Theme behaviour
+
+- **Background per screen.** `Repeater { model: screenModel }`, each delegate
+ positioned at `geometry` from the model, `Image` with
+ `fillMode: Image.PreserveAspectCrop`. A `base`-coloured overlay sits above it
+ for text contrast.
+- **Card on one screen.** `uiScreen` names an output (e.g. `DP-1`); empty or
+ unmatched means `screenModel.primary`. The card is centered in that screen's
+ geometry, so other screens show wallpaper only.
+- **Frosted card.** `ShaderEffectSource` samples the wallpaper at the card
+ rectangle, `MultiEffect` (QtQuick.Effects, available on Qt 6.11) blurs it, and
+ a rounded mask clips it to the card. If the effect fails to load, the card
+ falls back to a translucent `mantle` fill. `blur` in `theme.conf` disables it.
+- **Avatar.** Username prefilled with `userModel.lastUser`; the matching
+ `userModel` entry supplies `icon`; shown circular with a 2px `accent` ring and
+ an initials fallback when no face exists. `EnableAvatars` is on.
+- **Session selector.** A styled `QtQuick.Controls.Basic` combo bound to
+ `sessionModel` with `index: sessionModel.lastIndex`.
+- **Power bar.** Shutdown, reboot, suspend, hibernate, each enabled from
+ `sddm.canPowerOff` / `canReboot` / `canSuspend` / `canHibernate`, using icons
+ shipped in `theme/icons/` (no system icon theme is guaranteed in the greeter).
+- **Feedback.** `sddm.loginFailed` shakes the card and shows a `red` message;
+ Caps Lock shows a `yellow` hint via the `keyboard.capsLock` property.
+- **Idle hide.** As decision 7.
+- **Style.** 10px radius, 2px `accent` border on the card, `surface0` inputs,
+ accent-on-base for selected states, matching the rofi `common.rasi` identity.
+
+## Repository layout
+
+```
+theme/ the deployable theme, copied verbatim by root
+ Main.qml
+ components/LoginCard.qml
+ components/PowerBar.qml
+ components/Backdrop.qml
+ icons/*.svg
+ theme.conf
+ metadata.desktop
+docs/superpowers/specs/ this document and the implementation plan
+README.md
+LICENSE GPLv2 only
+```
+
+`theme/` as the deployable root keeps the copy honest: no README, LICENSE,
+`.git` or `docs/` leaks into `/usr/share/sddm/themes/udt/`, and no manifest
+listing files by hand.
+
+## UDT wiring
+
+- `bin/udt-accent`: add `write_sddm(name, image)`, called from `main()`
+ alongside `write_qml`. It reads the same `palette.rasi` dict, writes
+ `~/.local/share/udt/sddm-theme.conf` atomically with `chmod 0644` (the
+ existing atomic writer creates 0600). A near-grey wallpaper already falls back
+ to `FALLBACK`, so `accent` is always a real accent.
+- `bin/udt-accent --selftest`: extend with a check that the INI is emitted with
+ every expected key and that rewriting an existing file is idempotent and
+ updates `accent` and `background` in place.
+- `install.sh`: after the existing accent run, print a root block (below).
+ Guarded on this repository existing and on the deploy dir being readable;
+ non-fatal, same style as the waybar block.
+- `udt-palette` is unchanged. Its selftest must stay green.
+
+## Root finalize
+
+Printed by `install.sh`, run by the user as root:
+
+```
+install -d /usr/share/sddm/themes/udt
+cp -r <repo>/theme/. /usr/share/sddm/themes/udt/
+ln -sfn ~/.local/share/udt/sddm-theme.conf /usr/share/sddm/themes/udt/theme.conf.user
+printf '[Theme]\nCurrent=udt\n' > /etc/sddm.conf.d/udt.conf
+```
+
+`udt.conf` sorts after the existing `kde_settings.conf`, so `Current=udt` wins.
+The commands need no `sudo` because the user runs them in a root shell. The
+`cp` refreshes the code; the wallpaper and palette need no root re-run.
+
+## Testing
+
+- `sddm-greeter-qt6 --test-mode --theme <repo>/theme` as the login user: opens
+ a real greeter window. Verifies layout, colours, wallpaper, blur, idle hide,
+ session combo, avatar. Login and power actions are no-ops in this mode.
+- Read the theme as the greeter would:
+ `sudo -u sddm cat /usr/share/sddm/themes/udt/theme.conf.user` and
+ `sudo -u sddm test -r <wallpaper>` confirm the 0755/0700 reachability story
+ before trusting it.
+- `bin/udt-accent --selftest` and `bin/udt-palette --selftest` in UDT.
+- Confirm the rendered INI contains every key in the mapping table.
+
+## Risks
+
+- **A wallpaper outside `~/Pictures` may not be world-readable.** The card then
+ renders on `theme.conf`'s fallback colour. Documented, not defended against.
+- **`uiScreen` names a physical output.** Changing monitor layout or names needs
+ a one-line edit of the generated INI or the theme config; empty falls back to
+ primary, so the default never breaks.
+- **QtQuick.Effects blur depends on a working GL context in the greeter.**
+ Falls back to a flat translucent card, so a failure is cosmetic.
+- **SDDM version drift.** `config.stringValue` needs SDDM 0.20+ and
+ `theme.conf.user` override is documented for 0.20+. This machine is 0.21.0.
+
+## Out of scope
+
+- Bundling or installing fonts (decision 8).
+- A theme for any display manager other than SDDM.
+- Light schemes (`theme.conf` assumes a dark ground, as the rest of UDT does).
+- Per-scheme SDDM artwork; icons are palette-neutral line art.
+
+## Development Approach
+
+This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback.
+
+All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions.
+
+The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability.