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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
package grid
import (
"fmt"
"strings"
"time"
"ticktock/internal/activity"
"ticktock/internal/store"
)
// CellKind tags a LOGGED-column slot.
type CellKind int
const (
CellEmpty CellKind = iota
CellLabel
CellCont
)
// Cell is one grid-column slot (LOGGED or ACTIVITY).
type Cell struct {
Kind CellKind
Key string
Project string
Text string
Act ActKind // ACTIVITY-lane flavor; ActNone for LOGGED cells
}
// ActKind flavors an ACTIVITY-column cell for styling.
type ActKind int
const (
ActNone ActKind = iota
ActCalendar
ActActive
ActIdle
)
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, Project: e.Project, Text: labelFor(e)}
for r := startRow + 1; r <= endRow && r < g.Rows; r++ {
cells[r] = Cell{Kind: CellCont, Key: e.Key, Project: e.Project}
}
}
return cells
}
// Per-slot precedence ranks for the ACTIVITY lane. A calendar event's label
// slot is the human-meaningful anchor and always shows; captured activity
// beats idle; a calendar continuation fills otherwise-empty slots without
// hiding activity. Equal rank ⇒ later-painted wins (mirrors paintLogged).
const (
rankCalCont = 1
rankIdle = 2
rankActive = 3
rankCalLabel = 4
)
// activityLabel picks the display text for an active segment:
// domain, else title, else app (real logs have segments with both empty).
func activityLabel(s activity.Segment) string {
if s.Domain != "" {
return s.Domain
}
if s.Title != "" {
return s.Title
}
return s.App
}
// paintActivity builds the ACTIVITY column: one label on a segment/event's
// first covered slot, continuation glyphs on later covered slots, exactly as
// paintLogged does for tock entries, with rank-based overlap precedence.
func paintActivity(g Grid, segs []activity.Segment, events []activity.Event) []Cell {
cells := make([]Cell, g.Rows)
ranks := make([]int, g.Rows)
put := func(row, rank int, c Cell) {
if row < 0 || row >= g.Rows || rank < ranks[row] {
return
}
cells[row], ranks[row] = c, rank
}
span := func(start, end time.Time) (int, int) {
s := g.RowOf(minuteOfDay(start))
e := g.RowOf(minuteOfDay(end) - 1) // end exclusive, same as paintLogged
if e < s {
e = s
}
return s, e
}
for _, sg := range segs {
s, e := span(sg.Start, sg.End)
rank, act, label := rankActive, ActActive, activityLabel(sg)
if sg.Idle {
rank, act, label = rankIdle, ActIdle, "(idle)"
}
put(s, rank, Cell{Kind: CellLabel, Text: label, Act: act})
for r := s + 1; r <= e; r++ {
put(r, rank, Cell{Kind: CellCont, Act: act})
}
}
for _, ev := range events {
s, e := span(ev.Start, ev.End)
put(s, rankCalLabel, Cell{Kind: CellLabel, Text: "▪ " + ev.Title, Act: ActCalendar})
for r := s + 1; r <= e; r++ {
put(r, rankCalCont, Cell{Kind: CellCont, Act: ActCalendar})
}
}
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
}
|