fix(evidence): X11 sensor polls so tab/title changes are tracked
The X11 source only re-read the active window on _NET_ACTIVE_WINDOW changes, so switching a browser tab — which changes the window title but not the active window — was invisible. All the time on the new tab was credited to the stale title (e.g. reading a Consume article showed up as time on the Keel tab). Mirror the Windows sensor: poll the active window every 750ms and emit only when the window or its title changes. A shared, display-free pollLoop carries the change-dedup and is unit-tested for the exact regression (a title-only change must emit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package evidence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// pollLoop samples the active window via read and forwards snapshots through
|
||||
// onChange, but only when the observation changes — a different window OR a
|
||||
// different title. The title case is the one that matters: switching a browser
|
||||
// tab changes the window title without changing the active window, so a sensor
|
||||
// that re-reads only on active-window events never sees it and credits the stale
|
||||
// title. Polling re-reads on a fixed cadence, so a tab switch is caught within
|
||||
// one interval.
|
||||
//
|
||||
// It emits once immediately, then samples every interval, until ctx is
|
||||
// cancelled. WindowSnapshot is comparable, so the change check is a plain
|
||||
// equality. Factoring the loop here keeps the change-dedup identical across
|
||||
// sensors and lets it be tested without a display. (The Windows sensor predates
|
||||
// this and keeps its own equivalent loop.)
|
||||
func pollLoop(ctx context.Context, interval time.Duration, read func() WindowSnapshot, onChange func(WindowSnapshot)) {
|
||||
var last WindowSnapshot
|
||||
var haveLast bool
|
||||
emit := func() {
|
||||
s := read()
|
||||
if haveLast && s == last {
|
||||
return
|
||||
}
|
||||
last, haveLast = s, true
|
||||
onChange(s)
|
||||
}
|
||||
|
||||
emit() // immediate current window
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
emit()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user