diff --git a/internal/tui/commands.go b/internal/tui/commands.go index 536f872..a4dab85 100644 --- a/internal/tui/commands.go +++ b/internal/tui/commands.go @@ -222,6 +222,16 @@ func followLink(store *db.Store, linkText string) tea.Cmd { } } +func followLinkByID(store *db.Store, entityID string) tea.Cmd { + return func() tea.Msg { + entity, err := store.Get(context.Background(), entityID) + if err != nil { + return errMsg{err} + } + return linkFollowedMsg{entity} + } +} + func loadRailTags(store *db.Store) tea.Cmd { return func() tea.Msg { tags, err := store.ListTags(context.Background(), false) diff --git a/internal/tui/linkpicker.go b/internal/tui/linkpicker.go index 577fa73..11d6bf6 100644 --- a/internal/tui/linkpicker.go +++ b/internal/tui/linkpicker.go @@ -3,25 +3,52 @@ package tui import ( "strings" + "github.com/lerko/nib/internal/db" "github.com/lerko/nib/internal/link" ) +type linkKind int + +const ( + linkOutgoing linkKind = iota + linkBacklink +) + +type linkItem struct { + text string + entityID string + kind linkKind +} + type linkPickerModel struct { - links []string + items []linkItem cursor int } -func newLinkPicker(body string) linkPickerModel { - return linkPickerModel{ - links: link.ExtractLinks(body), +func newLinkPicker(body string, backlinks []db.Backlink) linkPickerModel { + var items []linkItem + + for _, lt := range link.ExtractLinks(body) { + items = append(items, linkItem{text: lt, kind: linkOutgoing}) } + for _, bl := range backlinks { + label := bl.Body + if bl.Title != nil { + label = *bl.Title + } else if len(label) > 50 { + label = label[:50] + "…" + } + items = append(items, linkItem{text: label, entityID: bl.EntityID, kind: linkBacklink}) + } + + return linkPickerModel{items: items} } -func (lp linkPickerModel) selected() string { - if len(lp.links) == 0 || lp.cursor >= len(lp.links) { - return "" +func (lp linkPickerModel) selected() linkItem { + if len(lp.items) == 0 || lp.cursor >= len(lp.items) { + return linkItem{} } - return lp.links[lp.cursor] + return lp.items[lp.cursor] } func (lp linkPickerModel) update(key string) linkPickerModel { @@ -31,7 +58,7 @@ func (lp linkPickerModel) update(key string) linkPickerModel { lp.cursor-- } case "down", "j": - if lp.cursor < len(lp.links)-1 { + if lp.cursor < len(lp.items)-1 { lp.cursor++ } } @@ -42,15 +69,31 @@ func (lp linkPickerModel) view(width int) string { var b strings.Builder b.WriteString(titleStyle.Render("follow link") + "\n\n") - if len(lp.links) == 0 { - b.WriteString(hintDescStyle.Render(" no [[links]] in this entry")) + if len(lp.items) == 0 { + b.WriteString(hintDescStyle.Render(" no links or backlinks")) b.WriteString("\n\n") b.WriteString(helpStyle.Render("esc:back")) return b.String() } - for i, lt := range lp.links { - label := "[[" + lt + "]]" + prevKind := linkKind(-1) + for i, item := range lp.items { + if item.kind != prevKind { + if item.kind == linkOutgoing { + b.WriteString(dateHeaderStyle.Render("── outgoing ──") + "\n") + } else { + b.WriteString(dateHeaderStyle.Render("── backlinks ──") + "\n") + } + prevKind = item.kind + } + + var label string + if item.kind == linkOutgoing { + label = "[[" + item.text + "]]" + } else { + label = "← " + item.text + } + if i == lp.cursor { b.WriteString(selectedItemStyle.Render(" " + label)) } else { diff --git a/internal/tui/model.go b/internal/tui/model.go index e0b21a3..3c163e4 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -860,7 +860,7 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case "[": if m.state == stateDetail && m.detail.entity != nil && m.detail.mode == detailPreview { - m.linkPicker = newLinkPicker(m.detail.entity.Body) + m.linkPicker = newLinkPicker(m.detail.entity.Body, m.detail.backlinks) m.state = stateLinkPicker return m, nil } @@ -964,14 +964,17 @@ func (m model) updateLinkPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.state = stateDetail return m, nil case "enter": - lt := m.linkPicker.selected() - if lt == "" { + item := m.linkPicker.selected() + if item.text == "" && item.entityID == "" { return m, nil } if m.detail.entity != nil { m.navStack = append(m.navStack, m.detail.entity.ID) } - return m, followLink(m.store, lt) + if item.kind == linkBacklink { + return m, followLinkByID(m.store, item.entityID) + } + return m, followLink(m.store, item.text) default: m.linkPicker = m.linkPicker.update(msg.String()) return m, nil