feat(tui): add search via capture bar and absorb flow

Search uses existing parse grammar ?prefix — type `?query #tag` in
capture bar to filter entities client-side. Substring match on
body+title+description with AND tag filtering. Esc clears search.

Absorb via m key on fluid entities — opens source picker showing all
other entities, enter merges source into target. Uses existing
store.Absorb() backend.
This commit is contained in:
2026-05-17 21:35:44 -04:00
parent ce335cabd6
commit 1066c0bc7d
10 changed files with 387 additions and 17 deletions
+18 -7
View File
@@ -13,6 +13,7 @@ import (
type listModel struct {
entities []*db.Entity
filtered []*db.Entity
cursor int
offset int
height int
@@ -25,6 +26,7 @@ func newListModel() listModel {
func (l *listModel) setEntities(entities []*db.Entity) {
l.entities = entities
l.filtered = nil
if l.cursor >= len(entities) {
l.cursor = max(0, len(entities)-1)
}
@@ -35,11 +37,19 @@ func (l *listModel) setSize(width, height int) {
l.height = height
}
func (l listModel) displayEntities() []*db.Entity {
if l.filtered != nil {
return l.filtered
}
return l.entities
}
func (l listModel) selected() *db.Entity {
if len(l.entities) == 0 || l.cursor >= len(l.entities) {
ents := l.displayEntities()
if len(ents) == 0 || l.cursor >= len(ents) {
return nil
}
return l.entities[l.cursor]
return ents[l.cursor]
}
func (l listModel) update(msg tea.KeyMsg) listModel {
@@ -52,7 +62,7 @@ func (l listModel) update(msg tea.KeyMsg) listModel {
}
}
case "down", "j":
if l.cursor < len(l.entities)-1 {
if l.cursor < len(l.displayEntities())-1 {
l.cursor++
visible := l.visibleCount()
if l.cursor >= l.offset+visible {
@@ -63,7 +73,7 @@ func (l listModel) update(msg tea.KeyMsg) listModel {
l.cursor = 0
l.offset = 0
case "end", "G":
l.cursor = max(0, len(l.entities)-1)
l.cursor = max(0, len(l.displayEntities())-1)
visible := l.visibleCount()
if l.cursor >= visible {
l.offset = l.cursor - visible + 1
@@ -74,7 +84,7 @@ func (l listModel) update(msg tea.KeyMsg) listModel {
l.offset = l.cursor
}
case "pgdown", "ctrl+d":
l.cursor = min(len(l.entities)-1, l.cursor+l.visibleCount())
l.cursor = min(len(l.displayEntities())-1, l.cursor+l.visibleCount())
visible := l.visibleCount()
if l.cursor >= l.offset+visible {
l.offset = l.cursor - visible + 1
@@ -84,11 +94,12 @@ func (l listModel) update(msg tea.KeyMsg) listModel {
}
func (l listModel) view(width int) string {
if len(l.entities) == 0 {
ents := l.displayEntities()
if len(ents) == 0 {
return statusStyle.Render("no entities")
}
groups := groupByDate(l.entities)
groups := groupByDate(ents)
type displayLine struct {
text string