feat(tui): add gitcrawl-style link picker
This commit is contained in:
parent
68a591e29b
commit
604415f50e
85
tui/tui.go
85
tui/tui.go
@ -539,9 +539,14 @@ const (
|
||||
actionCopyURL
|
||||
actionCopyTitle
|
||||
actionCopyDetail
|
||||
actionOpenLinkMenu
|
||||
actionCopyLinkMenu
|
||||
actionOpenPickedLink
|
||||
actionCopyPickedLink
|
||||
actionOpenFirstLink
|
||||
actionCopyFirstLink
|
||||
actionCopyAllLinks
|
||||
actionBackToActions
|
||||
actionQuit
|
||||
actionSortDefault
|
||||
actionSortNewest
|
||||
@ -556,6 +561,7 @@ const (
|
||||
type menuItem struct {
|
||||
label string
|
||||
action menuAction
|
||||
value string
|
||||
}
|
||||
|
||||
func (item menuItem) selectable() bool {
|
||||
@ -969,7 +975,7 @@ func (m *model) handleMenuMouse(msg tea.MouseMsg) {
|
||||
}
|
||||
m.menuIndex = index
|
||||
m.keepMenuVisible()
|
||||
_ = m.runMenuAction(m.menuItems[m.menuIndex].action)
|
||||
_ = m.runMenuItem(m.menuItems[m.menuIndex])
|
||||
}
|
||||
|
||||
func (m model) menuIndexAtMouse(x, y int) (int, bool) {
|
||||
@ -988,7 +994,7 @@ func (m *model) updateMenuKey(key tea.KeyMsg) tea.Cmd {
|
||||
page := maxInt(1, m.menuVisibleCount())
|
||||
if index, ok := visibleMenuShortcutIndex(key.String(), m.menuItems, m.menuOff, page); ok {
|
||||
m.menuIndex = index
|
||||
return m.runMenuAction(m.menuItems[m.menuIndex].action)
|
||||
return m.runMenuItem(m.menuItems[m.menuIndex])
|
||||
}
|
||||
switch key.String() {
|
||||
case "ctrl+c":
|
||||
@ -1017,7 +1023,11 @@ func (m *model) updateMenuKey(key tea.KeyMsg) tea.Cmd {
|
||||
m.keepMenuVisible()
|
||||
case "enter", " ":
|
||||
if len(m.menuItems) > 0 {
|
||||
return m.runMenuAction(m.menuItems[m.menuIndex].action)
|
||||
return m.runMenuItem(m.menuItems[m.menuIndex])
|
||||
}
|
||||
case "b":
|
||||
if m.menuTitle == "Open Link" || m.menuTitle == "Copy Link" {
|
||||
m.openActionMenuFor(m.menuContext)
|
||||
}
|
||||
case "s":
|
||||
m.openSortMenuFor(m.focus)
|
||||
@ -1059,8 +1069,8 @@ 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},
|
||||
menuItem{label: "Open body link...", action: actionOpenLinkMenu},
|
||||
menuItem{label: "Copy body link...", action: actionCopyLinkMenu},
|
||||
)
|
||||
if len(links) > 1 {
|
||||
items = append(items, menuItem{label: "Copy all body links", action: actionCopyAllLinks})
|
||||
@ -1133,6 +1143,31 @@ func (m *model) openHelpMenu() {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *model) openReferenceLinkMenu(mode string) {
|
||||
links := m.selectedReferenceLinks()
|
||||
if len(links) == 0 {
|
||||
m.status = "No body links found"
|
||||
return
|
||||
}
|
||||
title := "Copy Link"
|
||||
action := actionCopyPickedLink
|
||||
if mode == "open" {
|
||||
title = "Open Link"
|
||||
action = actionOpenPickedLink
|
||||
}
|
||||
items := make([]menuItem, 0, len(links)+1)
|
||||
for index, link := range links {
|
||||
items = append(items, menuItem{
|
||||
label: formatLinkChoiceLabel(link, index),
|
||||
action: action,
|
||||
value: link,
|
||||
})
|
||||
}
|
||||
items = append(items, menuItem{label: "Back to actions", action: actionBackToActions})
|
||||
m.openMenu(title, items)
|
||||
m.status = title
|
||||
}
|
||||
|
||||
func (m *model) openMenu(title string, items []menuItem) {
|
||||
m.menuOpen = true
|
||||
m.menuTitle = title
|
||||
@ -1156,7 +1191,11 @@ func (m *model) closeMenu() {
|
||||
}
|
||||
|
||||
func (m *model) runMenuAction(action menuAction) tea.Cmd {
|
||||
switch action {
|
||||
return m.runMenuItem(menuItem{action: action})
|
||||
}
|
||||
|
||||
func (m *model) runMenuItem(item menuItem) tea.Cmd {
|
||||
switch item.action {
|
||||
case actionClose:
|
||||
m.closeMenu()
|
||||
case actionFocusRows:
|
||||
@ -1172,6 +1211,36 @@ func (m *model) runMenuAction(action menuAction) tea.Cmd {
|
||||
m.openSortMenuFor(m.menuContext)
|
||||
case actionHelpMenu:
|
||||
m.openHelpMenu()
|
||||
case actionOpenLinkMenu:
|
||||
m.openReferenceLinkMenu("open")
|
||||
case actionCopyLinkMenu:
|
||||
m.openReferenceLinkMenu("copy")
|
||||
case actionOpenPickedLink:
|
||||
if strings.TrimSpace(item.value) == "" {
|
||||
m.status = "No body link found"
|
||||
m.closeMenu()
|
||||
return nil
|
||||
}
|
||||
if err := openURL(item.value); err != nil {
|
||||
m.status = err.Error()
|
||||
} else {
|
||||
m.status = "Opened " + item.value
|
||||
}
|
||||
m.closeMenu()
|
||||
case actionCopyPickedLink:
|
||||
if strings.TrimSpace(item.value) == "" {
|
||||
m.status = "No body link found"
|
||||
m.closeMenu()
|
||||
return nil
|
||||
}
|
||||
if err := copyText(item.value); err != nil {
|
||||
m.status = err.Error()
|
||||
} else {
|
||||
m.status = "Copied body link"
|
||||
}
|
||||
m.closeMenu()
|
||||
case actionBackToActions:
|
||||
m.openActionMenuFor(m.menuContext)
|
||||
case actionClearFilter:
|
||||
m.query = ""
|
||||
m.applyFilter()
|
||||
@ -1425,6 +1494,10 @@ func (m model) selectedReferenceLinks() []string {
|
||||
return itemReferenceLinks(item)
|
||||
}
|
||||
|
||||
func formatLinkChoiceLabel(url string, index int) string {
|
||||
return fmt.Sprintf("%2d %s", index+1, url)
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
width := maxInt(m.width, 40)
|
||||
height := maxInt(m.height, 24)
|
||||
|
||||
@ -718,13 +718,42 @@ 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 first body link", "Focus detail pane", "Sort focused pane", "Jump to row..."} {
|
||||
for _, want := range []string{"Open body link...", "Copy 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionMenuUsesGitcrawlStyleLinkPicker(t *testing.T) {
|
||||
m := newModel(Options{
|
||||
Title: "archive",
|
||||
Items: []Item{
|
||||
Row{Kind: "message", Title: "alpha", Text: "see https://example.com/a and https://example.com/b"}.ItemForLayout(LayoutChat),
|
||||
},
|
||||
})
|
||||
m.width = 100
|
||||
m.height = 16
|
||||
m.openActionMenuFor(focusRows)
|
||||
|
||||
if !menuContainsLabel(m.menuItems, "Open body link...") {
|
||||
t.Fatalf("action menu missing link picker: %#v", m.menuItems)
|
||||
}
|
||||
m.openReferenceLinkMenu("open")
|
||||
if m.menuTitle != "Open Link" {
|
||||
t.Fatalf("menu title = %q, want Open Link", m.menuTitle)
|
||||
}
|
||||
if len(m.menuItems) < 3 {
|
||||
t.Fatalf("link menu items = %#v", m.menuItems)
|
||||
}
|
||||
if m.menuItems[0].value != "https://example.com/a" || m.menuItems[1].value != "https://example.com/b" {
|
||||
t.Fatalf("link menu values = %#v", m.menuItems)
|
||||
}
|
||||
if !menuContainsLabel(m.menuItems, "Back to actions") {
|
||||
t.Fatalf("link menu missing back action: %#v", m.menuItems)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJumpModeSelectsFocusedPaneRows(t *testing.T) {
|
||||
m := newModel(Options{
|
||||
Title: "discrawl archive",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user