notify

package
v1.59.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 15, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package notify raises operating-system notifications for pane attention events and routes their activation back into the running TUI.

The package is split so that every file carrying logic is platform-neutral and every platform-specific file is close to logic-free. CI builds only Linux, so a branch behind //go:build windows is never exercised by `dev.sh test` — the same gap that let a statically-dead assignment in browse_roots.go pass every test in the tree.

Index

Constants

View Source
const ActivateHelperName = "quil-activate.exe"

ActivateHelperName is the windowless binary that handles a toast click.

Looked for beside the quil binary being registered, so a dev tree and a real install each register their own copy rather than whichever happens to be on PATH — the same rule findDaemonBinary follows.

View Source
const ActivateLogName = "notify-activate.log"

ActivateLogName is the file every toast click writes one line to.

View Source
const MaxFieldRunes = 120

MaxFieldRunes bounds each rendered field.

Separate from sanitising on purpose: a control-character filter preserves printable non-ASCII byte-identically, so it removes escapes without shortening anything. An unbounded project name would otherwise produce a toast payload Windows truncates or rejects on its own terms, which is a failure the user sees and we cannot explain.

Variables

View Source
var (
	// ErrUnsupported is returned by the setup commands on a platform with no
	// toast support. New() does NOT return it — it returns a nil Notifier, so
	// callers never branch on platform.
	ErrUnsupported = errors.New("notify: not supported on this platform")

	// ErrNotRegistered means the AUMID has no backing Start Menu shortcut, so
	// Windows will refuse to display a toast.
	ErrNotRegistered = errors.New("notify: run 'quil notify setup' first")
)
View Source
var ErrBadURI = errors.New("notify: malformed activation URI")

ErrBadURI rejects anything that is not a well-formed activation URI for the expected scheme.

Functions

func ActivationLogger

func ActivationLogger(dir string) func(string, ...any)

ActivationLogger returns the sink a click handler reports through.

The handler is spawned by Windows, lives for a moment, and — as the windowless helper — has no console at all, so a file is the only place it can speak. It is also the one component in this feature that could previously fail leaving no trace anywhere, which cost several rounds of diagnosis by correlating other processes' timestamps by hand.

dir is passed in rather than resolved here: the TUI binary knows QUIL_HOME from its own build flags, while the helper is told at registration time and has no other way to find it.

Deliberately best-effort and silent on failure. A diagnostic that can break an activation is worse than no diagnostic.

func BuildActivateURI

func BuildActivateURI(scheme string, pid int, paneID string) string

BuildActivateURI renders the toast's launch target.

The PID is what makes activation address the client that RAISED the toast rather than every client attached to the daemon. Several TUIs can be running against one workspace, and the toast belongs to exactly one of them.

func BuildToastXML

func BuildToastXML(n Notification) string

BuildToastXML renders the ToastGeneric payload.

activationType="protocol" is what lets the click reach a registered URI handler with no COM activator — the alternative needs a CLSID and a hand-implemented INotificationActivationCallback vtable, with no CGo available.

func DetachOwnConsole

func DetachOwnConsole()

DetachOwnConsole is a no-op off Windows: nothing here allocates a console window to a process that was launched without a terminal.

func Listen

func Listen(pid int, onActivate func(paneID string)) (io.Closer, error)

func ParseActivateURI

func ParseActivateURI(scheme, raw string) (pid int, paneID string, err error)

ParseActivateURI validates a URI handed to us by the operating system.

Registering a URI scheme makes this reachable by ANY local process, and by a web page behind a browser confirmation — so this is a trust boundary, not a formality. Everything it can produce is a PID and a pane ID that already passed ValidPaneID; there is deliberately no path from here to a command, an argument list or a filesystem path.

func PipeName

func PipeName(pid int) string

func Registered

func Registered(opts Options) (bool, error)

Registered returns (false, nil) rather than the error: it answers a question that has a true answer everywhere ("no"), and returning an error would make every caller special-case a platform that simply has no registration.

func Remove

func Remove(opts Options) ([]string, error)

func RunActivation

func RunActivation(scheme, raw string, logf func(string, ...any))

RunActivation delivers one toast click: it routes that click to the TUI.

Lives here rather than in a command so the two entry points cannot drift. `quil activate` is the original handler and stays for installs whose registry still points at it; `quil-activate` is the windowless helper setup registers now. A click must mean exactly the same thing whichever one Windows launches, and the security ceiling — parse, validate, send a pane id — has to be stated once.

It does NOT bring the terminal window forward, and that is a decision rather than an omission. Clicking a toast hands the foreground to the shell's notification host, which keeps it for as long as it likes — measured on one machine at 5.5 s in one click and over 10 s in the next, released on user input rather than on a timer. Every documented way of taking it back is refused for as long as the shell holds it, and by the time it lets go the user has moved on, so a window appearing then would be an interruption rather than a service. Windows highlights the taskbar button instead, which is its sanctioned way of saying an application wants attention. The pane is already switched and focused by then, so switching to the terminal lands the user exactly where the toast was about to send them.

logf is injected rather than chosen here because the two callers resolve their log directory differently: the TUI binary knows QUIL_HOME from its own build flags, while the helper is told at registration time.

Every path logs, including the ones that would otherwise return silently. A click that reaches a TUI which has since exited is an ordinary, expected outcome — but "nothing happened and nothing was written" is indistinguishable from a handler that never ran, and this feature has cost enough hours to that exact ambiguity.

func SendActivate

func SendActivate(pid int, paneID string) error

func Setup

func Setup(opts Options, exePath, home string) ([]string, error)

Setup, Remove, Registered and ShortcutPath exist on every platform so cmd/quil needs no build tags of its own; they refuse where there is nothing to register.

func ShortcutBaseName

func ShortcutBaseName(opts Options) string

ShortcutBaseName is the Start Menu filename for a variant.

Derived from the AUMID rather than stored beside it: the two must never name different variants, and a dev build writing production's shortcut is exactly the failure .claude/rules/dev-environment.md exists to prevent.

func ShortcutPath

func ShortcutPath(opts Options) (string, error)

func ValidPaneID

func ValidPaneID(id string) bool

ValidPaneID enforces the daemon's own pane-id shape: "pane-" plus exactly eight lowercase hex digits (see isValidHexID, internal/daemon/daemon.go).

Deliberately the STRICT format rather than hookevents.safePaneID's weaker "cannot escape a filepath.Join" rule. That rule exists to protect a path join; this one is the whole guarantee that a registry-reachable URI cannot name anything but a pane.

Types

type Notification

type Notification struct {
	Title  string
	Body   string
	Tag    string // per-pane key, used to withdraw the toast later
	Launch string // activation URI; empty means a toast with no click target
}

Notification is one toast.

Title and Body are expected to have already been through the caller's remote-text sanitiser — internal/tui does that at the build site, matching its render-only rule, since a project name round-trips back to the daemon on rename and must not be rewritten in state. This package escapes and bounds them, and independently drops anything that survived.

type Notifier

type Notifier interface {
	Notify(Notification) error
	Withdraw(tag string) error
	Close() error
}

Notifier raises and withdraws operating-system notifications.

Withdraw is on the interface from the start rather than added later: Windows toasts persist in Action Center indefinitely, so without it, answering a prompt leaves a toast still claiming the pane needs attention.

Both methods QUEUE work and return immediately. The returned error reports only whether the work could be queued — never whether a toast was displayed.

That asymmetry is deliberate and load-bearing. The only TUI caller runs on Bubble Tea's Update goroutine, and a toast is seven cross-process WinRT round-trips to WpnUserService — a shared user service that this package has measured stalling for tens of seconds. Blocking Update on it freezes keystrokes, rendering and every pane in every project, which is the same wedge class as the 2026-06-11/12 PTY-write incidents that put every PTY write behind a per-pane queue. Displays failures are logged, not returned.

Callers that genuinely need the display result — `quil notify test`, whose entire purpose is reporting the real HRESULT — use SyncNotifier instead.

func New

func New(opts Options) (Notifier, error)

New returns a nil Notifier everywhere but Windows.

Nil rather than a no-op struct, and nil rather than an error: the caller stores it in an interface field and checks that field once, so no call site carries a platform branch. Desktop toasts on macOS and Linux are separate, lesser work — no transport there supports click routing, which is this feature's point.

type Options

type Options struct {
	AUMID  string
	Scheme string
}

Options identifies which registration a process should use.

func Variant

func Variant(dev bool) Options

Variant returns the identifiers for a build.

These artifacts are machine-global — a Start Menu shortcut and an HKCU class key — which makes them the first thing Quil writes that QUIL_HOME cannot redirect. Namespacing them by build variant is what lets a dev instance coexist with production the way the daemons already do, and is also what makes the feature testable in dev mode at all.

type SyncNotifier

type SyncNotifier interface {
	NotifySync(Notification) error
	WithdrawSync(tag string) error
}

SyncNotifier is implemented by notifiers that can also wait for a DISPLAY result. Never used from the TUI — see the Notifier doc for why.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL