▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0

feat(autotrack): Run poll loop with SIGTERM/SIGINT segment flush

b44c48ebf976ecfed2cac22296e9054f24eb94a1
Kevin Kortum <kevinkortum@me.com> · 2026-07-08 12:49

parent 77789174

2 files changed

internal/autotrack/run.go +39 −0
@@ -0,0 +1,39 @@
+package autotrack
+
+import (
+	"os"
+	"time"
+
+	"ticktock/internal/config"
+)
+
+// Run executes the poll loop: sample → Classify → Step → WriteSegment →
+// sleep, until a signal arrives on sigc; then it flushes the open segment and
+// returns (parity with the Python daemon's SIGTERM/SIGINT flush). sample is
+// injected (ReadSample in production) so the loop is testable.
+func Run(dataDir string, tr config.Tracking, sample func() Sample, sigc <-chan os.Signal) {
+	var cur *Segment
+	var curKey Key
+	poll := time.Duration(tr.PollSecs) * time.Second
+	for {
+		now := time.Now()
+		s := sample()
+		hard := s.Locked || Away(s.Raw)
+		ctx := Classify(s.Idle, s.Raw, tr.IdleGraceSecs, s.Locked)
+		var emitted *Segment
+		cur, curKey, emitted = Step(cur, curKey, now, s.Idle, ctx, tr.IdleGraceSecs, hard)
+		if emitted != nil {
+			_ = WriteSegment(dataDir, emitted, tr.MinSecs) // an IO hiccup must not kill the loop
+		}
+		select {
+		case <-sigc:
+			if cur != nil {
+				closed := *cur
+				closed.End = time.Now()
+				_ = WriteSegment(dataDir, &closed, tr.MinSecs)
+			}
+			return
+		case <-time.After(poll):
+		}
+	}
+}
internal/autotrack/run_test.go +39 −0
@@ -0,0 +1,39 @@
+package autotrack
+
+import (
+	"os"
+	"strings"
+	"syscall"
+	"testing"
+	"time"
+
+	"ticktock/internal/config"
+)
+
+func TestRunFlushesOpenSegmentOnSignal(t *testing.T) {
+	dir := t.TempDir()
+	sigc := make(chan os.Signal, 1)
+	sample := func() Sample {
+		return Sample{Idle: 0, Raw: &Ctx{App: "TestApp", Title: "doc"}}
+	}
+	done := make(chan struct{})
+	go func() {
+		Run(dir, config.Tracking{PollSecs: 1, IdleGraceSecs: 900, MinSecs: 0}, sample, sigc)
+		close(done)
+	}()
+	time.Sleep(50 * time.Millisecond) // let the first poll open a segment
+	sigc <- syscall.SIGTERM
+	select {
+	case <-done:
+	case <-time.After(2 * time.Second):
+		t.Fatal("Run did not return after signal")
+	}
+	b, err := os.ReadFile(LogPath(dir, time.Now()))
+	if err != nil {
+		t.Fatalf("flush should have written today's log: %v", err)
+	}
+	line := strings.TrimSpace(string(b))
+	if !strings.Contains(line, `"app": "TestApp"`) || !strings.Contains(line, `"idle": false`) {
+		t.Errorf("flushed segment malformed: %s", line)
+	}
+}