summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-07-17-hyprsunset-qt-design.md
blob: 558ac15aacbad101c532445e93af9b34712e6e31 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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.