From bd3638fb00c0986bece4be31f6b734f1d3337c3d Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Thu, 5 Dec 2024 10:52:07 -0300 Subject: [PATCH] fix: timeouts --- choose/choose.go | 5 +++-- choose/command.go | 3 +++ confirm/command.go | 22 +++++++++++++--------- confirm/confirm.go | 3 ++- file/command.go | 5 ++++- file/file.go | 3 ++- filter/command.go | 3 +++ filter/filter.go | 3 ++- input/command.go | 5 ++++- input/input.go | 3 ++- internal/exit/exit.go | 14 +------------- pager/command.go | 12 +++++++++--- pager/pager.go | 2 ++ spin/command.go | 12 ++++++------ spin/spin.go | 5 ++--- 15 files changed, 58 insertions(+), 42 deletions(-) diff --git a/choose/choose.go b/choose/choose.go index cc72ec81c..42c4355ff 100644 --- a/choose/choose.go +++ b/choose/choose.go @@ -14,10 +14,9 @@ import ( "strings" "time" - "github.com/charmbracelet/gum/timeout" - "github.com/charmbracelet/bubbles/paginator" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/lipgloss" ) @@ -36,6 +35,7 @@ type model struct { currentOrder int paginator paginator.Model aborted bool + timedOut bool // styles cursorStyle lipgloss.Style @@ -63,6 +63,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case timeout.TickTimeoutMsg: if msg.TimeoutValue <= 0 { m.quitting = true + m.timedOut = true // If the user hasn't selected any items in a multi-select. // Then we select the item that they have pressed enter on. If they // have selected items, then we simply return them. diff --git a/choose/command.go b/choose/command.go index 03012691c..78fe0a1be 100644 --- a/choose/command.go +++ b/choose/command.go @@ -122,6 +122,9 @@ func (o Options) Run() error { if m.aborted { return exit.ErrAborted } + if m.timedOut { + return exit.ErrTimeout + } if o.Ordered && o.Limit > 1 { sort.Slice(m.items, func(i, j int) bool { return m.items[i].order < m.items[j].order diff --git a/confirm/command.go b/confirm/command.go index f02d7e173..965dd49a2 100644 --- a/confirm/command.go +++ b/confirm/command.go @@ -1,6 +1,7 @@ package confirm import ( + "errors" "os" "github.com/charmbracelet/gum/internal/exit" @@ -11,7 +12,7 @@ import ( // Run provides a shell script interface for prompting a user to confirm an // action with an affirmative or negative answer. func (o Options) Run() error { - m, err := tea.NewProgram(model{ + tm, err := tea.NewProgram(model{ affirmative: o.Affirmative, negative: o.Negative, confirmation: o.Default, @@ -24,16 +25,19 @@ func (o Options) Run() error { promptStyle: o.PromptStyle.ToLipgloss(), }, tea.WithOutput(os.Stderr)).Run() if err != nil { - return exit.Handle(err, o.Timeout) + return err } - if m.(model).aborted { - os.Exit(exit.StatusAborted) - } else if m.(model).confirmation { - os.Exit(0) + m := tm.(model) + if m.timedOut { + return exit.ErrTimeout + } + if m.aborted { + return exit.ErrAborted + } + if m.confirmation { + return nil } - os.Exit(1) - - return nil + return errors.New("not confirmed") } diff --git a/confirm/confirm.go b/confirm/confirm.go index c148da3a0..1c73eea0a 100644 --- a/confirm/confirm.go +++ b/confirm/confirm.go @@ -29,6 +29,7 @@ type model struct { timeout time.Duration confirmation bool + timedOut bool defaultSelection bool @@ -75,10 +76,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } case timeout.TickTimeoutMsg: - if msg.TimeoutValue <= 0 { m.quitting = true m.confirmation = m.defaultSelection + m.timedOut = true return m, tea.Quit } diff --git a/file/command.go b/file/command.go index c84360fe4..ac24a00aa 100644 --- a/file/command.go +++ b/file/command.go @@ -58,8 +58,11 @@ func (o Options) Run() error { if m.aborted { return exit.ErrAborted } + if m.timedOut { + return exit.ErrTimeout + } if m.selectedPath == "" { - os.Exit(1) + return errors.New("no file selected") } fmt.Println(m.selectedPath) diff --git a/file/file.go b/file/file.go index 3328d7990..a4457b7b5 100644 --- a/file/file.go +++ b/file/file.go @@ -24,6 +24,7 @@ type model struct { filepicker filepicker.Model selectedPath string aborted bool + timedOut bool quitting bool timeout time.Duration hasTimeout bool @@ -48,7 +49,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case timeout.TickTimeoutMsg: if msg.TimeoutValue <= 0 { m.quitting = true - m.aborted = true + m.timedOut = true return m, tea.Quit } m.timeout = msg.TimeoutValue diff --git a/filter/command.go b/filter/command.go index 5b58940fe..5bd0b0691 100644 --- a/filter/command.go +++ b/filter/command.go @@ -106,6 +106,9 @@ func (o Options) Run() error { if m.aborted { return exit.ErrAborted } + if m.timedOut { + return exit.ErrTimeout + } isTTY := term.IsTerminal(os.Stdout.Fd()) diff --git a/filter/filter.go b/filter/filter.go index b958931b9..70231cc27 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -38,6 +38,7 @@ type model struct { unselectedPrefix string height int aborted bool + timedOut bool quitting bool headerStyle lipgloss.Style matchStyle lipgloss.Style @@ -161,7 +162,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case timeout.TickTimeoutMsg: if msg.TimeoutValue <= 0 { m.quitting = true - m.aborted = true + m.timedOut = true return m, tea.Quit } m.timeout = msg.TimeoutValue diff --git a/input/command.go b/input/command.go index 26f5b7072..e14bff011 100644 --- a/input/command.go +++ b/input/command.go @@ -55,11 +55,14 @@ func (o Options) Run() error { if err != nil { return fmt.Errorf("failed to run input: %w", err) } - m := tm.(model) + m := tm.(model) if m.aborted { return exit.ErrAborted } + if m.timedOut { + return exit.ErrTimeout + } fmt.Println(m.textinput.Value()) return nil diff --git a/input/input.go b/input/input.go index 3d4a459bb..eb8f73c2f 100644 --- a/input/input.go +++ b/input/input.go @@ -22,6 +22,7 @@ type model struct { headerStyle lipgloss.Style textinput textinput.Model quitting bool + timedOut bool aborted bool timeout time.Duration hasTimeout bool @@ -51,7 +52,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case timeout.TickTimeoutMsg: if msg.TimeoutValue <= 0 { m.quitting = true - m.aborted = true + m.timedOut = true return m, tea.Quit } m.timeout = msg.TimeoutValue diff --git a/internal/exit/exit.go b/internal/exit/exit.go index 69485094d..095f63757 100644 --- a/internal/exit/exit.go +++ b/internal/exit/exit.go @@ -1,10 +1,6 @@ package exit -import ( - "errors" - "fmt" - "time" -) +import "errors" // StatusTimeout is the exit code for timed out commands. const StatusTimeout = 124 @@ -17,11 +13,3 @@ var ErrAborted = errors.New("user aborted") // ErrTimeout is the error returned when the timeout is reached. var ErrTimeout = errors.New("timeout") - -// Handle handles the error. -func Handle(err error, d time.Duration) error { - if errors.Is(err, ErrTimeout) { - return fmt.Errorf("%w after %s", ErrTimeout, d) - } - return err -} diff --git a/pager/command.go b/pager/command.go index 5131b96e3..1bdf1537a 100644 --- a/pager/command.go +++ b/pager/command.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/internal/stdin" ) @@ -29,7 +30,7 @@ func (o Options) Run() error { } } - model := model{ + tm, err := tea.NewProgram(model{ viewport: vp, helpStyle: o.HelpStyle.ToLipgloss(), content: o.Content, @@ -41,10 +42,15 @@ func (o Options) Run() error { matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(), timeout: o.Timeout, hasTimeout: o.Timeout > 0, - } - _, err := tea.NewProgram(model, tea.WithAltScreen()).Run() + }, tea.WithAltScreen()).Run() if err != nil { return fmt.Errorf("unable to start program: %w", err) } + + m := tm.(model) + if m.timedOut { + return exit.ErrTimeout + } + return nil } diff --git a/pager/pager.go b/pager/pager.go index f55201bd5..38e88d30e 100644 --- a/pager/pager.go +++ b/pager/pager.go @@ -30,6 +30,7 @@ type model struct { maxWidth int timeout time.Duration hasTimeout bool + timedOut bool } func (m model) Init() tea.Cmd { @@ -40,6 +41,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case timeout.TickTimeoutMsg: if msg.TimeoutValue <= 0 { + m.timedOut = true return m, tea.Quit } m.timeout = msg.TimeoutValue diff --git a/spin/command.go b/spin/command.go index 6bde58dcf..025f81d71 100644 --- a/spin/command.go +++ b/spin/command.go @@ -19,7 +19,7 @@ func (o Options) Run() error { s := spinner.New() s.Style = o.SpinnerStyle.ToLipgloss() s.Spinner = spinnerMap[o.Spinner] - m := model{ + tm, err := tea.NewProgram(model{ spinner: s, title: o.TitleStyle.ToLipgloss().Render(o.Title), command: o.Command, @@ -28,18 +28,18 @@ func (o Options) Run() error { showError: o.ShowError, timeout: o.Timeout, hasTimeout: o.Timeout > 0, - } - p := tea.NewProgram(m, tea.WithOutput(os.Stderr)) - mm, err := p.Run() - m = mm.(model) - + }, tea.WithOutput(os.Stderr)).Run() if err != nil { return fmt.Errorf("failed to run spin: %w", err) } + m := tm.(model) if m.aborted { return exit.ErrAborted } + if m.timedOut { + return exit.ErrTimeout + } // If the command succeeds, and we are printing output and we are in a TTY then push the STDOUT we got to the actual // STDOUT for piping or other things. diff --git a/spin/spin.go b/spin/spin.go index 9037cb099..30fd95d95 100644 --- a/spin/spin.go +++ b/spin/spin.go @@ -22,7 +22,6 @@ import ( "syscall" "time" - "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/x/term" @@ -37,6 +36,7 @@ type model struct { command []string quitting bool aborted bool + timedOut bool status int stdout string stderr string @@ -134,8 +134,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.TimeoutValue <= 0 { // grab current output before closing for piped instances m.stdout = outbuf.String() - - m.status = exit.StatusAborted + m.timedOut = true return m, tea.Quit } m.timeout = msg.TimeoutValue