a2dac64d1f
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)
105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
type hint struct {
|
|
key string
|
|
desc string
|
|
}
|
|
|
|
func renderHints(hints []hint) string {
|
|
parts := make([]string, len(hints))
|
|
for i, h := range hints {
|
|
parts[i] = hintKeyStyle.Render(h.key) + " " + hintDescStyle.Render(h.desc)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func renderTab(label, key string, active bool) string {
|
|
if active {
|
|
return hintKeyStyle.Render(label) + " " + hintDescStyle.Render(key)
|
|
}
|
|
return hintDescStyle.Render(label) + " " + hintKeyStyle.Render(key)
|
|
}
|
|
|
|
func renderStatusBar(m model, width int) string {
|
|
var leftParts []string
|
|
|
|
if m.status != "" {
|
|
leftParts = append(leftParts, statusStyle.Render(m.status))
|
|
} else if preview := m.input.previewText(); m.focus == focusCapture && preview != "" {
|
|
leftParts = append(leftParts, drawerPreviewStyle.Render(preview))
|
|
} else {
|
|
leftParts = append(leftParts, statusStyle.Render(countText(m)))
|
|
}
|
|
|
|
leftRendered := strings.Join(leftParts, " "+separatorStyle.Render("│")+" ")
|
|
right := renderHints(contextHints(m))
|
|
|
|
gap := width - lipgloss.Width(leftRendered) - lipgloss.Width(right)
|
|
if gap < 0 {
|
|
gap = 0
|
|
}
|
|
|
|
pad := lipgloss.NewStyle().Width(gap).Render("")
|
|
return leftRendered + pad + right
|
|
}
|
|
|
|
func countText(m model) string {
|
|
var total int
|
|
if m.mode == modeCards {
|
|
total = len(m.cards.filtered)
|
|
} else {
|
|
total = len(m.list.displayEntities())
|
|
}
|
|
if m.filterTag != "" {
|
|
return fmt.Sprintf("%d entities #%s", total, m.filterTag)
|
|
}
|
|
return fmt.Sprintf("%d entities", total)
|
|
}
|
|
|
|
func contextHints(m model) []hint {
|
|
switch m.state {
|
|
case stateDetail:
|
|
switch m.detail.mode {
|
|
case detailRun:
|
|
return []hint{{"space", "toggle"}, {"j/k", "nav"}, {"r", "reset"}, {"esc", "save+exit"}}
|
|
case detailFill:
|
|
return []hint{{"tab", "next"}, {"⇧tab", "prev"}, {"enter", "copy"}, {"esc", "cancel"}}
|
|
default:
|
|
return []hint{{"p", "promote"}, {"D", "demote"}, {"c", "copy"}, {"e", "edit"}, {"r", "run"}, {"f", "fill"}, {"!", "pin"}, {"esc", "back"}}
|
|
}
|
|
case stateTagFilter:
|
|
return []hint{{"j/k", "nav"}, {"enter", "select"}, {"esc", "cancel"}}
|
|
case stateConfirm:
|
|
return []hint{{"y", "confirm"}, {"n", "cancel"}}
|
|
case statePromote:
|
|
return []hint{{"j/k", "nav"}, {"enter", "select"}, {"esc", "cancel"}}
|
|
case stateAbsorb:
|
|
return []hint{{"j/k", "nav"}, {"enter", "absorb"}, {"esc", "cancel"}}
|
|
}
|
|
|
|
switch m.focus {
|
|
case focusCapture:
|
|
return []hint{{"enter", "submit"}, {"esc", "browse"}, {"?…", "search"}, {"-", "todo"}, {"@", "event"}}
|
|
case focusDetail:
|
|
if m.splitDetail {
|
|
return []hint{{"h", "list"}, {"c", "copy"}, {"e", "edit"}, {"p", "promote"}, {"!", "pin"}, {"tab", "capture"}}
|
|
}
|
|
return []hint{{"c", "copy"}, {"e", "edit"}, {"p", "promote"}, {"!", "pin"}, {"esc", "back"}}
|
|
default:
|
|
if m.splitDetail {
|
|
return []hint{{"l", "detail"}, {"d", "del"}, {"#", "filter"}, {"tab", "capture"}, {"?", "help"}}
|
|
}
|
|
if m.mode == modeCards {
|
|
return []hint{{"s", "sort"}, {"i", "intent"}, {"tab", "capture"}, {"?", "help"}}
|
|
}
|
|
return []hint{{"m", "absorb"}, {"d", "del"}, {"#", "filter"}, {"tab", "capture"}, {"?", "help"}}
|
|
}
|
|
}
|