Skip to content

Commit ae4b939

Browse files
seanb4tclaude
andcommitted
fix(tui): route keys to preview pane when focused and dismiss preview with q
When the preview pane has focus, keys now route to the email reader instead of the email list, and pressing 'q' dismisses the preview rather than quitting the app. Adds BDD tests for tab cycling to preview, key routing, and preview dismissal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 179c3ed commit ae4b939

8 files changed

Lines changed: 93 additions & 12 deletions

internal/tui/bdd_preview_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,76 @@ func TestBDD_Preview_EnterOpensFullscreen(t *testing.T) {
5252
assert.Equal(t, viewEmailReader, m.view)
5353
}
5454

55+
// BDD: As a user, after selecting an email, Tab cycles focus to the preview pane.
56+
func TestBDD_Preview_TabCyclesToPreview(t *testing.T) {
57+
m := setupDashboardWithEmails(t)
58+
59+
// Given: I've selected an email so preview is showing
60+
email := fastmail.Email{ID: "e1", Subject: "Hello", Body: "World",
61+
From: fastmail.EmailAddress{Name: "Alice", Email: "alice@test.com"}}
62+
er := newEmailReaderModel(email)
63+
er.isPreview = true
64+
er.setSize(80, 15)
65+
m.emailReader = &er
66+
m.panes.focus = PaneEmailList
67+
68+
// When: I press Tab
69+
m, _ = applyUpdate(m, tea.KeyMsg{Type: tea.KeyTab})
70+
71+
// Then: focus moves to PanePreview (not past it)
72+
assert.Equal(t, PanePreview, m.panes.focus)
73+
}
74+
75+
// BDD: As a user, when preview pane is focused, keys route to the email reader
76+
// (not the email list). Pressing 'j' should NOT move the email list cursor.
77+
func TestBDD_Preview_KeysRouteToReader(t *testing.T) {
78+
m := setupDashboardWithEmails(t)
79+
80+
// Given: preview is showing with a loaded email and focus is on preview
81+
email := fastmail.Email{ID: "e1", Subject: "Hello", Body: "Long email body\n\n\n\n\n\n\n\n\n\n\n\nBottom",
82+
From: fastmail.EmailAddress{Name: "Alice", Email: "alice@test.com"}}
83+
er := newEmailReaderModel(email)
84+
er.isPreview = true
85+
er.setSize(80, 10)
86+
// Simulate body loaded so viewport is initialized
87+
er, _ = er.update(emailBodyLoadedMsg{email: email})
88+
m.emailReader = &er
89+
m.panes.focus = PanePreview
90+
91+
// Record email list cursor before keypress
92+
cursorBefore := m.emailList.list.Index()
93+
94+
// When: I press 'j' (which scrolls in reader, moves cursor in list)
95+
m, _ = applyUpdate(m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")})
96+
97+
// Then: the email list cursor should NOT have moved (key went to reader, not list)
98+
assert.Equal(t, cursorBefore, m.emailList.list.Index(),
99+
"j should route to email reader when preview is focused, not move the email list cursor")
100+
}
101+
102+
// BDD: As a user, pressing 'q' with preview focused dismisses the preview, not quits.
103+
func TestBDD_Preview_QDismissesPreview(t *testing.T) {
104+
m := setupDashboardWithEmails(t)
105+
106+
// Given: preview is showing and focused
107+
email := fastmail.Email{ID: "e1", Subject: "Hello"}
108+
er := newEmailReaderModel(email)
109+
er.isPreview = true
110+
er.setSize(80, 10)
111+
m.emailReader = &er
112+
m.panes.focus = PanePreview
113+
114+
// When: I press 'q'
115+
m, _ = applyUpdate(m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")})
116+
117+
// Then: the app should NOT quit
118+
assert.False(t, m.quit, "q should dismiss preview, not quit the app")
119+
// And: preview should be cleared
120+
assert.Nil(t, m.emailReader)
121+
// And: focus should move back to email list
122+
assert.Equal(t, PaneEmailList, m.panes.focus)
123+
}
124+
55125
func setupDashboardWithEmails(t *testing.T) Model {
56126
t.Helper()
57127
m := New(nil)

internal/tui/testdata/TestIntegration_DashboardLoad.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
│ 5 items ││ 5 items │
66
│ ││ │
77
││ Inbox (3) │││ * — Weekly Team Standup Notes │
8-
││ 25 emails │││ 8h ago Here are the notes from today's standup. Backend team completed the API migrati… │
8+
││ 25 emails │││ 9h ago Here are the notes from today's standup. Backend team completed the API migrati… │
99
│ ││ │
1010
│ Drafts ││ ! — Project Deadline Update │
1111
│ 2 emails ││ 10h ago The deadline for Phase 2 has been moved to March 15th. Please update your proje… │

internal/tui/testdata/TestIntegration_NarrowTerminal.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
│ 5 items │
66
│ │
77
││ * — Weekly Team Standup Notes │
8-
││ 8h ago Here are the notes from today's standup. Backen…│
8+
││ 9h ago Here are the notes from today's standup. Backen…│
99
│ ••••• │
1010
│ │
1111
│ ↑/k up • ↓/j down • / filter • q quit • ? more │

internal/tui/testdata/TestIntegration_ReadEmail.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
│ 5 items ││ 5 items │
66
│ ││ │
77
││ Inbox (3) │││ * — Weekly Team Standup Notes │
8-
││ 25 emails │││ 8h ago Here are the notes from today's standup. Backend team completed the API migrati… │
8+
││ 25 emails │││ 9h ago Here are the notes from today's standup. Backend team completed the API migrati… │
99
│ ││ │
1010
│ Drafts ││ ! — Project Deadline Update │
1111
│ 2 emails ││ 10h ago The deadline for Phase 2 has been moved to March 15th. Please update your proje… │

internal/tui/testdata/TestIntegration_SelectMailbox_Arrows.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
│ 5 items ││ 5 items │
66
│ ││ │
77
│ Inbox (3) │││ * — Weekly Team Standup Notes │
8-
│ 25 emails │││ 8h ago Here are the notes from today's standup. Backend team completed the API migrati… │
8+
│ 25 emails │││ 9h ago Here are the notes from today's standup. Backend team completed the API migrati… │
99
│ ││ │
1010
│ Drafts ││ ! — Project Deadline Update │
1111
│ 2 emails ││ 10h ago The deadline for Phase 2 has been moved to March 15th. Please update your proje… │

internal/tui/testdata/TestIntegration_SelectMailbox_JK.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
│ 5 items ││ 5 items │
66
│ ││ │
77
│ Inbox (3) │││ * — Weekly Team Standup Notes │
8-
│ 25 emails │││ 8h ago Here are the notes from today's standup. Backend team completed the API migrati… │
8+
│ 25 emails │││ 9h ago Here are the notes from today's standup. Backend team completed the API migrati… │
99
│ ││ │
1010
│ Drafts ││ ! — Project Deadline Update │
1111
│ 2 emails ││ 10h ago The deadline for Phase 2 has been moved to March 15th. Please update your proje… │

internal/tui/testdata/TestIntegration_ToggleSidebar.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
│ 5 items │
66
│ │
77
││ * — Weekly Team Standup Notes │
8-
││ 8h ago Here are the notes from today's standup. Backend team completed the API migrati… │
8+
││ 9h ago Here are the notes from today's standup. Backend team completed the API migrati… │
99
│ │
1010
│ ! — Project Deadline Update │
1111
│ 10h ago The deadline for Phase 2 has been moved to March 15th. Please update your proje… │

internal/tui/tui.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,9 @@ func (m Model) handleGlobalKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd, bool) {
290290
}
291291
switch msg.String() {
292292
case "q":
293-
// In reader/move picker/thread/attachment/compose views, q means "go back" or is a character — let the view handle it
294-
if !m.isFullscreenView() && !m.isFiltering() {
293+
// In reader/move picker/thread/attachment/compose views, q means "go back" — let the view handle it.
294+
// When preview pane is focused, q dismisses the preview — also let updateView handle it.
295+
if !m.isFullscreenView() && !m.isFiltering() && m.panes.focus != PanePreview {
295296
m.quit = true
296297
return m, tea.Quit, true
297298
}
@@ -348,12 +349,19 @@ func (m Model) handleLayoutKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd, bool) { //n
348349
}
349350

350351
func (m Model) updateView(msg tea.Msg) (tea.Model, tea.Cmd) {
351-
// When sidebar is focused in a split-pane view, route keys to the mailbox
352-
// list so users can navigate mailboxes while the email list is visible.
352+
// When a non-default pane is focused in a split-pane view, route keys to
353+
// that pane so users can interact without changing the active view.
353354
// Skip in fullscreen/modal views and filter mode where keys belong elsewhere.
354-
if m.panes.focus == PaneMailbox && !m.isFullscreenView() && !m.isFiltering() {
355+
if !m.isFullscreenView() && !m.isFiltering() {
355356
if _, isKey := msg.(tea.KeyMsg); isKey {
356-
return m.updateMailboxList(msg)
357+
switch m.panes.focus {
358+
case PaneMailbox:
359+
return m.updateMailboxList(msg)
360+
case PanePreview:
361+
return m.updateEmailReader(msg)
362+
case PaneEmailList:
363+
// default: fall through to updateView's view-based routing
364+
}
357365
}
358366
}
359367

@@ -459,6 +467,9 @@ func (m Model) updateEmailReader(msg tea.Msg) (tea.Model, tea.Cmd) {
459467
if er.goBack {
460468
m.emailReader = nil
461469
m.view = viewEmailList
470+
if m.panes.focus == PanePreview {
471+
m.panes.focus = PaneEmailList
472+
}
462473
return m, nil
463474
}
464475

0 commit comments

Comments
 (0)