Skip to content

Commit 9d799bf

Browse files
committed
feat: implement visual setup progress modal overlay with styled progress bar
1 parent 58ea53d commit 9d799bf

1 file changed

Lines changed: 100 additions & 12 deletions

File tree

go/tui/tui.go

Lines changed: 100 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ type updatesLoadedMsg struct {
168168
Updates map[string]UpdateInfo
169169
}
170170

171+
type setupProgressMsg struct {
172+
Step int
173+
Total int
174+
Label string
175+
}
176+
171177
// ─────────────────────────────────────────────
172178
// Prompt states
173179
// ─────────────────────────────────────────────
@@ -183,6 +189,7 @@ const (
183189
PromptSettings
184190
PromptThemeDepConfirm
185191
PromptThemeDepMethod
192+
PromptSetupProgress
186193
)
187194

188195
var Program *tea.Program
@@ -263,6 +270,9 @@ type TuiModel struct {
263270
themeDepResolveChan chan themeDepDecision
264271
activeThemeDep string
265272
startupCheckDone bool
273+
setupStep int
274+
setupTotalSteps int
275+
setupLabel string
266276
}
267277

268278
func NewModel(root string) TuiModel {
@@ -285,6 +295,9 @@ func NewModel(root string) TuiModel {
285295
finalizedAdds: make(map[string]string),
286296
themeDepResolveChan: make(chan themeDepDecision, 1),
287297
startupCheckDone: false,
298+
setupStep: 0,
299+
setupTotalSteps: 0,
300+
setupLabel: "",
288301
}
289302
}
290303

@@ -781,7 +794,11 @@ func (m TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
781794
return m, tickCmd()
782795

783796
case tea.KeyMsg:
784-
if m.activePrompt != PromptNone {
797+
promptToShow := m.activePrompt
798+
if m.taskActive && (promptToShow == PromptNone || promptToShow == PromptUninstallConfirm || promptToShow == PromptInstallMethod) {
799+
promptToShow = PromptSetupProgress
800+
}
801+
if promptToShow != PromptNone {
785802
return m.handlePromptKeys(msg)
786803
}
787804
switch msg.String() {
@@ -990,6 +1007,12 @@ func (m TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9901007
} else {
9911008
m.statusMsg = "All packages are up to date."
9921009
}
1010+
1011+
case setupProgressMsg:
1012+
m.setupStep = msg.Step
1013+
m.setupTotalSteps = msg.Total
1014+
m.setupLabel = msg.Label
1015+
return m, nil
9931016
case promptThemeDepMsg:
9941017
m.activePrompt = PromptThemeDepConfirm
9951018
m.activeThemeDep = msg.DepName
@@ -1065,6 +1088,17 @@ func (m *TuiModel) addLog(line string) {
10651088
// ─────────────────────────────────────────────
10661089

10671090
func (m TuiModel) handlePromptKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
1091+
promptToShow := m.activePrompt
1092+
if m.taskActive && (promptToShow == PromptNone || promptToShow == PromptUninstallConfirm || promptToShow == PromptInstallMethod) {
1093+
promptToShow = PromptSetupProgress
1094+
}
1095+
if promptToShow == PromptSetupProgress {
1096+
if msg.String() == "ctrl+c" {
1097+
return m, tea.Quit
1098+
}
1099+
return m, nil
1100+
}
1101+
10681102
switch msg.String() {
10691103
case "esc", "q":
10701104
if m.activePrompt == PromptThemeDepConfirm || m.activePrompt == PromptThemeDepMethod {
@@ -1969,8 +2003,12 @@ func (m TuiModel) View() string {
19692003
// Catalog panel — always rendered; modal overlaid on top if active
19702004
catalogContent := m.renderCatalogPanel(catW-4, catalogH-2, showLogs)
19712005
catalogPanel = drawPanel(catW-1, catalogH, "Catalog", catalogContent, true)
1972-
if m.activePrompt != PromptNone {
1973-
modal := m.renderModal()
2006+
promptToShow := m.activePrompt
2007+
if m.taskActive && (promptToShow == PromptNone || promptToShow == PromptUninstallConfirm || promptToShow == PromptInstallMethod) {
2008+
promptToShow = PromptSetupProgress
2009+
}
2010+
if promptToShow != PromptNone {
2011+
modal := m.renderModal(promptToShow)
19742012
styledModal := lipgloss.NewStyle().
19752013
Border(lipgloss.RoundedBorder()).
19762014
BorderForeground(colorCyan).
@@ -1997,8 +2035,12 @@ func (m TuiModel) View() string {
19972035
// Catalog full width — always rendered; modal overlaid on top if active
19982036
catalogContent := m.renderCatalogPanel(w-4, catalogH-2, showLogs)
19992037
catalogPanel = drawPanel(w, catalogH, "Catalog", catalogContent, true)
2000-
if m.activePrompt != PromptNone {
2001-
modal := m.renderModal()
2038+
promptToShow := m.activePrompt
2039+
if m.taskActive && (promptToShow == PromptNone || promptToShow == PromptUninstallConfirm || promptToShow == PromptInstallMethod) {
2040+
promptToShow = PromptSetupProgress
2041+
}
2042+
if promptToShow != PromptNone {
2043+
modal := m.renderModal(promptToShow)
20022044
styledModal := lipgloss.NewStyle().
20032045
Border(lipgloss.RoundedBorder()).
20042046
BorderForeground(colorCyan).
@@ -2710,11 +2752,32 @@ func (m TuiModel) getInstallMethods(pkg *bridge.CatalogEntry) []InstallMethod {
27102752
// Modal
27112753
// ─────────────────────────────────────────────
27122754

2713-
func (m TuiModel) renderModal() string {
2755+
func renderProgressBar(step, total, width int) string {
2756+
if total <= 0 {
2757+
return lipgloss.NewStyle().Foreground(colorDim).Render(strings.Repeat("░", width))
2758+
}
2759+
filledWidth := (step * width) / total
2760+
if filledWidth > width {
2761+
filledWidth = width
2762+
}
2763+
if filledWidth < 0 {
2764+
filledWidth = 0
2765+
}
2766+
emptyWidth := width - filledWidth
2767+
filled := lipgloss.NewStyle().Foreground(colorCyan).Render(strings.Repeat("█", filledWidth))
2768+
empty := lipgloss.NewStyle().Foreground(colorDim).Render(strings.Repeat("░", emptyWidth))
2769+
return filled + empty
2770+
}
2771+
2772+
// ─────────────────────────────────────────────
2773+
// Modal
2774+
// ─────────────────────────────────────────────
2775+
2776+
func (m TuiModel) renderModal(prompt PromptType) string {
27142777
pkg := m.promptPkg
27152778
var content strings.Builder
27162779

2717-
if m.activePrompt == PromptInstallMethod {
2780+
if prompt == PromptInstallMethod {
27182781
content.WriteString(boldStyle.Render("Install source for ") + accentStyle.Render(pkg.ShortName) + "\n\n")
27192782
methods := m.getInstallMethods(pkg)
27202783
for i, mth := range methods {
@@ -2736,7 +2799,7 @@ func (m TuiModel) renderModal() string {
27362799
}
27372800
content.WriteString("\n" + subtleStyle.Render("↑↓ select Enter confirm Esc cancel"))
27382801
return modalStyle.Width(72).Render(content.String())
2739-
} else if m.activePrompt == PromptThemeDepMethod {
2802+
} else if prompt == PromptThemeDepMethod {
27402803
content.WriteString(boldStyle.Render("Install source for theme dependency: ") + accentStyle.Render(pkg.ShortName) + "\n\n")
27412804
methods := m.getInstallMethods(pkg)
27422805
for i, mth := range methods {
@@ -2758,7 +2821,7 @@ func (m TuiModel) renderModal() string {
27582821
}
27592822
content.WriteString("\n" + subtleStyle.Render("↑↓ select Enter confirm Esc cancel"))
27602823
return modalStyle.Width(72).Render(content.String())
2761-
} else if m.activePrompt == PromptThemeDepConfirm {
2824+
} else if prompt == PromptThemeDepConfirm {
27622825
content.WriteString(boldStyle.Render("Theme dependency required: ") + accentStyle.Render(pkg.ShortName) + "\n")
27632826
content.WriteString(subtleStyle.Render("The active or queued theme requires this package to function correctly.") + "\n\n")
27642827
content.WriteString(boldStyle.Render("Install ") + accentStyle.Render(pkg.ShortName) + boldStyle.Render("?") + "\n\n")
@@ -2776,7 +2839,7 @@ func (m TuiModel) renderModal() string {
27762839
content.WriteString(" ")
27772840
}
27782841
content.WriteString("\n\n" + subtleStyle.Render("← → select Enter confirm Esc cancel"))
2779-
} else if m.activePrompt == PromptUninstallConfirm {
2842+
} else if prompt == PromptUninstallConfirm {
27802843
content.WriteString(boldStyle.Render("Uninstall ") + errStyle.Render(pkg.ShortName) + boldStyle.Render("?") + "\n\n")
27812844
opts := []string{"Yes, uninstall", "No, keep it"}
27822845
for i, opt := range opts {
@@ -2792,7 +2855,7 @@ func (m TuiModel) renderModal() string {
27922855
content.WriteString(" ")
27932856
}
27942857
content.WriteString("\n\n" + subtleStyle.Render("← → select Enter confirm Esc cancel"))
2795-
} else if m.activePrompt == PromptForceReinstallConfirm {
2858+
} else if prompt == PromptForceReinstallConfirm {
27962859
content.WriteString(boldStyle.Render("Force re-download ") + errStyle.Render(pkg.ShortName) + boldStyle.Render("?") + "\n")
27972860
content.WriteString(warnStyle.Render("⚠ WARNING: This will delete the local directory and lose all modifications!") + "\n\n")
27982861
opts := []string{"Yes, wipe & reinstall", "No, cancel"}
@@ -2809,7 +2872,19 @@ func (m TuiModel) renderModal() string {
28092872
content.WriteString(" ")
28102873
}
28112874
content.WriteString("\n\n" + subtleStyle.Render("← → select Enter confirm Esc cancel"))
2812-
} else if m.activePrompt == PromptManagePackage {
2875+
} else if prompt == PromptSetupProgress {
2876+
spinnerFrames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
2877+
frame := spinnerFrames[m.tickCount%len(spinnerFrames)]
2878+
spin := lipgloss.NewStyle().Foreground(colorCyan).Bold(true).Render(frame)
2879+
2880+
content.WriteString(boldStyle.Render("Installing packages…") + "\n\n")
2881+
content.WriteString(" " + spin + " " + boldStyle.Render(m.setupLabel) + "\n\n")
2882+
2883+
bar := renderProgressBar(m.setupStep, m.setupTotalSteps, 40)
2884+
content.WriteString(" " + bar + "\n")
2885+
content.WriteString(fmt.Sprintf(" Step %d of %d\n", m.setupStep, m.setupTotalSteps))
2886+
return modalStyle.Width(72).Render(content.String())
2887+
} else if prompt == PromptManagePackage {
28132888
var leftCols []string
28142889
leftCols = append(leftCols, boldStyle.Render("Package:")+" "+accentStyle.Render(pkg.ShortName))
28152890
leftCols = append(leftCols, mutedStyle.Render("Type: ")+" "+boldStyle.Render(strings.ToUpper(pkg.Kind)))
@@ -3089,12 +3164,18 @@ func (m *TuiModel) RunSetupTask(adds map[string]string, p *tea.Program) {
30893164
go func() {
30903165
desktopJs := filepath.Join(m.workspaceRoot, "packages", "cli", "bin", "desktop.js")
30913166

3167+
totalSteps := len(adds) + 2
3168+
step := 0
3169+
p.Send(setupProgressMsg{Step: step, Total: totalSteps, Label: "Initializing setup task…"})
3170+
30923171
// 1. Process all additions
30933172
for pkgName, method := range adds {
3173+
step++
30943174
shortName := pkgName
30953175
if idx := strings.LastIndex(pkgName, "/"); idx >= 0 {
30963176
shortName = pkgName[idx+1:]
30973177
}
3178+
p.Send(setupProgressMsg{Step: step, Total: totalSteps, Label: fmt.Sprintf("Installing %s via %s…", shortName, method)})
30983179

30993180
args := []string{desktopJs, "add", shortName}
31003181
if method == "npm" {
@@ -3199,13 +3280,17 @@ func (m *TuiModel) RunSetupTask(adds map[string]string, p *tea.Program) {
31993280
}
32003281

32013282
// 2. Run pnpm install for cleanup/removal sync
3283+
step++
3284+
p.Send(setupProgressMsg{Step: step, Total: totalSteps, Label: "Installing dependencies (pnpm install)…"})
32023285
p.Send(logLineMsg(">>> Running pnpm install (syncing workspace)…"))
32033286
if err := runProcessAndStreamLogsSilent(m.workspaceRoot, "pnpm", []string{"install"}, p); err != nil {
32043287
p.Send(taskFinishedMsg{Success: false, Err: err})
32053288
return
32063289
}
32073290

32083291
// 3. Rebuild stubs
3292+
step++
3293+
p.Send(setupProgressMsg{Step: step, Total: totalSteps, Label: "Rebuilding stubs (prepare:modules)…"})
32093294
p.Send(logLineMsg(">>> Rebuilding stubs…"))
32103295
if err := runProcessAndStreamLogsSilent(m.workspaceRoot, "pnpm", []string{"run", "prepare:modules"}, p); err != nil {
32113296
p.Send(taskFinishedMsg{Success: false, Err: err})
@@ -3220,6 +3305,7 @@ func (m *TuiModel) RunUpdatePackageTask(pkgName string, shortName string, kind s
32203305
go func() {
32213306
p.Send(clearLogsMsg{})
32223307
p.Send(logLineMsg(fmt.Sprintf(">>> Starting update for %s…", shortName)))
3308+
p.Send(setupProgressMsg{Step: 1, Total: 3, Label: fmt.Sprintf("Updating %s…", shortName)})
32233309

32243310
desktopJs := filepath.Join(m.workspaceRoot, "packages", "cli", "bin", "desktop.js")
32253311

@@ -3254,13 +3340,15 @@ func (m *TuiModel) RunUpdatePackageTask(pkgName string, shortName string, kind s
32543340
}
32553341

32563342
// Run pnpm install
3343+
p.Send(setupProgressMsg{Step: 2, Total: 3, Label: "Installing dependencies (pnpm install)…"})
32573344
p.Send(logLineMsg(">>> Running pnpm install (syncing workspace)…"))
32583345
if err := runProcessAndStreamLogsSilent(m.workspaceRoot, "pnpm", []string{"install"}, p); err != nil {
32593346
p.Send(taskFinishedMsg{Success: false, Err: err})
32603347
return
32613348
}
32623349

32633350
// Rebuild stubs
3351+
p.Send(setupProgressMsg{Step: 3, Total: 3, Label: "Rebuilding stubs (prepare:modules)…"})
32643352
p.Send(logLineMsg(">>> Rebuilding stubs…"))
32653353
if err := runProcessAndStreamLogsSilent(m.workspaceRoot, "pnpm", []string{"run", "prepare:modules"}, p); err != nil {
32663354
p.Send(taskFinishedMsg{Success: false, Err: err})

0 commit comments

Comments
 (0)