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.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user