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 }