feat(notify): notify-send effector with nop fallback
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
// internal/notify/notify.go
|
||||||
|
|
||||||
|
// Package notify is Keel's desktop-notification effector: a best-effort
|
||||||
|
// "interrupt the user" primitive behind a Notifier port. On Linux with
|
||||||
|
// notify-send installed it pops a toast; everywhere else it is a silent nop.
|
||||||
|
// Mirrors internal/enforce: a pure OS primitive, all policy lives in callers.
|
||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notifier shows a desktop notification. Best-effort: it returns an error for
|
||||||
|
// diagnostics, but callers never block on it and treat failure as "no toast."
|
||||||
|
type Notifier interface {
|
||||||
|
Notify(ctx context.Context, title, body string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotifier returns the notify-send adapter when that binary is on PATH, else
|
||||||
|
// a nop. This makes a missing binary (or a non-Linux host) degrade to silence
|
||||||
|
// rather than an error.
|
||||||
|
func NewNotifier() Notifier {
|
||||||
|
if path, err := exec.LookPath("notify-send"); err == nil {
|
||||||
|
return sendNotifier{bin: path}
|
||||||
|
}
|
||||||
|
return nopNotifier{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendNotifier shells out to notify-send. The app name groups Keel's toasts.
|
||||||
|
type sendNotifier struct{ bin string }
|
||||||
|
|
||||||
|
func (s sendNotifier) Notify(ctx context.Context, title, body string) error {
|
||||||
|
cmd := exec.CommandContext(ctx, s.bin, "--app-name=Keel", title, body)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("notify: notify-send: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nopNotifier is the silent fallback.
|
||||||
|
type nopNotifier struct{}
|
||||||
|
|
||||||
|
func (nopNotifier) Notify(context.Context, string, string) error { return nil }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// internal/notify/notify_test.go
|
||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nopNotifier must be returned when notify-send is absent, and must never error.
|
||||||
|
func TestNopNotifierIsSilentAndSafe(t *testing.T) {
|
||||||
|
n := nopNotifier{}
|
||||||
|
if err := n.Notify(context.Background(), "title", "body"); err != nil {
|
||||||
|
t.Fatalf("nop Notify must return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotifier always returns a usable Notifier (never nil), whatever the host.
|
||||||
|
func TestNewNotifierNeverNil(t *testing.T) {
|
||||||
|
if NewNotifier() == nil {
|
||||||
|
t.Fatal("NewNotifier returned nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user