Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Calendar: show event `LOCATION` column by default in `calendar events` table output (appears after `SUMMARY` in every variant — `--all`, single-calendar, with/without `--weekday`). Embedded newlines in the location string are collapsed so multi-line addresses still render on one row.
- Drive: add `drive share --notify` for invite targets that require a Drive notification email.
- Calendar: keep `calendar appointments` as an explicit diagnostic because the Calendar API still rejects `eventTypes=appointmentSchedule`. (#329)
- CLI: add nested `docs tabs ...` and `forms questions ...` aliases for consistent sub-item command patterns while preserving existing flat commands. (#433)
Expand Down
49 changes: 49 additions & 0 deletions internal/cmd/calendar_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,55 @@ func TestListCalendarEvents_TableUsesCalendarTimezone(t *testing.T) {
}
}

// TestListCalendarEvents_TableIncludesLocation asserts that the events list
// table renders the LOCATION column by default and that embedded newlines in
// the location string are collapsed so the row stays on one line.
func TestListCalendarEvents_TableIncludesLocation(t *testing.T) {
svc, closeServer := newCalendarServiceForTest(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/calendars/cal1/events") && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"items": []map[string]any{
{
"id": "e1",
"summary": "Standup",
"location": "Bahnhofstrasse 1\n8001 Zürich",
"start": map[string]any{"dateTime": "2026-04-08T09:00:00Z"},
"end": map[string]any{"dateTime": "2026-04-08T09:15:00Z"},
},
{
"id": "e2",
"summary": "No-location event",
"start": map[string]any{"dateTime": "2026-04-08T10:00:00Z"},
"end": map[string]any{"dateTime": "2026-04-08T10:15:00Z"},
},
},
})
return
}
http.NotFound(w, r)
}))
defer closeServer()

text := captureStdout(t, func() {
ctx := newCalendarOutputContext(t, os.Stdout, io.Discard)
if err := listCalendarEvents(ctx, svc, "cal1", "2026-04-08T00:00:00Z", "2026-04-09T00:00:00Z", 10, "", false, false, "", "", "", "", false); err != nil {
t.Fatalf("listCalendarEvents: %v", err)
}
})

if !strings.Contains(text, "LOCATION") {
t.Fatalf("expected LOCATION header in output, got: %q", text)
}
if !strings.Contains(text, "Bahnhofstrasse 1 8001 Zürich") {
t.Fatalf("expected collapsed multi-line location, got: %q", text)
}
// Original newline must not leak into the rendered row.
if strings.Contains(text, "Bahnhofstrasse 1\n8001 Zürich") {
t.Fatalf("expected newline in location to be collapsed, got: %q", text)
}
}

func TestListCalendarEvents_JSONUsesCalendarTimezoneForLocalFields(t *testing.T) {
svc, closeServer := newCalendarServiceForTest(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
Expand Down
35 changes: 27 additions & 8 deletions internal/cmd/calendar_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,30 +188,30 @@ func renderCalendarEventsTable(ctx context.Context, events []*eventWithCalendar,

if showWeekday {
if includeCalendar {
fmt.Fprintln(w, "CALENDAR\tID\tSTART\tSTART_DOW\tEND\tEND_DOW\tSUMMARY")
fmt.Fprintln(w, "CALENDAR\tID\tSTART\tSTART_DOW\tEND\tEND_DOW\tSUMMARY\tLOCATION")
for _, e := range events {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", e.CalendarID, e.Id, eventDisplayStart(e), e.StartDayOfWeek, eventDisplayEnd(e), e.EndDayOfWeek, e.Summary)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", e.CalendarID, e.Id, eventDisplayStart(e), e.StartDayOfWeek, eventDisplayEnd(e), e.EndDayOfWeek, e.Summary, eventDisplayLocation(e))
}
} else {
fmt.Fprintln(w, "ID\tSTART\tSTART_DOW\tEND\tEND_DOW\tSUMMARY")
fmt.Fprintln(w, "ID\tSTART\tSTART_DOW\tEND\tEND_DOW\tSUMMARY\tLOCATION")
for _, e := range events {
startDay, endDay := e.StartDayOfWeek, e.EndDayOfWeek
if startDay == "" && endDay == "" {
startDay, endDay = eventDaysOfWeek(e.Event)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", e.Id, eventDisplayStart(e), startDay, eventDisplayEnd(e), endDay, e.Summary)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", e.Id, eventDisplayStart(e), startDay, eventDisplayEnd(e), endDay, e.Summary, eventDisplayLocation(e))
}
}
} else {
if includeCalendar {
fmt.Fprintln(w, "CALENDAR\tID\tSTART\tEND\tSUMMARY")
fmt.Fprintln(w, "CALENDAR\tID\tSTART\tEND\tSUMMARY\tLOCATION")
for _, e := range events {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", e.CalendarID, e.Id, eventDisplayStart(e), eventDisplayEnd(e), e.Summary)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", e.CalendarID, e.Id, eventDisplayStart(e), eventDisplayEnd(e), e.Summary, eventDisplayLocation(e))
}
} else {
fmt.Fprintln(w, "ID\tSTART\tEND\tSUMMARY")
fmt.Fprintln(w, "ID\tSTART\tEND\tSUMMARY\tLOCATION")
for _, e := range events {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", e.Id, eventDisplayStart(e), eventDisplayEnd(e), e.Summary)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", e.Id, eventDisplayStart(e), eventDisplayEnd(e), e.Summary, eventDisplayLocation(e))
}
}
}
Expand Down Expand Up @@ -269,6 +269,25 @@ func eventDisplayEnd(e *eventWithCalendar) string {
return eventEnd(e.Event)
}

// eventDisplayLocation returns the event location formatted for a single
// table cell. Newlines are collapsed and the value is trimmed so a multi-line
// address from the Calendar API does not break the row layout.
func eventDisplayLocation(e *eventWithCalendar) string {
if e == nil || e.Event == nil {
return ""
}
loc := strings.TrimSpace(e.Location)
if loc == "" {
return ""
}
// Calendar locations occasionally arrive with embedded newlines (pasted
// multi-line addresses); collapse them so the row stays on one line.
loc = strings.ReplaceAll(loc, "\r\n", " ")
loc = strings.ReplaceAll(loc, "\n", " ")
loc = strings.ReplaceAll(loc, "\t", " ")
return loc
}

func calendarDisplayTimezone(ctx context.Context, svc *calendar.Service, calendarID string, hints map[string]calendarTimezoneHint) (string, *time.Location) {
if hint, ok := hints[calendarID]; ok {
return hint.timezone, hint.loc
Expand Down