feat(grid): paint LOGGED column cells with row to entry key lookup
c9cc052da06bee1fa022bb4cb744322dab48d18a
Kevin Kortum <kevinkortum@me.com> · 2026-07-07 21:43
parent deeffb2d
2 files changed
internal/tui/grid/paint.go +75 −0
@@ -0,0 +1,75 @@
+package grid
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "ticktock/internal/store"
+)
+
+// CellKind tags a LOGGED-column slot.
+type CellKind int
+
+const (
+ CellEmpty CellKind = iota
+ CellLabel
+ CellCont
+)
+
+// Cell is one LOGGED-column slot.
+type Cell struct {
+ Kind CellKind
+ Key string
+ Text string
+}
+
+func hmm(d time.Duration) string {
+ total := int(d.Minutes())
+ return fmt.Sprintf("%d:%02d", total/60, total%60)
+}
+
+// labelFor renders a session's LOGGED label.
+func labelFor(e store.Entry) string {
+ tags := ""
+ if len(e.Tags) > 0 {
+ tags = " [" + strings.Join(e.Tags, ",") + "]"
+ }
+ return fmt.Sprintf("✓ %s%s (%s)", e.Description, tags, hmm(e.Duration))
+}
+
+// paintLogged builds the LOGGED column: one label per session on its first
+// covered slot, continuation glyphs on later covered slots. Later-start wins a
+// contested slot. Running entries run to the last row.
+func paintLogged(g Grid, entries []store.Entry) []Cell {
+ cells := make([]Cell, g.Rows)
+ for _, e := range entries {
+ startRow := g.RowOf(minuteOfDay(e.Start))
+ var endRow int
+ if e.Running() {
+ endRow = g.Rows - 1
+ } else {
+ // end is exclusive; last covered slot holds end-1 minute
+ endRow = g.RowOf(minuteOfDay(e.End) - 1)
+ }
+ if endRow < startRow {
+ endRow = startRow
+ }
+ cells[startRow] = Cell{Kind: CellLabel, Key: e.Key, Text: labelFor(e)}
+ for r := startRow + 1; r <= endRow && r < g.Rows; r++ {
+ cells[r] = Cell{Kind: CellCont, Key: e.Key}
+ }
+ }
+ return cells
+}
+
+// entryKeyAtRow returns the Key painted at row, if any.
+func entryKeyAtRow(cells []Cell, row int) (string, bool) {
+ if row < 0 || row >= len(cells) {
+ return "", false
+ }
+ if cells[row].Kind == CellEmpty || cells[row].Key == "" {
+ return "", false
+ }
+ return cells[row].Key, true
+}
internal/tui/grid/paint_test.go +48 −0
@@ -0,0 +1,48 @@
+package grid
+
+import (
+ "testing"
+
+ "ticktock/internal/store"
+)
+
+func TestPaintLoggedLabelAndContinuation(t *testing.T) {
+ g := BuildGrid(30, 7*60, 9*60, nil) // FirstMin 420, Rows 4 (07:00..09:00)
+ es := []store.Entry{{
+ Key: "2026-07-07-01", Start: at(7, 0), End: at(8, 30),
+ Description: "work", Duration: 90 * 60 * 1e9,
+ }}
+ cells := paintLogged(g, es)
+ if cells[0].Kind != CellLabel {
+ t.Fatalf("row0 kind=%v, want Label", cells[0].Kind)
+ }
+ if cells[1].Kind != CellCont || cells[2].Kind != CellCont {
+ t.Errorf("rows1-2 should be continuation")
+ }
+ if cells[3].Kind != CellEmpty {
+ t.Errorf("row3 should be empty (session ends 08:30)")
+ }
+ if cells[0].Key != "2026-07-07-01" || cells[2].Key != "2026-07-07-01" {
+ t.Errorf("key not propagated to covered slots")
+ }
+}
+
+func TestEntryKeyAtRow(t *testing.T) {
+ g := BuildGrid(30, 7*60, 9*60, nil)
+ es := []store.Entry{{Key: "K", Start: at(7, 30), End: at(8, 0), Description: "x"}}
+ cells := paintLogged(g, es)
+ if k, ok := entryKeyAtRow(cells, 1); !ok || k != "K" {
+ t.Errorf("row1 → %q,%v want K,true", k, ok)
+ }
+ if _, ok := entryKeyAtRow(cells, 0); ok {
+ t.Errorf("row0 empty should be false")
+ }
+}
+
+func TestLabelForFormatsDurationAndTags(t *testing.T) {
+ e := store.Entry{Description: "emails", Tags: []string{"admin"}, Duration: 50 * 60 * 1e9}
+ got := labelFor(e)
+ if got != "✓ emails [admin] (0:50)" {
+ t.Errorf("labelFor=%q", got)
+ }
+}