aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-10-19-notification-settings-design.md
blob: 9de79e5c3972b8a6323aa76ea743901e2d383239 (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
# Notification settings page

A settings window page for the notification daemon (`notifyd`, separate repo)
and the balloon shell (`notifications/`). Today nothing is configurable: the
daemon hardcodes its timeouts and caps, the balloon shell hardcodes DP-1
bottom-right, and DND always passes critical only.

Snooze is out of scope: the status tile already has a snooze switch with a
remembered minutes field.

## One file, three readers

`~/.config/notifyd/config.json`. A missing file, a missing key, or a value of
the wrong type reads as the default below, so the defaults reproduce today's
behaviour exactly.

```json
{
  "timeouts": { "low": 10, "normal": 10, "critical": 0 },
  "liveCap": 20,
  "historyCap": 20,
  "balloons": { "screen": "DP-1", "corner": "bottom-right" },
  "dndPassCritical": true,
  "muted": []
}
```

- `timeouts` are seconds, used only when the client sends `expire_timeout -1`.
  `0` means never expires. A client's explicit timeout still wins, as the
  freedesktop spec requires.
- `liveCap`, `historyCap`: 1..100. Out of range clamps.
- `balloons.screen`: a monitor name. An unknown name falls back to the first
  screen, the existing pattern.
- `balloons.corner`: `top-left`, `top-right`, `bottom-left`, `bottom-right`.
- `dndPassCritical`: under DND, whether critical balloons still show. Low and
  normal are always withheld under DND.
- `muted`: app names, matched exactly against the queue entry's `app`. A muted
  app gets no balloon; it still appears in the drawer's reserved space and in
  history (suppression lives in the balloon shell, see `notifications/README.md`).

Readers:

| reader | keys | picks up a change |
| --- | --- | --- |
| notifyd | `timeouts`, `liveCap`, `historyCap` | `notifyctl reload` |
| balloon shell | `balloons`, `dndPassCritical`, `muted` | watched `FileView` |
| settings page | all | watched `FileView` |

The daemon ignores keys it does not use, and the shells do too, so either side
can grow keys alone. A write preserves unknown keys (same rule as `Prefs.qml`).

## notifyd changes

- `internal/notify/config.go`: `Config` struct, `DefaultConfig()`,
  `LoadConfig(path) Config`. Malformed JSON logs and returns the defaults: a
  hand typo must never stop the daemon. Path is
  `$XDG_CONFIG_HOME/notifyd/config.json`, falling back to `~/.config`.
- `DefaultTimeoutMS` takes the config's timeouts instead of constants.
- `Store` reads `liveCap`/`historyCap` from fields set at construction and by
  `SetCaps`. A lowered cap trims at the next add, not at reload: a reload must
  not silently drop live notifications.
- `control.Reload()` D-Bus method re-reads the file and applies it under the
  store's lock; `notifyctl reload` drives it. A missing daemon makes
  `notifyctl` exit non-zero, which the page reports.
- `config_test.go`: defaults on missing file, defaults on malformed JSON,
  partial file merges over defaults, cap clamping.

## Quickshell changes

- `shared/Notify.qml` gains `config`, parsed from a watched `FileView` with the
  defaults applied, so both shells see the same resolved values.
- `notifications/Balloons.qml`: screen and anchors from
  `Notify.config.balloons`; the column grows away from the anchored edge
  (top corners stack downward). The balloon visibility check adds the mute
  list and `dndPassCritical`.
- `desktop/modules/notifications/`: a `Module` in group "Desktop", settings
  only, with a `SaveBar` (draft, save, discard), like HyprIdle. Rows:
  - Timeouts: a seconds field and a "never" switch per urgency.
  - Caps: two number fields.
  - Placement: screen chooser from `Quickshell.screens`, a four-corner picker.
  - Suppression: "Critical passes DND" switch; muted apps as removable chips,
    added from the app names in the current queue and history, or typed.
- Save writes the file through a `Process` running `cat > <file>` (the
  `notes` pattern: the exit code is the outcome) after `mkdir -p`, then runs
  `notifyctl reload`. If reload fails the page says the file was saved but the
  daemon was not reached.
- Search keywords cover timeout, history, mute, dnd, corner, monitor.

Editing `shared/Notify.qml` needs a forced reload of both shells (hot reload
does not follow the symlink), and adding the module directory needs
`desktop/shell.qml` touched or a restart.

## Docs

- notifyd `README.md`: the config file and `notifyctl reload`.
- `notifications/README.md`: placement, mute and DND now come from the file.
- `AGENTS.md`: the settings list in "What this is" gains notifications.

## Verification

- `go test ./...` in notifyd.
- `notify-send` with a 3s low timeout configured: balloon leaves at 3s.
- Muted app: no balloon, present in drawer list.
- Corner and screen changes: by eye, user's screen.