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)
+ }
+}