diff --git a/examples/burger/main.go b/examples/burger/main.go index e8b88c57..2d3b2dd2 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,7 +152,6 @@ func main() { ).WithAccessible(accessible) err := form.Run() - if err != nil { fmt.Println("Uh oh:", err) os.Exit(1) 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..58932825 --- /dev/null +++ b/spinner/examples/context-and-action-and-error/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "fmt" + "io" + "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, io.Writer) 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 new file mode 100644 index 00000000..20ade5fa --- /dev/null +++ b/spinner/examples/context-and-action/main.go @@ -0,0 +1,28 @@ +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() { + time.Sleep(time.Minute) + }). + Accessible(rand.Int()%2 == 0). + Run() + if err != nil { + log.Fatalln(err) + } + fmt.Println("Done!") +} diff --git a/spinner/examples/loading/main.go b/spinner/examples/loading/main.go index 34b7cb95..02e9af92 100644 --- a/spinner/examples/loading/main.go +++ b/spinner/examples/loading/main.go @@ -9,8 +9,11 @@ import ( func main() { action := func() { - time.Sleep(2 * time.Second) + time.Sleep(1 * time.Second) + } + 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/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 77bbf54f..27911906 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -1,12 +1,13 @@ package spinner import ( + "bytes" "context" "errors" "fmt" + "io" "os" "strings" - "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" @@ -23,12 +24,15 @@ import ( // ⣾ Loading... type Spinner struct { spinner spinner.Model - action func() + action func(ctx context.Context, w io.Writer) error ctx context.Context accessible bool output *termenv.Output title string titleStyle lipgloss.Style + + err error + buf bytes.Buffer } type Type spinner.Spinner @@ -62,6 +66,15 @@ 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 { + action() + return nil + } + return s +} + +// ActionErr sets the action of the spinner. +func (s *Spinner) ActionErr(action func(ctx context.Context, w io.Writer) error) *Spinner { s.action = action return s } @@ -98,24 +111,31 @@ func New() *Spinner { s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#F780E2")) return &Spinner{ - action: func() { time.Sleep(time.Second) }, spinner: s, + ctx: context.Background(), title: "Loading...", titleStyle: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}), output: termenv.NewOutput(os.Stdout), - ctx: nil, + buf: bytes.Buffer{}, } } // 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(s.ctx, &s.buf)} + } + 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": @@ -132,41 +152,30 @@ 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. func (s *Spinner) Run() error { - if s.accessible { - return s.runAccessible() - } - - hasCtx := s.ctx != nil - hasCtxErr := hasCtx && s.ctx.Err() != nil - - if hasCtxErr { + if s.ctx.Err() != nil { if errors.Is(s.ctx.Err(), context.Canceled) { return nil } return s.ctx.Err() } - p := tea.NewProgram(s, tea.WithContext(s.ctx), tea.WithOutput(os.Stderr)) - if s.ctx == nil { - go func() { - s.action() - p.Quit() - }() + if s.accessible { + return s.runAccessible() } - _, err := p.Run() - if errors.Is(err, tea.ErrProgramKilled) { - return nil - } else { - return err + m, err := tea.NewProgram(s, tea.WithContext(s.ctx), tea.WithOutput(os.Stderr)).Run() + mm := m.(*Spinner) + if mm.err != nil { + return mm.err } + return err } // runAccessible runs the spinner in an accessible mode (statically). @@ -176,30 +185,30 @@ func (s *Spinner) runAccessible() error { title := s.titleStyle.Render(strings.TrimSuffix(s.title, "...")) fmt.Println(title + frame) - if s.ctx == nil { - s.action() - s.output.ShowCursor() - s.output.CursorBack(len(frame) + len(title)) - return nil + actionDone := make(chan error) + if s.action != nil { + go func() { + actionDone <- s.action(s.ctx, os.Stdout) + }() } - actionDone := make(chan struct{}) - - go func() { - s.action() - actionDone <- struct{}{} - }() - for { select { 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 <-actionDone: + case err := <-actionDone: s.output.ShowCursor() s.output.CursorBack(len(frame) + len(title)) - return nil + return err } } } + +type doneMsg struct { + err error +}