Files
talk/internal/notify/protocol_windows.go
m3ta-chironandClaude Fable 5.1 00eb76128b feat(notify): Cancel button on Windows toasts via the talk: protocol handler
Windows toasts cannot call back into the daemon, so the Cancel button opens
a talk: link instead: `talk:cancel?id=<id>&port=<port>`. The daemon
registers the talk: URL protocol for the current user (HKCU, no admin) and
points it at `talk uri "%1"`; Windows starts that on the click, and `talk
uri` posts to the new endpoint POST /cancel-item, which cancels only the
announcement with that id (queue.CancelItem). The toast is tagged and
removed again when the announcement ends. For now the toast is shown under
Windows PowerShell's app identity, which unpackaged programs can use
without a Start Menu shortcut.

Verified on the Windows 11 VM: registry entry written by the daemon,
Start-Process "talk:cancel?id=…&port=7800" (what the button does) stops
the playing announcement within two seconds, no toast errors in the log.

Tests: `talk uri` against a real handler on a loopback port (playing vs
waiting id, dead daemon, talk:test, garbage links), POST /cancel-item
process test (only the playing item, waiting one plays next, 400 without
id).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 12:14:38 +02:00

37 lines
1.2 KiB
Go

//go:build windows
package notify
import (
"fmt"
"golang.org/x/sys/windows/registry"
)
// RegisterProtocol registers the talk: URL protocol for the current user
// (HKCU\Software\Classes\talk, no admin rights needed) so that a toast
// button with a talk: link starts `<exe> uri <link>`. Idempotent; the
// command is rewritten on every call so it follows the binary's location.
func RegisterProtocol(exe string) error {
root, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\talk`, registry.SET_VALUE)
if err != nil {
return fmt.Errorf("registering talk: protocol: %w", err)
}
defer root.Close()
if err := root.SetStringValue("", "URL:talk protocol"); err != nil {
return fmt.Errorf("registering talk: protocol: %w", err)
}
if err := root.SetStringValue("URL Protocol", ""); err != nil {
return fmt.Errorf("registering talk: protocol: %w", err)
}
cmdKey, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\talk\shell\open\command`, registry.SET_VALUE)
if err != nil {
return fmt.Errorf("registering talk: protocol: %w", err)
}
defer cmdKey.Close()
if err := cmdKey.SetStringValue("", fmt.Sprintf(`"%s" uri "%%1"`, exe)); err != nil {
return fmt.Errorf("registering talk: protocol: %w", err)
}
return nil
}