c26e2d2022
Status messages now use a sequence counter so rapid actions don't cause premature clearing. Detail pane shows scroll position and supports pgup/pgdown/g/G. Capture drawer border includes inline label. Cards view groups by intent (pinned/grab/read/fill) with gutter labels matching the stream view's date grouping pattern.
189 lines
3.4 KiB
Go
189 lines
3.4 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
|
||
label := "capture"
|
||
prefix := "── "
|
||
suffix := " "
|
||
dashCount := width - len(prefix) - len(label) - len(suffix)
|
||
if dashCount < 0 {
|
||
dashCount = 0
|
||
}
|
||
b.WriteString(drawerBorderStyle.Render(prefix) +
|
||
hintDescStyle.Render(label) +
|
||
drawerBorderStyle.Render(suffix+strings.Repeat("─", dashCount)))
|
||
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
|
||
}
|