From 2f5ef62a48f6d8543b193a5ae0e663c87f601712 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 17 Jul 2026 09:08:55 +0200 Subject: Add design spec for hyprsunset-qt Co-Authored-By: Claude Opus 4.8 --- .../specs/2026-07-17-hyprsunset-qt-design.md | 203 +++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-17-hyprsunset-qt-design.md (limited to 'docs/superpowers/specs') diff --git a/docs/superpowers/specs/2026-07-17-hyprsunset-qt-design.md b/docs/superpowers/specs/2026-07-17-hyprsunset-qt-design.md new file mode 100644 index 0000000..558ac15 --- /dev/null +++ b/docs/superpowers/specs/2026-07-17-hyprsunset-qt-design.md @@ -0,0 +1,203 @@ +# hyprsunset-qt — Design Spec + +Date: 2026-07-17 + +A small Qt6 GUI to edit `~/.config/hypr/hyprsunset.conf` and control the +`hyprsunset` daemon, with internet-fetched sunrise/sunset times for accurate +per-location scheduling. + +## Goals + +- Manage hyprsunset profiles (add/remove/edit) through a GUI. +- Fetch accurate sunrise/sunset times for the user's location and drop them + into the day/night profiles. +- Apply changes by restarting the daemon; offer live preview via IPC before + saving. +- Minimal dependencies: PyQt6 plus the Python standard library only. + +## Non-Goals + +- No config-format preservation of hand edits (comments/whitespace are + regenerated, see Config Model). +- No automatic/scheduled sunset refresh. Refetch is manual only. +- No packaging/distribution work in this spec (SlackBuild etc. is separate). + +## Stack + +- Language: Python 3, single file where practical. +- GUI: PyQt6 (Riverbank, GPL) — aligns with the project's GPLv2 license. +- Everything else from stdlib: `urllib.request`, `json`, `subprocess`, + `configparser`, `re`, `datetime`, `zoneinfo`, `pathlib`. + +## Architecture + +One Python file, internally split by concern: + +1. **Config model** — parse/serialize `hyprsunset.conf`. +2. **App settings** — load/write `~/.config/hyprsunset-qt/config`. +3. **Daemon control** — restart, live preview, status. +4. **Sun fetch** — geolocation + sunrise-sunset lookup, cached. +5. **UI** — PyQt6 main window wiring the above. + +Pure-logic units (1–4, minus the network/subprocess edges) are testable +without Qt or network. + +## Config Model + +Target file: `~/.config/hypr/hyprsunset.conf` (hyprlang `profile { ... }` +blocks). + +`Profile` (dataclass): +- `time: str` — `HH:MM` +- `identity: bool` +- `temperature: int | None` +- `gamma: float | None` + +Functions: +- `parse_config(text) -> list[Profile]` — regex over `profile { ... }` blocks, + read `time`, `identity`, `temperature`, `gamma` keys. +- `serialize(profiles) -> str` — **regenerate clean**. Emit a header comment + and a per-block comment tagging day/night for a human reader. Hand-edited + comments/whitespace are not preserved. + +**Day/night tagging** is inferred, not stored: the profile with +`identity = true` is the *day* profile (sunrise target); the profile with +`temperature` set is the *night* profile (sunset target). No extra state file. + +Example serialized output: + +``` +# Managed by hyprsunset-qt. Edits here are overwritten on save. + +# day profile — sunrise +profile { + time = 5:00 + identity = true +} + +# night profile — sunset +profile { + time = 19:40 + temperature = 5500 + gamma = 0.8 +} +``` + +## App Settings + +Path: `~/.config/hyprsunset-qt/config` (INI via `configparser`). Created with +defaults on first run. Lives in `.config` (not `.cache`) because the user's +`~/.cache` is wiped at reboot and would never persist. + +```ini +[location] +lat = 45.07 +lon = 7.68 +auto_detect = true ; if false, skip IP geolocation and use lat/lon above + +[cache] +path = ~/.config/hyprsunset-qt/sun.json ; sunset JSON cache location + +[daemon] +command = hyprsunset ; launch command, override if needed +``` + +- `~` is expanded on read. +- lat/lon double as manual override and last-known values. +- With `auto_detect = false`, no IP geolocation call is made (privacy). +- `cache.path` and `daemon.command` are advanced keys, edited by hand in the + file, not surfaced in the UI. lat/lon and auto_detect are edited via UI + fields and written back. + +`ponytail:` flat INI, no schema validation lib — add validation only if values +start breaking things. + +## Daemon Control + +hyprsunset v0.3.3: has an IPC socket +(`$XDG_RUNTIME_DIR/hypr//.hyprsunset.sock`) driven by +`hyprctl hyprsunset temperature|gamma|identity`, but **no config reload**. +Config is read only at daemon startup, so applying config changes requires a +restart. + +- `restart()` — `pkill hyprsunset`, then launch `[daemon].command` detached + (`subprocess.Popen`). Matches the bare `hl.exec_cmd("hyprsunset")` in the + user's Hyprland autostart. +- `live_preview(temperature, gamma, identity)` — push values via + `hyprctl hyprsunset temperature ` / `gamma ` / `identity`. Instant, no + restart, no file write. For testing a look before saving. +- `is_running()` — `pgrep hyprsunset` to show status and gate the Restart + button. + +Note: `hyprctl hyprsunset gamma`/`temperature` take/return integer percent and +Kelvin respectively; live preview maps the profile's gamma (0–2 float) to the +IPC's expected units. + +## Sun Fetch + +- `geolocate() -> (lat, lon)` — GET `http://ip-api.com/json` (free, no key), + read `lat`/`lon`. On failure, leave fields for manual entry. +- `fetch_sun(lat, lon) -> dict` — GET + `https://api.sunrise-sunset.org/json?lat=&lng=&formatted=0`. + Returns UTC ISO timestamps. Convert `sunrise`/`sunset` to local `HH:MM` using + `datetime` + `zoneinfo` (system local zone). +- **Cache** — write the raw response JSON plus the queried lat/lon to + `[cache].path`. Reused on app open. **Manual refresh only**: refetch happens + on [Fetch sun times] click or when lat/lon changes, never automatically. +- **Apply**: sunrise -> day profile `time`, sunset -> night profile `time`. + +**Attribution**: credit sunrise-sunset.org in the README (their usage terms +request it). + +Error handling: any network failure shows an error in a status label, keeps the +last cache, and never crashes. With no internet, all profile times remain fully +hand-editable. + +## UI Layout + +Single main window, top to bottom: + +``` +Daemon: ● running [Restart] +------------------------------------------------ +Location + Lat [45.07] Lon [7.68] [ ] auto [Detect] + [Fetch sun times] sunrise 05:12 sunset 19:40 +------------------------------------------------ +Profiles [+ Add] + time [05:00] [x] identity [ ] temp [ ] gamma [X] + time [19:40] [ ] identity [x] temp [5500] [x] gamma [0.8] [X] +------------------------------------------------ +[Live preview >] [Save + Restart] +``` + +- Each profile is a row widget: a time field, checkboxes toggling + identity/temperature/gamma (each enabling its spinbox), and a remove `[X]`. + `[+ Add]` appends a blank profile. +- **Live preview** pushes the selected profile's temp/gamma/identity via + `hyprctl`, without writing or restarting. +- **Save + Restart** validates all -> serializes -> writes conf -> restarts + daemon -> updates status. + +### Validation + +- `time` matches `HH:MM` (00:00–23:59). +- `temperature` integer, 1000–20000 K. +- `gamma` float, 0.0–2.0. +- Invalid field is marked (red) and blocks save. + +## Testing + +TDD for pure logic (no Qt, no network): +- `parse_config` / `serialize` round-trip. +- sunset/sunrise UTC -> local `HH:MM` conversion. +- settings load, defaults on first run, `~` expansion. +- cache read/write. + +Network, daemon, and Qt layers are thin wrappers — mocked or manually verified, +not unit-tested. + +## Open Questions + +None blocking. Advanced-key UI exposure and validation strictness can evolve +after first use. -- cgit v1.2.3