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