// Package autotrack ports the Python passive activity daemon: poll the // frontmost app / window title / browser tab / idle state, coalesce // consecutive same-context samples into segments, and append them to a daily // staging log (~/.local/share/ticktock/activity-YYYY-MM-DD.jsonl). It writes // NOTHING to tock — suggest-then-confirm. package autotrack import ( "net/url" "strings" "time" ) // Ctx is one classified foreground context. JSON tags match the Python // record keys (used by --once output and the winctx helper). type Ctx struct { App string `json:"app"` Title string `json:"title"` URL string `json:"url"` Domain string `json:"domain"` Idle bool `json:"idle"` } // Segment is an open (End zero) or closed span of one activity. type Segment struct { Ctx Start, End time.Time } // awayApps: frontmost one of these ⇒ locked/screensaver ⇒ away. var awayApps = map[string]bool{"loginwindow": true, "ScreenSaverEngine": true} // Away reports whether the raw context is the lock/screensaver window. func Away(raw *Ctx) bool { return raw != nil && awayApps[raw.App] } // Domain extracts the URL's hostname, lowercased, with "www." stripped — // parity with Python's urlparse(url).hostname.replace("www.", ""). func Domain(rawURL string) string { if rawURL == "" { return "" } u, err := url.Parse(rawURL) if err != nil { return "" } return strings.ReplaceAll(strings.ToLower(u.Hostname()), "www.", "") } // Classify maps (idle seconds, foreground context) to the segment context to // record. Locked or an away-app ⇒ idle immediately; idle >= idleGrace ⇒ idle; // otherwise the active context passes through (present-but-passive, e.g. // watching an agent run or being on a call, counts as active). nil when there // is nothing to record. func Classify(idle int, raw *Ctx, idleGrace int, locked bool) *Ctx { if locked || Away(raw) { return &Ctx{Idle: true} } if idle >= idleGrace { return &Ctx{Idle: true} } return raw } // Key identifies "the same activity". Idle is its own key; active is // (app, domain-or-title). The zero Key means no context. Comparable, so the // segmentation loop can use != directly. type Key struct { Valid bool Idle bool App string Rest string // domain, or title for non-browsers } // CtxKey computes the identity key for a classified context. func CtxKey(c *Ctx) Key { if c == nil { return Key{} } if c.Idle { return Key{Valid: true, Idle: true} } rest := c.Domain if rest == "" { rest = c.Title } return Key{Valid: true, App: c.App, Rest: rest} }