// Package config reads the optional ticktock config file. tock owns data; // this only tunes the grid's presentation (slot size, window, default project). package config import ( "encoding/json" "os" "path/filepath" ) // Config controls grid presentation. Zero value is not valid; use Default. type Config struct { SlotMinutes int GridStart string GridEnd string Project string } // Default returns the documented fallbacks used when config is absent. func Default() Config { return Config{SlotMinutes: 30, GridStart: "07:00", GridEnd: "21:00", Project: ""} } // 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"` } // 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 c.SlotMinutes < 5 { c.SlotMinutes = 5 } return c }