Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:34:16 +08:00

290 lines
7.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}