diff --git a/docs/tui.md b/docs/tui.md index d1ada41b..52d35561 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -53,7 +53,7 @@ uppercase belongs to Labels: | `t` | trash | | `!` | mark as spam | | `-` / `+` | ignore / stop ignoring | -| Space | select the thread for a bulk action | +| Space or `x` | select the thread for a bulk action | | Ctrl+B | preview every bulk-reply recipient, then write one reply to every selected thread | | Ctrl+U | recall a delayed bulk reply while HEY's undo window is open | | Ctrl+S | open The Screener | @@ -109,7 +109,7 @@ scroll, the same way the mail list does. The Imbox can wear cover art, the way the HEY web app does: everything you have already read goes under it, so the box ends at what still wants your attention instead of trailing off into a month of receipts. The divider stays and says how much is under there — press -`x` to peek, `x` again to close it, or `9` to open Previously Seen on its own screen. +`z` to peek, `z` again to close it, or `9` to open Previously Seen on its own screen. Press Ctrl+V to choose one: `blobs`, `grid`, `peace`, `terrazzo`, `topo` or `waves`, the same six covers redrawn as characters, so they work in any terminal rather than only the diff --git a/internal/tui/bulk_reply.go b/internal/tui/bulk_reply.go index 8af2c226..a31afe79 100644 --- a/internal/tui/bulk_reply.go +++ b/internal/tui/bulk_reply.go @@ -287,7 +287,7 @@ func replyNoun(count int) string { func (v *mailView) startBulkReply() tea.Cmd { postingIDs := v.actionList().selectedIDs() if len(postingIDs) == 0 { - v.notice = "Select threads with space before starting a bulk reply" + v.notice = "Select threads with space or x before starting a bulk reply" return nil } requestID, ctx := v.requests.begin(v.vc.ctx, mailRequestBulkReply) diff --git a/internal/tui/bulk_reply_test.go b/internal/tui/bulk_reply_test.go index 5c39e62c..c70f62f1 100644 --- a/internal/tui/bulk_reply_test.go +++ b/internal/tui/bulk_reply_test.go @@ -120,7 +120,7 @@ func TestTUIBulkReplyRequiresExplicitSelection(t *testing.T) { if cmd := view.HandleContentKey(keyPress("ctrl+b")); cmd != nil { t.Fatal("bulk reply without a selection should not make a request") } - if view.notice != "Select threads with space before starting a bulk reply" { + if view.notice != "Select threads with space or x before starting a bulk reply" { t.Errorf("notice = %q", view.notice) } if requests := state.snapshot(); len(requests) != 0 { diff --git a/internal/tui/calendar_views.go b/internal/tui/calendar_views.go index 26bffe68..62ec5ae8 100644 --- a/internal/tui/calendar_views.go +++ b/internal/tui/calendar_views.go @@ -652,7 +652,7 @@ func renderDayView(events, habits, countdowns []Recording, anchor, now time.Time // The day names itself above its hours — the subnav carries the calendar and the // view mode, so which day this is has nowhere else to be said — and the keys that - // move it sit on the same line, where the cover puts "x to peek". + // move it sit on the same line, where the cover puts "z to peek". b.WriteString(hintedSectionHeader(anchor.Local().Format("Monday, January 2"), hint, width)) b.WriteString("\n") diff --git a/internal/tui/content.go b/internal/tui/content.go index 84d2c38d..4a592de8 100644 --- a/internal/tui/content.go +++ b/internal/tui/content.go @@ -569,7 +569,7 @@ func (c *contentList) view() string { if label := c.sectionLabelAt(i); label != "" { if c.cover != coverNone && sectionOf(p) == sectionPreviouslySeen { - fmt.Fprintln(&b, hintedSectionHeader(label, "x to cover", c.width)) + fmt.Fprintln(&b, hintedSectionHeader(label, "z to cover", c.width)) } else { fmt.Fprintln(&b, sectionHeader(label, c.width)) } @@ -695,7 +695,7 @@ func (c *contentList) view() string { // The threads themselves are not rendered at all — that is the whole point of a // cover, and it is why the art can have every row the postings did not use. func (c *contentList) coverView(hidden, rowsUsed int) string { - hint := fmt.Sprintf("%d hidden · x to peek", hidden) + hint := fmt.Sprintf("%d hidden · z to peek", hidden) header := hintedSectionHeader(sectionPreviouslySeen.label(), hint, c.width) rows := c.height - rowsUsed - 1 @@ -716,7 +716,7 @@ func sectionHeader(label string, width int) string { } // hintedSectionHeader is a section label with a hint on its right, where the HEY web -// app puts a section's buttons: "Previously Seen ──── 34 hidden · x to peek", or +// app puts a section's buttons: "Previously Seen ──── 34 hidden · z to peek", or // "Habits ──── b to manage". func hintedSectionHeader(label, hint string, width int) string { rule := lipgloss.NewStyle().Foreground(colorChrome) diff --git a/internal/tui/cover_picker.go b/internal/tui/cover_picker.go index fe30369d..9e8a5745 100644 --- a/internal/tui/cover_picker.go +++ b/internal/tui/cover_picker.go @@ -95,7 +95,7 @@ func (p *coverPicker) view(styles styles, width int) string { b.WriteString(preview + "\n\n") } b.WriteString(strings.Join(wrapText( - "A cover hides the threads you have already read. Press x to look under it.", + "A cover hides the threads you have already read. Press z to look under it.", contentWidth), "\n")) return b.String() } diff --git a/internal/tui/covers_test.go b/internal/tui/covers_test.go index 79d438b6..8f087805 100644 --- a/internal/tui/covers_test.go +++ b/internal/tui/covers_test.go @@ -556,7 +556,7 @@ func TestCoverHidesPreviouslySeen(t *testing.T) { if !strings.Contains(view, sectionPreviouslySeen.label()) { t.Error("covered list dropped the Previously Seen divider") } - if !strings.Contains(view, "3 hidden · x to peek") { + if !strings.Contains(view, "3 hidden · z to peek") { t.Error("covered list gave no hint about what is under the cover") } for _, posting := range list.postings[1:] { @@ -599,7 +599,7 @@ func TestPeekingLiftsTheCover(t *testing.T) { if !strings.Contains(view, list.postings[2].Name) { t.Error("peeking did not reveal the seen threads") } - if !strings.Contains(view, "x to cover") { + if !strings.Contains(view, "z to cover") { t.Error("a peeked list does not say how to put the cover back") } @@ -627,7 +627,7 @@ func TestCoverDropsTheArtBeforeTheDivider(t *testing.T) { list := coveredList(coverTopo, coverMinRows+2, false, false, true) view := list.view() - if !strings.Contains(view, "1 hidden · x to peek") { + if !strings.Contains(view, "1 hidden · z to peek") { t.Error("a short covered list lost its divider") } if rows := strings.Count(view, "\n") + 1; rows > coverMinRows+2 { @@ -643,7 +643,7 @@ func TestCoverWithNothingUnread(t *testing.T) { t.Errorf("itemCount = %d, want 0", got) } view := list.view() - if !strings.Contains(view, "2 hidden · x to peek") { + if !strings.Contains(view, "2 hidden · z to peek") { t.Error("an all-read Imbox does not say what is under the cover") } if rows := strings.Count(view, "\n") + 1; rows != 20 { diff --git a/internal/tui/mail.go b/internal/tui/mail.go index 2a634ade..90ab30b1 100644 --- a/internal/tui/mail.go +++ b/internal/tui/mail.go @@ -1007,7 +1007,7 @@ func (v *mailView) HelpBindings() []helpBinding { } bindings := []helpBinding{ {"enter", "open"}, - {"space", "select"}, + {"space/x", "select"}, {"ctrl+b", "bulk reply"}, {"r", "reply"}, {"f", "forward"}, @@ -1044,7 +1044,7 @@ func (v *mailView) HelpBindings() []helpBinding { {"/", "search"}, {"ctrl+s", "screener"}, {"c", "compose"}, - {"space", "select"}, + {"space/x", "select"}, {"ctrl+b", "bulk reply"}, {"r", "reply"}, {"f", "forward"}, @@ -1066,9 +1066,9 @@ func (v *mailView) HelpBindings() []helpBinding { helpBinding{"ctrl+r", "reload"}, ) if v.postingList.cover != coverNone { - peek := helpBinding{"x", "peek under cover"} + peek := helpBinding{"z", "peek under cover"} if v.postingList.coverPeeked { - peek = helpBinding{"x", "cover"} + peek = helpBinding{"z", "cover"} } bindings = append(bindings, peek) } @@ -1377,7 +1377,7 @@ func (v *mailView) HandleContentKey(msg tea.KeyPressMsg) tea.Cmd { case "j": v.seenList.moveDown() return v.loadMoreSeenPostings() - case " ", "space": + case " ", "space", "x": v.seenList.toggleSelected() return nil case "ctrl+b": @@ -1417,7 +1417,7 @@ func (v *mailView) HandleContentKey(msg tea.KeyPressMsg) tea.Cmd { return v.startSearch() case "c": return v.startCompose() - case " ", "space": + case " ", "space", "x": v.postingList.toggleSelected() return nil case "ctrl+b": @@ -1431,7 +1431,7 @@ func (v *mailView) HandleContentKey(msg tea.KeyPressMsg) tea.Cmd { return v.startFolderPicker() case "n", "N": return v.startCollectionPicker() - case "x": + case "z": v.postingList.toggleCoverPeek() return nil case "ctrl+v": diff --git a/internal/tui/mail_test.go b/internal/tui/mail_test.go index 8da197f8..54fea5f1 100644 --- a/internal/tui/mail_test.go +++ b/internal/tui/mail_test.go @@ -3319,16 +3319,43 @@ func TestMailViewHaystackPickerAliases(t *testing.T) { } } -func TestMailViewCoverPeekUsesX(t *testing.T) { +func TestMailViewCoverPeekUsesZ(t *testing.T) { v := mailWithPostings() v.postingList.setCover(coverTopo) - v.HandleContentKey(keyPress("x")) + v.HandleContentKey(keyPress("z")) if !v.postingList.coverPeeked { - t.Fatal("x did not lift the cover") + t.Fatal("z did not lift the cover") + } + v.HandleContentKey(keyPress("z")) + if v.postingList.coverPeeked { + t.Fatal("z did not replace the cover") } +} + +// x selects like space does, matching the HEY desktop app — including on a covered +// Imbox, where x used to lift the cover (that is z's key now). +func TestMailViewSelectsWithXLikeSpace(t *testing.T) { + v := mailWithPostings() + v.postingList.setCover(coverTopo) + v.HandleContentKey(keyPress("x")) + if ids := v.postingList.selectedIDs(); len(ids) != 1 || ids[0] != 100 { + t.Fatalf("selected after x = %v, want [100]", ids) + } if v.postingList.coverPeeked { - t.Fatal("x did not replace the cover") + t.Error("x lifted the cover instead of selecting") + } + v.HandleContentKey(keyPress("x")) + if ids := v.postingList.selectedIDs(); len(ids) != 0 { + t.Errorf("selected after second x = %v, want none", ids) + } + + seen := mailWithPostings() + seen.seenActive = true + seen.seenList.setPostings(testPostings()) + seen.HandleContentKey(keyPress("x")) + if ids := seen.seenList.selectedIDs(); len(ids) != 1 || ids[0] != 100 { + t.Errorf("seen screen selected after x = %v, want [100]", ids) } } @@ -4269,7 +4296,7 @@ func TestMailViewSeenScreenHelpOffersTheThreadActions(t *testing.T) { v.seenActive = true bindings := v.HelpBindings() - for _, key := range []string{"enter", "space", "ctrl+b", "a", "l", "u", "t", "v", "b", "n"} { + for _, key := range []string{"enter", "space/x", "ctrl+b", "a", "l", "u", "t", "v", "b", "n"} { if !hasHelpBinding(bindings, key) { t.Errorf("seen screen help misses %q: %+v", key, bindings) }