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:
+22
-30
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user