Skip to content

Commit 7712dc6

Browse files
fix(calendar): expose multi-calendar pagination (#831)
Warn when multi-calendar event output is truncated and expose per-calendar continuation tokens in JSON. Co-authored-by: Andy Ye <[email protected]>
1 parent 32a6ddd commit 7712dc6

4 files changed

Lines changed: 106 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Fixed
1010

11+
- Calendar: report multi-calendar event truncation on stderr for text output and as per-calendar page tokens in JSON. (#831) — thanks @TurboTheTurtle.
1112
- Docs: update the Docker authentication example to persist file-keyring tokens with `GOG_HOME`. (#828, #830) — thanks @WadydX.
1213

1314
## 0.28.0 - 2026-06-15

internal/cmd/calendar_all_events_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func TestListAllCalendarsEvents_JSON(t *testing.T) {
3636
"attendees": []map[string]any{{"email": "[email protected]"}},
3737
},
3838
},
39+
"nextPageToken": "cal1-next",
3940
})
4041
return
4142
case strings.Contains(r.URL.Path, "/calendars/cal2/events") && r.Method == http.MethodGet:
@@ -68,14 +69,18 @@ func TestListAllCalendarsEvents_JSON(t *testing.T) {
6869
})
6970

7071
var parsed struct {
71-
Events []map[string]any `json:"events"`
72+
Events []map[string]any `json:"events"`
73+
NextPageTokens []calendarEventsNextPage `json:"nextPageTokens"`
7274
}
7375
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
7476
t.Fatalf("json parse: %v", err)
7577
}
7678
if len(parsed.Events) != 2 {
7779
t.Fatalf("unexpected events: %#v", parsed.Events)
7880
}
81+
if len(parsed.NextPageTokens) != 1 || parsed.NextPageTokens[0].CalendarID != "cal1" || parsed.NextPageTokens[0].NextPageToken != "cal1-next" {
82+
t.Fatalf("unexpected nextPageTokens: %#v", parsed.NextPageTokens)
83+
}
7984
}
8085

8186
// TestListAllCalendarsEvents_SortByStart verifies that --sort=start orders
@@ -127,14 +132,18 @@ func TestListAllCalendarsEvents_SortByStart(t *testing.T) {
127132
})
128133

129134
var parsed struct {
130-
Events []map[string]any `json:"events"`
135+
Events []map[string]any `json:"events"`
136+
NextPageTokens []calendarEventsNextPage `json:"nextPageTokens"`
131137
}
132138
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
133139
t.Fatalf("json parse: %v", err)
134140
}
135141
if len(parsed.Events) != 2 {
136142
t.Fatalf("expected 2 events, got %#v", parsed.Events)
137143
}
144+
if len(parsed.NextPageTokens) != 0 {
145+
t.Fatalf("unexpected nextPageTokens: %#v", parsed.NextPageTokens)
146+
}
138147
if got, _ := parsed.Events[0]["id"].(string); got != "early" {
139148
t.Fatalf("expected first event id 'early', got %q (events: %#v)", got, parsed.Events)
140149
}

internal/cmd/calendar_list.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ func listSelectedCalendarsEvents(ctx context.Context, svc *calendar.Service, cal
145145
func listCalendarIDsEvents(ctx context.Context, svc *calendar.Service, calendarIDs []string, from, to string, maxResults int64, page string, allPages bool, failEmpty bool, query, privatePropFilter, sharedPropFilter, fields string, showWeekday bool, showLocation bool, timezoneHints map[string]calendarTimezoneHint, sortKey, sortOrder string) error {
146146
u := ui.FromContext(ctx)
147147
all := []*eventWithCalendar{}
148+
nextPages := []calendarEventsNextPage{}
148149
for _, calID := range calendarIDs {
149150
calID = strings.TrimSpace(calID)
150151
if calID == "" {
@@ -159,11 +160,17 @@ func listCalendarIDsEvents(ctx context.Context, svc *calendar.Service, calendarI
159160
return resp.Items, resp.NextPageToken, nil
160161
}
161162

162-
events, _, err := loadPagedItems(page, allPages, fetch)
163+
events, nextPageToken, err := loadPagedItems(page, allPages, fetch)
163164
if err != nil {
164165
u.Err().Linef("calendar %s: %v", calID, err)
165166
continue
166167
}
168+
if nextPageToken != "" {
169+
nextPages = append(nextPages, calendarEventsNextPage{
170+
CalendarID: calID,
171+
NextPageToken: nextPageToken,
172+
})
173+
}
167174

168175
for _, e := range events {
169176
redactCalendarEventForOutput(ctx, e)
@@ -174,15 +181,42 @@ func listCalendarIDsEvents(ctx context.Context, svc *calendar.Service, calendarI
174181
sortEventsBy(all, sortKey, sortOrder)
175182

176183
if outfmt.IsJSON(ctx) {
177-
if err := outfmt.WriteJSON(ctx, stdoutWriter(ctx), map[string]any{"events": all}); err != nil {
184+
if err := outfmt.WriteJSON(ctx, stdoutWriter(ctx), map[string]any{
185+
"events": all,
186+
"nextPageTokens": nextPages,
187+
}); err != nil {
178188
return err
179189
}
180190
if len(all) == 0 {
181191
return failEmptyExit(failEmpty)
182192
}
183193
return nil
184194
}
185-
return renderCalendarEventsTable(ctx, all, "", true, showWeekday, showLocation, failEmpty, false)
195+
if err := renderCalendarEventsTable(ctx, all, "", true, showWeekday, showLocation, failEmpty, false); err != nil {
196+
return err
197+
}
198+
printCalendarEventsNextPageHint(u, len(calendarIDs), nextPages)
199+
return nil
200+
}
201+
202+
type calendarEventsNextPage struct {
203+
CalendarID string `json:"calendarId"`
204+
NextPageToken string `json:"nextPageToken"`
205+
}
206+
207+
func printCalendarEventsNextPageHint(u *ui.UI, calendarCount int, nextPages []calendarEventsNextPage) {
208+
if u == nil || len(nextPages) == 0 {
209+
return
210+
}
211+
if calendarCount == 1 {
212+
printNextPageHintWithAll(u, nextPages[0].NextPageToken, "--all-pages")
213+
return
214+
}
215+
if len(nextPages) == 1 {
216+
u.Err().Linef("# More results: use --all-pages to fetch every page (%s has more results)", nextPages[0].CalendarID)
217+
return
218+
}
219+
u.Err().Linef("# More results: use --all-pages to fetch every page (%d calendars have more results)", len(nextPages))
186220
}
187221

188222
func renderCalendarEventsTable(ctx context.Context, events []*eventWithCalendar, nextPageToken string, includeCalendar, showWeekday, showLocation, failEmpty bool, printPageHint bool) error {

internal/cmd/execute_calendar_events_text_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,60 @@ func TestExecute_CalendarEvents_Text_All(t *testing.T) {
100100
t.Fatalf("unexpected out=%q", out)
101101
}
102102
}
103+
104+
func TestExecute_CalendarEvents_Text_AllWithPagingHint(t *testing.T) {
105+
srv := httptest.NewServer(withPrimaryCalendar(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
106+
switch {
107+
case strings.Contains(r.URL.Path, "/users/me/calendarList"):
108+
w.Header().Set("Content-Type", "application/json")
109+
_ = json.NewEncoder(w).Encode(map[string]any{
110+
"items": []map[string]any{
111+
{"id": "c1"},
112+
{"id": "c2"},
113+
},
114+
})
115+
return
116+
case strings.Contains(r.URL.Path, "/calendars/c1/events"):
117+
w.Header().Set("Content-Type", "application/json")
118+
_ = json.NewEncoder(w).Encode(map[string]any{
119+
"items": []map[string]any{
120+
{"id": "e1", "summary": "S1", "start": map[string]any{"dateTime": "2025-12-17T10:00:00Z"}, "end": map[string]any{"dateTime": "2025-12-17T11:00:00Z"}},
121+
},
122+
"nextPageToken": "c1-next",
123+
})
124+
return
125+
case strings.Contains(r.URL.Path, "/calendars/c2/events"):
126+
w.Header().Set("Content-Type", "application/json")
127+
_ = json.NewEncoder(w).Encode(map[string]any{
128+
"items": []map[string]any{
129+
{"id": "e2", "summary": "S2", "start": map[string]any{"dateTime": "2025-12-17T12:00:00Z"}, "end": map[string]any{"dateTime": "2025-12-17T13:00:00Z"}},
130+
},
131+
})
132+
return
133+
default:
134+
http.NotFound(w, r)
135+
return
136+
}
137+
})))
138+
defer srv.Close()
139+
140+
svc, err := calendar.NewService(context.Background(),
141+
option.WithoutAuthentication(),
142+
option.WithHTTPClient(srv.Client()),
143+
option.WithEndpoint(srv.URL+"/"),
144+
)
145+
if err != nil {
146+
t.Fatalf("NewService: %v", err)
147+
}
148+
result := executeWithCalendarTestService(t, []string{"--account", "[email protected]", "calendar", "events", "--all", "--from", "2025-12-17T00:00:00Z", "--to", "2025-12-18T00:00:00Z"}, svc)
149+
if result.err != nil {
150+
t.Fatalf("Execute: %v", result.err)
151+
}
152+
if !strings.Contains(result.stderr, "# More results: use --all-pages to fetch every page (c1 has more results)") {
153+
t.Fatalf("unexpected stderr=%q", result.stderr)
154+
}
155+
out := result.stdout
156+
if !strings.Contains(out, "CALENDAR") || !strings.Contains(out, "c1") || !strings.Contains(out, "e1") || !strings.Contains(out, "S1") || !strings.Contains(out, "c2") || !strings.Contains(out, "e2") || !strings.Contains(out, "S2") {
157+
t.Fatalf("unexpected out=%q", out)
158+
}
159+
}

0 commit comments

Comments
 (0)