fix(tui): pin footer to bottom, style hint bar, auto-clear status
Content area now enforces full height so the context help bar stays pinned to the terminal bottom. Hint keys rendered with bold highlight color for scannability. Status messages (created, deleted, etc.) auto-clear after 2 seconds, reverting to the entity count.
This commit is contained in:
+25
-20
@@ -3,6 +3,7 @@ package tui
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
"github.com/lerko/nib/internal/db"
|
||||
)
|
||||
|
||||
const statusTimeout = 2 * time.Second
|
||||
|
||||
type viewState int
|
||||
|
||||
const (
|
||||
@@ -192,37 +195,37 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.input.reset()
|
||||
m.recalcSizes()
|
||||
m.status = "created"
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
|
||||
case entityDeletedMsg:
|
||||
m.status = "deleted"
|
||||
m.state = stateList
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
|
||||
case entityUpdatedMsg:
|
||||
m.status = msg.action
|
||||
if m.state == stateDetail {
|
||||
m.detail.setEntity(msg.entity)
|
||||
}
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
|
||||
case entityPromotedMsg:
|
||||
m.status = fmt.Sprintf("promoted → %s", msg.cardType)
|
||||
m.state = stateList
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
|
||||
case entityDemotedMsg:
|
||||
m.status = "demoted → fluid"
|
||||
return m, m.reloadDetail(msg.id)
|
||||
return m, tea.Batch(m.reloadDetail(msg.id), clearStatusAfter(statusTimeout))
|
||||
|
||||
case entityCopiedMsg:
|
||||
m.status = "copied"
|
||||
return m, nil
|
||||
return m, clearStatusAfter(statusTimeout)
|
||||
|
||||
case entityAbsorbedMsg:
|
||||
m.status = "absorbed"
|
||||
m.state = stateList
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
|
||||
case absorbSourcesLoadedMsg:
|
||||
m.absorb = newAbsorbModel(msg.targetID)
|
||||
@@ -234,12 +237,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case stepsPersistedMsg:
|
||||
m.status = "steps saved"
|
||||
m.detail.mode = detailPreview
|
||||
return m, m.reloadDetail(m.detail.entity.ID)
|
||||
return m, tea.Batch(m.reloadDetail(m.detail.entity.ID), clearStatusAfter(statusTimeout))
|
||||
|
||||
case templateCopiedMsg:
|
||||
m.status = "copied resolved"
|
||||
m.detail.mode = detailPreview
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
|
||||
case tagsLoadedMsg:
|
||||
m.filter.setTags(msg.tags)
|
||||
@@ -249,10 +252,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case editorFinishedMsg:
|
||||
if msg.err != nil {
|
||||
m.err = msg.err
|
||||
} else {
|
||||
m.status = "updated"
|
||||
return m, m.reloadAfterEdit()
|
||||
}
|
||||
return m, m.reloadAfterEdit()
|
||||
m.status = "updated"
|
||||
return m, tea.Batch(m.reloadAfterEdit(), clearStatusAfter(statusTimeout))
|
||||
|
||||
case confirmTimeoutMsg:
|
||||
if m.state == stateConfirm {
|
||||
@@ -261,6 +264,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case statusClearMsg:
|
||||
m.status = ""
|
||||
return m, nil
|
||||
|
||||
case errMsg:
|
||||
m.err = msg.err
|
||||
return m, nil
|
||||
@@ -415,7 +422,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.mode == modeCards && m.state == stateList {
|
||||
m.cardsSort = m.cardsSort.next()
|
||||
m.status = "sort: " + m.cardsSort.String()
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
}
|
||||
return m, nil
|
||||
|
||||
@@ -532,7 +539,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if e != nil {
|
||||
if e.CardType != nil {
|
||||
m.status = "target must be fluid"
|
||||
return m, nil
|
||||
return m, clearStatusAfter(statusTimeout)
|
||||
}
|
||||
return m, loadAbsorbSources(m.store, e.ID)
|
||||
}
|
||||
@@ -543,7 +550,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if e != nil {
|
||||
if e.CardType != nil {
|
||||
m.status = "already a card"
|
||||
return m, nil
|
||||
return m, clearStatusAfter(statusTimeout)
|
||||
}
|
||||
m.promote = newPromoteModel(e.ID, e.Body)
|
||||
m.state = statePromote
|
||||
@@ -555,7 +562,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.state == stateDetail && m.detail.entity != nil {
|
||||
if m.detail.entity.CardType == nil {
|
||||
m.status = "already fluid"
|
||||
return m, nil
|
||||
return m, clearStatusAfter(statusTimeout)
|
||||
}
|
||||
return m, demoteEntity(m.store, m.detail.entity.ID)
|
||||
}
|
||||
@@ -758,6 +765,8 @@ func (m model) View() string {
|
||||
header := m.headerView()
|
||||
footer := m.footerView()
|
||||
|
||||
content = lipgloss.NewStyle().Width(m.width).Height(m.contentHeight()).Render(content)
|
||||
|
||||
return header + "\n" + content + "\n" + footer
|
||||
}
|
||||
|
||||
@@ -824,10 +833,6 @@ func (m model) footerView() string {
|
||||
return errorStyle.Render("error: " + m.err.Error())
|
||||
}
|
||||
|
||||
if m.status != "" {
|
||||
return statusStyle.Render(m.status) + " " + helpStyle.Render(contextHints(m))
|
||||
}
|
||||
|
||||
return renderStatusBar(m, m.width)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user