detached 3a8691d

Refactor push deploy spinner to reuse Bubble Tea component

commit
3a8691d798f329b1d70aac76d9b700d9f021eb03
author
brice · · 2026-02-08 17:00 UTC
parent
b8f764f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +69 -26
M internal/adapters/in/cli/push.go +69 -26
@@ -7,9 +7,8 @@ import (
77 "os/exec"
88 "regexp"
99 "strings"
10- "time"
1110
12- "github.com/charmbracelet/lipgloss"
11+ tea "github.com/charmbracelet/bubbletea"
1312 "github.com/spf13/cobra"
1413
1514 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
@@ -256,37 +255,81 @@ func deployWithSpinner(ctx context.Context, client *remote.Client, pushDomain st
256255 return client.Deploy(ctx, pushDomain)
257256 }
258257
259- type deployOutcome struct {
260- result *remote.DeployResult
261- err error
262- }
263-
264258 done := make(chan deployOutcome, 1)
265259 go func() {
266260 result, err := client.Deploy(ctx, pushDomain)
267261 done <- deployOutcome{result: result, err: err}
268262 }()
269263
270- frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
271- frameStyle := lipgloss.NewStyle().Foreground(styles.ColorPrimary)
272- message := styles.Theme.Muted.Render(fmt.Sprintf("Deploying %s...", pushDomain))
273-
274- ticker := time.NewTicker(90 * time.Millisecond)
275- defer ticker.Stop()
276-
277- frame := 0
278- for {
279- select {
280- case out := <-done:
281- fmt.Print("\r\033[K")
282- return out.result, out.err
283- case <-ticker.C:
284- fmt.Printf("\r%s %s", frameStyle.Render(frames[frame%len(frames)]), message)
285- frame++
286- case <-ctx.Done():
287- fmt.Print("\r\033[K")
288- return nil, ctx.Err()
264+ model := newDeploySpinnerModel(pushDomain, done)
265+ final, err := tea.NewProgram(model, tea.WithContext(ctx)).Run()
266+ fmt.Print("\r\033[K")
267+ if err != nil {
268+ return nil, err
269+ }
270+
271+ deployModel, ok := final.(deploySpinnerModel)
272+ if !ok {
273+ return nil, fmt.Errorf("spinner exited with unexpected model type %T", final)
274+ }
275+ if !deployModel.finished {
276+ return nil, fmt.Errorf("deploy spinner exited before deploy result was received")
277+ }
278+
279+ return deployModel.outcome.result, deployModel.outcome.err
280+}
281+
282+type deployOutcome struct {
283+ result *remote.DeployResult
284+ err error
285+}
286+
287+type deployDoneMsg deployOutcome
288+
289+type deploySpinnerModel struct {
290+ spinner components.SpinnerModel
291+ done <-chan deployOutcome
292+ outcome deployOutcome
293+ finished bool
294+}
295+
296+func newDeploySpinnerModel(pushDomain string, done <-chan deployOutcome) deploySpinnerModel {
297+ return deploySpinnerModel{
298+ spinner: components.NewSpinner(
299+ components.WithMessage(fmt.Sprintf("Deploying %s...", pushDomain)),
300+ components.WithSpinnerType(components.SpinnerMiniDot),
301+ ),
302+ done: done,
303+ }
304+}
305+
306+func (m deploySpinnerModel) Init() tea.Cmd {
307+ return tea.Batch(m.spinner.Init(), waitForDeployDone(m.done))
308+}
309+
310+func (m deploySpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
311+ switch msg := msg.(type) {
312+ case deployDoneMsg:
313+ m.outcome = deployOutcome(msg)
314+ m.finished = true
315+ return m, tea.Quit
316+ default:
317+ updated, cmd := m.spinner.Update(msg)
318+ spinnerModel, ok := updated.(components.SpinnerModel)
319+ if ok {
320+ m.spinner = spinnerModel
289321 }
322+ return m, cmd
323+ }
324+}
325+
326+func (m deploySpinnerModel) View() string {
327+ return m.spinner.View()
328+}
329+
330+func waitForDeployDone(done <-chan deployOutcome) tea.Cmd {
331+ return func() tea.Msg {
332+ return deployDoneMsg(<-done)
290333 }
291334 }
292335
@@ -7,9 +7,8 @@ import (
7 "os/exec"7 "os/exec"
8 "regexp"8 "regexp"
9 "strings"9 "strings"
10- "time"
11 10
12- "github.com/charmbracelet/lipgloss"11+ tea "github.com/charmbracelet/bubbletea"
13 "github.com/spf13/cobra"12 "github.com/spf13/cobra"
14 13
15 "github.com/bnema/gordon/internal/adapters/in/cli/remote"14 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
@@ -256,37 +255,81 @@ func deployWithSpinner(ctx context.Context, client *remote.Client, pushDomain st
256 return client.Deploy(ctx, pushDomain)255 return client.Deploy(ctx, pushDomain)
257 }256 }
258 257
259- type deployOutcome struct {
260- result *remote.DeployResult
261- err error
262- }
263-
264 done := make(chan deployOutcome, 1)258 done := make(chan deployOutcome, 1)
265 go func() {259 go func() {
266 result, err := client.Deploy(ctx, pushDomain)260 result, err := client.Deploy(ctx, pushDomain)
267 done <- deployOutcome{result: result, err: err}261 done <- deployOutcome{result: result, err: err}
268 }()262 }()
269 263
270- frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}264+ model := newDeploySpinnerModel(pushDomain, done)
271- frameStyle := lipgloss.NewStyle().Foreground(styles.ColorPrimary)265+ final, err := tea.NewProgram(model, tea.WithContext(ctx)).Run()
272- message := styles.Theme.Muted.Render(fmt.Sprintf("Deploying %s...", pushDomain))266+ fmt.Print("\r\033[K")
273-267+ if err != nil {
274- ticker := time.NewTicker(90 * time.Millisecond)268+ return nil, err
275- defer ticker.Stop()269+ }
276-270+
277- frame := 0271+ deployModel, ok := final.(deploySpinnerModel)
278- for {272+ if !ok {
279- select {273+ return nil, fmt.Errorf("spinner exited with unexpected model type %T", final)
280- case out := <-done:274+ }
281- fmt.Print("\r\033[K")275+ if !deployModel.finished {
282- return out.result, out.err276+ return nil, fmt.Errorf("deploy spinner exited before deploy result was received")
283- case <-ticker.C:277+ }
284- fmt.Printf("\r%s %s", frameStyle.Render(frames[frame%len(frames)]), message)278+
285- frame++279+ return deployModel.outcome.result, deployModel.outcome.err
286- case <-ctx.Done():280+}
287- fmt.Print("\r\033[K")281+
288- return nil, ctx.Err()282+type deployOutcome struct {
283+ result *remote.DeployResult
284+ err error
285+}
286+
287+type deployDoneMsg deployOutcome
288+
289+type deploySpinnerModel struct {
290+ spinner components.SpinnerModel
291+ done <-chan deployOutcome
292+ outcome deployOutcome
293+ finished bool
294+}
295+
296+func newDeploySpinnerModel(pushDomain string, done <-chan deployOutcome) deploySpinnerModel {
297+ return deploySpinnerModel{
298+ spinner: components.NewSpinner(
299+ components.WithMessage(fmt.Sprintf("Deploying %s...", pushDomain)),
300+ components.WithSpinnerType(components.SpinnerMiniDot),
301+ ),
302+ done: done,
303+ }
304+}
305+
306+func (m deploySpinnerModel) Init() tea.Cmd {
307+ return tea.Batch(m.spinner.Init(), waitForDeployDone(m.done))
308+}
309+
310+func (m deploySpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
311+ switch msg := msg.(type) {
312+ case deployDoneMsg:
313+ m.outcome = deployOutcome(msg)
314+ m.finished = true
315+ return m, tea.Quit
316+ default:
317+ updated, cmd := m.spinner.Update(msg)
318+ spinnerModel, ok := updated.(components.SpinnerModel)
319+ if ok {
320+ m.spinner = spinnerModel
289 }321 }
322+ return m, cmd
323+ }
324+}
325+
326+func (m deploySpinnerModel) View() string {
327+ return m.spinner.View()
328+}
329+
330+func waitForDeployDone(done <-chan deployOutcome) tea.Cmd {
331+ return func() tea.Msg {
332+ return deployDoneMsg(<-done)
290 }333 }
291 }334 }
292 335

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close