From 0daf8c2849844ac66bcea8349b9c733f98feff1e Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Tue, 2 Jun 2026 12:30:16 -0400 Subject: [PATCH] Add Windows polling active-window sensor --- internal/evidence/source_other.go | 2 +- internal/evidence/windows.go | 51 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 internal/evidence/windows.go diff --git a/internal/evidence/source_other.go b/internal/evidence/source_other.go index d9edd74..5cec967 100644 --- a/internal/evidence/source_other.go +++ b/internal/evidence/source_other.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build !linux && !windows package evidence diff --git a/internal/evidence/windows.go b/internal/evidence/windows.go new file mode 100644 index 0000000..79aae4f --- /dev/null +++ b/internal/evidence/windows.go @@ -0,0 +1,51 @@ +//go:build windows + +package evidence + +import ( + "context" + "time" + + "antidrift/internal/winapi" +) + +// pollInterval is how often the Windows sensor samples the foreground window. +// ~1s latency on a window switch is immaterial for a focus tracker, and polling +// avoids the message-loop/callback machinery a SetWinEventHook source needs. +const pollInterval = 750 * time.Millisecond + +// NewSource returns the Windows active-window sensor (polling). +func NewSource() Source { return windowsSource{} } + +type windowsSource struct{} + +// Watch emits the current window immediately, then samples every pollInterval, +// emitting only when the foreground window or its title changes. A read with no +// foreground window yields an Unavailable snapshot (once, until it recovers). It +// runs until ctx is cancelled. It never panics the daemon. +func (windowsSource) Watch(ctx context.Context, onChange func(WindowSnapshot)) { + var tr foregroundTracker + poll := func() { + hwnd, title, class, ok := winapi.ForegroundWindow() + if !tr.changed(ok, hwnd, title) { + return + } + if !ok { + onChange(WindowSnapshot{Health: EvidenceHealth{Available: false, Reason: "no foreground window"}}) + return + } + onChange(WindowSnapshot{Title: title, Class: class, Health: EvidenceHealth{Available: true}}) + } + + poll() // immediate current window + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + poll() + } + } +}