77222ff1b8
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.
117 lines
2.1 KiB
Go
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()
|
|
}
|