1066c0bc7d
Search uses existing parse grammar ?prefix — type `?query #tag` in capture bar to filter entities client-side. Substring match on body+title+description with AND tag filtering. Esc clears search. Absorb via m key on fluid entities — opens source picker showing all other entities, enter merges source into target. Uses existing store.Absorb() backend.
137 lines
2.7 KiB
Go
137 lines
2.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"github.com/lerko/nib/internal/db"
|
|
"github.com/lerko/nib/internal/display"
|
|
)
|
|
|
|
type absorbModel struct {
|
|
targetID string
|
|
sources []*db.Entity
|
|
cursor int
|
|
offset int
|
|
height int
|
|
}
|
|
|
|
func newAbsorbModel(targetID string) absorbModel {
|
|
return absorbModel{targetID: targetID}
|
|
}
|
|
|
|
func (a *absorbModel) setSources(entities []*db.Entity) {
|
|
a.sources = nil
|
|
for _, e := range entities {
|
|
if e.ID != a.targetID {
|
|
a.sources = append(a.sources, e)
|
|
}
|
|
}
|
|
a.cursor = 0
|
|
a.offset = 0
|
|
}
|
|
|
|
func (a *absorbModel) setHeight(h int) {
|
|
a.height = h
|
|
}
|
|
|
|
func (a absorbModel) selectedSource() *db.Entity {
|
|
if len(a.sources) == 0 || a.cursor >= len(a.sources) {
|
|
return nil
|
|
}
|
|
return a.sources[a.cursor]
|
|
}
|
|
|
|
func (a absorbModel) update(msg tea.KeyMsg) absorbModel {
|
|
switch msg.String() {
|
|
case "up", "k":
|
|
if a.cursor > 0 {
|
|
a.cursor--
|
|
if a.cursor < a.offset {
|
|
a.offset = a.cursor
|
|
}
|
|
}
|
|
case "down", "j":
|
|
if a.cursor < len(a.sources)-1 {
|
|
a.cursor++
|
|
visible := a.visibleCount()
|
|
if a.cursor >= a.offset+visible {
|
|
a.offset = a.cursor - visible + 1
|
|
}
|
|
}
|
|
}
|
|
return a
|
|
}
|
|
|
|
func (a absorbModel) view(width int) string {
|
|
if len(a.sources) == 0 {
|
|
return statusStyle.Render("no other entities")
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString(titleStyle.Render("absorb into " + display.FormatID(a.targetID)))
|
|
b.WriteString("\n")
|
|
b.WriteString(helpStyle.Render("select source to merge"))
|
|
b.WriteString("\n\n")
|
|
|
|
visible := a.visibleCount() - 4
|
|
if visible <= 0 {
|
|
visible = 10
|
|
}
|
|
end := min(a.offset+visible, len(a.sources))
|
|
|
|
for i := a.offset; i < end; i++ {
|
|
e := a.sources[i]
|
|
line := renderAbsorbSource(e, width-4)
|
|
|
|
if i == a.cursor {
|
|
b.WriteString(selectedItemStyle.Render(" " + line))
|
|
} else {
|
|
b.WriteString(listItemStyle.Render(line))
|
|
}
|
|
if i < end-1 {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (a absorbModel) visibleCount() int {
|
|
if a.height <= 0 {
|
|
return 20
|
|
}
|
|
return a.height
|
|
}
|
|
|
|
func renderAbsorbSource(e *db.Entity, maxWidth int) string {
|
|
glyph := glyphStyle.Render(display.DisplayGlyph(e.Glyph, e.CardType))
|
|
id := idStyle.Render("[" + display.FormatID(e.ID) + "]")
|
|
|
|
body := e.Body
|
|
if e.Title != nil {
|
|
body = *e.Title
|
|
}
|
|
|
|
var tags string
|
|
if len(e.Tags) > 0 {
|
|
limit := min(2, len(e.Tags))
|
|
tagParts := make([]string, limit)
|
|
for i := 0; i < limit; i++ {
|
|
tagParts[i] = tagStyle.Render("#" + e.Tags[i])
|
|
}
|
|
tags = " " + strings.Join(tagParts, " ")
|
|
}
|
|
|
|
line := fmt.Sprintf("%s %s%s %s", glyph, body, tags, id)
|
|
|
|
if maxWidth > 0 && len(stripAnsi(line)) > maxWidth {
|
|
body = truncate(body, maxWidth-20)
|
|
line = fmt.Sprintf("%s %s%s %s", glyph, body, tags, id)
|
|
}
|
|
|
|
return line
|
|
}
|