Files
lerko 989aa86679 fix(tui): compute truncation budget from actual overhead, not magic numbers
Tags wrapped past pane edge when detail split narrowed the list.
Truncation used fixed constants that didn't account for real tag width.
Now measures everything-except-body and gives body exactly what remains.
2026-05-20 18:49:38 -04:00

140 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 {
overhead := len(stripAnsi(line)) - len([]rune(body))
body = truncate(body, maxWidth-overhead)
line = fmt.Sprintf("%s %s%s", glyph, body, tags)
}
return line
}