diff --git a/tui/tui.go b/tui/tui.go index 04863cf..80d45f7 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -539,6 +539,7 @@ const ( actionCycleGroup actionOpenURL actionCopyURL + actionCopyMarkdownLink actionCopyTitle actionCopyDetail actionOpenLinkMenu @@ -1066,6 +1067,7 @@ func (m *model) openActionMenuFor(context paneFocus) { selectedItems = append([]menuItem{ {label: "Open selected URL", action: actionOpenURL}, {label: "Copy selected URL", action: actionCopyURL}, + {label: "Copy markdown link", action: actionCopyMarkdownLink}, }, selectedItems...) } items := []menuItem{ @@ -1075,12 +1077,16 @@ func (m *model) openActionMenuFor(context paneFocus) { if links := m.selectedReferenceLinks(); len(links) > 0 { items = append(items, menuSection("Links"), + menuItem{label: "Open first body link", action: actionOpenFirstLink}, + menuItem{label: "Copy first body link", action: actionCopyFirstLink}, + ) + } + if links := m.selectedReferenceLinks(); len(links) > 1 { + items = append(items, menuItem{label: "Open body link...", action: actionOpenLinkMenu}, menuItem{label: "Copy body link...", action: actionCopyLinkMenu}, + menuItem{label: "Copy all body links", action: actionCopyAllLinks}, ) - if len(links) > 1 { - items = append(items, menuItem{label: "Copy all body links", action: actionCopyAllLinks}) - } } items = append(items, []menuItem{ menuSection("Pane"), @@ -1270,6 +1276,9 @@ func (m *model) runMenuItem(item menuItem) tea.Cmd { case actionCopyURL: m.copySelectedURL() m.closeMenu() + case actionCopyMarkdownLink: + m.copySelectedMarkdownLink() + m.closeMenu() case actionCopyTitle: m.copySelectedTitle() m.closeMenu() @@ -1415,6 +1424,31 @@ func (m *model) copySelectedURL() { m.status = "Copied selected URL" } +func (m *model) copySelectedMarkdownLink() { + item, ok := m.selectedItem() + if !ok || strings.TrimSpace(item.URL) == "" { + m.status = "No URL for selected row" + return + } + url := strings.TrimSpace(item.URL) + title := strings.TrimSpace(item.Title) + if title == "" { + title = url + } + if err := copyText("[" + escapeMarkdownLinkLabel(title) + "](" + url + ")"); err != nil { + m.status = err.Error() + return + } + m.status = "Copied markdown link" +} + +func escapeMarkdownLinkLabel(value string) string { + value = strings.ReplaceAll(value, `\`, `\\`) + value = strings.ReplaceAll(value, `[`, `\[`) + value = strings.ReplaceAll(value, `]`, `\]`) + return value +} + func (m *model) copySelectedTitle() { item, ok := m.selectedItem() if !ok || strings.TrimSpace(item.Title) == "" { diff --git a/tui/tui_test.go b/tui/tui_test.go index e7a607d..3eea896 100644 --- a/tui/tui_test.go +++ b/tui/tui_test.go @@ -835,7 +835,7 @@ func TestRightClickOpensSharedActionMenu(t *testing.T) { if !strings.Contains(view, "Open selected URL") || !strings.Contains(view, "Copy selected detail") || !strings.Contains(view, "Links") { t.Fatalf("action menu missing expected commands:\n%s", view) } - for _, want := range []string{"Open body link...", "Copy body link...", "Focus detail pane", "Sort focused pane", "Jump to row..."} { + for _, want := range []string{"Copy markdown link", "Open first body link", "Copy first body link", "Focus detail pane", "Sort focused pane", "Jump to row..."} { if !menuContainsLabel(m.menuItems, want) { t.Fatalf("action menu items missing %q: %#v", want, m.menuItems) } @@ -853,8 +853,10 @@ func TestActionMenuUsesGitcrawlStyleLinkPicker(t *testing.T) { m.height = 16 m.openActionMenuFor(focusRows) - if !menuContainsLabel(m.menuItems, "Open body link...") { - t.Fatalf("action menu missing link picker: %#v", m.menuItems) + for _, want := range []string{"Open first body link", "Copy first body link", "Open body link...", "Copy body link...", "Copy all body links"} { + if !menuContainsLabel(m.menuItems, want) { + t.Fatalf("action menu missing %q: %#v", want, m.menuItems) + } } m.openReferenceLinkMenu("open") if m.menuTitle != "Open Link" { @@ -996,12 +998,13 @@ func TestActionMenuCopyAndOpenSelectedRow(t *testing.T) { t.Fatalf("open action opened=%v status=%q", opened, m.status) } m.copySelectedURL() + m.copySelectedMarkdownLink() m.copySelectedTitle() m.copySelectedDetail() - if len(copied) != 3 { + if len(copied) != 4 { t.Fatalf("copied = %#v", copied) } - if copied[0] != "https://example.com/launch" || copied[1] != "Launch Plan" || !strings.Contains(copied[2], "Ship the TUI.") { + if copied[0] != "https://example.com/launch" || copied[1] != "[Launch Plan](https://example.com/launch)" || copied[2] != "Launch Plan" || !strings.Contains(copied[3], "Ship the TUI.") { t.Fatalf("copied values = %#v", copied) } }