diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-17 12:35:49 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-17 12:35:49 +0200 |
| commit | 5e3de7cbf39b54114244acbde998364b5b49ea24 (patch) | |
| tree | 75b7edb80af24303e9d2deb4998c4911511eb3f2 | |
| download | sddm-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.md | 290 |
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. |
