aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-15 19:35:20 +0200
committerDanilo M. <danix@danix.xyz>2026-09-15 19:35:20 +0200
commitb535aae6cb0c7a8fc2670bdd796449b128eff616 (patch)
tree67d1aac6504a1c6a3315f1b12690c2eb4d1cf153 /docs
parent9c65aeb157fe16ab5a651f39c92902440f2d5344 (diff)
downloadquickshell-b535aae6cb0c7a8fc2670bdd796449b128eff616.tar.gz
quickshell-b535aae6cb0c7a8fc2670bdd796449b128eff616.zip
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.
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/specs/2026-09-15-appearance-sunset-idle-icons-design.md214
1 files changed, 214 insertions, 0 deletions
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 `theme<TAB>name<TAB>path`; 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 `<name>.svg`, and a shape may instead be a `<name>/` 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 `<name>.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.