682eb603fa
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>
45 lines
1.3 KiB
Go
45 lines
1.3 KiB
Go
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()
|
|
}
|
|
}
|
|
}
|