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

feat(config): tracking block (poll/idle-grace/min secs) with python-parity defaults

39ee59ad25282fc53da32205dcdf3a62ba5b2633
Kevin Kortum <kevinkortum@me.com> · 2026-07-08 12:01

parent 03f5912c

2 files changed

internal/config/config.go +39 −7
@@ -1,5 +1,6 @@
 // Package config reads the optional ticktock config file. tock owns data;
-// this only tunes the grid's presentation (slot size, window, default project).
+// this tunes the grid's presentation (slot size, window, default project)
+// and the autotrack daemon's polling/idle thresholds.
 package config
 
 import (
@@ -8,25 +9,45 @@ 	"os"
 	"path/filepath"
 )
 
-// Config controls grid presentation. Zero value is not valid; use Default.
+// Config controls grid presentation and daemon tuning. Zero value is not
+// valid; use Default.
 type Config struct {
 	SlotMinutes int
 	GridStart   string
 	GridEnd     string
 	Project     string
+	Tracking    Tracking
+}
+
+// Tracking tunes the autotrack daemon; read from config.json's `tracking`
+// object. Defaults 5, 900, 15 match the Python daemon's constants.
+type Tracking struct {
+	PollSecs      int // fine poll → tighter boundaries, catch short switches
+	IdleGraceSecs int // HID gap < this stays active; >= → idle
+	MinSecs       int // drop segments shorter than this
 }
 
 // Default returns the documented fallbacks used when config is absent.
 func Default() Config {
-	return Config{SlotMinutes: 30, GridStart: "07:00", GridEnd: "21:00", Project: ""}
+	return Config{
+		SlotMinutes: 30, GridStart: "07:00", GridEnd: "21:00", Project: "",
+		Tracking: Tracking{PollSecs: 5, IdleGraceSecs: 900, MinSecs: 15},
+	}
 }
 
 // raw mirrors the on-disk JSON; pointers distinguish "absent" from "zero".
 type raw struct {
-	SlotMinutes *int    `json:"slot_minutes"`
-	GridStart   *string `json:"grid_start"`
-	GridEnd     *string `json:"grid_end"`
-	Project     *string `json:"project"`
+	SlotMinutes *int         `json:"slot_minutes"`
+	GridStart   *string      `json:"grid_start"`
+	GridEnd     *string      `json:"grid_end"`
+	Project     *string      `json:"project"`
+	Tracking    *rawTracking `json:"tracking"`
+}
+
+type rawTracking struct {
+	PollSecs      *int `json:"poll_secs"`
+	IdleGraceSecs *int `json:"idle_grace_secs"`
+	MinSecs       *int `json:"min_secs"`
 }
 
 // Load reads ~/.config/ticktock/config.json, falling back to defaults.
@@ -61,6 +82,17 @@ 		c.GridEnd = *r.GridEnd
 	}
 	if r.Project != nil {
 		c.Project = *r.Project
+	}
+	if r.Tracking != nil {
+		if r.Tracking.PollSecs != nil {
+			c.Tracking.PollSecs = *r.Tracking.PollSecs
+		}
+		if r.Tracking.IdleGraceSecs != nil {
+			c.Tracking.IdleGraceSecs = *r.Tracking.IdleGraceSecs
+		}
+		if r.Tracking.MinSecs != nil {
+			c.Tracking.MinSecs = *r.Tracking.MinSecs
+		}
 	}
 	if c.SlotMinutes < 5 {
 		c.SlotMinutes = 5
internal/config/config_test.go +35 −0
@@ -40,3 +40,38 @@ 	if c.Project != "ARCHER" {
 		t.Errorf("project=%q, want ARCHER", c.Project)
 	}
 }
+
+func TestTrackingDefaults(t *testing.T) {
+	c := Default()
+	if c.Tracking.PollSecs != 5 || c.Tracking.IdleGraceSecs != 900 || c.Tracking.MinSecs != 15 {
+		t.Fatalf("bad tracking defaults: %+v", c.Tracking)
+	}
+}
+
+func TestLoadFromMergesTracking(t *testing.T) {
+	p := filepath.Join(t.TempDir(), "config.json")
+	if err := os.WriteFile(p, []byte(`{"tracking":{"poll_secs":10,"min_secs":30}}`), 0o644); err != nil {
+		t.Fatal(err)
+	}
+	c := LoadFrom(p)
+	if c.Tracking.PollSecs != 10 {
+		t.Errorf("poll_secs=%d, want 10", c.Tracking.PollSecs)
+	}
+	if c.Tracking.IdleGraceSecs != 900 { // absent key keeps default
+		t.Errorf("idle_grace_secs=%d, want 900", c.Tracking.IdleGraceSecs)
+	}
+	if c.Tracking.MinSecs != 30 {
+		t.Errorf("min_secs=%d, want 30", c.Tracking.MinSecs)
+	}
+}
+
+func TestLoadFromMalformedTrackingFallsBack(t *testing.T) {
+	p := filepath.Join(t.TempDir(), "config.json")
+	if err := os.WriteFile(p, []byte(`{"tracking":"nope"}`), 0o644); err != nil {
+		t.Fatal(err)
+	}
+	c := LoadFrom(p)
+	if c.Tracking != Default().Tracking {
+		t.Errorf("malformed tracking should keep defaults, got %+v", c.Tracking)
+	}
+}