Add enforce.Guard port with X11 and no-op adapters
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user