aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-09-17-sddm-theme-udt-design.md
blob: 34104cea27f0ef31317cf79099391aef29cc8b70 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# 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.

### 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 <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.