From 9700db59d28a167a45d60b690b5fef673b819b59 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 24 Jun 2024 14:58:43 -0300 Subject: [PATCH 01/23] feat(spinner): make action return an error --- spinner/examples/loading/main.go | 10 +++++++--- spinner/spinner.go | 12 ++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/spinner/examples/loading/main.go b/spinner/examples/loading/main.go index 34b7cb95..24230ade 100644 --- a/spinner/examples/loading/main.go +++ b/spinner/examples/loading/main.go @@ -8,9 +8,13 @@ import ( ) func main() { - action := func() { - time.Sleep(2 * time.Second) + action := func() error { + time.Sleep(1 * time.Second) + return nil + } + if err := spinner.New().Title("Preparing your burger...").Action(action).Run(); err != nil { + fmt.Println("Failed:", err) + return } - _ = spinner.New().Title("Preparing your burger...").Action(action).Run() fmt.Println("Order up!") } diff --git a/spinner/spinner.go b/spinner/spinner.go index 77bbf54f..a6493283 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -23,7 +23,7 @@ import ( // ⣾ Loading... type Spinner struct { spinner spinner.Model - action func() + action func() error ctx context.Context accessible bool output *termenv.Output @@ -61,7 +61,7 @@ func (s *Spinner) Title(title string) *Spinner { } // Action sets the action of the spinner. -func (s *Spinner) Action(action func()) *Spinner { +func (s *Spinner) Action(action func() error) *Spinner { s.action = action return s } @@ -98,7 +98,7 @@ func New() *Spinner { s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#F780E2")) return &Spinner{ - action: func() { time.Sleep(time.Second) }, + action: func() error { time.Sleep(time.Second); return nil }, spinner: s, title: "Loading...", titleStyle: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}), @@ -153,10 +153,11 @@ func (s *Spinner) Run() error { return s.ctx.Err() } + var actionErr error p := tea.NewProgram(s, tea.WithContext(s.ctx), tea.WithOutput(os.Stderr)) if s.ctx == nil { go func() { - s.action() + actionErr = s.action() p.Quit() }() } @@ -165,6 +166,9 @@ func (s *Spinner) Run() error { if errors.Is(err, tea.ErrProgramKilled) { return nil } else { + if actionErr != nil { + return actionErr + } return err } } From 3dd5b753ce94778c47bf7428d6b910e90486e244 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 25 Jun 2024 13:24:25 -0300 Subject: [PATCH 02/23] fix: improvements --- spinner/examples/context-and-action/main.go | 29 ++++++++ spinner/spinner.go | 77 +++++++++++---------- 2 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 spinner/examples/context-and-action/main.go diff --git a/spinner/examples/context-and-action/main.go b/spinner/examples/context-and-action/main.go new file mode 100644 index 00000000..17d4e056 --- /dev/null +++ b/spinner/examples/context-and-action/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "fmt" + "log" + "math/rand" + "time" + + "github.com/charmbracelet/huh/spinner" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + err := spinner.New(). + Context(ctx). + Action(func() error { + time.Sleep(time.Minute) + return nil + }). + Accessible(rand.Int()%2 == 0). + Run() + if err != nil { + log.Fatalln(err) + } + fmt.Println("Done!") +} diff --git a/spinner/spinner.go b/spinner/spinner.go index a6493283..7ae340bc 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -29,6 +29,8 @@ type Spinner struct { output *termenv.Output title string titleStyle lipgloss.Style + + err error } type Type spinner.Spinner @@ -98,24 +100,29 @@ func New() *Spinner { s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#F780E2")) return &Spinner{ - action: func() error { time.Sleep(time.Second); return nil }, spinner: s, title: "Loading...", titleStyle: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}), output: termenv.NewOutput(os.Stdout), - ctx: nil, } } // Init initializes the spinner. func (s *Spinner) Init() tea.Cmd { - return s.spinner.Tick + return tea.Batch(s.spinner.Tick, func() tea.Msg { + if s.action != nil { + return doneMsg{err: s.action()} + } + return nil + }) } // Update updates the spinner. func (s *Spinner) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case spinner.TickMsg: + case doneMsg: + s.err = msg.err + return s, tea.Quit case tea.KeyMsg: switch msg.String() { case "ctrl+c": @@ -139,38 +146,34 @@ func (s *Spinner) View() string { // Run runs the spinner. func (s *Spinner) Run() error { - if s.accessible { - return s.runAccessible() - } - hasCtx := s.ctx != nil - hasCtxErr := hasCtx && s.ctx.Err() != nil - if hasCtxErr { + if hasCtx && s.ctx.Err() != nil { if errors.Is(s.ctx.Err(), context.Canceled) { return nil } return s.ctx.Err() } - var actionErr error - p := tea.NewProgram(s, tea.WithContext(s.ctx), tea.WithOutput(os.Stderr)) - if s.ctx == nil { - go func() { - actionErr = s.action() - p.Quit() - }() + // sets a dummy action if the spinner does not have a context nor an action. + if !hasCtx && s.action == nil { + s.action = func() error { + time.Sleep(time.Second) + return nil + } } - _, err := p.Run() - if errors.Is(err, tea.ErrProgramKilled) { - return nil - } else { - if actionErr != nil { - return actionErr - } - return err + if s.accessible { + return s.runAccessible() + } + + p := tea.NewProgram(s, tea.WithContext(s.ctx), tea.WithOutput(os.Stderr)) + m, err := p.Run() + mm := m.(*Spinner) + if mm.err != nil { + return mm.err } + return err } // runAccessible runs the spinner in an accessible mode (statically). @@ -181,18 +184,18 @@ func (s *Spinner) runAccessible() error { fmt.Println(title + frame) if s.ctx == nil { - s.action() + err := s.action() s.output.ShowCursor() s.output.CursorBack(len(frame) + len(title)) - return nil + return err } - actionDone := make(chan struct{}) - - go func() { - s.action() - actionDone <- struct{}{} - }() + actionDone := make(chan error) + if s.action != nil { + go func() { + actionDone <- s.action() + }() + } for { select { @@ -200,10 +203,14 @@ func (s *Spinner) runAccessible() error { s.output.ShowCursor() s.output.CursorBack(len(frame) + len(title)) return s.ctx.Err() - case <-actionDone: + case err := <-actionDone: s.output.ShowCursor() s.output.CursorBack(len(frame) + len(title)) - return nil + return err } } } + +type doneMsg struct { + err error +} From 27305976a1f3c0945e47674ab781a7f97116b2ad Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 25 Jun 2024 13:30:42 -0300 Subject: [PATCH 03/23] fix: remove default action --- spinner/spinner.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spinner/spinner.go b/spinner/spinner.go index 7ae340bc..e12d32c8 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "strings" - "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" @@ -157,10 +156,8 @@ func (s *Spinner) Run() error { // sets a dummy action if the spinner does not have a context nor an action. if !hasCtx && s.action == nil { - s.action = func() error { - time.Sleep(time.Second) - return nil - } + // there's nothing to do! + return nil } if s.accessible { From 529eadf56421870bfd3c1ce723cb5bb1380a1f1b Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 25 Jun 2024 13:33:45 -0300 Subject: [PATCH 04/23] chore: ref --- spinner/spinner.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spinner/spinner.go b/spinner/spinner.go index e12d32c8..cbe92f0f 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -164,8 +164,7 @@ func (s *Spinner) Run() error { return s.runAccessible() } - p := tea.NewProgram(s, tea.WithContext(s.ctx), tea.WithOutput(os.Stderr)) - m, err := p.Run() + m, err := tea.NewProgram(s, tea.WithContext(s.ctx), tea.WithOutput(os.Stderr)).Run() mm := m.(*Spinner) if mm.err != nil { return mm.err From 3e5350ee35b78062fcb99659e70a1a55fa78dcbd Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 25 Jun 2024 13:48:31 -0300 Subject: [PATCH 05/23] docs: fix example Signed-off-by: Carlos Alexandro Becker --- examples/burger/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/burger/main.go b/examples/burger/main.go index e8b88c57..d282bb74 100644 --- a/examples/burger/main.go +++ b/examples/burger/main.go @@ -51,7 +51,7 @@ type Burger struct { func main() { var burger Burger - var order = Order{Burger: burger} + order := Order{Burger: burger} // Should we run in accessible mode? accessible, _ := strconv.ParseBool(os.Getenv("ACCESSIBLE")) @@ -152,14 +152,14 @@ func main() { ).WithAccessible(accessible) err := form.Run() - if err != nil { fmt.Println("Uh oh:", err) os.Exit(1) } - prepareBurger := func() { + prepareBurger := func() error { time.Sleep(2 * time.Second) + return nil } _ = spinner.New().Title("Preparing your burger...").Accessible(accessible).Action(prepareBurger).Run() From 29633a056ebeacbe3003638b4bb884d64bf9be37 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 24 Jul 2024 16:17:17 -0400 Subject: [PATCH 06/23] context Signed-off-by: Carlos Alexandro Becker --- go.mod | 1 - go.sum | 15 ---------- .../context-and-action-and-error/main.go | 28 +++++++++++++++++++ spinner/examples/context-and-action/main.go | 3 +- spinner/examples/loading/main.go | 3 +- spinner/spinner.go | 22 +++++++++++---- 6 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 spinner/examples/context-and-action-and-error/main.go diff --git a/go.mod b/go.mod index 10cca395..bac19726 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,6 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect diff --git a/go.sum b/go.sum index c60d29f4..9c0f8c76 100644 --- a/go.sum +++ b/go.sum @@ -6,26 +6,16 @@ github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40= -github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0= github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= -github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= -github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= -github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= -github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/strings v0.0.0-20240617190524-788ec55faed1 h1:VZIQzjwFE0EamzG2v8HfemeisB8X02Tl0BZBnJ0PeU8= -github.com/charmbracelet/x/exp/strings v0.0.0-20240617190524-788ec55faed1/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE= github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU= -github.com/charmbracelet/x/input v0.1.2 h1:QJAZr33eOhDowkkEQ24rsJy4Llxlm+fRDf/cQrmqJa0= -github.com/charmbracelet/x/input v0.1.2/go.mod h1:LGBim0maUY4Pitjn/4fHnuXb4KirU3DODsyuHuXdOyA= github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg= github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU= github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= @@ -42,8 +32,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= @@ -60,13 +48,10 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= diff --git a/spinner/examples/context-and-action-and-error/main.go b/spinner/examples/context-and-action-and-error/main.go new file mode 100644 index 00000000..0b6fd2c2 --- /dev/null +++ b/spinner/examples/context-and-action-and-error/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/charmbracelet/huh/spinner" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + err := spinner.New(). + Context(ctx). + ActionErr(func(context.Context) error { + time.Sleep(time.Minute) + return nil + }). + Accessible(false). + Run() + if err != nil { + log.Fatalln(err) + } + fmt.Println("Done!") +} diff --git a/spinner/examples/context-and-action/main.go b/spinner/examples/context-and-action/main.go index 17d4e056..20ade5fa 100644 --- a/spinner/examples/context-and-action/main.go +++ b/spinner/examples/context-and-action/main.go @@ -16,9 +16,8 @@ func main() { err := spinner.New(). Context(ctx). - Action(func() error { + Action(func() { time.Sleep(time.Minute) - return nil }). Accessible(rand.Int()%2 == 0). Run() diff --git a/spinner/examples/loading/main.go b/spinner/examples/loading/main.go index 24230ade..02e9af92 100644 --- a/spinner/examples/loading/main.go +++ b/spinner/examples/loading/main.go @@ -8,9 +8,8 @@ import ( ) func main() { - action := func() error { + action := func() { time.Sleep(1 * time.Second) - return nil } if err := spinner.New().Title("Preparing your burger...").Action(action).Run(); err != nil { fmt.Println("Failed:", err) diff --git a/spinner/spinner.go b/spinner/spinner.go index cbe92f0f..5a1e7d7e 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -22,7 +22,7 @@ import ( // ⣾ Loading... type Spinner struct { spinner spinner.Model - action func() error + action func(ctx context.Context) error ctx context.Context accessible bool output *termenv.Output @@ -62,7 +62,16 @@ func (s *Spinner) Title(title string) *Spinner { } // Action sets the action of the spinner. -func (s *Spinner) Action(action func() error) *Spinner { +func (s *Spinner) Action(action func()) *Spinner { + s.action = func(ctx context.Context) error { + action() + return nil + } + return s +} + +// ActionErr sets the action of the spinner. +func (s *Spinner) ActionErr(action func(ctx context.Context) error) *Spinner { s.action = action return s } @@ -110,7 +119,7 @@ func New() *Spinner { func (s *Spinner) Init() tea.Cmd { return tea.Batch(s.spinner.Tick, func() tea.Msg { if s.action != nil { - return doneMsg{err: s.action()} + return doneMsg{err: s.action(s.ctx)} } return nil }) @@ -180,7 +189,7 @@ func (s *Spinner) runAccessible() error { fmt.Println(title + frame) if s.ctx == nil { - err := s.action() + err := s.action(context.Background()) s.output.ShowCursor() s.output.CursorBack(len(frame) + len(title)) return err @@ -189,7 +198,7 @@ func (s *Spinner) runAccessible() error { actionDone := make(chan error) if s.action != nil { go func() { - actionDone <- s.action() + actionDone <- s.action(s.ctx) }() } @@ -198,6 +207,9 @@ func (s *Spinner) runAccessible() error { case <-s.ctx.Done(): s.output.ShowCursor() s.output.CursorBack(len(frame) + len(title)) + if errors.Is(s.ctx.Err(), context.Canceled) { + return nil + } return s.ctx.Err() case err := <-actionDone: s.output.ShowCursor() From 7dd7b8d64aada6add40ef6d3ec515a6e563f800d Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 24 Jul 2024 16:22:19 -0400 Subject: [PATCH 07/23] docs: fix example Signed-off-by: Carlos Alexandro Becker --- examples/burger/main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/burger/main.go b/examples/burger/main.go index d282bb74..2d3b2dd2 100644 --- a/examples/burger/main.go +++ b/examples/burger/main.go @@ -157,9 +157,8 @@ func main() { os.Exit(1) } - prepareBurger := func() error { + prepareBurger := func() { time.Sleep(2 * time.Second) - return nil } _ = spinner.New().Title("Preparing your burger...").Accessible(accessible).Action(prepareBurger).Run() From d8d0704fb78f6957a2e86a97f2d8649785d863cd Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 24 Jul 2024 16:31:26 -0400 Subject: [PATCH 08/23] fix: the spinner can actually always have a context Signed-off-by: Carlos Alexandro Becker --- spinner/spinner.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/spinner/spinner.go b/spinner/spinner.go index 5a1e7d7e..9482612c 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -109,6 +109,7 @@ func New() *Spinner { return &Spinner{ spinner: s, + ctx: context.Background(), title: "Loading...", titleStyle: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}), output: termenv.NewOutput(os.Stdout), @@ -154,21 +155,13 @@ func (s *Spinner) View() string { // Run runs the spinner. func (s *Spinner) Run() error { - hasCtx := s.ctx != nil - - if hasCtx && s.ctx.Err() != nil { + if s.ctx.Err() != nil { if errors.Is(s.ctx.Err(), context.Canceled) { return nil } return s.ctx.Err() } - // sets a dummy action if the spinner does not have a context nor an action. - if !hasCtx && s.action == nil { - // there's nothing to do! - return nil - } - if s.accessible { return s.runAccessible() } @@ -188,13 +181,6 @@ func (s *Spinner) runAccessible() error { title := s.titleStyle.Render(strings.TrimSuffix(s.title, "...")) fmt.Println(title + frame) - if s.ctx == nil { - err := s.action(context.Background()) - s.output.ShowCursor() - s.output.CursorBack(len(frame) + len(title)) - return err - } - actionDone := make(chan error) if s.action != nil { go func() { From 94bdd03adea0c506d13b0cdf1b508c7d9aaead5b Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Thu, 25 Jul 2024 16:47:57 -0400 Subject: [PATCH 09/23] feat: allow a writer too --- spinner/examples/printing/main.go | 30 ++++++++++++++++++++++++++++++ spinner/spinner.go | 18 +++++++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 spinner/examples/printing/main.go diff --git a/spinner/examples/printing/main.go b/spinner/examples/printing/main.go new file mode 100644 index 00000000..a040ea65 --- /dev/null +++ b/spinner/examples/printing/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/charmbracelet/huh/spinner" +) + +func main() { + action := func(_ context.Context, w io.Writer) error { + fmt.Fprintln(w, "Added bottom bun") + time.Sleep(time.Second) + fmt.Fprintln(w, "Added patty") + time.Sleep(time.Second) + fmt.Fprintln(w, "Added condiments") + time.Sleep(time.Second) + fmt.Fprintln(w, "Added top bun") + time.Sleep(time.Second) + return nil + } + _ = spinner.New(). + Title("Preparing your burger"). + ActionErr(action). + // Accessible(true). + Run() + fmt.Println("Order up!") +} diff --git a/spinner/spinner.go b/spinner/spinner.go index 9482612c..27911906 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -1,9 +1,11 @@ package spinner import ( + "bytes" "context" "errors" "fmt" + "io" "os" "strings" @@ -22,7 +24,7 @@ import ( // ⣾ Loading... type Spinner struct { spinner spinner.Model - action func(ctx context.Context) error + action func(ctx context.Context, w io.Writer) error ctx context.Context accessible bool output *termenv.Output @@ -30,6 +32,7 @@ type Spinner struct { titleStyle lipgloss.Style err error + buf bytes.Buffer } type Type spinner.Spinner @@ -63,7 +66,7 @@ func (s *Spinner) Title(title string) *Spinner { // Action sets the action of the spinner. func (s *Spinner) Action(action func()) *Spinner { - s.action = func(ctx context.Context) error { + s.action = func(context.Context, io.Writer) error { action() return nil } @@ -71,7 +74,7 @@ func (s *Spinner) Action(action func()) *Spinner { } // ActionErr sets the action of the spinner. -func (s *Spinner) ActionErr(action func(ctx context.Context) error) *Spinner { +func (s *Spinner) ActionErr(action func(ctx context.Context, w io.Writer) error) *Spinner { s.action = action return s } @@ -113,6 +116,7 @@ func New() *Spinner { title: "Loading...", titleStyle: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}), output: termenv.NewOutput(os.Stdout), + buf: bytes.Buffer{}, } } @@ -120,7 +124,7 @@ func New() *Spinner { func (s *Spinner) Init() tea.Cmd { return tea.Batch(s.spinner.Tick, func() tea.Msg { if s.action != nil { - return doneMsg{err: s.action(s.ctx)} + return doneMsg{err: s.action(s.ctx, &s.buf)} } return nil }) @@ -148,9 +152,9 @@ func (s *Spinner) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (s *Spinner) View() string { var title string if s.title != "" { - title = s.titleStyle.Render(s.title) + " " + title = s.titleStyle.Render(s.title) } - return s.spinner.View() + title + return s.buf.String() + s.spinner.View() + title } // Run runs the spinner. @@ -184,7 +188,7 @@ func (s *Spinner) runAccessible() error { actionDone := make(chan error) if s.action != nil { go func() { - actionDone <- s.action(s.ctx) + actionDone <- s.action(s.ctx, os.Stdout) }() } From c3f64dbfd1e74d89ae23ee657279e68c4dd3fca1 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Thu, 25 Jul 2024 16:50:45 -0400 Subject: [PATCH 10/23] fix: example --- spinner/examples/context-and-action-and-error/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spinner/examples/context-and-action-and-error/main.go b/spinner/examples/context-and-action-and-error/main.go index 0b6fd2c2..58932825 100644 --- a/spinner/examples/context-and-action-and-error/main.go +++ b/spinner/examples/context-and-action-and-error/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "io" "log" "time" @@ -15,7 +16,7 @@ func main() { err := spinner.New(). Context(ctx). - ActionErr(func(context.Context) error { + ActionErr(func(context.Context, io.Writer) error { time.Sleep(time.Minute) return nil }). From af998cb9b37aa6f63006a5112bbd93d48a466c9b Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 10:09:31 -0300 Subject: [PATCH 11/23] fix: spinner --- spinner/spinner.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/spinner/spinner.go b/spinner/spinner.go index 27911906..394cbd41 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -1,11 +1,9 @@ package spinner import ( - "bytes" "context" "errors" "fmt" - "io" "os" "strings" @@ -24,15 +22,13 @@ import ( // ⣾ Loading... type Spinner struct { spinner spinner.Model - action func(ctx context.Context, w io.Writer) error + action func(ctx context.Context) error ctx context.Context accessible bool output *termenv.Output title string titleStyle lipgloss.Style - - err error - buf bytes.Buffer + err error } type Type spinner.Spinner @@ -66,7 +62,7 @@ func (s *Spinner) Title(title string) *Spinner { // Action sets the action of the spinner. func (s *Spinner) Action(action func()) *Spinner { - s.action = func(context.Context, io.Writer) error { + s.action = func(context.Context) error { action() return nil } @@ -74,7 +70,10 @@ func (s *Spinner) Action(action func()) *Spinner { } // ActionErr sets the action of the spinner. -func (s *Spinner) ActionErr(action func(ctx context.Context, w io.Writer) error) *Spinner { +// +// This is just like [Action], but allows the action to use a `context.Context` +// and to return an error. +func (s *Spinner) ActionErr(action func(context.Context) error) *Spinner { s.action = action return s } @@ -116,7 +115,6 @@ func New() *Spinner { title: "Loading...", titleStyle: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}), output: termenv.NewOutput(os.Stdout), - buf: bytes.Buffer{}, } } @@ -124,7 +122,7 @@ func New() *Spinner { func (s *Spinner) Init() tea.Cmd { return tea.Batch(s.spinner.Tick, func() tea.Msg { if s.action != nil { - return doneMsg{err: s.action(s.ctx, &s.buf)} + return doneMsg{err: s.action(s.ctx)} } return nil }) @@ -154,7 +152,7 @@ func (s *Spinner) View() string { if s.title != "" { title = s.titleStyle.Render(s.title) } - return s.buf.String() + s.spinner.View() + title + return s.spinner.View() + title } // Run runs the spinner. @@ -188,7 +186,7 @@ func (s *Spinner) runAccessible() error { actionDone := make(chan error) if s.action != nil { go func() { - actionDone <- s.action(s.ctx, os.Stdout) + actionDone <- s.action(s.ctx) }() } From d419fd89821b699d7311fcc5d8a0e3d331fddca9 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 10:09:59 -0300 Subject: [PATCH 12/23] chore: update examples --- .../context-and-action-and-error/main.go | 3 +- spinner/examples/printing/main.go | 30 ------------------- 2 files changed, 1 insertion(+), 32 deletions(-) delete mode 100644 spinner/examples/printing/main.go diff --git a/spinner/examples/context-and-action-and-error/main.go b/spinner/examples/context-and-action-and-error/main.go index 58932825..0b6fd2c2 100644 --- a/spinner/examples/context-and-action-and-error/main.go +++ b/spinner/examples/context-and-action-and-error/main.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "io" "log" "time" @@ -16,7 +15,7 @@ func main() { err := spinner.New(). Context(ctx). - ActionErr(func(context.Context, io.Writer) error { + ActionErr(func(context.Context) error { time.Sleep(time.Minute) return nil }). diff --git a/spinner/examples/printing/main.go b/spinner/examples/printing/main.go deleted file mode 100644 index a040ea65..00000000 --- a/spinner/examples/printing/main.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "time" - - "github.com/charmbracelet/huh/spinner" -) - -func main() { - action := func(_ context.Context, w io.Writer) error { - fmt.Fprintln(w, "Added bottom bun") - time.Sleep(time.Second) - fmt.Fprintln(w, "Added patty") - time.Sleep(time.Second) - fmt.Fprintln(w, "Added condiments") - time.Sleep(time.Second) - fmt.Fprintln(w, "Added top bun") - time.Sleep(time.Second) - return nil - } - _ = spinner.New(). - Title("Preparing your burger"). - ActionErr(action). - // Accessible(true). - Run() - fmt.Println("Order up!") -} From 90cdd12d1574a7ddc047da421f1ae0daae5f56f2 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 10:14:14 -0300 Subject: [PATCH 13/23] chore: code review --- spinner/examples/context-and-action-and-error/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spinner/examples/context-and-action-and-error/main.go b/spinner/examples/context-and-action-and-error/main.go index 0b6fd2c2..1483006e 100644 --- a/spinner/examples/context-and-action-and-error/main.go +++ b/spinner/examples/context-and-action-and-error/main.go @@ -16,7 +16,7 @@ func main() { err := spinner.New(). Context(ctx). ActionErr(func(context.Context) error { - time.Sleep(time.Minute) + time.Sleep(time.Second * 5) return nil }). Accessible(false). From e7121fc2fb6464bfc2ed286030211552e3149b28 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 10:42:56 -0300 Subject: [PATCH 14/23] Update spinner/spinner.go Co-authored-by: Christian Rocha --- spinner/spinner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spinner/spinner.go b/spinner/spinner.go index 63245c1e..7177f41b 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -73,7 +73,7 @@ func (s *Spinner) Action(action func()) *Spinner { // // This is just like [Action], but allows the action to use a `context.Context` // and to return an error. -func (s *Spinner) ActionErr(action func(context.Context) error) *Spinner { +func (s *Spinner) ActionWithErr(action func(context.Context) error) *Spinner { s.action = action return s } From 8c7bc4a59f632da24049fa2d417a95b04d3db22a Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 10:45:09 -0300 Subject: [PATCH 15/23] test: fix --- spinner/spinner_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spinner/spinner_test.go b/spinner/spinner_test.go index 2e62f8d1..6ad7dc19 100644 --- a/spinner/spinner_test.go +++ b/spinner/spinner_test.go @@ -106,7 +106,7 @@ func TestSpinnerUpdate(t *testing.T) { } func TestAccessibleSpinner(t *testing.T) { - s := New().Accessible(true) + s := New().Accessible(true).Action(func() {}) err := s.Run() if err != nil { t.Errorf("Run() in accessible mode returned an error: %v", err) From 4019c516d7131067e8dfd21a72e33dc2e60b2c78 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 13:20:18 -0300 Subject: [PATCH 16/23] test: improvements --- .../context-and-action-and-error/main.go | 4 +- spinner/spinner.go | 16 ++-- spinner/spinner_test.go | 81 ++++++++++++++++--- 3 files changed, 76 insertions(+), 25 deletions(-) diff --git a/spinner/examples/context-and-action-and-error/main.go b/spinner/examples/context-and-action-and-error/main.go index 1483006e..a213bf51 100644 --- a/spinner/examples/context-and-action-and-error/main.go +++ b/spinner/examples/context-and-action-and-error/main.go @@ -15,8 +15,8 @@ func main() { err := spinner.New(). Context(ctx). - ActionErr(func(context.Context) error { - time.Sleep(time.Second * 5) + ActionWithErr(func(context.Context) error { + time.Sleep(5 * time.Second) return nil }). Accessible(false). diff --git a/spinner/spinner.go b/spinner/spinner.go index 7177f41b..dfed0731 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -2,7 +2,6 @@ package spinner import ( "context" - "errors" "fmt" "os" "strings" @@ -69,7 +68,7 @@ func (s *Spinner) Action(action func()) *Spinner { return s } -// ActionErr sets the action of the spinner. +// ActionWithErr sets the action of the spinner. // // This is just like [Action], but allows the action to use a `context.Context` // and to return an error. @@ -122,7 +121,8 @@ func New() *Spinner { func (s *Spinner) Init() tea.Cmd { return tea.Batch(s.spinner.Tick, func() tea.Msg { if s.action != nil { - return doneMsg{err: s.action(s.ctx)} + err := s.action(s.ctx) + return doneMsg{err} } return nil }) @@ -157,11 +157,8 @@ func (s *Spinner) View() string { // Run runs the spinner. func (s *Spinner) Run() error { - if s.ctx.Err() != nil { - if errors.Is(s.ctx.Err(), context.Canceled) { - return nil - } - return s.ctx.Err() + if err := s.ctx.Err(); err != nil { + return err } if s.accessible { @@ -195,9 +192,6 @@ func (s *Spinner) runAccessible() error { case <-s.ctx.Done(): s.output.ShowCursor() s.output.CursorBack(len(frame) + len(title)) - if errors.Is(s.ctx.Err(), context.Canceled) { - return nil - } return s.ctx.Err() case err := <-actionDone: s.output.ShowCursor() diff --git a/spinner/spinner_test.go b/spinner/spinner_test.go index 6ad7dc19..99fa8444 100644 --- a/spinner/spinner_test.go +++ b/spinner/spinner_test.go @@ -2,9 +2,11 @@ package spinner import ( "context" + "errors" "reflect" "strings" "testing" + "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" @@ -45,15 +47,23 @@ func TestSpinnerView(t *testing.T) { } func TestSpinnerContextCancellation(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - - s := New().Context(ctx) - cancel() // Cancel before running + exercise(t, func() *Spinner { + ctx, cancel := context.WithCancel(context.Background()) + s := New().Context(ctx) + cancel() // Cancel before running + return s + }, requireContextCanceled) +} - err := s.Run() - if err != nil { - t.Errorf("Run() returned an error after context cancellation: %v", err) - } +func TestSpinnerContextCancellationWhileRunning(t *testing.T) { + exercise(t, func() *Spinner { + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(100 * time.Millisecond) + cancel() + }() + return New().Accessible(true).Context(ctx) + }, requireContextCanceled) } func TestSpinnerStyleMethods(t *testing.T) { @@ -105,10 +115,57 @@ func TestSpinnerUpdate(t *testing.T) { } } -func TestAccessibleSpinner(t *testing.T) { - s := New().Accessible(true).Action(func() {}) - err := s.Run() +func TestSpinnerSimple(t *testing.T) { + exercise(t, func() *Spinner { + return New().Action(func() {}) + }, requireNoError) +} + +func TestSpinnerWithContextAndAction(t *testing.T) { + exercise(t, func() *Spinner { + ctx := context.Background() + return New().Context(ctx).Action(func() {}) + }, requireNoError) +} + +func TestSpinnerWithActionError(t *testing.T) { + fake := errors.New("fake") + exercise(t, func() *Spinner { + return New().ActionWithErr(func(context.Context) error { return fake }) + }, requireErrorIs(fake)) +} + +func exercise(t *testing.T, factory func() *Spinner, checker func(tb testing.TB, err error)) { + t.Helper() + t.Run("accessible", func(t *testing.T) { + err := factory().Accessible(true).Run() + checker(t, err) + }) + t.Run("regular", func(t *testing.T) { + err := factory().Accessible(false).Run() + checker(t, err) + }) +} + +func requireNoError(tb testing.TB, err error) { + tb.Helper() if err != nil { - t.Errorf("Run() in accessible mode returned an error: %v", err) + tb.Errorf("expected no error, got %v", err) + } +} + +func requireErrorIs(target error) func(tb testing.TB, err error) { + return func(tb testing.TB, err error) { + tb.Helper() + if !errors.Is(err, target) { + tb.Errorf("expected error to be %v, got %v", target, err) + } + } +} + +func requireContextCanceled(tb testing.TB, err error) { + tb.Helper() + if !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrProgramKilled) { + tb.Errorf("expected to get a context canceled error, got %v", err) } } From 2c802b8e67a7f57dff33f2d5ebf8d8d68d4f21be Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 13:47:19 -0300 Subject: [PATCH 17/23] fix: tests on ci --- spinner/spinner_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/spinner/spinner_test.go b/spinner/spinner_test.go index 99fa8444..ad0cea58 100644 --- a/spinner/spinner_test.go +++ b/spinner/spinner_test.go @@ -3,6 +3,7 @@ package spinner import ( "context" "errors" + "io" "reflect" "strings" "testing" @@ -11,6 +12,7 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" ) func TestNewSpinner(t *testing.T) { @@ -138,11 +140,15 @@ func TestSpinnerWithActionError(t *testing.T) { func exercise(t *testing.T, factory func() *Spinner, checker func(tb testing.TB, err error)) { t.Helper() t.Run("accessible", func(t *testing.T) { - err := factory().Accessible(true).Run() + s := factory().Accessible(true) + s.output = termenv.NewOutput(io.Discard) + err := s.Run() checker(t, err) }) t.Run("regular", func(t *testing.T) { - err := factory().Accessible(false).Run() + s := factory().Accessible(false) + s.output = termenv.NewOutput(io.Discard) + err := s.Run() checker(t, err) }) } From f91ef19b8fdf10dfacccb169ce34c27fc59a43be Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 15:23:03 -0300 Subject: [PATCH 18/23] feat: update go, spinner.Output, more tests --- go.mod | 2 +- spinner/go.mod | 2 +- spinner/spinner.go | 39 ++++++++++++++++++++++++++++++--------- spinner/spinner_test.go | 15 ++++++++------- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index ec547767..9a6c73b3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/charmbracelet/huh -go 1.21 +go 1.22 require ( github.com/catppuccin/go v0.2.0 diff --git a/spinner/go.mod b/spinner/go.mod index 44ec53f6..07bcff65 100644 --- a/spinner/go.mod +++ b/spinner/go.mod @@ -1,6 +1,6 @@ module github.com/charmbracelet/huh/spinner -go 1.19 +go 1.22 require ( github.com/charmbracelet/bubbles v0.20.0 diff --git a/spinner/spinner.go b/spinner/spinner.go index dfed0731..84a389f7 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -1,8 +1,10 @@ package spinner import ( + "cmp" "context" "fmt" + "io" "os" "strings" @@ -24,9 +26,9 @@ type Spinner struct { action func(ctx context.Context) error ctx context.Context accessible bool - output *termenv.Output title string titleStyle lipgloss.Style + output io.Writer err error } @@ -59,6 +61,13 @@ func (s *Spinner) Title(title string) *Spinner { return s } +// Output set the output for the spinner. +// Default is STDOUT when [Accessible], STDERR otherwise. +func (s *Spinner) Output(w io.Writer) *Spinner { + s.output = w + return s +} + // Action sets the action of the spinner. func (s *Spinner) Action(action func()) *Spinner { s.action = func(context.Context) error { @@ -110,10 +119,8 @@ func New() *Spinner { return &Spinner{ spinner: s, - ctx: context.Background(), title: "Loading...", titleStyle: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#00020A", Dark: "#FFFDF5"}), - output: termenv.NewOutput(os.Stdout), } } @@ -157,6 +164,12 @@ func (s *Spinner) View() string { // Run runs the spinner. func (s *Spinner) Run() error { + if s.ctx == nil && s.action == nil { + return nil + } + if s.ctx == nil { + s.ctx = context.Background() + } if err := s.ctx.Err(); err != nil { return err } @@ -165,7 +178,12 @@ func (s *Spinner) Run() error { return s.runAccessible() } - m, err := tea.NewProgram(s, tea.WithContext(s.ctx), tea.WithOutput(os.Stderr)).Run() + m, err := tea.NewProgram( + s, + tea.WithContext(s.ctx), + tea.WithOutput(s.output), + tea.WithInput(nil), + ).Run() mm := m.(*Spinner) if mm.err != nil { return mm.err @@ -175,11 +193,18 @@ func (s *Spinner) Run() error { // runAccessible runs the spinner in an accessible mode (statically). func (s *Spinner) runAccessible() error { - s.output.HideCursor() + tty := cmp.Or[io.Writer](s.output, os.Stdout) + output := termenv.NewOutput(tty) + output.HideCursor() frame := s.spinner.Style.Render("...") title := s.titleStyle.Render(strings.TrimSuffix(s.title, "...")) fmt.Println(title + frame) + defer func() { + output.ShowCursor() + output.CursorBack(len(frame) + len(title)) + }() + actionDone := make(chan error) if s.action != nil { go func() { @@ -190,12 +215,8 @@ func (s *Spinner) runAccessible() error { for { select { case <-s.ctx.Done(): - s.output.ShowCursor() - s.output.CursorBack(len(frame) + len(title)) return s.ctx.Err() case err := <-actionDone: - s.output.ShowCursor() - s.output.CursorBack(len(frame) + len(title)) return err } } diff --git a/spinner/spinner_test.go b/spinner/spinner_test.go index ad0cea58..1f69c22f 100644 --- a/spinner/spinner_test.go +++ b/spinner/spinner_test.go @@ -12,7 +12,6 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/muesli/termenv" ) func TestNewSpinner(t *testing.T) { @@ -140,15 +139,17 @@ func TestSpinnerWithActionError(t *testing.T) { func exercise(t *testing.T, factory func() *Spinner, checker func(tb testing.TB, err error)) { t.Helper() t.Run("accessible", func(t *testing.T) { - s := factory().Accessible(true) - s.output = termenv.NewOutput(io.Discard) - err := s.Run() + err := factory(). + Accessible(true). + Output(io.Discard). + Run() checker(t, err) }) t.Run("regular", func(t *testing.T) { - s := factory().Accessible(false) - s.output = termenv.NewOutput(io.Discard) - err := s.Run() + err := factory(). + Accessible(false). + Output(io.Discard). + Run() checker(t, err) }) } From 2587a4323e643e2226ec948222ad5e768a35eb47 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 15:59:20 -0300 Subject: [PATCH 19/23] test: sleeps --- spinner/spinner_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spinner/spinner_test.go b/spinner/spinner_test.go index 1f69c22f..f44373c9 100644 --- a/spinner/spinner_test.go +++ b/spinner/spinner_test.go @@ -60,10 +60,10 @@ func TestSpinnerContextCancellationWhileRunning(t *testing.T) { exercise(t, func() *Spinner { ctx, cancel := context.WithCancel(context.Background()) go func() { - time.Sleep(100 * time.Millisecond) + time.Sleep(250 * time.Millisecond) cancel() }() - return New().Accessible(true).Context(ctx) + return New().Context(ctx) }, requireContextCanceled) } From 6017a8093f0edb11ee9d0d25c4dcd7489b2d59fb Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 17:52:44 -0300 Subject: [PATCH 20/23] Update spinner.go Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com> --- spinner/spinner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spinner/spinner.go b/spinner/spinner.go index 84a389f7..8f0e4c34 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -79,7 +79,7 @@ func (s *Spinner) Action(action func()) *Spinner { // ActionWithErr sets the action of the spinner. // -// This is just like [Action], but allows the action to use a `context.Context` +// This is just like [Spinner.Action], but allows the action to use a `context.Context` // and to return an error. func (s *Spinner) ActionWithErr(action func(context.Context) error) *Spinner { s.action = action From 11c3535cf57cfc6b88f1b193f60f64c6e70afa57 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 21 Jan 2025 17:52:50 -0300 Subject: [PATCH 21/23] Update spinner.go Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com> --- spinner/spinner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spinner/spinner.go b/spinner/spinner.go index 8f0e4c34..d0fd1616 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -62,7 +62,7 @@ func (s *Spinner) Title(title string) *Spinner { } // Output set the output for the spinner. -// Default is STDOUT when [Accessible], STDERR otherwise. +// Default is STDOUT when [Spinner.Accessible], STDERR otherwise. func (s *Spinner) Output(w io.Writer) *Spinner { s.output = w return s From f3f033d40ca96f4092fb8af560a3610b456dd0e7 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 22 Jan 2025 11:41:36 -0300 Subject: [PATCH 22/23] Update spinner/spinner_test.go Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com> --- spinner/spinner_test.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spinner/spinner_test.go b/spinner/spinner_test.go index f44373c9..5589d22d 100644 --- a/spinner/spinner_test.go +++ b/spinner/spinner_test.go @@ -172,7 +172,11 @@ func requireErrorIs(target error) func(tb testing.TB, err error) { func requireContextCanceled(tb testing.TB, err error) { tb.Helper() - if !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrProgramKilled) { - tb.Errorf("expected to get a context canceled error, got %v", err) - } + switch { + case errors.Is(err, context.Canceled): + case errors.Is(err, tea.ErrProgramKilled): + + default: + tb.Errorf("expected to get a context canceled error, got %v", err) + } } From a16be32ea60ccadf5b769ee167ccae0a349766c1 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 22 Jan 2025 11:42:34 -0300 Subject: [PATCH 23/23] chore: fmt --- spinner/spinner_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spinner/spinner_test.go b/spinner/spinner_test.go index 5589d22d..2f04d9dd 100644 --- a/spinner/spinner_test.go +++ b/spinner/spinner_test.go @@ -173,10 +173,9 @@ func requireErrorIs(target error) func(tb testing.TB, err error) { func requireContextCanceled(tb testing.TB, err error) { tb.Helper() switch { - case errors.Is(err, context.Canceled): - case errors.Is(err, tea.ErrProgramKilled): - - default: - tb.Errorf("expected to get a context canceled error, got %v", err) - } + case errors.Is(err, context.Canceled): + case errors.Is(err, tea.ErrProgramKilled): + default: + tb.Errorf("expected to get a context canceled error, got %v", err) + } }