diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3cd7ab6..349d23964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Calendar: report multi-calendar event truncation on stderr for text output and as per-calendar page tokens in JSON. (#831) — thanks @TurboTheTurtle. - Docs: update the Docker authentication example to persist file-keyring tokens with `GOG_HOME`. (#828, #830) — thanks @WadydX. ## 0.28.0 - 2026-06-15 diff --git a/internal/cmd/calendar_all_events_test.go b/internal/cmd/calendar_all_events_test.go index 3c613ba85..62b6e1e12 100644 --- a/internal/cmd/calendar_all_events_test.go +++ b/internal/cmd/calendar_all_events_test.go @@ -36,6 +36,7 @@ func TestListAllCalendarsEvents_JSON(t *testing.T) { "attendees": []map[string]any{{"email": "a@example.com"}}, }, }, + "nextPageToken": "cal1-next", }) return case strings.Contains(r.URL.Path, "/calendars/cal2/events") && r.Method == http.MethodGet: @@ -68,7 +69,8 @@ func TestListAllCalendarsEvents_JSON(t *testing.T) { }) var parsed struct { - Events []map[string]any `json:"events"` + Events []map[string]any `json:"events"` + NextPageTokens []calendarEventsNextPage `json:"nextPageTokens"` } if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil { t.Fatalf("json parse: %v", err) @@ -76,6 +78,9 @@ func TestListAllCalendarsEvents_JSON(t *testing.T) { if len(parsed.Events) != 2 { t.Fatalf("unexpected events: %#v", parsed.Events) } + if len(parsed.NextPageTokens) != 1 || parsed.NextPageTokens[0].CalendarID != "cal1" || parsed.NextPageTokens[0].NextPageToken != "cal1-next" { + t.Fatalf("unexpected nextPageTokens: %#v", parsed.NextPageTokens) + } } // TestListAllCalendarsEvents_SortByStart verifies that --sort=start orders @@ -127,7 +132,8 @@ func TestListAllCalendarsEvents_SortByStart(t *testing.T) { }) var parsed struct { - Events []map[string]any `json:"events"` + Events []map[string]any `json:"events"` + NextPageTokens []calendarEventsNextPage `json:"nextPageTokens"` } if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil { t.Fatalf("json parse: %v", err) @@ -135,6 +141,9 @@ func TestListAllCalendarsEvents_SortByStart(t *testing.T) { if len(parsed.Events) != 2 { t.Fatalf("expected 2 events, got %#v", parsed.Events) } + if len(parsed.NextPageTokens) != 0 { + t.Fatalf("unexpected nextPageTokens: %#v", parsed.NextPageTokens) + } if got, _ := parsed.Events[0]["id"].(string); got != "early" { t.Fatalf("expected first event id 'early', got %q (events: %#v)", got, parsed.Events) } diff --git a/internal/cmd/calendar_list.go b/internal/cmd/calendar_list.go index 759dd982c..843360f51 100644 --- a/internal/cmd/calendar_list.go +++ b/internal/cmd/calendar_list.go @@ -145,6 +145,7 @@ func listSelectedCalendarsEvents(ctx context.Context, svc *calendar.Service, cal 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 { u := ui.FromContext(ctx) all := []*eventWithCalendar{} + nextPages := []calendarEventsNextPage{} for _, calID := range calendarIDs { calID = strings.TrimSpace(calID) if calID == "" { @@ -159,11 +160,17 @@ func listCalendarIDsEvents(ctx context.Context, svc *calendar.Service, calendarI return resp.Items, resp.NextPageToken, nil } - events, _, err := loadPagedItems(page, allPages, fetch) + events, nextPageToken, err := loadPagedItems(page, allPages, fetch) if err != nil { u.Err().Linef("calendar %s: %v", calID, err) continue } + if nextPageToken != "" { + nextPages = append(nextPages, calendarEventsNextPage{ + CalendarID: calID, + NextPageToken: nextPageToken, + }) + } for _, e := range events { redactCalendarEventForOutput(ctx, e) @@ -174,7 +181,10 @@ func listCalendarIDsEvents(ctx context.Context, svc *calendar.Service, calendarI sortEventsBy(all, sortKey, sortOrder) if outfmt.IsJSON(ctx) { - if err := outfmt.WriteJSON(ctx, stdoutWriter(ctx), map[string]any{"events": all}); err != nil { + if err := outfmt.WriteJSON(ctx, stdoutWriter(ctx), map[string]any{ + "events": all, + "nextPageTokens": nextPages, + }); err != nil { return err } if len(all) == 0 { @@ -182,7 +192,31 @@ func listCalendarIDsEvents(ctx context.Context, svc *calendar.Service, calendarI } return nil } - return renderCalendarEventsTable(ctx, all, "", true, showWeekday, showLocation, failEmpty, false) + if err := renderCalendarEventsTable(ctx, all, "", true, showWeekday, showLocation, failEmpty, false); err != nil { + return err + } + printCalendarEventsNextPageHint(u, len(calendarIDs), nextPages) + return nil +} + +type calendarEventsNextPage struct { + CalendarID string `json:"calendarId"` + NextPageToken string `json:"nextPageToken"` +} + +func printCalendarEventsNextPageHint(u *ui.UI, calendarCount int, nextPages []calendarEventsNextPage) { + if u == nil || len(nextPages) == 0 { + return + } + if calendarCount == 1 { + printNextPageHintWithAll(u, nextPages[0].NextPageToken, "--all-pages") + return + } + if len(nextPages) == 1 { + u.Err().Linef("# More results: use --all-pages to fetch every page (%s has more results)", nextPages[0].CalendarID) + return + } + u.Err().Linef("# More results: use --all-pages to fetch every page (%d calendars have more results)", len(nextPages)) } func renderCalendarEventsTable(ctx context.Context, events []*eventWithCalendar, nextPageToken string, includeCalendar, showWeekday, showLocation, failEmpty bool, printPageHint bool) error { diff --git a/internal/cmd/execute_calendar_events_text_test.go b/internal/cmd/execute_calendar_events_text_test.go index b352af183..c61f42a7e 100644 --- a/internal/cmd/execute_calendar_events_text_test.go +++ b/internal/cmd/execute_calendar_events_text_test.go @@ -100,3 +100,60 @@ func TestExecute_CalendarEvents_Text_All(t *testing.T) { t.Fatalf("unexpected out=%q", out) } } + +func TestExecute_CalendarEvents_Text_AllWithPagingHint(t *testing.T) { + srv := httptest.NewServer(withPrimaryCalendar(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.Contains(r.URL.Path, "/users/me/calendarList"): + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "items": []map[string]any{ + {"id": "c1"}, + {"id": "c2"}, + }, + }) + return + case strings.Contains(r.URL.Path, "/calendars/c1/events"): + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "items": []map[string]any{ + {"id": "e1", "summary": "S1", "start": map[string]any{"dateTime": "2025-12-17T10:00:00Z"}, "end": map[string]any{"dateTime": "2025-12-17T11:00:00Z"}}, + }, + "nextPageToken": "c1-next", + }) + return + case strings.Contains(r.URL.Path, "/calendars/c2/events"): + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "items": []map[string]any{ + {"id": "e2", "summary": "S2", "start": map[string]any{"dateTime": "2025-12-17T12:00:00Z"}, "end": map[string]any{"dateTime": "2025-12-17T13:00:00Z"}}, + }, + }) + return + default: + http.NotFound(w, r) + return + } + }))) + defer srv.Close() + + svc, err := calendar.NewService(context.Background(), + option.WithoutAuthentication(), + option.WithHTTPClient(srv.Client()), + option.WithEndpoint(srv.URL+"/"), + ) + if err != nil { + t.Fatalf("NewService: %v", err) + } + result := executeWithCalendarTestService(t, []string{"--account", "a@b.com", "calendar", "events", "--all", "--from", "2025-12-17T00:00:00Z", "--to", "2025-12-18T00:00:00Z"}, svc) + if result.err != nil { + t.Fatalf("Execute: %v", result.err) + } + if !strings.Contains(result.stderr, "# More results: use --all-pages to fetch every page (c1 has more results)") { + t.Fatalf("unexpected stderr=%q", result.stderr) + } + out := result.stdout + 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") { + t.Fatalf("unexpected out=%q", out) + } +}