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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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 }).