1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
// 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}
}
|