4517b2e37c
CI / test (pull_request) Successful in 2m25s
Link picker now shows both outgoing [[links]] and backlinks in a unified list with section headers. Backlinks follow by entity ID directly, outgoing links resolve by text. Navigating into a backlink works the same as following an outgoing link — pushes to nav stack, esc pops back.
109 lines
2.2 KiB
Go
109 lines
2.2 KiB
Go
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 {
|
|
items []linkItem
|
|
cursor int
|
|
}
|
|
|
|
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() linkItem {
|
|
if len(lp.items) == 0 || lp.cursor >= len(lp.items) {
|
|
return linkItem{}
|
|
}
|
|
return lp.items[lp.cursor]
|
|
}
|
|
|
|
func (lp linkPickerModel) update(key string) linkPickerModel {
|
|
switch key {
|
|
case "up", "k":
|
|
if lp.cursor > 0 {
|
|
lp.cursor--
|
|
}
|
|
case "down", "j":
|
|
if lp.cursor < len(lp.items)-1 {
|
|
lp.cursor++
|
|
}
|
|
}
|
|
return lp
|
|
}
|
|
|
|
func (lp linkPickerModel) view(width int) string {
|
|
var b strings.Builder
|
|
b.WriteString(titleStyle.Render("follow link") + "\n\n")
|
|
|
|
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()
|
|
}
|
|
|
|
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 {
|
|
b.WriteString(listItemStyle.Render(label))
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
b.WriteString("\n")
|
|
b.WriteString(helpStyle.Render("enter:follow esc:back"))
|
|
return b.String()
|
|
}
|