Skip to content

Commit 2db42b1

Browse files
committed
feat: interleave today events with tasks
1 parent 9796ccb commit 2db42b1

4 files changed

Lines changed: 158 additions & 37 deletions

File tree

internal/cli/items.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,33 @@ func formatTimeRange(start, end time.Time) string {
5757
}
5858
return fmt.Sprintf("%s-%s", startText, end.Format("15:04"))
5959
}
60+
61+
func hasTime(value time.Time) bool {
62+
return value.Hour() != 0 || value.Minute() != 0 || value.Second() != 0 || value.Nanosecond() != 0
63+
}
64+
65+
func formatDueText(due time.Time) string {
66+
if due.IsZero() {
67+
return ""
68+
}
69+
dateText := due.Format("2006-01-02")
70+
if hasTime(due) {
71+
return fmt.Sprintf("%s %s", dateText, due.Format("15:04"))
72+
}
73+
return dateText
74+
}
75+
76+
func formatAllDayRange(start, end time.Time) string {
77+
if start.IsZero() {
78+
return ""
79+
}
80+
startText := start.Format("2006-01-02")
81+
if end.IsZero() || !end.After(start) {
82+
return startText
83+
}
84+
last := end.AddDate(0, 0, -1)
85+
if sameDay(start, last) {
86+
return startText
87+
}
88+
return fmt.Sprintf("%s..%s", startText, last.Format("2006-01-02"))
89+
}

internal/cli/taskquery.go

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -201,24 +201,49 @@ func buildNextItems(ctx queryContext, showBacklog bool) ([]list.Item, error) {
201201
if len(b.tasks) == 0 && len(todayEvents) == 0 {
202202
continue
203203
}
204-
sort.SliceStable(todayEvents, func(i, j int) bool {
205-
if todayEvents[i].AllDay != todayEvents[j].AllDay {
206-
return todayEvents[i].AllDay
204+
type todayEntry struct {
205+
item list.Item
206+
timed bool
207+
start time.Time
208+
kind int
209+
label string
210+
}
211+
entries := make([]todayEntry, 0, len(todayEvents)+len(b.tasks))
212+
for _, e := range todayEvents {
213+
entries = append(entries, todayEntry{
214+
item: e,
215+
timed: !e.AllDay,
216+
start: e.Start,
217+
kind: 0,
218+
label: e.Summary,
219+
})
220+
}
221+
for _, t := range b.tasks {
222+
entries = append(entries, todayEntry{
223+
item: t,
224+
timed: hasTime(t.Due),
225+
start: t.Due,
226+
kind: 1,
227+
label: t.TitleVal,
228+
})
229+
}
230+
sort.SliceStable(entries, func(i, j int) bool {
231+
if entries[i].timed != entries[j].timed {
232+
return !entries[i].timed
207233
}
208-
if !todayEvents[i].Start.Equal(todayEvents[j].Start) {
209-
return todayEvents[i].Start.Before(todayEvents[j].Start)
234+
if entries[i].timed {
235+
if !entries[i].start.Equal(entries[j].start) {
236+
return entries[i].start.Before(entries[j].start)
237+
}
210238
}
211-
if !todayEvents[i].End.Equal(todayEvents[j].End) {
212-
return todayEvents[i].End.Before(todayEvents[j].End)
239+
if entries[i].kind != entries[j].kind {
240+
return entries[i].kind < entries[j].kind
213241
}
214-
return todayEvents[i].Summary < todayEvents[j].Summary
242+
return entries[i].label < entries[j].label
215243
})
216244
items = append(items, taskItem{TitleVal: b.name, IsHeader: true})
217-
for _, e := range todayEvents {
218-
items = append(items, e)
219-
}
220-
for _, t := range b.tasks {
221-
items = append(items, t)
245+
for _, entry := range entries {
246+
items = append(items, entry.item)
222247
}
223248
continue
224249
}

internal/cli/taskquery_test.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ func TestBuildNextItemsIncludesTodayEvents(t *testing.T) {
132132
calID := "cal-1"
133133

134134
items := []*tasks.Task{
135-
{Id: "1", Title: "Task Today", Status: "needsAction", Due: time.Date(2026, 1, 3, 12, 0, 0, 0, loc).Format(time.RFC3339)},
135+
{Id: "1", Title: "All Day Task", Status: "needsAction", Due: time.Date(2026, 1, 3, 0, 0, 0, 0, loc).Format(time.RFC3339)},
136+
{Id: "2", Title: "Timed Task", Status: "needsAction", Due: time.Date(2026, 1, 3, 15, 0, 0, 0, loc).Format(time.RFC3339)},
136137
}
137138

138139
calendars := []*calendar.CalendarListEntry{
@@ -171,20 +172,24 @@ func TestBuildNextItemsIncludesTodayEvents(t *testing.T) {
171172
if todayIdx == -1 {
172173
t.Fatalf("missing Today header")
173174
}
174-
if len(got) <= todayIdx+3 {
175-
t.Fatalf("expected Today bucket to include events and task")
175+
if len(got) <= todayIdx+4 {
176+
t.Fatalf("expected Today bucket to include events and tasks")
176177
}
177178

178179
firstEvent, ok := got[todayIdx+1].(calendarEventItem)
179180
if !ok || !firstEvent.AllDay || firstEvent.Summary != "All Day" {
180181
t.Fatalf("expected all-day event first, got %#v", got[todayIdx+1])
181182
}
182-
secondEvent, ok := got[todayIdx+2].(calendarEventItem)
183-
if !ok || secondEvent.Summary != "Standup" {
184-
t.Fatalf("expected timed event second, got %#v", got[todayIdx+2])
183+
secondTask, ok := got[todayIdx+2].(taskItem)
184+
if !ok || secondTask.TitleVal != "All Day Task" {
185+
t.Fatalf("expected all-day task second, got %#v", got[todayIdx+2])
185186
}
186-
if task, ok := got[todayIdx+3].(taskItem); !ok || task.TitleVal != "Task Today" {
187-
t.Fatalf("expected task after events, got %#v", got[todayIdx+3])
187+
thirdEvent, ok := got[todayIdx+3].(calendarEventItem)
188+
if !ok || thirdEvent.Summary != "Standup" {
189+
t.Fatalf("expected timed event third, got %#v", got[todayIdx+3])
190+
}
191+
if task, ok := got[todayIdx+4].(taskItem); !ok || task.TitleVal != "Timed Task" {
192+
t.Fatalf("expected timed task fourth, got %#v", got[todayIdx+4])
188193
}
189194
}
190195

internal/cli/tui.go

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ func (t taskItem) Description() string {
9999
}
100100
dueText := ""
101101
if t.HasDue {
102-
dueText = fmt.Sprintf("due %s", t.Due.Format("2006-01-02"))
102+
if formatted := formatDueText(t.Due); formatted != "" {
103+
dueText = fmt.Sprintf("due %s", formatted)
104+
}
103105
}
104106
return dueText
105107
}
@@ -866,23 +868,82 @@ func (m *tuiModel) splitPane(left, right string) string {
866868
}
867869

868870
func (m *tuiModel) detailsView() string {
869-
task, ok := m.selectedTask()
870-
if !ok {
871-
return gray("Select a task to see details")
872-
}
873-
dueText := "None"
874-
if task.HasDue {
875-
dueText = task.Due.Format("2006-01-02")
876-
}
877871
label := lipgloss.NewStyle().Foreground(colorMuted)
878-
lines := []string{
879-
lipgloss.NewStyle().Bold(true).Render(task.TitleVal),
880-
"",
881-
fmt.Sprintf("%s %s", label.Render("List:"), task.ListName),
882-
fmt.Sprintf("%s %s", label.Render("Section:"), task.Section),
883-
fmt.Sprintf("%s %s", label.Render("Due:"), dueText),
872+
item := m.tasksList.SelectedItem()
873+
if item == nil {
874+
return gray("Select a task or event to see details")
875+
}
876+
877+
switch value := item.(type) {
878+
case taskItem:
879+
if value.IsHeader {
880+
return gray("Select a task or event to see details")
881+
}
882+
dueText := "None"
883+
if value.HasDue {
884+
if formatted := formatDueText(value.Due); formatted != "" {
885+
dueText = formatted
886+
}
887+
}
888+
lines := []string{
889+
lipgloss.NewStyle().Bold(true).Render(value.TitleVal),
890+
"",
891+
fmt.Sprintf("%s %s", label.Render("List:"), value.ListName),
892+
fmt.Sprintf("%s %s", label.Render("Section:"), value.Section),
893+
fmt.Sprintf("%s %s", label.Render("Due:"), dueText),
894+
}
895+
return strings.Join(lines, "\n")
896+
case searchItem:
897+
task := value.Task
898+
if task.IsHeader {
899+
return gray("Select a task or event to see details")
900+
}
901+
dueText := "None"
902+
if task.HasDue {
903+
if formatted := formatDueText(task.Due); formatted != "" {
904+
dueText = formatted
905+
}
906+
}
907+
lines := []string{
908+
lipgloss.NewStyle().Bold(true).Render(task.TitleVal),
909+
"",
910+
fmt.Sprintf("%s %s", label.Render("List:"), task.ListName),
911+
fmt.Sprintf("%s %s", label.Render("Section:"), task.Section),
912+
fmt.Sprintf("%s %s", label.Render("Due:"), dueText),
913+
}
914+
return strings.Join(lines, "\n")
915+
case calendarEventItem:
916+
when := ""
917+
if value.AllDay {
918+
if formatted := formatAllDayRange(value.Start, value.End); formatted != "" {
919+
when = fmt.Sprintf("%s (all-day)", formatted)
920+
}
921+
} else {
922+
dateText := value.Start.Format("2006-01-02")
923+
timeText := formatTimeRange(value.Start, value.End)
924+
if timeText != "" {
925+
when = fmt.Sprintf("%s %s", dateText, timeText)
926+
} else {
927+
when = dateText
928+
}
929+
}
930+
if when == "" {
931+
when = "Unknown"
932+
}
933+
calendarName := strings.TrimSpace(value.CalendarName)
934+
if calendarName == "" {
935+
calendarName = value.CalendarID
936+
}
937+
lines := []string{
938+
lipgloss.NewStyle().Bold(true).Render(value.Summary),
939+
"",
940+
fmt.Sprintf("%s %s", label.Render("Calendar:"), calendarName),
941+
fmt.Sprintf("%s %s", label.Render("When:"), when),
942+
}
943+
return strings.Join(lines, "\n")
944+
default:
945+
return gray("Select a task or event to see details")
884946
}
885-
return strings.Join(lines, "\n")
886947
}
887948

888949
func renderStatus(msg string) string {

0 commit comments

Comments
 (0)