Skip to content
Open
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
4 changes: 2 additions & 2 deletions docs/tui.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/bulk_reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/bulk_reply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/calendar_views.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
6 changes: 3 additions & 3 deletions internal/tui/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/cover_picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
8 changes: 4 additions & 4 deletions internal/tui/covers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:] {
Expand Down Expand Up @@ -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")
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
14 changes: 7 additions & 7 deletions internal/tui/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -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"},
Expand All @@ -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)
}
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1aa5b1: the no-selection notice now reads "Select threads with space or x before starting a bulk reply", and the assertion in bulk_reply_test.go pins the new text.

v.postingList.toggleSelected()
return nil
case "ctrl+b":
Expand All @@ -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":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1aa5b1. The shortcuts moved from README.md to docs/tui.md on main; the rebase updated them there: the key table lists Space or x for selection, and the cover paragraph names z to peek and z again to close it.

v.postingList.toggleCoverPeek()
return nil
case "ctrl+v":
Expand Down
37 changes: 32 additions & 5 deletions internal/tui/mail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
Expand Down