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:
+91
-67
@@ -18,7 +18,6 @@ type viewState int
|
||||
const (
|
||||
stateList viewState = iota
|
||||
stateDetail
|
||||
stateInput
|
||||
stateTagFilter
|
||||
stateConfirm
|
||||
statePromote
|
||||
@@ -65,7 +64,8 @@ func (s cardsSort) next() cardsSort {
|
||||
type focusPane int
|
||||
|
||||
const (
|
||||
focusList focusPane = iota
|
||||
focusCapture focusPane = iota
|
||||
focusList
|
||||
focusDetail
|
||||
)
|
||||
|
||||
@@ -100,18 +100,30 @@ type model struct {
|
||||
}
|
||||
|
||||
func newModel(store *db.Store) model {
|
||||
inp := newInputModel()
|
||||
inp.ti.Focus()
|
||||
return model{
|
||||
store: store,
|
||||
state: stateList,
|
||||
mode: modeStream,
|
||||
focus: focusCapture,
|
||||
list: newListModel(),
|
||||
cards: newCardsModel(),
|
||||
detail: newDetailModel(),
|
||||
input: newInputModel(),
|
||||
input: inp,
|
||||
filter: newFilterModel(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) setFocus(f focusPane) tea.Cmd {
|
||||
m.focus = f
|
||||
if f == focusCapture {
|
||||
return m.input.ti.Focus()
|
||||
}
|
||||
m.input.ti.Blur()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *model) setStatus(msg string) tea.Cmd {
|
||||
m.statusSeq++
|
||||
m.status = msg
|
||||
@@ -119,7 +131,7 @@ func (m *model) setStatus(msg string) tea.Cmd {
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return loadEntities(m.store, m.listParams())
|
||||
return tea.Batch(loadEntities(m.store, m.listParams()), m.input.ti.Focus())
|
||||
}
|
||||
|
||||
func (m model) listParams() db.ListParams {
|
||||
@@ -192,9 +204,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
|
||||
case entityCreatedMsg:
|
||||
m.state = stateList
|
||||
m.input.reset()
|
||||
m.recalcSizes()
|
||||
m.input.clearText()
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("created"))
|
||||
|
||||
case entityDeletedMsg:
|
||||
@@ -268,8 +278,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tea.KeyMsg:
|
||||
m.err = nil
|
||||
switch m.state {
|
||||
case stateInput:
|
||||
return m.updateInput(msg)
|
||||
case stateTagFilter:
|
||||
return m.updateTagFilter(msg)
|
||||
case stateConfirm:
|
||||
@@ -281,6 +289,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
default:
|
||||
return m.updateKeys(msg)
|
||||
}
|
||||
|
||||
default:
|
||||
if m.focus == focusCapture {
|
||||
var cmd tea.Cmd
|
||||
m.input.ti, cmd = m.input.ti.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -294,8 +309,58 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if msg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
if m.focus == focusCapture {
|
||||
return m.updateCapture(msg)
|
||||
}
|
||||
return m.updateBrowse(msg)
|
||||
}
|
||||
|
||||
func (m model) updateCapture(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
val := m.input.ti.Value()
|
||||
if val == "" {
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, cmd
|
||||
}
|
||||
result := m.input.submit()
|
||||
if result == nil {
|
||||
return m, nil
|
||||
}
|
||||
if result.query {
|
||||
m.searchQuery = result.body
|
||||
m.searchTags = result.tags
|
||||
m.input.clearText()
|
||||
m.applySearch()
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, cmd
|
||||
}
|
||||
if result.entity != nil {
|
||||
return m, createEntity(m.store, result.entity)
|
||||
}
|
||||
return m, nil
|
||||
case "esc", "tab":
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, cmd
|
||||
}
|
||||
m.input = m.input.updateKey(msg)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.splitDetail && m.state == stateList {
|
||||
switch msg.String() {
|
||||
case "tab":
|
||||
if m.focus == focusList {
|
||||
m.focus = focusDetail
|
||||
return m, nil
|
||||
}
|
||||
cmd := m.setFocus(focusCapture)
|
||||
return m, cmd
|
||||
case "l":
|
||||
if m.focus == focusList {
|
||||
m.focus = focusDetail
|
||||
@@ -311,9 +376,6 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.focus = focusList
|
||||
return m, nil
|
||||
}
|
||||
m.splitDetail = false
|
||||
m.recalcSizes()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.focus == focusDetail {
|
||||
@@ -380,9 +442,6 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
|
||||
case "q":
|
||||
if m.state == stateList {
|
||||
return m, tea.Quit
|
||||
@@ -418,7 +477,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "tab":
|
||||
case "i":
|
||||
if m.mode == modeCards && m.state == stateList {
|
||||
m.cards.setIntent(m.cards.intent.next())
|
||||
if m.hasSearch() {
|
||||
@@ -428,12 +487,14 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "tab":
|
||||
cmd := m.setFocus(focusCapture)
|
||||
return m, cmd
|
||||
|
||||
case "a":
|
||||
if m.state == stateList {
|
||||
m.state = stateInput
|
||||
m.input.focus()
|
||||
m.recalcSizes()
|
||||
return m, m.input.ti.Focus()
|
||||
cmd := m.setFocus(focusCapture)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
case "esc":
|
||||
@@ -488,6 +549,10 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.status = ""
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
}
|
||||
if m.state == stateList {
|
||||
cmd := m.setFocus(focusCapture)
|
||||
return m, cmd
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "d":
|
||||
@@ -630,37 +695,6 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) updateInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.state = stateList
|
||||
m.input.reset()
|
||||
m.recalcSizes()
|
||||
return m, nil
|
||||
case "enter":
|
||||
result := m.input.submit()
|
||||
if result == nil {
|
||||
return m, nil
|
||||
}
|
||||
if result.query {
|
||||
m.searchQuery = result.body
|
||||
m.searchTags = result.tags
|
||||
m.state = stateList
|
||||
m.input.reset()
|
||||
m.recalcSizes()
|
||||
m.applySearch()
|
||||
return m, nil
|
||||
}
|
||||
if result.entity != nil {
|
||||
return m, createEntity(m.store, result.entity)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.input = m.input.updateKey(msg)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) updateTagFilter(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc", "q":
|
||||
@@ -729,7 +763,7 @@ func (m model) View() string {
|
||||
|
||||
var content string
|
||||
switch m.state {
|
||||
case stateList, stateInput, stateConfirm:
|
||||
case stateList, stateConfirm:
|
||||
listContent := m.listContent()
|
||||
if m.splitDetail {
|
||||
lw, rw := m.splitWidths()
|
||||
@@ -752,11 +786,12 @@ func (m model) View() string {
|
||||
}
|
||||
|
||||
header := m.headerView()
|
||||
footer := m.footerView()
|
||||
captureBar := m.input.viewBar(m.width, m.focus == focusCapture)
|
||||
statusLine := m.statusLine()
|
||||
|
||||
content = lipgloss.NewStyle().Width(m.width).Height(m.contentHeight()).Render(content)
|
||||
|
||||
return header + "\n" + content + "\n" + footer
|
||||
return header + "\n" + content + "\n" + captureBar + "\n" + statusLine
|
||||
}
|
||||
|
||||
func (m model) listContent() string {
|
||||
@@ -806,11 +841,7 @@ func (m model) headerView() string {
|
||||
return header
|
||||
}
|
||||
|
||||
func (m model) footerView() string {
|
||||
if m.state == stateInput {
|
||||
return m.input.view(m.width)
|
||||
}
|
||||
|
||||
func (m model) statusLine() string {
|
||||
if m.state == stateConfirm {
|
||||
return renderConfirm(m.confirmID)
|
||||
}
|
||||
@@ -823,14 +854,7 @@ func (m model) footerView() string {
|
||||
}
|
||||
|
||||
func (m model) contentHeight() int {
|
||||
return m.height - 3 - m.drawerHeight()
|
||||
}
|
||||
|
||||
func (m model) drawerHeight() int {
|
||||
if m.state == stateInput {
|
||||
return drawerLines()
|
||||
}
|
||||
return 0
|
||||
return m.height - 4
|
||||
}
|
||||
|
||||
func (m *model) recalcSizes() {
|
||||
|
||||
Reference in New Issue
Block a user