feat(tui): stumble mode — resurface stale entries card by card
Card-by-card walkthrough of entries untouched for 30+ days. Prevents write-mostly decay by bringing old entries back to attention. - S from list triggers stumble, loads entries where modified_at < 30d - Single-card view with markdown body, glyph, tags, age indicator - Actions: n skip, d dismiss, ! pin, p promote, m absorb, esc exit - Progress indicator: stumble [3/12] - After promote/absorb from stumble, returns to deck (not list) - "All caught up" screen when deck exhausted - DB: add ModifiedBefore to ListParams, modified_at sort column
This commit is contained in:
+86
-3
@@ -22,6 +22,7 @@ const (
|
||||
stateConfirm
|
||||
statePromote
|
||||
stateAbsorb
|
||||
stateStumble
|
||||
)
|
||||
|
||||
type viewMode int
|
||||
@@ -85,6 +86,7 @@ type model struct {
|
||||
promote promoteModel
|
||||
absorb absorbModel
|
||||
tagRail tagRailModel
|
||||
stumble stumbleModel
|
||||
showHelp bool
|
||||
|
||||
focus focusPane
|
||||
@@ -231,8 +233,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus(msg.action))
|
||||
|
||||
case entityPromotedMsg:
|
||||
m.state = stateList
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus(fmt.Sprintf("promoted → %s", msg.cardType)))
|
||||
if !m.stumble.done && len(m.stumble.entries) > 0 {
|
||||
m.stumble.advance()
|
||||
m.state = stateStumble
|
||||
} else {
|
||||
m.state = stateList
|
||||
}
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), loadRailTags(m.store), m.setStatus(fmt.Sprintf("promoted → %s", msg.cardType)))
|
||||
|
||||
case entityDemotedMsg:
|
||||
return m, tea.Batch(m.reloadDetail(msg.id), m.setStatus("demoted → fluid"))
|
||||
@@ -241,7 +248,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, m.setStatus("copied")
|
||||
|
||||
case entityAbsorbedMsg:
|
||||
m.state = stateList
|
||||
if !m.stumble.done && len(m.stumble.entries) > 0 {
|
||||
m.stumble.advance()
|
||||
m.state = stateStumble
|
||||
} else {
|
||||
m.state = stateList
|
||||
}
|
||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), loadRailTags(m.store), m.setStatus("absorbed"))
|
||||
|
||||
case absorbSourcesLoadedMsg:
|
||||
@@ -265,6 +277,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.state = stateTagFilter
|
||||
return m, nil
|
||||
|
||||
case staleEntitiesLoadedMsg:
|
||||
m.stumble = newStumbleModel()
|
||||
m.stumble.setEntries(msg.entities)
|
||||
m.stumble.setSize(m.width, m.contentHeight())
|
||||
m.state = stateStumble
|
||||
return m, nil
|
||||
|
||||
case stumbleActionMsg:
|
||||
return m, m.setStatus(msg.action)
|
||||
|
||||
case editorFinishedMsg:
|
||||
if msg.err != nil {
|
||||
m.err = msg.err
|
||||
@@ -300,6 +322,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m.updatePromote(msg)
|
||||
case stateAbsorb:
|
||||
return m.updateAbsorb(msg)
|
||||
case stateStumble:
|
||||
return m.updateStumble(msg)
|
||||
default:
|
||||
return m.updateKeys(msg)
|
||||
}
|
||||
@@ -532,6 +556,12 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "S":
|
||||
if m.state == stateList {
|
||||
return m, loadStaleEntities(m.store)
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "i":
|
||||
if m.mode == modeCards && m.state == stateList {
|
||||
m.cards.setIntent(m.cards.intent.next())
|
||||
@@ -818,6 +848,57 @@ func (m model) updateAbsorb(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) updateStumble(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.stumble.done {
|
||||
m.state = stateList
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
}
|
||||
|
||||
switch msg.String() {
|
||||
case "esc", "q":
|
||||
m.state = stateList
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
case "n", "right":
|
||||
m.stumble.advance()
|
||||
if m.stumble.done {
|
||||
return m, m.setStatus("all caught up")
|
||||
}
|
||||
return m, nil
|
||||
case "d":
|
||||
e := m.stumble.current()
|
||||
if e != nil {
|
||||
m.stumble.removeCurrent()
|
||||
return m, stumbleDismiss(m.store, e.ID)
|
||||
}
|
||||
return m, nil
|
||||
case "!":
|
||||
e := m.stumble.current()
|
||||
if e != nil {
|
||||
m.stumble.advance()
|
||||
return m, stumblePin(m.store, e.ID)
|
||||
}
|
||||
return m, nil
|
||||
case "p":
|
||||
e := m.stumble.current()
|
||||
if e != nil {
|
||||
m.promote = newPromoteModel(e.ID, e.Body)
|
||||
m.state = statePromote
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
case "m":
|
||||
e := m.stumble.current()
|
||||
if e != nil {
|
||||
if e.CardType != nil {
|
||||
return m, m.setStatus("target must be fluid")
|
||||
}
|
||||
return m, loadAbsorbSources(m.store, e.ID)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.showHelp {
|
||||
return renderHelp(m.width, m.height)
|
||||
@@ -852,6 +933,8 @@ func (m model) View() string {
|
||||
content = m.promote.view(m.width)
|
||||
case stateAbsorb:
|
||||
content = m.absorb.view(m.width)
|
||||
case stateStumble:
|
||||
content = m.stumble.view()
|
||||
}
|
||||
|
||||
header := m.headerView()
|
||||
|
||||
Reference in New Issue
Block a user