# 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//.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 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. ### Security: the user-writable file `/usr/share/sddm/themes/udt/theme.conf.user` is a symlink into the login user's home. This was raised during design and deliberately accepted. The reasoning: - **No code execution.** The file is INI parsed by `QSettings`. The only QML the greeter runs is root-owned. - **The reader is already untrusted.** SDDM runs the greeter as the unprivileged `sddm` user and reaches the privileged helper over a socket that exposes only login. A crafted config value does not yield root. - **Only the owner can write it.** The file is owned by the login user; no other local account can write it, and that user is already the machine owner. Accepted residual risks, both recorded rather than defended against: - **A path lever.** The writable file carries `background`, so it can point the greeter's `Image` at any file the `sddm` user can read. The worst case is an arbitrary image decode on the login screen. - **Root scripts must not follow the link.** `rm -rf` and `chown -R` do not follow symlinks, but `echo >`, `sed -i` and some archive tools do. The printed root block uses only `ln -sfn` and `cp`, which do not write through the link. Any future root command touching this path must keep that property. The stricter alternative, a root-created drop-box with an accent-only writable file, was offered and declined in favour of the simpler spec. ### 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 /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 /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 ` 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.