Files
nib-v1/internal/tui/absorb.go
T
lerko 3eb778f31b fix(tui): clean up stream row density — drop ID, fix newline leak, align margins
ID cluttered rows and caused wrapping on long entries. Body newlines
leaked into stream rendering extra unindented lines. Cursor glyph
shifted selected rows 1 col right of unselected.

Remove ID from all row renderers (detail pane already shows it),
collapse multiline body to first line, cap tags to 2 in stream,
and reserve cursor column on unselected rows for consistent alignment.
2026-05-20 18:12:18 -04:00

139 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))
body := e.Body
if e.Title != nil {
body = *e.Title
}
if idx := strings.IndexByte(body, '\n'); idx >= 0 {
body = body[:idx]
}
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", glyph, body, tags)
if maxWidth > 0 && len(stripAnsi(line)) > maxWidth {
body = truncate(body, maxWidth-6)
line = fmt.Sprintf("%s %s%s", glyph, body, tags)
}
return line
}