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 }