From b535aae6cb0c7a8fc2670bdd796449b128eff616 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 15 Sep 2026 19:35:20 +0200 Subject: docs: add the appearance sunset/idle/icons design Design for three new appearance tabs: a hyprsunset-qt-equivalent profile editor, a hypridle timeout/enable editor that preserves the file's comments, and an icon/cursor selector with live previews. Records the preview mechanisms probed on this box (GTK icon lookup, hyprcursor .hlc unzip, xcur2png) and scopes the hardcoded-theme propagation out as follow-ups. --- ...26-09-15-appearance-sunset-idle-icons-design.md | 214 +++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-15-appearance-sunset-idle-icons-design.md (limited to 'docs') diff --git a/docs/superpowers/specs/2026-09-15-appearance-sunset-idle-icons-design.md b/docs/superpowers/specs/2026-09-15-appearance-sunset-idle-icons-design.md new file mode 100644 index 0000000..2a43993 --- /dev/null +++ b/docs/superpowers/specs/2026-09-15-appearance-sunset-idle-icons-design.md @@ -0,0 +1,214 @@ +# appearance: Sunset, Idle, Icons tabs + +Three tabs join Wallpaper and Theme in the `appearance/` drawer. Sunset edits +`hyprsunset.conf` the way the user's own `hyprsunset-qt` does, Idle edits the +timeouts and enables of `hypridle.conf`, and Icons switches the icon and cursor +themes with a live preview of each. + +## Why + +The drawer already owns "what the desktop looks like": wallpaper, accent, +scheme. These three are the same kind of choice that currently lives outside it, +in a separate PyQt app (`hyprsunset-qt`), in a text file (`hypridle.conf`), and +in `gsettings` plus half a dozen hardcoded names. Putting them behind the same +Tab key is the point. It is not a rewrite of hyprsunset-qt; it is the same +config, edited from the drawer. + +## Tab bar and IPC + +The bar becomes Wallpaper, Theme, Sunset, Idle, Icons. The Tab key currently +toggles between two tabs; it becomes a cycle over the five. `shell.qml` gains +`sunset`, `idle` and `icons` IPC verbs alongside `wallpaper` and `theme`, so a +keybind or a `qs ipc call` opens any tab directly. `show()` also refreshes the +new lists, the way it already refreshes schemes and wallpapers, because this +shell is autostarted and runs all session. + +## Files + + appearance/Hyprsunset.qml profile parse/serialize, location, apply + appearance/Hypridle.qml listener parse/serialize, apply + appearance/Icons.qml theme lists, preview resolution, apply + appearance/SunsetTab.qml tab UI + appearance/IdleTab.qml tab UI + appearance/IconsTab.qml tab UI + +`AppearancePanel.qml` grows only the tab bar entries and the `Loader` switch. +The singletons follow `Udt.qml` and `Wallpapers.qml`: one `pragma Singleton`, +`Quickshell`/`Quickshell.Io`/`QtQuick`, a `Process` per job, signals for +results. + +## Sunset + +A faithful port of `hyprsunset-qt`'s behaviour, because that app already defines +what "correct" is for this file. + +**Parse.** `~/.config/hypr/hyprsunset.conf`, `profile { ... }` blocks, the same +regexes as `sunset-qt/hyprsunset_qt/config.py`: `time`, `identity` (bool), +`temperature` (int), `gamma` (float). Serialize with the same +`# Managed by hyprsunset-qt. Edits here are overwritten on save.` header and the +same derived `# day profile -- sunrise` / `# night profile -- sunset` comments +(first block with `identity`, first with `temperature`). Writing the identical +format is what lets the Qt app and the drawer edit the same file without either +clobbering the other's meaning. + +**UI.** One row per profile: a time field, an identity checkbox, a temperature +enable + field (1000..20000, default 5500), a gamma enable + field (0..2, step +0.05, default 1.0), and a remove button. An Add button appends `time = 0:00`. + +**Location.** Read and written from the existing +`~/.config/hyprsunset-qt/config` (`[location]` lat/lon/auto_detect, +`[cache] path`, `[daemon] command`), so both tools agree. Detect runs `curl` +against `ip-api.com/json`; Fetch runs `curl` against +`api.sunrise-sunset.org/json?lat=..&lng=..&formatted=0` with the browser +User-Agent `sunset-qt` uses (Cloudflare 403s the default), writes the response +verbatim to the cache path, and sets the day row's time to the local sunrise and +the night row's to the local sunset. Only Fetch refreshes, matching the Qt app. + +**Preview.** `hyprctl hyprsunset temperature|gamma|identity` on the night +profile (first with a temperature), else the first profile. Gamma goes as an +integer percent (`round(gamma * 100)`), the only form the IPC takes. No file +write, no restart. + +**Save.** Validate every profile (time `HH:MM`, temp 1000..20000, gamma 0..2); +on failure show the offending value and stop. On success write the config, save +the location settings, then restart: `pkill -x hyprsunset` followed by a +detached `hyprsunset`. `Quickshell.execDetached` is the detached start, its +`start_new_session` equivalent. + +## Idle + +The commands are fixed; only each listener's timeout and whether it runs are +editable. That is deliberate: a typo in `on-timeout` can suspend the machine, and +the current commands (`notify-send`, `loginctl lock-session`, the Lua +`hl.dsp.dpms` form, `loginctl suspend`) are already correct and are not the thing +being customised. + +**Parse.** Everything before the first `listener {` is kept verbatim, so the +`general` block and the long rationale comment about screensavers survive a +save. Each `listener { ... }` block becomes `{ timeout, on-timeout, on-resume, +enabled }`; the command strings are copied, never edited. + +**Serialize.** The preserved prefix, then the listener blocks. A disabled +listener is written commented out, so the intent stays visible in the file +rather than vanishing. + +**UI.** A row per listener: a description derived from the command ("notify +before lock", "lock session", "monitors off", "suspend"), an enable toggle, and a +seconds field shown as `m:ss`. + +**Save.** Write the file, then restart `hypridle` (`pkill -x hypridle`, +detached start). `hypridle` has no reload signal on this install, so a restart is +the only way to apply, and the notice says the idle timers reset. + +## Icons + +Two sections, icon themes and cursor themes, each with a preview and a current +marker. + +**Lists.** Icon themes are directories with an `index.theme` carrying icon +directories; cursor themes are those with a `cursors/` directory or a +hyprcursor `manifest.hl`, scanned in `~/.icons`, `~/.local/share/icons` and +`/usr/share/icons`. The current values come from `gsettings`. + +**Icon preview.** `Quickshell.iconPath` cannot show a non-active theme: it reads +the platform theme or `QS_ICON_THEME`, fixed at load. So a single `python3 -c` +process runs GTK's `IconTheme.set_custom_theme(name)` plus `lookup_icon` for a +fixed sample set (`folder`, `text-x-generic`, `image-x-generic`, +`network-wireless`, `audio-x-generic`, `video-x-generic`, `battery-full`, +`printer`) over every theme, printing `themenamepath`; the tab renders +those with `IconImage`. One process per tab open, not one per theme. + +**Cursor preview.** Cursor themes here are hyprcursor: a `.hlc` shape is a zip +holding `.svg`, and a shape may instead be a `/` directory with the +same SVG. `unzip -p` extracts `left_ptr` (falling back through `default`, +`pointer`, `hand2`) into the per-shell cache directory and `Image` renders it. +A legacy Xcursor theme (`cursors/left_ptr` binary) goes through `xcur2png`. +Hovering a cursor theme also runs `hyprctl setcursor NAME 24` for a real live +preview, reverted to the current theme on leave and on close. + +**Apply, icons.** `gsettings set org.gnome.desktop.interface icon-theme NAME`, +and rewrite `icon_theme=` in `~/.config/qt6ct/qt6ct.conf` and +`~/.config/qt5ct/qt5ct.conf` when present. Apps need restarting. + +**Apply, cursor.** `hyprctl setcursor NAME 24` now, `gsettings set +org.gnome.desktop.interface cursor-theme NAME` for GTK, and rewrite +`HYPRCURSOR_THEME` / `XCURSOR_THEME` in +`~/.config/hypr/sections/environment.lua` for the next login. Newly started apps +take the env then; a relogin covers the rest. + +## What was verified before writing this + +Against this machine, quickshell 0.3.1: + +- `hyprctl setcursor hypr_bibata-modern-amber 24` returns `ok`. +- `gsettings` reports icon-theme `Material-Black-Plum-Suru` and cursor-theme + `hypr_bibata-modern-amber`; `qt6ct.conf` and `qt5ct.conf` both carry + `icon_theme=Material-Black-Plum-Suru`. +- GTK lookup resolved all eight sample names to absolute SVG paths under + `Material-Black-Plum-Suru`, with correct size directories (status at 48, the + rest at 32). +- A hyprcursor `.hlc` is a zip whose entries are `.svg` and `meta.hl`; + `unzip -p` extracts the SVG. `XCursor-Pro-Dark-Hyprcursor` uses the + directory shape instead. `xcur2png` converted a legacy + `breeze_cursors/cursors/left_ptr` into 11 PNGs. +- `setsid`, `pkill`, `pgrep`, `gsettings`, `hyprctl`, `curl`, `unzip` and + `xcur2png` are all on PATH; `gi` + `Gtk` import in the session's `python3`. +- `hypridle` carries no reload/SIGHUP handling, so a restart is required. +- `hyprsunset-qt`'s profile block format, validation ranges and restart + (`pkill` then a new session) were read from its source, as was the + location/cache INI. +- `hypridle.conf` has all its comments before the first `listener`, and its + `general` block plus four listeners. + +## Traps this has to clear + +Recorded in AGENTS.md and all apply: a config with no visible window exits (the +1x1 keepalive stays); QML has no `String.matchAll` (exec loops); a reused +`Process` needs `running = false` before `running = true`; `qs` is the process +name and `pkill -f` matches the caller, so `-x` only. + +One is new here. Adding a component *file* does not hot-reload: the directory's +`qmldir` is regenerated only when a file that imports it reloads. The three new +tab components need the shell restarted, or `appearance/shell.qml` edited, once +during development. + +A second is specific to editing these files. `hyprsunset.conf` is managed by +hyprsunset-qt and `environment.lua` is the live Hyprland config; both are +user config, not this repo, and writes are confined to the exact lines named +above. The icon theme also appears hardcoded in `wb-icon`, the udt templates, +rofi and the waybar taskbar module, so a switch here does not reach those until +they are unhardcoded, which is a follow-up, not this change. + +## Checks + +Each singleton gets a `selftest` reachable as +`qs -p . ipc call appearance selftest`, in the spirit of `wb-icon --selftest`: +Sunset round-trips a fixture profile block, Idle round-trips a fixture with a +disabled listener and asserts the comments before the first listener survive, +Icons asserts the eight lookups resolve and that a cursor preview file is +produced. Results are logged. `qs -p .` runs the component and the three tabs are +exercised by hand. + +The real verification is a diff: copy `hyprsunset.conf`, `hypridle.conf`, +`qt6ct.conf` and `environment.lua`, save from each tab, and confirm only the +expected lines moved and the parsers reproduce them exactly. A save with no +edits must produce a byte-identical file. + +## Deliberately left out + +- **Unhardcoding the icon theme.** udt `templates/qt-gtk/*`, waybar-theme-udt + `bin/wb-icon` and `modules/extras/taskbar.jsonc`, rofi `config.rasi`, + `hyprqt6engine.conf`, `firefox-clean.desktop`. A README note here and a TODO in + each of those repos, per the decision. +- **Adding or removing idle listeners.** Timeout and enable only. +- **Editing idle commands.** Fixed by design. +- **A location map or manual timezone.** Lat/lon and the API's times only. +- **The Qt apps' own live reload.** Icons apply to the stored config; the notice + says what still needs restarting rather than pretending it is done. + +## Done means + +Tab cycles all five; each new tab opens over IPC; Sunset saves an +`hyprsunset.conf` that `hyprsunset-qt` would write and restarts the daemon; Idle +toggles and retimes listeners while keeping the file's comments; Icons previews and +switches both themes. The user judges the visual result. -- cgit v1.2.3