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
|
package autotrack
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// stampLayout matches Python isoformat(timespec="seconds"): local time, no
// offset, e.g. 2026-07-07T08:59:19.
const stampLayout = "2006-01-02T15:04:05"
// Record is one on-disk activity line: {start, end, secs, app, title, url,
// domain, idle} โ byte-compatible with the Python daemon's
// json.dumps(..., ensure_ascii=False) output.
type Record struct {
Start, End time.Time
Secs int
App, Title, URL, Domain string
Idle bool
}
// jsonStr encodes s as a JSON string without HTML escaping (parity with
// Python, which writes <, >, & and non-ASCII raw).
func jsonStr(s string) string {
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.SetEscapeHTML(false)
_ = enc.Encode(s) // encoding a plain string cannot fail
return string(bytes.TrimRight(b.Bytes(), "\n"))
}
// EncodeLine renders the record exactly as the Python daemon did, including
// json.dumps' ", " / ": " separators and key order.
func (r Record) EncodeLine() string {
return fmt.Sprintf(
`{"start": %q, "end": %q, "secs": %d, "app": %s, "title": %s, "url": %s, "domain": %s, "idle": %t}`,
r.Start.Format(stampLayout), r.End.Format(stampLayout), r.Secs,
jsonStr(r.App), jsonStr(r.Title), jsonStr(r.URL), jsonStr(r.Domain), r.Idle)
}
// LogPath is the daily staging log for the given moment under dir.
func LogPath(dir string, when time.Time) string {
return filepath.Join(dir, "activity-"+when.Format("2006-01-02")+".jsonl")
}
// WriteSegment appends seg to its start-day's log under dir, creating dir if
// needed. Segments shorter than minSecs are dropped as noise. nil is a no-op.
func WriteSegment(dir string, seg *Segment, minSecs int) error {
if seg == nil {
return nil
}
secs := seg.End.Sub(seg.Start).Seconds()
if secs < float64(minSecs) {
return nil
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
rec := Record{
Start: seg.Start, End: seg.End, Secs: int(secs),
App: seg.App, Title: seg.Title, URL: seg.URL, Domain: seg.Domain, Idle: seg.Idle,
}
f, err := os.OpenFile(LogPath(dir, seg.Start), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(rec.EncodeLine() + "\n")
return err
}
|