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
12 changes: 12 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ hey journal list --limit
hey journal read
hey journal write
hey journal write --content
hey label
hey label --all
hey label --limit
hey label --page
hey label add
hey label add --to
hey label create
hey label remove
hey label remove --from
hey labels
hey labels --all
hey labels --limit
hey move
hey move --to
hey recordings
Expand Down
5 changes: 5 additions & 0 deletions API-COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ The remaining HTML-reading gaps use the SDK's authenticated HTML helper and are
| `/asidebox.json` | GET | SDK `Boxes().GetAsidebox` | `hey box asidebox` | covered |
| `/laterbox.json` | GET | SDK `Boxes().GetLaterbox` | `hey box laterbox` | covered |
| `/bubblebox.json` | GET | SDK `Boxes().GetBubblebox` | `hey box bubblebox` | covered |
| `/my/navigation.json` | GET | SDK `Identity().GetNavigation` | `hey labels`, Mail TUI navigation | covered |
| `/folders/{id}.json` | GET | SDK `Folders().GetPage` | `hey label <id>`, Mail TUI labels | covered |
| `/postings/filings.json` | POST | SDK `Postings().File` | `hey label add`, TUI `g` | covered |
| `/postings/filings.json` | DELETE | SDK `Postings().Unfile` | `hey label remove`, TUI `g` | covered |
| `/postings/folders.json` | POST | SDK `Postings().CreateFolder` | `hey label create`, TUI `g` | covered |
| `/advanced_search.json` | GET | SDK `Search().Search` | `hey search`, TUI `/` | covered |
| `/advanced_search_filters.json` | GET | SDK `Search().Filters` | `hey search filters` | covered |
| `/contacts.json` | GET | SDK `Contacts().List` | `hey contacts list`, Contacts TUI | covered |
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ accounts, press Ctrl+A to switch between All Accounts and individual email addre
Switching cancels requests from the previous account and reloads the active section;
Calendar and Journal remain identity-wide.

Navigate between Mail, Contacts, Calendar, and Journal. In Mail, use `/` to search, Enter to open a thread, `r` to reply, `f` to forward, `m` to move, `t` to trash, `s` to mark as spam, `-` to ignore, and `+` to stop ignoring. Select threads with Space and press `b` to preview every bulk-reply recipient before writing and sending one reply to all selected threads. A delayed bulk reply can be recalled with `u` while HEY's undo window remains open. Search results retain the matching-message summary; use `n` and `p` to move between result pages.
Navigate between Mail, Contacts, Calendar, and Journal. Mail navigation includes HEY boxes followed by your labels. Use `n` and `p` to page through a label. Use `/` to search, Enter to open a thread, `r` to reply, `f` to forward, `m` to move, `g` to add, create, or remove labels, `t` to trash, `s` to mark as spam, `-` to ignore, and `+` to stop ignoring. Select threads with Space and press `b` to preview every bulk-reply recipient before writing and sending one reply to all selected threads. A delayed bulk reply can be recalled with `u` while HEY's undo window remains open. Search results retain the matching-message summary; use `n` and `p` to move between result pages.

Thread attachments always appear with their filename, media type, and size. Use `[` and `]` to select an attachment, `s` to save it without replacing an existing file, and `o` to download and open it in an external application. Attachments never open automatically. Kitty and Ghostty can show inline images. Foot and other terminals use visible text markers.

Expand All @@ -117,6 +117,12 @@ hey boxes --quiet --jq '.[].id'
```bash
hey boxes # list mailboxes
hey box imbox # list email threads in a box (by name or ID)
hey labels # list labels and their IDs
hey label 789 --all # list all email threads with a label
hey label add 12345 --to 789 # add a label to a thread
hey label create "Travel receipts" 12345 # create and add a label
hey label remove 12345 --from 789 # remove one label
hey label remove 12345 --from all # remove every label
hey search "quarterly planning" # search threads and matching messages
hey search --from jane@example.com --date last_30_days # refine a search
hey search filters # list available refinement values
Expand Down Expand Up @@ -159,7 +165,7 @@ Contact updates preserve omitted name, email, and alias fields. Supplying `--ali

`--attach` is repeatable on `hey compose`, `hey reply`, and `hey bulk-reply send`, and attachment-only messages are supported. The CLI validates and uploads every file before sending the email. `hey attachments <topic_id>` returns stable message-and-position IDs such as `456:1`; pass an ID to `hey attachments save`. Saving uses the original filename by default, accepts `--output` for a file or directory, and preserves existing files unless `--force` is set.

Organization actions take the `id` values returned by `hey box --json` or `hey search --json`. Move destinations are Imbox, The Feed, Set Aside, Reply Later, or Paper Trail. Bubble Up requires a scheduled date and is not available through `hey move`. Trashing a shared thread removes your access instead of deleting it for everyone. Ignored threads remain in their box and can be restored with `hey stop-ignoring`.
Organization actions take the `id` values returned by `hey box --json`, `hey label --json`, or `hey search --json`. Label IDs come from `hey labels`; `hey label` returns `next_page` and `total_count`, accepts `--page <next_page>` for continuation, and supports `--all` for complete traversal. HEY creates a label while adding it to at least one thread, so `hey label create` requires thread item IDs. Move destinations are Imbox, The Feed, Set Aside, Reply Later, or Paper Trail. Bubble Up requires a scheduled date and is not available through `hey move`. Trashing a shared thread removes your access instead of deleting it for everyone. Ignored threads remain in their box and can be restored with `hey stop-ignoring`.

### Calendars

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
charm.land/bubbles/v2 v2.1.1
charm.land/bubbletea/v2 v2.0.8
charm.land/lipgloss/v2 v2.0.6
github.com/basecamp/hey-sdk/go v0.6.0
github.com/basecamp/hey-sdk/go v0.6.1
github.com/charmbracelet/x/ansi v0.11.8
github.com/itchyny/gojq v0.12.19
github.com/mattn/go-runewidth v0.0.27
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/basecamp/hey-sdk/go v0.6.0 h1:maoTjI41fb25L7d3KUbZq/CdzNTH96JGu8/oOukUnTs=
github.com/basecamp/hey-sdk/go v0.6.0/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk=
github.com/basecamp/hey-sdk/go v0.6.1 h1:NlruAUq1GOk+VCkc1HyxAsvLCbNf54n0kZ6kNfhZkyc=
github.com/basecamp/hey-sdk/go v0.6.1/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/box.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ func (c *boxCommand) run(cmd *cobra.Command, args []string) error {
table.addRow([]string{"Thread", "From", "Summary", "Date"})
for _, p := range postings {
displayID := resolvePostingTopicID(p)
if displayID == 0 {
displayID = p.Id
}
table.addRow([]string{fmt.Sprintf("%d", displayID), p.Creator.Name, truncate(p.Summary, 60), formatDate(p.CreatedAt)})
}
table.print()
Expand Down
19 changes: 13 additions & 6 deletions internal/cmd/drafts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import (
"testing"
)

func runStyledDraftsCommand(t *testing.T, handler http.Handler, args ...string) (string, error) {
func runStyledCommand(t *testing.T, handler http.Handler, args ...string) (string, error) {
t.Helper()
return runFormattedCommand(t, handler, []string{"--styled"}, args...)
}

func runFormattedCommand(t *testing.T, handler http.Handler, formatArgs []string, args ...string) (string, error) {
t.Helper()
previousColorDisabled := colorDisabled
colorDisabled = false
Expand All @@ -30,7 +35,9 @@ func runStyledDraftsCommand(t *testing.T, handler http.Handler, args ...string)
var stdout, stderr bytes.Buffer
root.SetOut(&stdout)
root.SetErr(&stderr)
root.SetArgs(append([]string{"--styled", "--base-url", server.URL, "drafts"}, args...))
rootArgs := append([]string{}, formatArgs...)
rootArgs = append(rootArgs, "--base-url", server.URL)
root.SetArgs(append(rootArgs, args...))

err := root.Execute()
return stdout.String(), err
Expand Down Expand Up @@ -90,13 +97,13 @@ func TestDraftsCommandAllOverridesLimit(t *testing.T) {

func TestDraftsCommandStyledTable(t *testing.T) {
const fullSummary = "Notes from the quarterly planning meeting including decisions and follow-up assignments"
stdout, err := runStyledDraftsCommand(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
stdout, err := runStyledCommand(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `[
{"id":101,"summary":"`+fullSummary+`","subject":"Quarterly planning follow-up","updated_at":"2026-08-20T09:30:00Z"},
{"id":102,"summary":"Travel details","subject":"Team retreat itinerary","updated_at":"2026-08-19T14:00:00Z"}
]`)
}), "--limit", "1")
}), "drafts", "--limit", "1")
if err != nil {
t.Fatalf("execute styled drafts: %v", err)
}
Expand All @@ -111,9 +118,9 @@ func TestDraftsCommandStyledTable(t *testing.T) {
}

func TestDraftsCommandStyledEmpty(t *testing.T) {
stdout, err := runStyledDraftsCommand(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
stdout, err := runStyledCommand(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
}), "drafts")
if err != nil {
t.Fatalf("execute styled drafts: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var curatedCategories = []struct {
}{
{
heading: "EMAIL",
names: []string{"boxes", "box", "search", "contacts", "threads", "attachments", "compose", "reply", "bulk-reply", "forward", "drafts", "seen", "unseen", "move", "trash", "spam", "ignore", "stop-ignoring"},
names: []string{"boxes", "box", "labels", "label", "search", "contacts", "threads", "attachments", "compose", "reply", "bulk-reply", "forward", "drafts", "seen", "unseen", "move", "trash", "spam", "ignore", "stop-ignoring"},
},
{
heading: "CALENDAR & TASKS",
Expand Down
4 changes: 3 additions & 1 deletion internal/cmd/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestCuratedCommandHelpUsesUserFacingLanguage(t *testing.T) {

func TestEmailCommandHelpKeepsPostingAsAnInternalTerm(t *testing.T) {
root := newRootCmd()
for _, name := range []string{"boxes", "box", "search", "seen", "unseen", "move", "trash", "spam", "ignore", "stop-ignoring"} {
for _, name := range []string{"boxes", "box", "labels", "label", "search", "seen", "unseen", "move", "trash", "spam", "ignore", "stop-ignoring"} {
t.Run(name, func(t *testing.T) {
command, _, err := root.Find([]string{name})
if err != nil {
Expand Down Expand Up @@ -94,6 +94,8 @@ USAGE
EMAIL
boxes List your HEY boxes
box List email threads in a box
labels List your email labels
label View and manage an email label
search Search email threads and messages
contacts Manage contacts
threads Read a thread
Expand Down
Loading
Loading