feat(tui): add split-pane detail, compact date headers, and input drawer

Three layout improvements for better space utilization:

- Compact date headers: date labels render as left gutter column instead
  of standalone lines, saving one line per date group in stream view
- Input drawer: capture bar expands to 4-line drawer with border, hints,
  and live preview of parsed entity/search query
- Split-pane detail: wide terminals (>=100 cols) show list and detail
  side-by-side with h/l focus switching, falling back to full-screen
  detail on narrow terminals
This commit is contained in:
2026-05-19 19:55:37 -04:00
parent e09919b679
commit f89ca8acb9
7 changed files with 413 additions and 95 deletions
+22 -30
View File
@@ -93,6 +93,8 @@ func (l listModel) update(msg tea.KeyMsg) listModel {
return l
}
const dateGutterWidth = 9
func (l listModel) view(width int) string {
ents := l.displayEntities()
if len(ents) == 0 {
@@ -100,22 +102,24 @@ func (l listModel) view(width int) string {
}
groups := groupByDate(ents)
entityWidth := width - 4 - dateGutterWidth
type displayLine struct {
text string
entityIdx int
isHeader bool
}
var lines []displayLine
entityIdx := 0
for _, g := range groups {
lines = append(lines, displayLine{
text: dateHeaderStyle.Render("── " + g.label + " ──"),
isHeader: true,
})
for _, e := range g.entities {
line := renderEntity(e, width-4)
for i, e := range g.entities {
var gutter string
if i == 0 {
gutter = gutterStyle.Render(padRight(g.label, 6) + " │ ")
} else {
gutter = gutterStyle.Render(" │ ")
}
line := gutter + renderEntity(e, entityWidth)
lines = append(lines, displayLine{
text: line,
entityIdx: entityIdx,
@@ -124,21 +128,17 @@ func (l listModel) view(width int) string {
}
}
cursorLine := l.cursorDisplayLine(groups)
visible := l.visibleCount()
offset := 0
if cursorLine >= visible {
offset = cursorLine - visible + 1
if l.cursor >= visible {
offset = l.cursor - visible + 1
}
var b strings.Builder
end := min(offset+visible, len(lines))
for i := offset; i < end; i++ {
dl := lines[i]
if dl.isHeader {
b.WriteString(dl.text)
} else if dl.entityIdx == l.cursor {
if dl.entityIdx == l.cursor {
b.WriteString(selectedItemStyle.Render(" " + dl.text))
} else {
b.WriteString(listItemStyle.Render(dl.text))
@@ -151,22 +151,6 @@ func (l listModel) view(width int) string {
return b.String()
}
func (l listModel) cursorDisplayLine(groups []dateGroup) int {
line := 0
entityIdx := 0
for _, g := range groups {
line++
for range g.entities {
if entityIdx == l.cursor {
return line
}
line++
entityIdx++
}
}
return 0
}
func (l listModel) visibleCount() int {
if l.height <= 0 {
return 20
@@ -203,6 +187,14 @@ func formatDateLabel(t time.Time) string {
return strings.ToLower(t.Format("Jan 2"))
}
func padRight(s string, n int) string {
r := []rune(s)
if len(r) >= n {
return string(r[:n])
}
return s + strings.Repeat(" ", n-len(r))
}
func renderEntity(e *db.Entity, maxWidth int) string {
glyphStr := display.DisplayGlyph(e.Glyph, e.CardType)
style := glyphStyle