Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ hey event add --start-time
hey event add --starts-on
hey event add --time-zone
hey event add --title
hey event day
hey event day --all
hey event day --limit
hey event delete
hey event edit
hey event edit --all-day
Expand Down Expand Up @@ -193,6 +196,9 @@ hey event list --calendar
hey event list --ends-on
hey event list --limit
hey event list --starts-on
hey event week
hey event week --all
hey event week --limit
hey forward
hey forward --bcc
hey forward --cc
Expand Down
4 changes: 2 additions & 2 deletions API-COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ which is what `Entries().ListDraftsPage` and `hey draft list --page` exist for.
| `/calendar/habits.json` | POST | SDK `Habits().Create` | `hey habit create`, Calendar TUI `a` | covered |
| `/calendar/habits/{id}.json` | PATCH | SDK `Habits().Update` | `hey habit edit <id>`, Calendar TUI `e` | covered |
| `/calendar/habits/{id}.json` | DELETE | SDK `Habits().Delete` | `hey habit delete <id>`, Calendar TUI `x` | covered |
| `/calendar/days/{date}.json` | GET | SDK `CalendarPeriods().Day` | Calendar TUI day view | covered |
| `/calendar/weeks/{date}.json` | GET | SDK `CalendarPeriods().Week` | `hey habit list`, Calendar TUI week view | covered: a period expands a recurrence into its occurrences and is the only read that carries habits, which a calendar's recordings listing does not |
| `/calendar/days/{date}.json` | GET | SDK `CalendarPeriods().Day` | `hey event day [date]`, Calendar TUI day view | covered |
| `/calendar/weeks/{date}.json` | GET | SDK `CalendarPeriods().Week` | `hey event week [date]`, `hey habit list`, Calendar TUI week view | covered: a period expands a recurrence into its occurrences and is the only read that carries habits, which a calendar's recordings listing does not |
| `/calendar/years/{date}.json` | GET | SDK `CalendarPeriods().Year` | Calendar TUI year view | covered |
| `/calendar/days/{date}/habits/{id}/completions.json` | POST | SDK `Habits().Complete` | `hey habit complete <id>` | covered |
| `/calendar/days/{date}/habits/{id}/completions.json` | DELETE | SDK `Habits().Uncomplete` | `hey habit uncomplete <id>` | covered |
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,8 @@ rendering when the output is piped, `--ids-only` for one ID per line, and `--cou
bare number. `--ids-only` and `--count` need list data, so they work on `hey box list`,
`hey box view`, `hey label list`, `hey label view`, `hey collection list`, `hey collection view`,
`hey workflow list`, `hey workflow view`, `hey clip list`, `hey snippet list`, `hey draft list`, `hey search`, `hey contact list`, `hey screener list`, `hey screener history`, `hey calendar list`,
`hey event list`, `hey todo list`, `hey habit list`, `hey timetrack list` and
`hey journal list`.
`hey event list`, `hey event day`, `hey event week`, `hey todo list`, `hey habit list`,
`hey timetrack list` and `hey journal list`.
The
data-only formats print any pagination notice on stderr, so the IDs on stdout stay
pipeable. `hey clip list --ids-only` and `--count` cover the newest page only because the
Expand Down Expand Up @@ -565,6 +565,10 @@ hey event list # every calendar, from today onward
hey event list --calendar 123 --starts-on 2026-01-01 --ends-on 2026-01-31
hey event list --count # how many events in the window

hey event day # today as HEY draws it, recurrences expanded
hey event day 2026-09-02 # one day
hey event week 2026-09-02 # the week that day falls in

hey event add "Design review" --starts-on 2026-09-02 --start-time 14:00 --end-time 15:00
hey event add "Sarah's birthday" --starts-on 2026-09-02 # no time, so all day
hey event add "Standup" --start-time 09:15 --repeat every_weekday --remind 10m
Expand All @@ -580,6 +584,14 @@ newest start time, not creation time, so use the ID returned by `hey event add`
edit or delete rather than choosing an event by its position in the list. A repeating event
lists once as the series it is stored as, not once per day it falls on.

`hey event day` and `hey event week` read a span the way HEY's own views draw it: a
repeating event is expanded into the occurrences that fall inside it, each carrying that
day's own times, an `occurrence_id`, and the id of the series it repeats — which is what
`hey event edit` and `hey event delete` take. A period covers the calendars switched on in
HEY, the same set the app draws, so `day` and `week` take no `--calendar` — only `--limit`
and `--all`. With no date they read the account's own today, whatever zone the machine
runs in.

An event with no `--start-time` is an all-day event, and a `--start-time` with no
`--end-time` runs for an hour. Clock times are read in `--time-zone`, which defaults to the
machine's own zone; without one HEY would read them as UTC.
Expand Down
60 changes: 13 additions & 47 deletions internal/cmd/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ func newEventsCommand() *eventsCommand {
Use: "event",
Short: "Read and manage calendar events",
Annotations: map[string]string{
"agent_notes": "Subcommands: list, add, edit, delete. list reads every calendar unless --calendar names one, and lists a repeating event once as its series rather than once per day. An edit is not a patch on HEY's side: it resends the notes, location, link, attached email, reminders and time zones the event already carries, so notes lose their formatting and a countdown is removed unless --countdown names one again.",
"agent_notes": "Subcommands: list, day, week, add, edit, delete. \"What's on the schedule today?\" is answered by day, not list: day and week read the span as HEY draws it, with a repeating event expanded into the occurrences inside it, over the calendars switched on in HEY. list reads what calendars hold — every calendar unless --calendar names oneand a repeating event is one row, its series, on the day the series began. An edit is not a patch on HEY's side: it resends the notes, location, link, attached email, reminders and time zones the event already carries, so notes lose their formatting and a countdown is removed unless --countdown names one again.",
},
}

eventsCommand.cmd.AddCommand(newEventsListCommand().cmd)
eventsCommand.cmd.AddCommand(newEventsDayCommand().cmd)
eventsCommand.cmd.AddCommand(newEventsWeekCommand().cmd)
Comment thread
jeremy marked this conversation as resolved.
Comment thread
jeremy marked this conversation as resolved.
eventsCommand.cmd.AddCommand(newEventsAddCommand().cmd)
eventsCommand.cmd.AddCommand(newEventsEditCommand().cmd)
eventsCommand.cmd.AddCommand(newEventsDeleteCommand().cmd)
Expand All @@ -55,6 +57,11 @@ func newEventsListCommand() *eventsListCommand {
eventsListCommand.cmd = &cobra.Command{
Use: "list",
Short: "List calendar events",
Long: `List the events calendars hold over a date window.

A repeating event is stored once, so it lists once, as its series, on the day the series
began. For the events of a day or a week as HEY draws them — occurrences of a repeating
series expanded into the days they fall on — read 'hey event day' or 'hey event week'.`,
Example: ` hey event list
hey event list --starts-on 2026-01-01 --ends-on 2026-01-31
hey event list --calendar 123 --limit 5 --json`,
Expand Down Expand Up @@ -89,60 +96,19 @@ func (c *eventsListCommand) run(cmd *cobra.Command, args []string) error {
}
notice := output.TruncationNotice(len(events), total)

if writer.IsStyled() {
if len(events) == 0 {
fmt.Fprintf(cmd.OutOrStdout(), "No events %s.\n", window.describe())
return nil
}

table := newTable(cmd.OutOrStdout())
table.addRow([]string{"ID", "Title", "Starts", "Ends", "Calendar"})
for _, event := range events {
table.addRow([]string{
fmt.Sprintf("%d", event.Id), event.Title,
eventBoundary(event.StartsAt, event.AllDay),
eventBoundary(event.EndsAt, event.AllDay),
event.Calendar.Name,
})
}
table.print()
if notice != "" {
fmt.Fprintln(cmd.OutOrStdout(), notice)
}
return nil
}

return writeOK(events,
output.WithSummary(fmt.Sprintf("%d events (%s)", len(events), window.describe())),
output.WithNotice(notice),
output.WithBreadcrumbs(
output.Breadcrumb{
Action: "add",
Command: "hey event add '...'",
Description: "Create an event",
},
output.Breadcrumb{
Action: "edit",
Command: "hey event edit <id>",
Description: "Change an event",
},
output.Breadcrumb{
Action: "delete",
Command: "hey event delete <id>",
Description: "Delete an event",
},
),
)
return writeEventRows(cmd, events, window.describe(), notice)
}

// eventBoundary writes one end of an event: a day for an all-day event, a day and a clock
// time for a timed one. An all-day event has no time of day, and printing it midnight reads
// as an event that starts at midnight.
// as an event that starts at midnight. HEY's JSON is always UTC, so a timed boundary
// converts to the reader's clock the way HEY's own views draw it, while an all-day date is
// the day it names and does not shift.
func eventBoundary(at time.Time, allDay bool) string {
if allDay {
return formatDate(at)
}
return formatTimestamp(at)
return formatTimestamp(at.Local())
}

// add
Expand Down
Loading
Loading