feat(config): calendars show/hide block for timeline calendar filtering
37e5d689acfe027dc90d3d34a7780245200f2991
Kevin Kortum <kevinkortum@me.com> · 2026-07-08 12:18
parent dd5263db
2 files changed
internal/config/config.go +24 −5
@@ -17,6 +17,7 @@ GridStart string
GridEnd string
Project string
Tracking Tracking
+ Calendars Calendars
}
// Tracking tunes the autotrack daemon; read from config.json's `tracking`
@@ -27,6 +28,14 @@ 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{
@@ -37,17 +46,23 @@ }
// 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"`
+ 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.
@@ -93,6 +108,10 @@ }
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
internal/config/config_test.go +34 −1
@@ -3,6 +3,7 @@
import (
"os"
"path/filepath"
+ "reflect"
"testing"
)
@@ -15,7 +16,7 @@ }
func TestLoadFromMissingFileReturnsDefaults(t *testing.T) {
c := LoadFrom(filepath.Join(t.TempDir(), "nope.json"))
- if c != Default() {
+ if !reflect.DeepEqual(c, Default()) {
t.Fatalf("missing file should give defaults, got %+v", c)
}
}
@@ -75,3 +76,35 @@ if c.Tracking != Default().Tracking {
t.Errorf("malformed tracking should keep defaults, got %+v", c.Tracking)
}
}
+
+func TestLoadFromMergesCalendars(t *testing.T) {
+ p := filepath.Join(t.TempDir(), "config.json")
+ if err := os.WriteFile(p, []byte(`{"calendars":{"show":["Work","Focus"],"hide":["Holidays"]}}`), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ c := LoadFrom(p)
+ if !reflect.DeepEqual(c.Calendars.Show, []string{"Work", "Focus"}) {
+ t.Errorf("show=%v, want [Work Focus]", c.Calendars.Show)
+ }
+ if !reflect.DeepEqual(c.Calendars.Hide, []string{"Holidays"}) {
+ t.Errorf("hide=%v, want [Holidays]", c.Calendars.Hide)
+ }
+}
+
+func TestCalendarsDefaultEmpty(t *testing.T) {
+ c := Default()
+ if len(c.Calendars.Show) != 0 || len(c.Calendars.Hide) != 0 {
+ t.Errorf("default calendars should be empty (all pass), got %+v", c.Calendars)
+ }
+}
+
+func TestLoadFromMalformedCalendarsFallsBack(t *testing.T) {
+ p := filepath.Join(t.TempDir(), "config.json")
+ if err := os.WriteFile(p, []byte(`{"calendars":"nope"}`), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ c := LoadFrom(p)
+ if !reflect.DeepEqual(c.Calendars, Default().Calendars) {
+ t.Errorf("malformed calendars should keep defaults, got %+v", c.Calendars)
+ }
+}