▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0
3.2 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// 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
	Calendars   Calendars
}

// 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
}

// Calendars selects which calendar names appear in the grid's ACTIVITY lane.
// Non-empty Show is an allowlist (case-insensitive exact match on calendar
// name); Hide is a blocklist applied after Show. Both empty ⇒ all pass.
type Calendars struct {
	Show []string
	Hide []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: "",
		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"`
	Calendars   *rawCalendars `json:"calendars"`
}

type rawTracking struct {
	PollSecs      *int `json:"poll_secs"`
	IdleGraceSecs *int `json:"idle_grace_secs"`
	MinSecs       *int `json:"min_secs"`
}

type rawCalendars struct {
	Show []string `json:"show"`
	Hide []string `json:"hide"`
}

// 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 r.Calendars != nil {
		c.Calendars.Show = r.Calendars.Show
		c.Calendars.Hide = r.Calendars.Hide
	}
	if c.SlotMinutes < 5 {
		c.SlotMinutes = 5
	}
	return c
}