feat(tui): add tag autocomplete and query composition
CI / test (pull_request) Successful in 2m31s

Tag autocomplete shows suggestions when typing #partial in capture bar.
Tab/enter accepts, up/down navigates, esc dismisses.

Query composition extends ? search with date filters (@today, @week,
@month, <7d, >30d), card type filters (^snippet), all composable
with existing text and tag filters.
This commit is contained in:
2026-05-21 12:12:07 -04:00
parent 29bd7d3dc6
commit e22e040688
8 changed files with 485 additions and 38 deletions
+112
View File
@@ -0,0 +1,112 @@
package tui
import (
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/lerko/nib/internal/db"
)
const maxSuggestions = 5
type autocompleteModel struct {
suggestions []string
cursor int
active bool
prefix string
tokenStart int
tokenEnd int
}
func (a *autocompleteModel) moveUp() {
if a.cursor > 0 {
a.cursor--
}
}
func (a *autocompleteModel) moveDown() {
if a.cursor < len(a.suggestions)-1 {
a.cursor++
}
}
func (a autocompleteModel) selected() string {
if len(a.suggestions) == 0 || a.cursor >= len(a.suggestions) {
return ""
}
return a.suggestions[a.cursor]
}
func (a autocompleteModel) visibleCount() int {
if len(a.suggestions) > maxSuggestions {
return maxSuggestions
}
return len(a.suggestions)
}
func (a autocompleteModel) view(width int) string {
if !a.active || len(a.suggestions) == 0 {
return ""
}
var b strings.Builder
n := a.visibleCount()
for i := 0; i < n; i++ {
tag := "#" + a.suggestions[i]
if i == a.cursor {
b.WriteString(acSelectedStyle.Render(" " + tag))
} else {
b.WriteString(acItemStyle.Render(" " + tag))
}
if i < n-1 {
b.WriteString("\n")
}
}
if len(a.suggestions) > maxSuggestions {
b.WriteString("\n")
b.WriteString(acItemStyle.Render(" …"))
}
box := lipgloss.NewStyle().
Width(min(30, width)).
Render(b.String())
return box
}
func tagTokenAtCursor(val string, cursorPos int) (tokenStart, tokenEnd int, prefix string, ok bool) {
if cursorPos > len(val) {
cursorPos = len(val)
}
start := cursorPos
for start > 0 && val[start-1] != ' ' {
start--
}
if start >= len(val) || val[start] != '#' {
return 0, 0, "", false
}
end := cursorPos
for end < len(val) && val[end] != ' ' {
end++
}
prefix = strings.ToLower(val[start+1 : cursorPos])
return start, end, prefix, true
}
func filterTagSuggestions(tags []db.TagCount, prefix string) []string {
if prefix == "" {
return nil
}
prefix = strings.ToLower(prefix)
var result []string
for _, tc := range tags {
lower := strings.ToLower(tc.Tag)
if strings.HasPrefix(lower, prefix) && lower != prefix {
result = append(result, tc.Tag)
}
}
return result
}