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() }