feat(tui): always-visible capture bar with focus cycling

Replace drawer-based input with permanent capture bar at bottom.
Focus defaults to capture on startup — open nib, start typing.

- Remove stateInput; route via focusCapture/focusList/focusDetail
- Tab cycles: capture → list → detail (split) → capture
- Esc cascades: clear search → clear filter → focus capture
- Capture bar shows blinking cursor when focused, dims when not
- Intent cycling moved from tab to i (tab now cycles focus)
- Parse preview shown inline in status bar while typing
- Content area constant height (no layout thrash from drawer)
This commit is contained in:
2026-05-20 14:11:46 -04:00
parent 3daa5a2e11
commit a2dac64d1f
5 changed files with 141 additions and 132 deletions
+17 -47
View File
@@ -19,7 +19,6 @@ type inputResult struct {
type inputModel struct {
ti textinput.Model
active bool
preview *parse.Result
}
@@ -31,15 +30,8 @@ func newInputModel() inputModel {
return inputModel{ti: ti}
}
func (i *inputModel) focus() {
i.active = true
i.ti.Focus()
}
func (i *inputModel) reset() {
i.active = false
func (i *inputModel) clearText() {
i.ti.SetValue("")
i.ti.Blur()
i.preview = nil
}
@@ -101,33 +93,21 @@ func (i inputModel) updateKey(msg tea.KeyMsg) inputModel {
return i
}
func (i inputModel) view(width int) string {
var b strings.Builder
label := "capture"
prefix := "── "
suffix := " "
dashCount := width - len(prefix) - len(label) - len(suffix)
if dashCount < 0 {
dashCount = 0
func (i inputModel) viewBar(width int, focused bool) string {
tiView := i.ti.View()
if focused {
return tiView
}
b.WriteString(drawerBorderStyle.Render(prefix) +
hintDescStyle.Render(label) +
drawerBorderStyle.Render(suffix+strings.Repeat("─", dashCount)))
b.WriteString("\n")
b.WriteString(i.ti.View())
b.WriteString("\n")
b.WriteString(drawerHintsStyle.Render(renderHints([]hint{
{"enter", "submit"}, {"esc", "cancel"}, {"?", "search"},
{"-", "todo"}, {"@", "event"}, {"!", "reminder"},
})))
b.WriteString("\n")
b.WriteString(i.renderPreview(width))
return b.String()
val := i.ti.Value()
if val != "" {
return hintDescStyle.Render(" " + val)
}
return hintDescStyle.Render(" capture a thought…")
}
func (i inputModel) renderPreview(width int) string {
func (i inputModel) previewText() string {
if i.preview == nil {
return drawerPreviewStyle.Render("")
return ""
}
p := i.preview
@@ -140,7 +120,7 @@ func (i inputModel) renderPreview(width int) string {
for _, t := range p.FilterTags {
q += " #" + t
}
return drawerPreviewStyle.Render("search: " + q)
return "search: " + q
}
glyph := glyphForParsed(p.Glyph)
@@ -152,22 +132,16 @@ func (i inputModel) renderPreview(width int) string {
var parts []string
parts = append(parts, glyph, body)
for _, t := range p.Tags {
parts = append(parts, tagStyle.Render("#"+t))
parts = append(parts, "#"+t)
}
if p.Pin {
parts = append(parts, pinnedStyle.Render("•"))
parts = append(parts, "•")
}
if p.CardSuffix != nil {
parts = append(parts, affordanceStyle.Render(*p.CardSuffix))
parts = append(parts, *p.CardSuffix)
}
line := strings.Join(parts, " ")
maxW := width - 4
if maxW > 0 && len(stripAnsi(line)) > maxW {
line = truncate(line, maxW)
}
return drawerPreviewStyle.Render(line)
return strings.Join(parts, " ")
}
func glyphForParsed(glyph string) string {
@@ -182,7 +156,3 @@ func glyphForParsed(glyph string) string {
return "—"
}
}
func drawerLines() int {
return 3
}