Files
nib-v1/internal/tui/input.go
lerko e22e040688
CI / test (pull_request) Successful in 2m31s
feat(tui): add tag autocomplete and query composition
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.
2026-05-21 12:12:07 -04:00

178 lines
3.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tui
import (
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/lerko/nib/internal/db"
"github.com/lerko/nib/internal/parse"
)
type inputResult struct {
entity *db.Entity
query bool
body string
tags []string
dateFrom *string
dateTo *string
cardType *db.CardType
}
type inputModel struct {
ti textinput.Model
preview *parse.Result
}
func newInputModel() inputModel {
ti := textinput.New()
ti.Placeholder = "capture a thought…"
ti.Prompt = inputPromptStyle.Render(" ")
ti.CharLimit = 500
return inputModel{ti: ti}
}
func (i *inputModel) clearText() {
i.ti.SetValue("")
i.preview = nil
}
func (i inputModel) submit() *inputResult {
val := i.ti.Value()
if val == "" {
return nil
}
parsed, err := parse.Parse(val)
if err != nil {
return nil
}
if parsed.Query {
r := &inputResult{
query: true,
body: parsed.Body,
tags: parsed.FilterTags,
dateFrom: parsed.QueryDateFrom,
dateTo: parsed.QueryDateTo,
}
if parsed.QueryCardType != nil {
ct := db.CardType(*parsed.QueryCardType)
r.cardType = &ct
}
return r
}
e := &db.Entity{
Body: parsed.Body,
Title: parsed.Title,
Glyph: db.Glyph(parsed.Glyph),
Tags: parsed.Tags,
}
if parsed.TimeAnchor != nil {
e.TimeAnchor = parsed.TimeAnchor
}
if parsed.CardSuffix != nil {
ct := db.CardType(*parsed.CardSuffix)
e.CardType = &ct
}
if parsed.Pin {
e.Pinned = true
}
if parsed.Description != nil {
e.Description = parsed.Description
}
return &inputResult{entity: e}
}
func (i inputModel) updateKey(msg tea.KeyMsg) inputModel {
i.ti, _ = i.ti.Update(msg)
val := i.ti.Value()
if val != "" {
parsed, err := parse.Parse(val)
if err == nil {
i.preview = parsed
} else {
i.preview = nil
}
} else {
i.preview = nil
}
return i
}
func (i inputModel) viewBar(width int, focused bool) string {
tiView := i.ti.View()
if focused {
return tiView
}
val := i.ti.Value()
if val != "" {
return hintDescStyle.Render(" " + val)
}
return hintDescStyle.Render(" capture a thought…")
}
func (i inputModel) previewText() string {
if i.preview == nil {
return ""
}
p := i.preview
if p.Query {
q := "?"
if p.Body != "" {
q += p.Body
}
for _, t := range p.FilterTags {
q += " #" + t
}
if p.QueryDateFrom != nil {
q += " from:" + *p.QueryDateFrom
}
if p.QueryDateTo != nil {
q += " to:" + *p.QueryDateTo
}
if p.QueryCardType != nil {
q += " ^" + *p.QueryCardType
}
return "search: " + q
}
glyph := glyphForParsed(p.Glyph)
body := p.Body
if p.Title != nil {
body = *p.Title
}
var parts []string
parts = append(parts, glyph, body)
for _, t := range p.Tags {
parts = append(parts, "#"+t)
}
if p.Pin {
parts = append(parts, "•")
}
if p.CardSuffix != nil {
parts = append(parts, *p.CardSuffix)
}
return strings.Join(parts, " ")
}
func glyphForParsed(glyph string) string {
switch glyph {
case "todo":
return "○"
case "event":
return "◇"
case "reminder":
return "△"
default:
return "—"
}
}