diff --git a/strix/interface/tui/internal/app/model_test.go b/strix/interface/tui/internal/app/model_test.go index 4a5c4564..40363b39 100644 --- a/strix/interface/tui/internal/app/model_test.go +++ b/strix/interface/tui/internal/app/model_test.go @@ -890,6 +890,14 @@ func TestRunningViewerShowsCompleteWrappedURL(t *testing.T) { if want := strings.Count(model.viewerView(model.viewerContentWidth()), "\n") + 3; model.viewerHeight() != want { t.Fatalf("viewer height = %d, want %d", model.viewerHeight(), want) } + + raw := model.viewerView(18) + if got, want := strings.Count(raw, ansi.SetHyperlink(url)), len(urlLines); got != want { + t.Fatalf("every wrapped line should link to the full URL: got %d links for %d lines in %q", got, want, raw) + } + if got := strings.Count(raw, ansi.ResetHyperlink()); got != len(urlLines) { + t.Fatalf("hyperlinks not closed: %d resets for %d lines", got, len(urlLines)) + } } func TestVerticalScrollbarThumbTracksScrollOffset(t *testing.T) { diff --git a/strix/interface/tui/internal/app/view.go b/strix/interface/tui/internal/app/view.go index 363bd6bd..495b2568 100644 --- a/strix/interface/tui/internal/app/view.go +++ b/strix/interface/tui/internal/app/view.go @@ -177,6 +177,18 @@ func wrapBlock(value string, width int) string { return strings.Join(out, "\n") } +// hyperlinkBlock wraps a URL to the column width and marks every wrapped line +// as an OSC 8 hyperlink to the whole URL. Terminals that linkify by text only +// see the first line of a wrapped URL, which for the viewer meant opening it +// with a truncated token. +func hyperlinkBlock(url string, width int, style lipgloss.Style) string { + lines := strings.Split(wrapBlock(url, width), "\n") + for i, line := range lines { + lines[i] = ansi.SetHyperlink(url) + style.Render(line) + ansi.ResetHyperlink() + } + return strings.Join(lines, "\n") +} + // scrollbarThumb brightens the bar being dragged so the grab reads as taking // hold of it. func (m Model) scrollbarThumb(target scrollbarTarget) lipgloss.Color { @@ -603,8 +615,7 @@ func (m Model) viewerView(width int) string { case "running": status := lipgloss.NewStyle().Foreground(green).Render("● Viewer running") if m.snapshot.ViewerURL != nil && strings.TrimSpace(*m.snapshot.ViewerURL) != "" { - url := wrapBlock(strings.TrimSpace(*m.snapshot.ViewerURL), width) - return status + "\n" + lipgloss.NewStyle().Foreground(dim).Render(url) + return status + "\n" + hyperlinkBlock(strings.TrimSpace(*m.snapshot.ViewerURL), width, lipgloss.NewStyle().Foreground(dim)) } return status case "unavailable":