aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-07-17 09:08:55 +0200
committerDanilo M. <danix@danix.xyz>2026-07-17 09:08:55 +0200
commit2f5ef62a48f6d8543b193a5ae0e663c87f601712 (patch)
treebc9d26441b65bb3442e048dbfcf06799484f9f88 /docs/superpowers
downloadhyprsunset-qt-2f5ef62a48f6d8543b193a5ae0e663c87f601712.tar.gz
hyprsunset-qt-2f5ef62a48f6d8543b193a5ae0e663c87f601712.zip
Add design spec for hyprsunset-qt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/specs/2026-07-17-hyprsunset-qt-design.md203
1 files changed, 203 insertions, 0 deletions
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/<sig>/.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 <n>` / `gamma <n>` / `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=<lat>&lng=<lon>&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.