// Package config reads the optional ticktock config file. tock owns data; // this tunes the grid's presentation (slot size, window, default project) // and the autotrack daemon's polling/idle thresholds. package config import ( "encoding/json" "os" "path/filepath" ) // 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: "", 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"` 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. func Load() Config { home, err := os.UserHomeDir() if err != nil { return Default() } return LoadFrom(filepath.Join(home, ".config", "ticktock", "config.json")) } // LoadFrom merges the JSON at path over Default(). Any read/parse error, or a // missing key, leaves that field at its default. SlotMinutes floors at 5. func LoadFrom(path string) Config { c := Default() b, err := os.ReadFile(path) if err != nil { return c } var r raw if err := json.Unmarshal(b, &r); err != nil { return c } if r.SlotMinutes != nil { c.SlotMinutes = *r.SlotMinutes } if r.GridStart != nil { c.GridStart = *r.GridStart } if r.GridEnd != nil { 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 } return c }