feat(notify): desktop notification per announcement with a Cancel button

Every playing announcement now shows a desktop notification with its text
and, on Linux, a Cancel button (Abbrechen for German) that stops exactly
that announcement; the notification is closed when the announcement ends.
This brings back what the old talk.sh had with notify-send --action, as a
default: a long announcement can be cut short without a terminal.

Implementation: the freedesktop Notifications service on the session bus
via godbus (pure Go): one connection per process, Notify with the action
list, ActionInvoked dispatched to the announcement, CloseNotification at
the end, expire_timeout 0 so the button stays for the whole announcement.
Without a session bus the notify-send fallback shows a plain notification;
macOS and Windows show one without a button for now. queue.CancelItem(id)
cancels only the playing item. Config [notify] on_speak (default true, env
TALK_NOTIFY). `talk test --toast` now shows the real thing and waits for a
click; `talk doctor` names the service and whether it supports the button.

Config hygiene found on the way: a key written below a [table] header
belongs to that table in TOML (lang under [voice] was silently ignored).
config.Load now collects unknown keys (undecoded ones and non-language
keys under [voice.<provider>]) and `talk doctor` warns with the hint.

Tests: process test on a private dbus-daemon with a fake
org.freedesktop.Notifications service (Notify payload, cancel click aborts
the synthesis, CloseNotification), queue.CancelItem, config keys. All test
binaries point DBUS_SESSION_BUS_ADDRESS at a dead socket so no test ever
pops a notification on the developer's desktop. flake vendorHash updated
for the new dependency.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-24 12:10:49 +02:00
1 parent 6f3e7c9f35
commit 83a15af9c5
22 files changed
+806 -45

No files matched your search

+27 -8
View File
@@ -78,9 +78,9 @@ Keys never go into config.toml.
- **`talk test`** speaks a fixed test sentence directly in this process (not
via the daemon; mute and quiet hours do not apply here, an active mute is
mentioned) and shows which tier spoke and how long synthesis and playback
took. `talk test --toast` sends a test desktop notification through the
same path `quiet_mode = toast` uses, so you can check notifications
without a quiet window or a daemon restart.
took. `talk test --toast` shows a test notification with the Cancel button
and waits for a click, so you can check the notification setup without
speaking anything.
- **`talk voices`** lists the curated voice catalog (ElevenLabs premade,
OpenAI, Edge neural voices per locale) as a table; `*` marks the voice
talk uses without `--voice` (config `voice.<provider>.<lang>` or the
@@ -95,6 +95,26 @@ Keys never go into config.toml.
very end. Without a terminal (pipe, agent) setup asks nothing and only
shows the effective configuration.
### Notifications with a Cancel button
Every announcement that plays also shows a desktop notification with its
text and, on Linux, a **Cancel** button (German: Abbrechen) that stops that
announcement without a terminal. The notification disappears when the
announcement ends. talk talks to the freedesktop notification service on the
session bus directly (any daemon with the "actions" capability: mako, dunst,
GNOME Shell, KDE, quickshell); without a session bus it falls back to
notify-send, on macOS to Notification Center and on Windows to a toast, all
without a button. Turn it off with:
```toml
[notify]
on_speak = false # env TALK_NOTIFY=false
```
`talk test --toast` shows one such notification, waits a few seconds for a
click on Cancel and reports what happened; `talk doctor` names the service
and whether it supports the button.
### Language
talk detects German and English per announcement (word lists plus umlauts;
@@ -183,11 +203,10 @@ hours are opt-in like mute). During quiet hours audio is dropped, also for
high (burst is a rate right, not a quiet right):
- `quiet_mode = "discard"` (default): drop silently.
- `quiet_mode = "toast"`: a desktop notification instead of audio, through
the system's own tools (Linux `notify-send`, macOS Notification Center,
Windows toast), best effort: if the tool is missing, only the log line
remains and `talk doctor` shows it under "Toast". Try it with
`talk test --toast`.
- `quiet_mode = "toast"`: a desktop notification instead of audio (the
notification service or the system's own tool, see "Notifications"), best
effort: if nothing is available, only the log line remains and
`talk doctor` shows it under "Toast".
- `now:true` **breaks out of quiet hours too** (a deliberate interruption).
**Length and age:** two more laws against what prompts like to do, reading