feat(tui): add bubbletea terminal UI with entity list, detail, and capture

Adds `nib tui` command and `make tui` target. Scrollable entity list
with j/k navigation, enter for detail view, `a` to capture new entries
using the existing parse grammar, and `d` to delete.
This commit is contained in:
2026-05-17 20:07:45 -04:00
parent d995d1e708
commit 36999cd825
12 changed files with 789 additions and 2 deletions
+76
View File
@@ -0,0 +1,76 @@
package tui
import (
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/lerko/nib/internal/db"
"github.com/lerko/nib/internal/parse"
)
type inputModel struct {
ti textinput.Model
active bool
}
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) focus() {
i.active = true
i.ti.Focus()
}
func (i *inputModel) reset() {
i.active = false
i.ti.SetValue("")
i.ti.Blur()
}
func (i inputModel) submit() *db.Entity {
val := i.ti.Value()
if val == "" {
return nil
}
parsed, err := parse.Parse(val)
if err != nil {
return nil
}
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 e
}
func (i inputModel) updateKey(msg tea.KeyMsg) inputModel {
i.ti, _ = i.ti.Update(msg)
return i
}
func (i inputModel) view(width int) string {
return i.ti.View()
}