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:
+102
-3
@@ -1,6 +1,9 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
@@ -16,8 +19,9 @@ type inputResult struct {
|
||||
}
|
||||
|
||||
type inputModel struct {
|
||||
ti textinput.Model
|
||||
active bool
|
||||
ti textinput.Model
|
||||
active bool
|
||||
preview *parse.Result
|
||||
}
|
||||
|
||||
func newInputModel() inputModel {
|
||||
@@ -37,6 +41,7 @@ func (i *inputModel) reset() {
|
||||
i.active = false
|
||||
i.ti.SetValue("")
|
||||
i.ti.Blur()
|
||||
i.preview = nil
|
||||
}
|
||||
|
||||
func (i inputModel) submit() *inputResult {
|
||||
@@ -83,9 +88,103 @@ func (i inputModel) submit() *inputResult {
|
||||
|
||||
func (i inputModel) updateKey(msg tea.KeyMsg) inputModel {
|
||||
i.ti, _ = i.ti.Update(msg)
|
||||
val := i.ti.Value()
|
||||
if val != "" {
|
||||
parsed, err := parse.Parse(val)
|
||||
if err == nil {
|
||||
i.preview = parsed
|
||||
} else {
|
||||
i.preview = nil
|
||||
}
|
||||
} else {
|
||||
i.preview = nil
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (i inputModel) view(width int) string {
|
||||
return i.ti.View()
|
||||
var b strings.Builder
|
||||
b.WriteString(drawerBorderStyle.Render(strings.Repeat("─", width)))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(i.ti.View())
|
||||
b.WriteString("\n")
|
||||
b.WriteString(drawerHintsStyle.Render("enter:submit esc:cancel ?:search -:todo @:event !:reminder"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(i.renderPreview(width))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (i inputModel) renderPreview(width int) string {
|
||||
if i.preview == nil {
|
||||
return drawerPreviewStyle.Render("")
|
||||
}
|
||||
|
||||
p := i.preview
|
||||
|
||||
if p.Query {
|
||||
q := "?"
|
||||
if p.Body != "" {
|
||||
q += p.Body
|
||||
}
|
||||
for _, t := range p.FilterTags {
|
||||
q += " #" + t
|
||||
}
|
||||
return drawerPreviewStyle.Render("search: " + q)
|
||||
}
|
||||
|
||||
glyph := glyphForParsed(p.Glyph)
|
||||
body := p.Body
|
||||
if p.Title != nil {
|
||||
body = *p.Title
|
||||
}
|
||||
|
||||
var parts []string
|
||||
parts = append(parts, glyph, body)
|
||||
for _, t := range p.Tags {
|
||||
parts = append(parts, tagStyle.Render("#"+t))
|
||||
}
|
||||
if p.Pin {
|
||||
parts = append(parts, pinnedStyle.Render("•"))
|
||||
}
|
||||
if p.CardSuffix != nil {
|
||||
parts = append(parts, affordanceStyle.Render(*p.CardSuffix))
|
||||
}
|
||||
|
||||
line := strings.Join(parts, " ")
|
||||
maxW := width - 4
|
||||
if maxW > 0 && len(stripAnsi(line)) > maxW {
|
||||
line = truncate(line, maxW)
|
||||
}
|
||||
|
||||
return drawerPreviewStyle.Render(line)
|
||||
}
|
||||
|
||||
func glyphForParsed(glyph string) string {
|
||||
switch glyph {
|
||||
case "todo":
|
||||
return "○"
|
||||
case "event":
|
||||
return "◇"
|
||||
case "reminder":
|
||||
return "△"
|
||||
default:
|
||||
return "—"
|
||||
}
|
||||
}
|
||||
|
||||
func drawerLines() int {
|
||||
return 3
|
||||
}
|
||||
|
||||
// formatPreviewEntity builds a preview string showing how the entity will appear
|
||||
func formatPreviewEntity(p *parse.Result) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
glyph := glyphForParsed(p.Glyph)
|
||||
body := p.Body
|
||||
if p.Title != nil {
|
||||
body = *p.Title
|
||||
}
|
||||
return fmt.Sprintf("%s %s", glyph, body)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user