Files
nib-v1/internal/tui/run.go
T
lerko 77222ff1b8 feat(tui): add interactive run mode for checklists and fill mode for templates
Run mode (r key on checklist cards): cursor navigates steps, space
toggles done/undone, r resets all, esc saves changes to DB and exits.
Persists step state — improvement over web which discards on exit.

Fill mode (f key on template cards): tab/shift-tab navigates slots,
type to fill values, enter resolves template and copies to clipboard
with use count increment. Esc cancels without copying.

Both modes are sub-states of detail view, keeping architecture simple.
2026-05-17 21:53:55 -04:00

117 lines
2.1 KiB
Go

package tui
import (
"encoding/json"
"fmt"
"strings"
)
type runStep struct {
Text string `json:"text"`
Done bool `json:"done"`
}
type runModel struct {
steps []runStep
cursor int
entityID string
dirty bool
}
func newRunModel(entityID string, cardData *string) runModel {
m := runModel{entityID: entityID}
m.steps = parseChecklist(cardData)
return m
}
func parseChecklist(cardData *string) []runStep {
if cardData == nil {
return nil
}
var data struct {
Steps []runStep `json:"steps"`
}
if err := json.Unmarshal([]byte(*cardData), &data); err != nil {
return nil
}
return data.Steps
}
func (r runModel) stepsJSON() string {
b, _ := json.Marshal(map[string]any{"steps": r.steps})
return string(b)
}
func (r runModel) doneCount() int {
n := 0
for _, s := range r.steps {
if s.Done {
n++
}
}
return n
}
func (r runModel) update(key string) runModel {
switch key {
case "up", "k":
if r.cursor > 0 {
r.cursor--
}
case "down", "j":
if r.cursor < len(r.steps)-1 {
r.cursor++
}
case " ":
if r.cursor < len(r.steps) {
r.steps[r.cursor].Done = !r.steps[r.cursor].Done
r.dirty = true
}
case "r":
for i := range r.steps {
r.steps[i].Done = false
}
r.dirty = true
}
return r
}
func (r runModel) view(width int) string {
if len(r.steps) == 0 {
return statusStyle.Render("no steps")
}
var b strings.Builder
done := r.doneCount()
total := len(r.steps)
pct := 0
if total > 0 {
pct = done * 100 / total
}
header := fmt.Sprintf("▶ running %d/%d done (%d%%)", done, total, pct)
b.WriteString(detailHeaderStyle.Render(header))
b.WriteString("\n\n")
for i, step := range r.steps {
var line string
if step.Done {
line = checkDoneStyle.Render("[✓] " + step.Text)
} else {
line = checkPendingStyle.Render("[ ] " + step.Text)
}
if i == r.cursor {
b.WriteString(selectedItemStyle.Render(" " + line))
} else {
b.WriteString(listItemStyle.Render(line))
}
b.WriteString("\n")
}
b.WriteString("\n")
b.WriteString(helpStyle.Render("space:toggle r:reset esc:save+exit"))
return b.String()
}