From d23b9bdb46d422d41f0eb058434a5d27dfcf70ee Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 30 Sep 2026 12:06:55 -0400 Subject: [PATCH 1/3] Open a long link once the footer can show its host A selected link could only be opened when its whole destination fit on the footer row, so a long tracking URL was stuck behind "Enlarge the terminal" on all but the widest screens. The footer now keeps the scheme and host in full and cuts the rest of the path short, which still says where the link goes, and Enter opens it. A destination with no host, like a mailto link, still has to fit in full. --- docs/tui.md | 2 +- internal/tui/mail.go | 28 +++++++++++++++-- internal/tui/mail_test.go | 64 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/docs/tui.md b/docs/tui.md index 41c32b70..49efcbe3 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -70,7 +70,7 @@ Sending a reply or a forward from a thread closes the thread and returns you to While reading a thread, the From header shows the actual send-as address when HEY records one separately from the account user. -While reading a thread, links can be selected without a mouse. Tab selects the next link in document order and Shift+Tab selects the previous one; both wrap at the ends. A fixed row above the shortcut bar shows the complete selected destination without moving the thread. Press Enter to open it; opening stays unavailable until the terminal is wide enough to show the complete destination. Press Escape once to clear the selection, and Escape again to leave the thread. A thread with no selectable links keeps the normal global Tab focus behavior. Existing OSC 8 mouse links remain available. +While reading a thread, links can be selected without a mouse. Tab selects the next link in document order and Shift+Tab selects the previous one; both wrap at the ends. A fixed row above the shortcut bar shows the complete selected destination without moving the thread. Press Enter to open it. A destination too long for the row keeps its scheme and host in full and cuts the rest short; opening stays unavailable until the terminal is wide enough to show at least that much. Press Escape once to clear the selection, and Escape again to leave the thread. A thread with no selectable links keeps the normal global Tab focus behavior. Existing OSC 8 mouse links remain available. Most of those keep working while you are reading a thread, the way the web app's topic toolbar stays live: `r`, `f`, `v`, `b`, `u`, `i`, `l`, `a`, `d`, `p` and `t` all act on diff --git a/internal/tui/mail.go b/internal/tui/mail.go index 82b02e19..e0092b2f 100644 --- a/internal/tui/mail.go +++ b/internal/tui/mail.go @@ -1753,7 +1753,8 @@ func (v *mailView) threadNotices() []string { // LinkFooter reserves one footer row for a thread with selectable links. Its text // stays blank until a link is selected, so moving through links never changes the -// viewport's height. A destination can open only when the footer shows it in full. +// viewport's height. A destination can open only when the footer shows at least +// its scheme and host in full. func (v *mailView) LinkFooter() (text string, visible bool) { if !v.inThread || v.modal != nil || len(v.links) == 0 { return "", false @@ -1771,13 +1772,36 @@ func (v *mailView) linkDestinationFooter() (string, bool) { return "", false } destination := terminal.SanitizeLine(v.links[v.selectedLink].destination) - footer := "Open: " + destination + " (press Enter to visit)" + const prefix, suffix = "Open: ", " (press Enter to visit)" + footer := prefix + destination + suffix if lipgloss.Width(footer) <= width { return footer, true } + // A long destination can still open when the footer shows where it goes: the + // scheme and host in full, with only the tail of the path cut short. + room := width - lipgloss.Width(prefix+suffix) + if origin := linkOrigin(destination); origin != "" && room >= lipgloss.Width(origin)+3 { + return prefix + truncateToWidth(destination, room) + suffix, true + } return truncateToWidth("Enlarge the terminal to inspect this link", width), false } +// linkOrigin is a destination up to the end of its authority — "https://example.com" +// of "https://example.com/path" — or empty when it has none, as a mailto link does. +func linkOrigin(destination string) string { + scheme, rest, ok := strings.Cut(destination, "://") + if !ok { + return "" + } + if end := strings.IndexAny(rest, "/?#"); end >= 0 { + rest = rest[:end] + } + if rest == "" { + return "" + } + return scheme + "://" + rest +} + func (v *mailView) linkDestinationReviewable() bool { _, reviewable := v.linkDestinationFooter() return reviewable diff --git a/internal/tui/mail_test.go b/internal/tui/mail_test.go index 0cf72ef1..a45616f4 100644 --- a/internal/tui/mail_test.go +++ b/internal/tui/mail_test.go @@ -553,6 +553,70 @@ func TestLinkDestinationMustFitFooterBeforeOpening(t *testing.T) { } } +func TestLongLinkDestinationOpensWhenItsHostFits(t *testing.T) { + v := newMailView(testVC()) + v.inThread = true + v.topicID = 100 + v.selectedLink = 0 + destination := "https://bulletin.example.com/T/OFC4/L2S/9330/B2827790/MNew/757620/28297471/JXSWDt/1/10333590/PxFblM2x.html?h=_GUL3Rht552Ov_esGif3" + v.links = []mailLink{{destination: destination, key: "501\x000"}} + v.selectedLinkKey = v.links[0].key + v.vc.width = 80 + v.contentHeight = 12 + + footer, visible := v.LinkFooter() + if !visible || !v.linkDestinationReviewable() { + t.Fatalf("link footer = %q visible=%v, want a reviewable footer", footer, visible) + } + if !strings.HasPrefix(footer, "Open: https://bulletin.example.com/") || !strings.HasSuffix(footer, "... (press Enter to visit)") { + t.Errorf("link footer = %q, want the host in full and the path cut short", footer) + } + if width := lipgloss.Width(footer); width > v.vc.width { + t.Errorf("link footer is %d cells wide, want at most %d", width, v.vc.width) + } + + var opened string + v.vc.openURL = func(destination string) error { + opened = destination + return nil + } + cmd, handled := v.handleLinkKey(keyPress("enter")) + if !handled || cmd == nil { + t.Fatal("a long destination with a visible host was not opened") + } + runCmd(cmd) + if opened != destination { + t.Errorf("opened %q, want the complete destination %q", opened, destination) + } + + v.vc.width = 50 + if footer, _ := v.LinkFooter(); v.linkDestinationReviewable() || !strings.HasPrefix(footer, "Enlarge the terminal") { + t.Errorf("footer too narrow for the host = %q reviewable=%v, want the enlarge hint", footer, v.linkDestinationReviewable()) + } + + v.vc.width = 80 + v.links[0].destination = "mailto:" + strings.Repeat("quarterly.reports.", 5) + "@example.com" + if v.linkDestinationReviewable() { + t.Error("a long destination with no host was marked reviewable without being shown in full") + } +} + +func TestLinkOrigin(t *testing.T) { + for destination, want := range map[string]string{ + "https://example.com/quarterly-report": "https://example.com", + "https://example.com?utm_source=newsletter": "https://example.com", + "http://example.com:8080#top": "http://example.com:8080", + "https://example.com@phishing.example.org/login": "https://example.com@phishing.example.org", + "https://example.com": "https://example.com", + "mailto:annie.bryan@example.com": "", + "https:///quarterly-report": "", + } { + if got := linkOrigin(destination); got != want { + t.Errorf("linkOrigin(%q) = %q, want %q", destination, got, want) + } + } +} + func TestMailViewLeavesBubbledUpThreadAloneWhenOpened(t *testing.T) { v, recorded := mailWithTestServer(t, http.StatusNoContent) v.postingList.postings[0].BubbledUp = true From 42adc8a7d34a91f055a5eb56b72efdf7a0f84875 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 30 Sep 2026 12:14:30 -0400 Subject: [PATCH 2/3] Stop promising the link row always shows the whole destination --- docs/tui.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tui.md b/docs/tui.md index 49efcbe3..bc0a27d6 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -70,7 +70,7 @@ Sending a reply or a forward from a thread closes the thread and returns you to While reading a thread, the From header shows the actual send-as address when HEY records one separately from the account user. -While reading a thread, links can be selected without a mouse. Tab selects the next link in document order and Shift+Tab selects the previous one; both wrap at the ends. A fixed row above the shortcut bar shows the complete selected destination without moving the thread. Press Enter to open it. A destination too long for the row keeps its scheme and host in full and cuts the rest short; opening stays unavailable until the terminal is wide enough to show at least that much. Press Escape once to clear the selection, and Escape again to leave the thread. A thread with no selectable links keeps the normal global Tab focus behavior. Existing OSC 8 mouse links remain available. +While reading a thread, links can be selected without a mouse. Tab selects the next link in document order and Shift+Tab selects the previous one; both wrap at the ends. A fixed row above the shortcut bar shows the selected destination without moving the thread. Press Enter to open it. A destination too long for the row keeps its scheme and host in full and cuts the rest short; opening stays unavailable until the terminal is wide enough to show at least that much. Press Escape once to clear the selection, and Escape again to leave the thread. A thread with no selectable links keeps the normal global Tab focus behavior. Existing OSC 8 mouse links remain available. Most of those keep working while you are reading a thread, the way the web app's topic toolbar stays live: `r`, `f`, `v`, `b`, `u`, `i`, `l`, `a`, `d`, `p` and `t` all act on From 654f926cc9a8e2dc0654b53e02b7dba16a05e30f Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 30 Sep 2026 12:28:07 -0400 Subject: [PATCH 3/3] Show a long link in full by wrapping it instead of trimming it Trimming the path kept the host readable, but the point of the status row is seeing exactly where a link goes before opening it. A destination too long for one row now wraps onto the rows it needs, taken from the thread while that link is selected, and Enter opens it. When the terminal is too short for all of those rows the enlarge hint comes back and Enter does nothing. --- docs/tui.md | 2 +- internal/tui/mail.go | 41 +++++++------------ internal/tui/mail_test.go | 86 ++++----------------------------------- internal/tui/tui.go | 24 ++++++++--- internal/tui/tui_test.go | 59 +++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 111 deletions(-) diff --git a/docs/tui.md b/docs/tui.md index bc0a27d6..0b3efb97 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -70,7 +70,7 @@ Sending a reply or a forward from a thread closes the thread and returns you to While reading a thread, the From header shows the actual send-as address when HEY records one separately from the account user. -While reading a thread, links can be selected without a mouse. Tab selects the next link in document order and Shift+Tab selects the previous one; both wrap at the ends. A fixed row above the shortcut bar shows the selected destination without moving the thread. Press Enter to open it. A destination too long for the row keeps its scheme and host in full and cuts the rest short; opening stays unavailable until the terminal is wide enough to show at least that much. Press Escape once to clear the selection, and Escape again to leave the thread. A thread with no selectable links keeps the normal global Tab focus behavior. Existing OSC 8 mouse links remain available. +While reading a thread, links can be selected without a mouse. Tab selects the next link in document order and Shift+Tab selects the previous one; both wrap at the ends. A fixed row above the shortcut bar shows the selected destination without moving the thread. A destination too long for one row wraps onto as many rows as it needs, so you always see all of it. Press Enter to open it; opening stays unavailable until the terminal is tall enough to show the whole destination. Press Escape once to clear the selection, and Escape again to leave the thread. A thread with no selectable links keeps the normal global Tab focus behavior. Existing OSC 8 mouse links remain available. Most of those keep working while you are reading a thread, the way the web app's topic toolbar stays live: `r`, `f`, `v`, `b`, `u`, `i`, `l`, `a`, `d`, `p` and `t` all act on diff --git a/internal/tui/mail.go b/internal/tui/mail.go index e0092b2f..4609c5b7 100644 --- a/internal/tui/mail.go +++ b/internal/tui/mail.go @@ -9,6 +9,7 @@ import ( "charm.land/bubbles/v2/viewport" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + "github.com/charmbracelet/x/ansi" "github.com/basecamp/hey-sdk/go/pkg/generated" hey "github.com/basecamp/hey-sdk/go/pkg/hey" @@ -1752,9 +1753,9 @@ func (v *mailView) threadNotices() []string { } // LinkFooter reserves one footer row for a thread with selectable links. Its text -// stays blank until a link is selected, so moving through links never changes the -// viewport's height. A destination can open only when the footer shows at least -// its scheme and host in full. +// stays blank until a link is selected, and a destination that fits keeps to that +// row, so moving between such links never changes the viewport's height. A longer +// one wraps onto the rows it needs to be shown in full. func (v *mailView) LinkFooter() (text string, visible bool) { if !v.inThread || v.modal != nil || len(v.links) == 0 { return "", false @@ -1772,34 +1773,20 @@ func (v *mailView) linkDestinationFooter() (string, bool) { return "", false } destination := terminal.SanitizeLine(v.links[v.selectedLink].destination) - const prefix, suffix = "Open: ", " (press Enter to visit)" - footer := prefix + destination + suffix + const suffix = " (press Enter to visit)" + footer := "Open: " + destination + suffix if lipgloss.Width(footer) <= width { return footer, true } - // A long destination can still open when the footer shows where it goes: the - // scheme and host in full, with only the tail of the path cut short. - room := width - lipgloss.Width(prefix+suffix) - if origin := linkOrigin(destination); origin != "" && room >= lipgloss.Width(origin)+3 { - return prefix + truncateToWidth(destination, room) + suffix, true - } - return truncateToWidth("Enlarge the terminal to inspect this link", width), false -} - -// linkOrigin is a destination up to the end of its authority — "https://example.com" -// of "https://example.com/path" — or empty when it has none, as a mailto link does. -func linkOrigin(destination string) string { - scheme, rest, ok := strings.Cut(destination, "://") - if !ok { - return "" - } - if end := strings.IndexAny(rest, "/?#"); end >= 0 { - rest = rest[:end] - } - if rest == "" { - return "" + // A destination too long for one row wraps onto as many as it needs, so the + // reader sees all of it before it opens. The model decides whether they fit. + lines := strings.Split(ansi.Hardwrap("Open: "+destination, width, true), "\n") + if last := len(lines) - 1; lipgloss.Width(lines[last]+suffix) <= width { + lines[last] += suffix + } else { + lines = append(lines, truncateToWidth(strings.TrimSpace(suffix), width)) } - return scheme + "://" + rest + return strings.Join(lines, "\n"), true } func (v *mailView) linkDestinationReviewable() bool { diff --git a/internal/tui/mail_test.go b/internal/tui/mail_test.go index a45616f4..3a2aa31c 100644 --- a/internal/tui/mail_test.go +++ b/internal/tui/mail_test.go @@ -482,7 +482,7 @@ func TestMailViewKeepsAPartialThreadsNoticeAndLeavesItUnseen(t *testing.T) { } } -func TestLinkDestinationMustFitFooterBeforeOpening(t *testing.T) { +func TestLinkFooterShowsTheWholeDestinationBeforeOpening(t *testing.T) { v := newMailView(testVC()) v.inThread = true v.topicID = 100 @@ -530,20 +530,16 @@ func TestLinkDestinationMustFitFooterBeforeOpening(t *testing.T) { v.vc.width = 24 footer, visible = v.LinkFooter() - if !visible || v.linkDestinationReviewable() || strings.Contains(footer, destination) { - t.Errorf("narrow link footer = %q visible=%v reviewable=%v", footer, visible, v.linkDestinationReviewable()) + if !visible || !v.linkDestinationReviewable() || !strings.Contains(strings.ReplaceAll(footer, "\n", ""), destination) { + t.Errorf("narrow link footer = %q visible=%v reviewable=%v, want the whole destination wrapped", footer, visible, v.linkDestinationReviewable()) } - opened = "" - if cmd, handled := v.handleLinkKey(keyPress("enter")); !handled || cmd != nil || opened != "" { - t.Errorf("hidden destination opened: handled=%v command=%v destination=%q", handled, cmd != nil, opened) - } - - v.vc.width = 3 - if v.linkDestinationReviewable() { - t.Error("destination wider than a tiny terminal was marked reviewable") + for _, line := range strings.Split(footer, "\n") { + if lipgloss.Width(line) > v.vc.width { + t.Errorf("wrapped footer line %q is wider than %d", line, v.vc.width) + } } - if cmd, handled := v.handleLinkKey(keyPress("enter")); !handled || cmd != nil { - t.Errorf("tiny terminal opened destination: handled=%v command=%v", handled, cmd != nil) + if !strings.HasSuffix(footer, "(press Enter to visit)") { + t.Errorf("wrapped footer = %q, want it to end with the action", footer) } v.selectedLink = -1 @@ -553,70 +549,6 @@ func TestLinkDestinationMustFitFooterBeforeOpening(t *testing.T) { } } -func TestLongLinkDestinationOpensWhenItsHostFits(t *testing.T) { - v := newMailView(testVC()) - v.inThread = true - v.topicID = 100 - v.selectedLink = 0 - destination := "https://bulletin.example.com/T/OFC4/L2S/9330/B2827790/MNew/757620/28297471/JXSWDt/1/10333590/PxFblM2x.html?h=_GUL3Rht552Ov_esGif3" - v.links = []mailLink{{destination: destination, key: "501\x000"}} - v.selectedLinkKey = v.links[0].key - v.vc.width = 80 - v.contentHeight = 12 - - footer, visible := v.LinkFooter() - if !visible || !v.linkDestinationReviewable() { - t.Fatalf("link footer = %q visible=%v, want a reviewable footer", footer, visible) - } - if !strings.HasPrefix(footer, "Open: https://bulletin.example.com/") || !strings.HasSuffix(footer, "... (press Enter to visit)") { - t.Errorf("link footer = %q, want the host in full and the path cut short", footer) - } - if width := lipgloss.Width(footer); width > v.vc.width { - t.Errorf("link footer is %d cells wide, want at most %d", width, v.vc.width) - } - - var opened string - v.vc.openURL = func(destination string) error { - opened = destination - return nil - } - cmd, handled := v.handleLinkKey(keyPress("enter")) - if !handled || cmd == nil { - t.Fatal("a long destination with a visible host was not opened") - } - runCmd(cmd) - if opened != destination { - t.Errorf("opened %q, want the complete destination %q", opened, destination) - } - - v.vc.width = 50 - if footer, _ := v.LinkFooter(); v.linkDestinationReviewable() || !strings.HasPrefix(footer, "Enlarge the terminal") { - t.Errorf("footer too narrow for the host = %q reviewable=%v, want the enlarge hint", footer, v.linkDestinationReviewable()) - } - - v.vc.width = 80 - v.links[0].destination = "mailto:" + strings.Repeat("quarterly.reports.", 5) + "@example.com" - if v.linkDestinationReviewable() { - t.Error("a long destination with no host was marked reviewable without being shown in full") - } -} - -func TestLinkOrigin(t *testing.T) { - for destination, want := range map[string]string{ - "https://example.com/quarterly-report": "https://example.com", - "https://example.com?utm_source=newsletter": "https://example.com", - "http://example.com:8080#top": "http://example.com:8080", - "https://example.com@phishing.example.org/login": "https://example.com@phishing.example.org", - "https://example.com": "https://example.com", - "mailto:annie.bryan@example.com": "", - "https:///quarterly-report": "", - } { - if got := linkOrigin(destination); got != want { - t.Errorf("linkOrigin(%q) = %q, want %q", destination, got, want) - } - } -} - func TestMailViewLeavesBubbledUpThreadAloneWhenOpened(t *testing.T) { v, recorded := mailWithTestServer(t, http.StatusNoContent) v.postingList.postings[0].BubbledUp = true diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 9527344d..1e293344 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -688,7 +688,7 @@ func (m model) View() tea.View { } footerH := 1 + helpH if linkFooterVisible { - footerH++ + footerH += 1 + strings.Count(linkFooter, "\n") } padLines := m.height - contentLines - footerH - 1 for range max(padLines, 0) { @@ -797,7 +797,18 @@ func (m *model) updateHelpBindings() { } } +// linkFooter is the link status as it goes on screen. A destination that wraps onto +// more rows than the terminal can spare is replaced by a hint, and cannot be opened +// until the terminal is enlarged. func (m model) linkFooter() (string, bool) { + text, visible := m.activeLinkFooter() + if visible && strings.Contains(text, "\n") && !m.linkFooterFits() { + return truncateToWidth("Enlarge the terminal to inspect this link", max(m.width, 0)), true + } + return text, visible +} + +func (m model) activeLinkFooter() (string, bool) { if provider, ok := m.activeView.(linkFooterProvider); ok { return provider.LinkFooter() } @@ -805,7 +816,7 @@ func (m model) linkFooter() (string, bool) { } func (m model) linkFooterFits() bool { - _, visible := m.linkFooter() + text, visible := m.activeLinkFooter() if !visible { return false } @@ -813,18 +824,19 @@ func (m model) linkFooterFits() bool { if m.mailWatchNotice() != "" { statusHeight = 1 } - return m.height >= headerHeight+m.help.height()+3+statusHeight+1 + return m.height >= headerHeight+m.help.height()+3+statusHeight+1+strings.Count(text, "\n") } // contentHeight gives the active view every row that is not navigation or a // visible footer. Ordinary help has two clear rows above its divider; a link -// footer uses one of them for its stable status row. +// footer uses one of them for its status row, and takes any further rows a +// wrapped destination needs from the content. func (m model) contentHeight() int { footerHeight := 0 helpHeight := m.help.height() - _, linkFooterVisible := m.linkFooter() + linkFooter, linkFooterVisible := m.linkFooter() if helpHeight > 0 || linkFooterVisible { - footerHeight = helpHeight + 3 + footerHeight = helpHeight + 3 + strings.Count(linkFooter, "\n") } statusHeight := 0 if m.mailWatchNotice() != "" { diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index bf0ab5c2..6d5c77ee 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -1757,6 +1757,65 @@ func TestQLeavesAThreadWithASelectedLink(t *testing.T) { } } +func TestRootModelWrapsALongDestinationOntoTheRowsItNeeds(t *testing.T) { + m := openLinkThreadThroughModel(t) + var opened []string + m.mailView.vc.openURL = func(destination string) error { + opened = append(opened, destination) + return nil + } + updated, _ := m.Update(tea.WindowSizeMsg{Width: 40, Height: 30}) + m = updated.(model) + unselectedViewportHeight := m.mailView.topicViewport.Height() + for range 2 { + updated, _ = m.Update(keyPress("tab")) + m = updated.(model) + } + if m.mailView.selectedLink != 1 { + t.Fatalf("selected link = %d, want second occurrence", m.mailView.selectedLink) + } + + screen := stripANSI(m.View().Content) + for _, line := range []string{"Open: https://example.org/second?full=de", "stination (press Enter to visit)"} { + if !strings.Contains(screen, line) { + t.Errorf("wrapped footer row %q is not on screen: %q", line, screen) + } + } + if got := strings.Count(m.View().Content, "\n") + 1; got > m.height { + t.Errorf("screen draws %d rows, want at most %d", got, m.height) + } + if got := m.mailView.topicViewport.Height(); got != unselectedViewportHeight-1 { + t.Errorf("viewport height = %d, want %d to make room for the second footer row", got, unselectedViewportHeight-1) + } + selected := m.mailView.links[m.mailView.selectedLink] + visibleStart := m.mailView.topicViewport.YOffset() + visibleEnd := visibleStart + m.mailView.topicViewport.Height() - 1 + if selected.startLine < visibleStart || selected.endLine > visibleEnd { + t.Errorf("selected range %d-%d is outside viewport %d-%d", selected.startLine, selected.endLine, visibleStart, visibleEnd) + } + + updated, cmd := m.Update(keyPress("enter")) + m = updated.(model) + if cmd == nil { + t.Fatal("enter did not open a destination shown in full over two rows") + } + runCmd(cmd) + if len(opened) != 1 || opened[0] != "https://example.org/second?full=destination" { + t.Fatalf("opened destinations = %q, want the exact second destination", opened) + } + + // One row short of what the wrapped destination needs: a hint instead, and no opening. + updated, _ = m.Update(tea.WindowSizeMsg{Width: 40, Height: headerHeight + m.help.height() + 4}) + m = updated.(model) + screen = stripANSI(m.View().Content) + if !strings.Contains(screen, "Enlarge the terminal") || strings.Contains(screen, "Open: https://example.org/second") { + t.Errorf("short terminal footer = %q, want the enlarge hint instead of a partial destination", screen) + } + if _, cmd = m.Update(keyPress("enter")); cmd != nil || len(opened) != 1 { + t.Errorf("enter opened a destination the footer could not show: command=%v opened=%q", cmd != nil, opened) + } +} + func TestRootModelNavigatesThreadLinksAndOpensExactDestination(t *testing.T) { m := openLinkThreadThroughModel(t) var opened []string