feat(tui): add cards view, mode switching, promote picker, and card detail

Stream/cards toggle with 1/2 keys. Cards view with intent filtering
(tab cycles grab/read/fill/all), sort cycling (s key), pinned-first
ordering, and affordance badges. Promote picker (p key) with card type
selection and auto-detection from body content. Detail view renders
card_data per type: checklist steps, template slots, decision fields,
link URLs.

Extracts generateCardData to internal/carddata for reuse across cmd
and tui packages.
This commit is contained in:
2026-05-17 21:14:14 -04:00
parent c2ea63dd16
commit ce335cabd6
12 changed files with 786 additions and 112 deletions
+102
View File
@@ -0,0 +1,102 @@
package carddata
import (
"encoding/json"
"regexp"
"strings"
"github.com/lerko/nib/internal/db"
)
var TemplateSlotRe = regexp.MustCompile(`\$\{(\w+)\}`)
func GenerateCardData(ct db.CardType, body string) *string {
var data string
switch ct {
case db.CardTemplate:
matches := TemplateSlotRe.FindAllStringSubmatch(body, -1)
type slot struct {
Name string `json:"name"`
Default string `json:"default"`
}
var slots []slot
seen := map[string]bool{}
for _, m := range matches {
name := m[1]
if !seen[name] {
slots = append(slots, slot{Name: name, Default: ""})
seen[name] = true
}
}
if slots == nil {
slots = []slot{}
}
b, _ := json.Marshal(map[string]any{"slots": slots})
data = string(b)
case db.CardChecklist:
type step struct {
Text string `json:"text"`
Done bool `json:"done"`
}
var steps []step
for _, line := range strings.Split(body, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "[ ]") || strings.HasPrefix(line, "[x]") {
text := strings.TrimSpace(line[3:])
done := strings.HasPrefix(line, "[x]")
steps = append(steps, step{Text: text, Done: done})
}
}
if steps == nil {
steps = []step{{Text: body, Done: false}}
}
b, _ := json.Marshal(map[string]any{"steps": steps})
data = string(b)
case db.CardDecision:
b, _ := json.Marshal(map[string]any{
"chose": "",
"why": "",
"rejected": []string{},
})
data = string(b)
case db.CardLink:
url := ""
for _, word := range strings.Fields(body) {
if strings.HasPrefix(word, "http://") || strings.HasPrefix(word, "https://") {
url = word
break
}
}
b, _ := json.Marshal(map[string]any{"url": url})
data = string(b)
default:
data = "{}"
}
return &data
}
func DetectCardType(body string) *db.CardType {
if TemplateSlotRe.MatchString(body) {
ct := db.CardTemplate
return &ct
}
if strings.Contains(body, "chose:") || strings.Contains(body, "why:") {
ct := db.CardDecision
return &ct
}
if strings.Contains(body, "[ ]") || strings.Contains(body, "[x]") {
ct := db.CardChecklist
return &ct
}
for _, word := range strings.Fields(body) {
if strings.HasPrefix(word, "http://") || strings.HasPrefix(word, "https://") {
ct := db.CardLink
return &ct
}
}
return nil
}