4e0ac8402f
Content area now enforces full height so the context help bar stays pinned to the terminal bottom. Hint keys rendered with bold highlight color for scannability. Status messages (created, deleted, etc.) auto-clear after 2 seconds, reverting to the entity count.
180 lines
3.2 KiB
Go
180 lines
3.2 KiB
Go
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
|
||
}
|
||
|
||
type inputModel struct {
|
||
ti textinput.Model
|
||
active bool
|
||
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) focus() {
|
||
i.active = true
|
||
i.ti.Focus()
|
||
}
|
||
|
||
func (i *inputModel) reset() {
|
||
i.active = false
|
||
i.ti.SetValue("")
|
||
i.ti.Blur()
|
||
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 {
|
||
return &inputResult{
|
||
query: true,
|
||
body: parsed.Body,
|
||
tags: parsed.FilterTags,
|
||
}
|
||
}
|
||
|
||
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) view(width int) string {
|
||
var b strings.Builder
|
||
b.WriteString(drawerBorderStyle.Render(strings.Repeat("─", width)))
|
||
b.WriteString("\n")
|
||
b.WriteString(i.ti.View())
|
||
b.WriteString("\n")
|
||
b.WriteString(drawerHintsStyle.Render(renderHints([]hint{
|
||
{"enter", "submit"}, {"esc", "cancel"}, {"?", "search"},
|
||
{"-", "todo"}, {"@", "event"}, {"!", "reminder"},
|
||
})))
|
||
b.WriteString("\n")
|
||
b.WriteString(i.renderPreview(width))
|
||
return b.String()
|
||
}
|
||
|
||
func (i inputModel) renderPreview(width int) string {
|
||
if i.preview == nil {
|
||
return drawerPreviewStyle.Render("")
|
||
}
|
||
|
||
p := i.preview
|
||
|
||
if p.Query {
|
||
q := "?"
|
||
if p.Body != "" {
|
||
q += p.Body
|
||
}
|
||
for _, t := range p.FilterTags {
|
||
q += " #" + t
|
||
}
|
||
return drawerPreviewStyle.Render("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, tagStyle.Render("#"+t))
|
||
}
|
||
if p.Pin {
|
||
parts = append(parts, pinnedStyle.Render("•"))
|
||
}
|
||
if p.CardSuffix != nil {
|
||
parts = append(parts, affordanceStyle.Render(*p.CardSuffix))
|
||
}
|
||
|
||
line := strings.Join(parts, " ")
|
||
maxW := width - 4
|
||
if maxW > 0 && len(stripAnsi(line)) > maxW {
|
||
line = truncate(line, maxW)
|
||
}
|
||
|
||
return drawerPreviewStyle.Render(line)
|
||
}
|
||
|
||
func glyphForParsed(glyph string) string {
|
||
switch glyph {
|
||
case "todo":
|
||
return "○"
|
||
case "event":
|
||
return "◇"
|
||
case "reminder":
|
||
return "△"
|
||
default:
|
||
return "—"
|
||
}
|
||
}
|
||
|
||
func drawerLines() int {
|
||
return 3
|
||
}
|