Skip to content

Feature Request: Multi-Calendar Selection for gog calendar events #129

@gwpl

Description

@gwpl

Feature Request: Multi-Calendar Selection for gog calendar events

Proposal

Add support for querying a specific subset of calendars in gog calendar events, via one or more of:

Numeric index (--calendars 1,2,5) — quick selection by position from gog calendar calendars list:

gog calendar events --calendars 1,2,5 --today

Repeated flag (--cal) — robust, handles calendar names containing commas:

gog calendar events --cal "Work" --cal "Family" --cal "Meeting, Planning & Review" --today

Comma-separated names/IDs (--calendars) — concise for simple names:

gog calendar events --calendars "Work,Family,Holidays" --today

All approaches would support mixing: numeric indices, calendar names (case-insensitive), and full calendar IDs.


Rationale

API Efficiency

The Google Calendar API's Events.list endpoint requires a single calendarId per request — there's no native "query multiple calendars" parameter. Currently, users who need events from 2-3 specific calendars must either:

  1. Use --all — makes N+1 API calls (1 CalendarList + N Events calls for all calendars), then filter results client-side
  2. Run multiple commandsgog calendar events cal1 --today && gog calendar events cal2 --today

For users with many calendars (10-25 is common), option 1 wastes API quota and bandwidth fetching unwanted data. A --calendars flag would make only the necessary calls.

Ergonomics

  • Numeric indices — After running gog calendar calendars, users can quickly select by number: --calendars 1,2,5 instead of copying long IDs
  • Name-based lookup — Calendar IDs like v9c8pfp6n57g49cacq96r8vlh4@group.calendar.google.com are not user-friendly; names like "Work" or "Family" are
  • Common workflow — Many users have a "daily driver" set of 2-4 calendars they check regularly
  • Scriptability — Easier to maintain scripts with meaningful names than opaque IDs

Current Implementation Understanding

I traced through the codebase to understand how --all works:

Flow (internal/cmd/calendar.go + calendar_list.go)

  1. Flag validation (calendar.go:156-161) — --all and calendarId are mutually exclusive
  2. Routing (calendar.go:184-187) — branches to listAllCalendarsEvents() vs listCalendarEvents()
  3. Calendar enumeration (calendar_list.go:~90) — calls svc.CalendarList.List() to get all calendars
  4. Event fetching (calendar_list.go:~100-130) — iterates calResp.Items, calling svc.Events.List(cal.Id) for each
  5. Error handling — individual calendar failures are logged but don't abort the operation (graceful degradation)
  6. Output enrichmenteventWithCalendar wrapper tracks source calendar for display

Existing Multi-Value Patterns

The codebase already has precedents:

Command Flag/Arg Type Example
freebusy <calendarIds> comma-separated gog calendar freebusy cal1,cal2
conflicts --calendars comma-separated --calendars "primary,work"
create --reminder repeated flag --reminder popup:30m --reminder email:1d

Name Resolution Precedent

Gmail labels already implement name-to-ID resolution in gmail_labels_utils.go:

func resolveLabelIDs(labels []string, nameToID map[string]string) []string

This pattern (case-insensitive lookup with fallback to treating input as ID) could be adapted for calendars.


Proposed Design

Command Structure

Two complementary approaches, following existing patterns in the codebase:

Option A: Repeated flag (--cal) — handles names with commas

type CalendarEventsCmd struct {
    CalendarID string   `arg:"" name:"calendarId" optional:"" help:"Calendar ID (default: primary)"`
    Cal        []string `name:"cal" help:"Calendar ID or name (can be repeated)"`
    All        bool     `name:"all" help:"Fetch events from all calendars"`
    // ... existing fields unchanged ...
}
# Handles calendar names containing commas safely
gog calendar events --cal "Meeting, Planning & Review" --cal "Work" --cal "Family" --today

Option B: Comma-separated (--calendars) — concise for simple names

type CalendarEventsCmd struct {
    CalendarID string `arg:"" name:"calendarId" optional:"" help:"Calendar ID (default: primary)"`
    Calendars  string `name:"calendars" help:"Comma-separated calendar IDs or names"`
    All        bool   `name:"all" help:"Fetch events from all calendars"`
    // ... existing fields unchanged ...
}
# Shorter syntax when names don't contain commas
gog calendar events --calendars "Work,Family,Holidays" --today

Both options could coexist (merged into single list before resolution), or just one could be implemented — whichever fits the project's style better.

Mutual Exclusivity

calendarId (positional)  ─┬─ mutually exclusive
--cal / --calendars      ─┤
--all                    ─┘

(If both --cal and --calendars are supported, they could be additive rather than exclusive.)

Calendar Resolution (Optional Enhancement)

A utility similar to Gmail's label resolution, extended to support numeric indices:

func resolveCalendarIDs(ctx context.Context, svc *calendar.Service, inputs []string) ([]string, error)

Resolution order for each input:

  1. Numeric index — if input is a number (e.g., "1", "2"), resolve to Nth calendar from CalendarList.List() (1-indexed to match display)
  2. Name match — case-insensitive match against cal.Summary
  3. ID match — case-insensitive match against cal.Id
  4. Fallback — treat as literal calendar ID (forward-compatible)

This allows intuitive usage after running gog calendar calendars:

$ gog calendar calendars
ID                                      NAME
primary                                 Personal
abc123@group.calendar.google.com        Work
xyz789@group.calendar.google.com        Meeting, Planning & Review

$ gog calendar events --calendars 1,2 --today   # Personal + Work
$ gog calendar events --calendars "Work,Personal" --today  # same result

Corner Cases

Scenario Behavior
Empty input Error: "no calendars specified"
Numeric index 1,2,3 Resolve to Nth calendar from list (1-indexed)
Index out of range Error: "calendar index N out of range (have M calendars)"
Unknown name/ID Treat as literal ID (API will return 404 if invalid)
Duplicate entries Deduplicate before querying
Name with commas Use --cal repeated flag: --cal "Planning, Review & Sync"
Special chars (&, spaces, quotes) Work naturally — shell quoting handles them
Calendar named "1" or "2" Numeric takes precedence; use full ID or --cal "1" for literal name

Calendar names containing commas are the key reason to support the repeated --cal flag pattern. Examples of real-world calendar names that would break comma-separated parsing:

  • "Meeting, Planning & Review"
  • "John, Jane & Family"
  • "Project X, Phase 2"

With --cal repeated flag, these work safely:

gog calendar events --cal "Meeting, Planning & Review" --cal "Work" --today

Usage Examples

# Numeric indices — quick selection after viewing `gog calendar calendars`
gog calendar events --calendars 1,2,5 --today
gog calendar events --calendars 1 --week

# Repeated flag (--cal) — safe for any calendar name, including those with commas
gog calendar events --cal "Work" --cal "Family" --cal "Holidays" --today
gog calendar events --cal "Meeting, Planning & Review" --cal "Project X, Phase 2" --week

# Comma-separated names (--calendars) — concise when names have no commas
gog calendar events --calendars "Work,Family,Holidays" --today
gog calendar events --calendars "primary,shared@team.com" --week

# Mixed: indices, names, and IDs
gog calendar events --calendars "1,Work,user@example.com" --today
gog calendar events --cal "1" --cal "My Calendar" --cal "user@example.com" --today

# With other flags (composable)
gog calendar events --calendars 1,2 --from 2024-01-01 --to 2024-01-31 --json

Implementation Suggestions

These are just gentle suggestions — you know the codebase best:

  1. New utility file — Perhaps calendar_names_utils.go for fetchCalendarNameToID() and resolveCalendarIDs(), mirroring the Gmail label resolution pattern

  2. New handler function — Something like listSelectedCalendarsEvents() in calendar_list.go, similar to listAllCalendarsEvents() but accepting a []string of resolved IDs

  3. Flag choice — The --cal []string repeated flag (like --reminder) handles edge cases better; --calendars comma-separated is more concise. Either or both would be valuable

  4. Merge inputs — If both flags are supported, they could be merged: append(c.Cal, splitCSV(c.Calendars)...)

  5. Validation in Run() — Add mutual exclusivity check for --cal/--calendars alongside existing --all / calendarId checks

  6. Reuse splitCSV() — Already handles trimming and empty filtering (for --calendars option)

  7. Testscalendar_selected_events_test.go following the pattern of calendar_all_events_test.go

Of course, if there's a simpler approach or architectural considerations I've missed, I trust your judgment entirely.


Summary

Aspect Details
Numeric indices --calendars 1,2,5 (quick selection from list)
Repeated flag --cal "cal1" --cal "cal2" (handles commas in names)
Comma-separated --calendars "cal1,cal2" (concise alternative)
Lookup By index, name (case-insensitive), or ID
API calls 1 (CalendarList for resolution) + N (selected calendars only)
Backward compat 100% — existing behavior unchanged
Precedent --cal follows --reminder pattern; --calendars follows freebusy/conflicts

Thank you for building such a useful tool — it's been great for scripting Google Workspace workflows.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions