feat(tui): add link picker and navigation history #45
@@ -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)
|
||||
|
||||
+56
-13
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user