feat(tui): status debounce, scroll indicator, drawer label, card grouping
Status messages now use a sequence counter so rapid actions don't cause premature clearing. Detail pane shows scroll position and supports pgup/pgdown/g/G. Capture drawer border includes inline label. Cards view groups by intent (pinned/grab/read/fill) with gutter labels matching the stream view's date grouping pattern.
This commit is contained in:
+32
-43
@@ -94,8 +94,9 @@ type model struct {
|
||||
searchQuery string
|
||||
searchTags []string
|
||||
|
||||
status string
|
||||
err error
|
||||
status string
|
||||
statusSeq int
|
||||
err error
|
||||
}
|
||||
|
||||
func newModel(store *db.Store) model {
|
||||
@@ -111,6 +112,12 @@ func newModel(store *db.Store) model {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) setStatus(msg string) tea.Cmd {
|
||||
m.statusSeq++
|
||||
m.status = msg
|
||||
return clearStatusAfter(statusTimeout, m.statusSeq)
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return loadEntities(m.store, m.listParams())
|
||||
}
|
||||
@@ -143,20 +150,14 @@ func (m model) hasSearch() bool {
|
||||
|
||||
func (m *model) applySearch() {
|
||||
if m.mode == modeCards {
|
||||
filtered := filterEntities(m.cards.entities, m.searchQuery, m.searchTags)
|
||||
m.cards.filtered = nil
|
||||
var pinned, rest []*db.Entity
|
||||
for _, e := range filtered {
|
||||
if !matchesIntent(e, m.cards.intent) {
|
||||
continue
|
||||
}
|
||||
if e.Pinned {
|
||||
pinned = append(pinned, e)
|
||||
} else {
|
||||
rest = append(rest, e)
|
||||
searchFiltered := filterEntities(m.cards.entities, m.searchQuery, m.searchTags)
|
||||
var intentFiltered []*db.Entity
|
||||
for _, e := range searchFiltered {
|
||||
if matchesIntent(e, m.cards.intent) {
|
||||
intentFiltered = append(intentFiltered, e)
|
||||
}
|
||||
}
|
||||
m.cards.filtered = append(pinned, rest...)
|
||||
m.cards.filtered, m.cards.groups = sortAndGroupCards(intentFiltered, m.cards.intent)
|
||||
if m.cards.cursor >= len(m.cards.filtered) {
|
||||
m.cards.cursor = max(0, len(m.cards.filtered)-1)
|
||||
}
|
||||
@@ -194,38 +195,31 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.state = stateList
|
||||
m.input.reset()
|
||||
m.recalcSizes()
|
||||
m.status = "created"
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("created"))
|
||||
|
||||
case entityDeletedMsg:
|
||||
m.status = "deleted"
|
||||
m.state = stateList
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("deleted"))
|
||||
|
||||
case entityUpdatedMsg:
|
||||
m.status = msg.action
|
||||
if m.state == stateDetail {
|
||||
m.detail.setEntity(msg.entity)
|
||||
}
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus(msg.action))
|
||||
|
||||
case entityPromotedMsg:
|
||||
m.status = fmt.Sprintf("promoted → %s", msg.cardType)
|
||||
m.state = stateList
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus(fmt.Sprintf("promoted → %s", msg.cardType)))
|
||||
|
||||
case entityDemotedMsg:
|
||||
m.status = "demoted → fluid"
|
||||
return m, tea.Batch(m.reloadDetail(msg.id), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(m.reloadDetail(msg.id), m.setStatus("demoted → fluid"))
|
||||
|
||||
case entityCopiedMsg:
|
||||
m.status = "copied"
|
||||
return m, clearStatusAfter(statusTimeout)
|
||||
return m, m.setStatus("copied")
|
||||
|
||||
case entityAbsorbedMsg:
|
||||
m.status = "absorbed"
|
||||
m.state = stateList
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("absorbed"))
|
||||
|
||||
case absorbSourcesLoadedMsg:
|
||||
m.absorb = newAbsorbModel(msg.targetID)
|
||||
@@ -235,14 +229,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
|
||||
case stepsPersistedMsg:
|
||||
m.status = "steps saved"
|
||||
m.detail.mode = detailPreview
|
||||
return m, tea.Batch(m.reloadDetail(m.detail.entity.ID), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(m.reloadDetail(m.detail.entity.ID), m.setStatus("steps saved"))
|
||||
|
||||
case templateCopiedMsg:
|
||||
m.status = "copied resolved"
|
||||
m.detail.mode = detailPreview
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("copied resolved"))
|
||||
|
||||
case tagsLoadedMsg:
|
||||
m.filter.setTags(msg.tags)
|
||||
@@ -254,8 +246,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.err = msg.err
|
||||
return m, m.reloadAfterEdit()
|
||||
}
|
||||
m.status = "updated"
|
||||
return m, tea.Batch(m.reloadAfterEdit(), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(m.reloadAfterEdit(), m.setStatus("updated"))
|
||||
|
||||
case confirmTimeoutMsg:
|
||||
if m.state == stateConfirm {
|
||||
@@ -265,7 +256,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
|
||||
case statusClearMsg:
|
||||
m.status = ""
|
||||
if msg.seq == m.statusSeq {
|
||||
m.status = ""
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case errMsg:
|
||||
@@ -421,8 +414,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case "s":
|
||||
if m.mode == modeCards && m.state == stateList {
|
||||
m.cardsSort = m.cardsSort.next()
|
||||
m.status = "sort: " + m.cardsSort.String()
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), clearStatusAfter(statusTimeout))
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("sort: "+m.cardsSort.String()))
|
||||
}
|
||||
return m, nil
|
||||
|
||||
@@ -538,8 +530,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
e := m.selectedEntity()
|
||||
if e != nil {
|
||||
if e.CardType != nil {
|
||||
m.status = "target must be fluid"
|
||||
return m, clearStatusAfter(statusTimeout)
|
||||
return m, m.setStatus("target must be fluid")
|
||||
}
|
||||
return m, loadAbsorbSources(m.store, e.ID)
|
||||
}
|
||||
@@ -549,8 +540,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
e := m.selectedEntity()
|
||||
if e != nil {
|
||||
if e.CardType != nil {
|
||||
m.status = "already a card"
|
||||
return m, clearStatusAfter(statusTimeout)
|
||||
return m, m.setStatus("already a card")
|
||||
}
|
||||
m.promote = newPromoteModel(e.ID, e.Body)
|
||||
m.state = statePromote
|
||||
@@ -561,8 +551,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case "D":
|
||||
if m.state == stateDetail && m.detail.entity != nil {
|
||||
if m.detail.entity.CardType == nil {
|
||||
m.status = "already fluid"
|
||||
return m, clearStatusAfter(statusTimeout)
|
||||
return m, m.setStatus("already fluid")
|
||||
}
|
||||
return m, demoteEntity(m.store, m.detail.entity.ID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user