kunchenguid--no-mistakes
26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
290 行
7.2 KiB
Go
290 行
7.2 KiB
Go
package wizard
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/muesli/termenv"
|
|
)
|
|
|
|
// ANSI-only palette, matching internal/tui/theme.go.
|
|
const (
|
|
ansiRed = "1"
|
|
ansiGreen = "2"
|
|
ansiYellow = "3"
|
|
ansiBlue = "4"
|
|
ansiCyan = "6"
|
|
ansiBrightBlack = "8"
|
|
)
|
|
|
|
func init() {
|
|
lipgloss.SetColorProfile(termenv.ANSI)
|
|
}
|
|
|
|
var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
|
|
|
// View renders the wizard.
|
|
func (m Model) View() string {
|
|
width := m.width
|
|
if width < 60 {
|
|
width = 60
|
|
}
|
|
m.width = width
|
|
|
|
var content strings.Builder
|
|
|
|
for i, s := range m.steps {
|
|
content.WriteString(m.renderStep(s))
|
|
content.WriteString("\n")
|
|
if i < len(m.steps)-1 {
|
|
content.WriteString(dimStyle().Render("│"))
|
|
content.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
box := renderBox("Setup", strings.TrimRight(content.String(), "\n"), width)
|
|
|
|
var out strings.Builder
|
|
out.WriteString(setTerminalTitle(m.terminalTitle()))
|
|
out.WriteString(box)
|
|
out.WriteString("\n")
|
|
|
|
if m.waiting {
|
|
out.WriteString("\n")
|
|
out.WriteString(" " + blueStyle().Render(spinnerFrames[m.spinnerFrame%len(spinnerFrames)]) + " " + dimStyle().Render("waiting for run…"))
|
|
out.WriteString("\n")
|
|
}
|
|
|
|
if bar := m.renderActionBar(); bar != "" {
|
|
out.WriteString("\n")
|
|
out.WriteString(bar)
|
|
out.WriteString("\n")
|
|
}
|
|
|
|
out.WriteString("\n")
|
|
out.WriteString(m.renderFooter())
|
|
out.WriteString("\n")
|
|
|
|
if m.err != nil {
|
|
out.WriteString("\n")
|
|
out.WriteString(redStyle().Render("error: " + m.err.Error()))
|
|
out.WriteString("\n")
|
|
}
|
|
|
|
return out.String()
|
|
}
|
|
|
|
func (m Model) renderStep(s *step) string {
|
|
icon, style := stepIconAndStyle(s, m.spinnerFrame)
|
|
header := style.Render(icon) + " " + stepLabel(s.id)
|
|
|
|
switch s.status {
|
|
case statDone:
|
|
return header + " " + dimStyle().Render(s.result)
|
|
|
|
case statSkipped:
|
|
if s.skipReason != "" {
|
|
return header + " " + dimStyle().Render(s.skipReason)
|
|
}
|
|
return header
|
|
|
|
case statFailed:
|
|
lines := []string{header + " " + redStyle().Render(s.errMsg)}
|
|
return strings.Join(lines, "\n")
|
|
|
|
case statInput:
|
|
return header + " " + m.inlineInputView(header)
|
|
|
|
case statAgent:
|
|
return header + " " + dimStyle().Render(agentLabel(s.id))
|
|
|
|
case statRunning:
|
|
return header + " " + dimStyle().Render(runningLabel(s.id, s.result))
|
|
|
|
case statConfirm:
|
|
return header + " " + dimStyle().Render(confirmLabel(m.cfg.GateRemote, m.targetBranch))
|
|
}
|
|
return header
|
|
}
|
|
|
|
func (m Model) inlineInputView(header string) string {
|
|
input := m.input
|
|
contentWidth := m.width - 4
|
|
availableWidth := contentWidth - lipgloss.Width(header) - 2
|
|
promptWidth := lipgloss.Width(input.Prompt)
|
|
fieldWidth := availableWidth - promptWidth - 1
|
|
if fieldWidth < 1 {
|
|
fieldWidth = 1
|
|
}
|
|
input.Width = fieldWidth
|
|
input.SetCursor(input.Position())
|
|
return input.View()
|
|
}
|
|
|
|
func (m Model) renderActionBar() string {
|
|
s := m.activeStep()
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
switch s.status {
|
|
case statInput:
|
|
return " " + boldStyle().Render("⏎") + " submit"
|
|
case statConfirm:
|
|
return " " + boldStyle().Render("y") + " push " + boldStyle().Render("n") + " skip"
|
|
case statFailed:
|
|
return " " + boldStyle().Render("r") + " retry"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m Model) renderFooter() string {
|
|
if m.confirmQuit {
|
|
msg := " " + boldStyle().Render("q") + " again to abort " + dimStyle().Render(m.sideEffectsDescription()+" will stay")
|
|
return redStyle().Render(msg)
|
|
}
|
|
if m.hasSideEffects() {
|
|
return " " + boldStyle().Render("q") + " abort " + dimStyle().Render(m.sideEffectsDescription()+" stays")
|
|
}
|
|
return " " + boldStyle().Render("q") + " quit"
|
|
}
|
|
|
|
func (m Model) sideEffectsDescription() string {
|
|
parts := []string{}
|
|
if m.branchCreated {
|
|
parts = append(parts, "branch "+m.targetBranch)
|
|
}
|
|
if m.commitMade {
|
|
parts = append(parts, "commit")
|
|
}
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
return strings.Join(parts, " + ")
|
|
}
|
|
|
|
func stepLabel(id stepID) string {
|
|
switch id {
|
|
case stepBranch:
|
|
return "Branch"
|
|
case stepCommit:
|
|
return "Commit"
|
|
case stepPush:
|
|
return "Push"
|
|
}
|
|
return "?"
|
|
}
|
|
|
|
func inputLabel(id stepID) string {
|
|
switch id {
|
|
case stepBranch:
|
|
return "branch name (blank to let the agent suggest):"
|
|
case stepCommit:
|
|
return "commit message (blank to let the agent suggest):"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func agentLabel(id stepID) string {
|
|
switch id {
|
|
case stepBranch:
|
|
return "asking agent for a branch name…"
|
|
case stepCommit:
|
|
return "asking agent for a commit message…"
|
|
}
|
|
return "asking agent…"
|
|
}
|
|
|
|
func runningLabel(id stepID, value string) string {
|
|
switch id {
|
|
case stepBranch:
|
|
return "creating branch " + value + "…"
|
|
case stepCommit:
|
|
return "committing \"" + value + "\"…"
|
|
case stepPush:
|
|
return "pushing to gate…"
|
|
}
|
|
return "working…"
|
|
}
|
|
|
|
func confirmLabel(remote, branch string) string {
|
|
if remote == "" {
|
|
remote = "no-mistakes"
|
|
}
|
|
return fmt.Sprintf("push %s to %s gate?", branch, remote)
|
|
}
|
|
|
|
func stepIconAndStyle(s *step, spinnerFrame int) (string, lipgloss.Style) {
|
|
switch s.status {
|
|
case statDone:
|
|
return "✓", greenStyle()
|
|
case statSkipped:
|
|
return "–", dimStyle()
|
|
case statFailed:
|
|
return "✗", redStyle()
|
|
case statAgent, statRunning:
|
|
if len(spinnerFrames) == 0 {
|
|
return "◉", blueStyle()
|
|
}
|
|
return spinnerFrames[spinnerFrame%len(spinnerFrames)], blueStyle()
|
|
case statInput:
|
|
return "⏸", yellowStyle()
|
|
case statConfirm:
|
|
return "⏸", yellowStyle()
|
|
}
|
|
return "○", dimStyle()
|
|
}
|
|
|
|
// Style helpers — kept as functions so tests can swap the color profile.
|
|
|
|
func dimStyle() lipgloss.Style {
|
|
return lipgloss.NewStyle().Foreground(lipgloss.Color(ansiBrightBlack))
|
|
}
|
|
func greenStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(lipgloss.Color(ansiGreen)) }
|
|
func redStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(lipgloss.Color(ansiRed)) }
|
|
func yellowStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(lipgloss.Color(ansiYellow)) }
|
|
func blueStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(lipgloss.Color(ansiBlue)) }
|
|
func cyanStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(lipgloss.Color(ansiCyan)) }
|
|
func boldStyle() lipgloss.Style { return lipgloss.NewStyle().Bold(true) }
|
|
|
|
// renderBox mirrors internal/tui/box.go: a rounded-border box with a cyan
|
|
// bold title embedded in the top border.
|
|
func renderBox(title, content string, width int) string {
|
|
if width < 6 {
|
|
width = 6
|
|
}
|
|
titleStyled := cyanStyle().Bold(true).Render(title)
|
|
borderColor := dimStyle()
|
|
|
|
titleWidth := lipgloss.Width(titleStyled)
|
|
fillWidth := width - 5 - titleWidth
|
|
if fillWidth < 1 {
|
|
fillWidth = 1
|
|
}
|
|
topBorder := borderColor.Render("╭─ ") + titleStyled + " " + borderColor.Render(strings.Repeat("─", fillWidth)+"╮")
|
|
|
|
contentWidth := width - 4
|
|
if contentWidth < 1 {
|
|
contentWidth = 1
|
|
}
|
|
|
|
var lines []string
|
|
for _, cl := range strings.Split(content, "\n") {
|
|
visWidth := lipgloss.Width(cl)
|
|
pad := contentWidth - visWidth
|
|
if pad < 0 {
|
|
pad = 0
|
|
}
|
|
line := borderColor.Render("│") + " " + cl + strings.Repeat(" ", pad) + " " + borderColor.Render("│")
|
|
lines = append(lines, line)
|
|
}
|
|
|
|
fill := width - 2
|
|
if fill < 1 {
|
|
fill = 1
|
|
}
|
|
bottom := borderColor.Render("╰" + strings.Repeat("─", fill) + "╯")
|
|
|
|
return topBorder + "\n" + strings.Join(lines, "\n") + "\n" + bottom
|
|
}
|