▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0
2.5 KB raw
 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}
}