From 7fdcae5d14ac6143544d8e5b0285ab7da3d7ebf9 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Mon, 1 Jun 2026 12:33:02 -0400 Subject: [PATCH] Add enforce.Guard port with X11 and no-op adapters Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/enforce/enforce.go | 16 +++++++++ internal/enforce/enforce_test.go | 14 ++++++++ internal/enforce/guard_other.go | 12 +++++++ internal/enforce/x11.go | 43 ++++++++++++++++++++++++ internal/enforce/x11_integration_test.go | 23 +++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 internal/enforce/enforce.go create mode 100644 internal/enforce/enforce_test.go create mode 100644 internal/enforce/guard_other.go create mode 100644 internal/enforce/x11.go create mode 100644 internal/enforce/x11_integration_test.go diff --git a/internal/enforce/enforce.go b/internal/enforce/enforce.go new file mode 100644 index 0000000..1413459 --- /dev/null +++ b/internal/enforce/enforce.go @@ -0,0 +1,16 @@ +// Package enforce makes drift cost something at the OS level. Tier A minimizes +// the active window when the session is enforcing and the drift judge has +// confirmed the window is off-task. The Guard is a pure OS primitive; all +// policy — whether and when to enforce — lives in the session controller. +package enforce + +import "context" + +// Guard performs OS-level enforcement actions on demand. +type Guard interface { + // MinimizeActive minimizes the currently-focused window. It is idempotent + // (minimizing an already-minimized window is harmless) and best-effort: it + // returns an error for diagnostics, but callers never block on it and treat + // failure as "enforcement did nothing this time." + MinimizeActive(ctx context.Context) error +} diff --git a/internal/enforce/enforce_test.go b/internal/enforce/enforce_test.go new file mode 100644 index 0000000..864d3bd --- /dev/null +++ b/internal/enforce/enforce_test.go @@ -0,0 +1,14 @@ +package enforce + +import "testing" + +// NewGuard must return a usable Guard on every platform (real on linux, no-op +// elsewhere). We assert non-nil only: calling MinimizeActive here would touch a +// real X server on linux, which the integration test covers under a DISPLAY +// guard. The behavioural contract is exercised in the session package via a fake +// Guard. +func TestNewGuardReturnsUsableGuard(t *testing.T) { + if g := NewGuard(); g == nil { + t.Fatal("NewGuard returned nil") + } +} diff --git a/internal/enforce/guard_other.go b/internal/enforce/guard_other.go new file mode 100644 index 0000000..d009114 --- /dev/null +++ b/internal/enforce/guard_other.go @@ -0,0 +1,12 @@ +//go:build !linux + +package enforce + +import "context" + +// NewGuard returns a no-op guard on platforms without the X11 adapter. +func NewGuard() Guard { return noopGuard{} } + +type noopGuard struct{} + +func (noopGuard) MinimizeActive(context.Context) error { return nil } diff --git a/internal/enforce/x11.go b/internal/enforce/x11.go new file mode 100644 index 0000000..1ed1222 --- /dev/null +++ b/internal/enforce/x11.go @@ -0,0 +1,43 @@ +//go:build linux + +package enforce + +import ( + "context" + "fmt" + + "github.com/jezek/xgbutil" + "github.com/jezek/xgbutil/ewmh" + "github.com/jezek/xgbutil/icccm" +) + +// NewGuard returns the real X11 window-minimize guard. +func NewGuard() Guard { return x11Guard{} } + +type x11Guard struct{} + +// MinimizeActive iconifies the currently-focused window by sending an ICCCM +// WM_CHANGE_STATE -> IconicState client message to the root window. It opens a +// short-lived X connection per call: enforcement fires at most once per drift +// observation (which is debounce-gated upstream), so there is no shared +// connection or event loop to manage, and nothing to race on shutdown. Any X +// failure is returned for the caller to log; the caller never blocks on it. +func (x11Guard) MinimizeActive(_ context.Context) error { + X, err := xgbutil.NewConn() + if err != nil { + return fmt.Errorf("enforce: cannot connect to X server: %w", err) + } + defer X.Conn().Close() + + active, err := ewmh.ActiveWindowGet(X) + if err != nil { + return fmt.Errorf("enforce: no active window: %w", err) + } + if active == 0 { + return nil // nothing focused; nothing to minimize + } + if err := ewmh.ClientEvent(X, active, "WM_CHANGE_STATE", icccm.StateIconic); err != nil { + return fmt.Errorf("enforce: minimize request failed: %w", err) + } + return nil +} diff --git a/internal/enforce/x11_integration_test.go b/internal/enforce/x11_integration_test.go new file mode 100644 index 0000000..bdd0dd2 --- /dev/null +++ b/internal/enforce/x11_integration_test.go @@ -0,0 +1,23 @@ +//go:build linux + +package enforce + +import ( + "context" + "os" + "testing" + "time" +) + +func TestX11GuardMinimizeActiveDoesNotPanic(t *testing.T) { + if os.Getenv("DISPLAY") == "" { + t.Skip("no DISPLAY; skipping live X11 minimize smoke test") + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + // Either it minimizes the active window or returns an error (e.g. no active + // window); we only assert it returns without panicking. + if err := NewGuard().MinimizeActive(ctx); err != nil { + t.Logf("MinimizeActive returned (acceptable): %v", err) + } +}