diff --git a/choose/choose.go b/choose/choose.go index 42eae176e..2c2e1173e 100644 --- a/choose/choose.go +++ b/choose/choose.go @@ -12,13 +12,11 @@ package choose import ( "strings" - "time" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/paginator" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/lipgloss" ) @@ -112,8 +110,6 @@ type model struct { numSelected int currentOrder int paginator paginator.Model - aborted bool - timedOut bool showHelp bool help help.Model keymap keymap @@ -123,8 +119,6 @@ type model struct { headerStyle lipgloss.Style itemStyle lipgloss.Style selectedItemStyle lipgloss.Style - hasTimeout bool - timeout time.Duration } type item struct { @@ -133,28 +127,13 @@ type item struct { order int } -func (m model) Init() tea.Cmd { - return timeout.Init(m.timeout, nil) -} +func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: return m, nil - 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. - if m.numSelected < 1 { - m.items[m.index].selected = true - } - return m, tea.Quit - } - m.timeout = msg.TimeoutValue - return m, timeout.Tick(msg.TimeoutValue, msg.Data) + case tea.KeyMsg: start, end := m.paginator.GetSliceBounds(len(m.items)) km := m.keymap @@ -199,9 +178,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m = m.deselectAll() } case key.Matches(msg, km.Abort): - m.aborted = true m.quitting = true - return m, tea.Quit + return m, tea.Interrupt case key.Matches(msg, km.Toggle): if m.limit == 1 { break // no op @@ -262,7 +240,6 @@ func (m model) View() string { } var s strings.Builder - var timeoutStr string start, end := m.paginator.GetSliceBounds(len(m.items)) for i, item := range m.items[start:end] { @@ -273,10 +250,7 @@ func (m model) View() string { } if item.selected { - if m.hasTimeout { - timeoutStr = timeout.Str(m.timeout) - } - s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text + timeoutStr)) + s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text)) } else if i == m.index%m.height { s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text)) } else { diff --git a/choose/command.go b/choose/command.go index 26e6e23e8..bcd052b3d 100644 --- a/choose/command.go +++ b/choose/command.go @@ -11,12 +11,11 @@ import ( "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/paginator" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/gum/internal/stdin" + "github.com/charmbracelet/gum/internal/timeout" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/term" - - "github.com/charmbracelet/gum/internal/exit" - "github.com/charmbracelet/gum/internal/stdin" ) // Run provides a shell script interface for choosing between different through @@ -102,8 +101,7 @@ func (o Options) Run() error { km.ToggleAll.SetEnabled(true) } - // Disable Keybindings since we will control it ourselves. - tm, err := tea.NewProgram(model{ + m := model{ index: startingIndex, currentOrder: currentOrder, height: o.Height, @@ -120,22 +118,24 @@ func (o Options) Run() error { itemStyle: o.ItemStyle.ToLipgloss(), selectedItemStyle: o.SelectedItemStyle.ToLipgloss(), numSelected: currentSelected, - hasTimeout: o.Timeout > 0, - timeout: o.Timeout, showHelp: o.ShowHelp, help: help.New(), keymap: km, - }, tea.WithOutput(os.Stderr)).Run() - if err != nil { - return fmt.Errorf("failed to start tea program: %w", err) } - m := tm.(model) - if m.aborted { - return exit.ErrAborted - } - if m.timedOut { - return exit.ErrTimeout + + ctx, cancel := timeout.Context(o.Timeout) + defer cancel() + + // Disable Keybindings since we will control it ourselves. + tm, err := tea.NewProgram( + m, + tea.WithOutput(os.Stderr), + tea.WithContext(ctx), + ).Run() + if err != nil { + return fmt.Errorf("unable to pick selection: %w", err) } + m = tm.(model) 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 e31dd9fa7..026912d33 100644 --- a/confirm/command.go +++ b/confirm/command.go @@ -5,21 +5,20 @@ import ( "os" "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/gum/internal/exit" - tea "github.com/charmbracelet/bubbletea" ) // 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 { - tm, err := tea.NewProgram(model{ + ctx, cancel := timeout.Context(o.Timeout) + defer cancel() + + m := model{ affirmative: o.Affirmative, negative: o.Negative, confirmation: o.Default, defaultSelection: o.Default, - timeout: o.Timeout, - hasTimeout: o.Timeout > 0, keys: defaultKeymap(o.Affirmative, o.Negative), help: help.New(), showHelp: o.ShowHelp, @@ -27,18 +26,17 @@ func (o Options) Run() error { selectedStyle: o.SelectedStyle.ToLipgloss(), unselectedStyle: o.UnselectedStyle.ToLipgloss(), promptStyle: o.PromptStyle.ToLipgloss(), - }, tea.WithOutput(os.Stderr)).Run() + } + tm, err := tea.NewProgram( + m, + tea.WithOutput(os.Stderr), + tea.WithContext(ctx), + ).Run() if err != nil { - return err + return fmt.Errorf("unable to confirm: %w", err) } - m := tm.(model) - if m.timedOut { - return exit.ErrTimeout - } - if m.aborted { - return exit.ErrAborted - } + m = tm.(model) if m.confirmation { return nil } diff --git a/confirm/confirm.go b/confirm/confirm.go index c258068f1..95d911ccb 100644 --- a/confirm/confirm.go +++ b/confirm/confirm.go @@ -11,11 +11,8 @@ package confirm import ( - "time" - "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/gum/timeout" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -81,15 +78,11 @@ type model struct { affirmative string negative string quitting bool - aborted bool - hasTimeout bool showHelp bool help help.Model keys keymap - timeout time.Duration confirmation bool - timedOut bool defaultSelection bool @@ -99,9 +92,7 @@ type model struct { unselectedStyle lipgloss.Style } -func (m model) Init() tea.Cmd { - return timeout.Init(m.timeout, m.defaultSelection) -} +func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { @@ -111,8 +102,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch { case key.Matches(msg, m.keys.Abort): m.confirmation = false - m.aborted = true - fallthrough + return m, tea.Interrupt case key.Matches(msg, m.keys.Quit): m.confirmation = false m.quitting = true @@ -134,16 +124,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.confirmation = true 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 - } - - m.timeout = msg.TimeoutValue - return m, timeout.Tick(msg.TimeoutValue, msg.Data) } return m, nil } @@ -153,23 +133,14 @@ func (m model) View() string { return "" } - var aff, neg, timeoutStrYes, timeoutStrNo string - timeoutStrNo = "" - timeoutStrYes = "" - if m.hasTimeout { - if m.defaultSelection { - timeoutStrYes = timeout.Str(m.timeout) - } else { - timeoutStrNo = timeout.Str(m.timeout) - } - } + var aff, neg string if m.confirmation { - aff = m.selectedStyle.Render(m.affirmative + timeoutStrYes) - neg = m.unselectedStyle.Render(m.negative + timeoutStrNo) + aff = m.selectedStyle.Render(m.affirmative) + neg = m.unselectedStyle.Render(m.negative) } else { - aff = m.unselectedStyle.Render(m.affirmative + timeoutStrYes) - neg = m.selectedStyle.Render(m.negative + timeoutStrNo) + aff = m.unselectedStyle.Render(m.affirmative) + neg = m.selectedStyle.Render(m.negative) } // If the option is intentionally empty, do not show it. diff --git a/file/command.go b/file/command.go index 6c6d26e29..28ef0aa43 100644 --- a/file/command.go +++ b/file/command.go @@ -9,7 +9,7 @@ import ( "github.com/charmbracelet/bubbles/filepicker" "github.com/charmbracelet/bubbles/help" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/gum/internal/exit" + "github.com/charmbracelet/gum/internal/timeout" ) // Run is the interface to picking a file. @@ -48,25 +48,23 @@ func (o Options) Run() error { fp.Styles.FileSize = o.FileSizeStyle.ToLipgloss() m := model{ filepicker: fp, - timeout: o.Timeout, - hasTimeout: o.Timeout > 0, - aborted: false, showHelp: o.ShowHelp, help: help.New(), keymap: defaultKeymap(), } - tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run() + ctx, cancel := timeout.Context(o.Timeout) + defer cancel() + + tm, err := tea.NewProgram( + &m, + tea.WithOutput(os.Stderr), + tea.WithContext(ctx), + ).Run() if err != nil { return fmt.Errorf("unable to pick selection: %w", err) } m = tm.(model) - if m.aborted { - return exit.ErrAborted - } - if m.timedOut { - return exit.ErrTimeout - } if m.selectedPath == "" { return errors.New("no file selected") } diff --git a/file/file.go b/file/file.go index b32776800..b3ddede89 100644 --- a/file/file.go +++ b/file/file.go @@ -13,13 +13,10 @@ package file import ( - "time" - "github.com/charmbracelet/bubbles/filepicker" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/lipgloss" ) @@ -58,22 +55,13 @@ func (k keymap) ShortHelp() []key.Binding { type model struct { filepicker filepicker.Model selectedPath string - aborted bool - timedOut bool quitting bool - timeout time.Duration - hasTimeout bool showHelp bool help help.Model keymap keymap } -func (m model) Init() tea.Cmd { - return tea.Batch( - timeout.Init(m.timeout, nil), - m.filepicker.Init(), - ) -} +func (m model) Init() tea.Cmd { return m.filepicker.Init() } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { @@ -84,21 +72,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, keyAbort): - m.aborted = true m.quitting = true - return m, tea.Quit + return m, tea.Interrupt case key.Matches(msg, keyQuit): m.quitting = true return m, tea.Quit } - case timeout.TickTimeoutMsg: - if msg.TimeoutValue <= 0 { - m.quitting = true - m.timedOut = true - return m, tea.Quit - } - m.timeout = msg.TimeoutValue - return m, timeout.Tick(msg.TimeoutValue, msg.Data) } var cmd tea.Cmd m.filepicker, cmd = m.filepicker.Update(msg) diff --git a/filter/command.go b/filter/command.go index 15093add1..1264534b8 100644 --- a/filter/command.go +++ b/filter/command.go @@ -10,13 +10,12 @@ import ( "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/gum/internal/files" + "github.com/charmbracelet/gum/internal/stdin" + "github.com/charmbracelet/gum/internal/timeout" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/term" "github.com/sahilm/fuzzy" - - "github.com/charmbracelet/gum/internal/exit" - "github.com/charmbracelet/gum/internal/files" - "github.com/charmbracelet/gum/internal/stdin" ) // Run provides a shell script interface for filtering through options, powered @@ -50,7 +49,13 @@ func (o Options) Run() error { return nil } - options := []tea.ProgramOption{tea.WithOutput(os.Stderr)} + ctx, cancel := timeout.Context(o.Timeout) + defer cancel() + + options := []tea.ProgramOption{ + tea.WithOutput(os.Stderr), + tea.WithContext(ctx), + } if o.Height == 0 { options = append(options, tea.WithAltScreen()) } @@ -100,8 +105,6 @@ func (o Options) Run() error { limit: o.Limit, reverse: o.Reverse, fuzzy: o.Fuzzy, - timeout: o.Timeout, - hasTimeout: o.Timeout > 0, sort: o.Sort && o.FuzzySort, strict: o.Strict, showHelp: o.ShowHelp, @@ -113,14 +116,8 @@ func (o Options) Run() error { if err != nil { return fmt.Errorf("unable to run filter: %w", err) } - m := tm.(model) - if m.aborted { - return exit.ErrAborted - } - if m.timedOut { - return exit.ErrTimeout - } + m := tm.(model) isTTY := term.IsTerminal(os.Stdout.Fd()) // allSelections contains values only if limit is greater diff --git a/filter/filter.go b/filter/filter.go index 956d502f4..576ba13d3 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -12,9 +12,6 @@ package filter import ( "strings" - "time" - - "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -100,8 +97,6 @@ type model struct { selectedPrefix string unselectedPrefix string height int - aborted bool - timedOut bool quitting bool headerStyle lipgloss.Style matchStyle lipgloss.Style @@ -116,14 +111,10 @@ type model struct { showHelp bool keymap keymap help help.Model - timeout time.Duration - hasTimeout bool strict bool } -func (m model) Init() tea.Cmd { - return timeout.Init(m.timeout, nil) -} +func (m model) Init() tea.Cmd { return nil } func (m model) View() string { if m.quitting { @@ -240,15 +231,6 @@ func (m model) helpView() string { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { - case timeout.TickTimeoutMsg: - if msg.TimeoutValue <= 0 { - m.quitting = true - m.timedOut = true - return m, tea.Quit - } - m.timeout = msg.TimeoutValue - return m, timeout.Tick(msg.TimeoutValue, msg.Data) - case tea.WindowSizeMsg: if m.height == 0 || m.height > msg.Height { m.viewport.Height = msg.Height - lipgloss.Height(m.textinput.View()) @@ -269,9 +251,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { km := m.keymap switch { case key.Matches(msg, km.Abort): - m.aborted = true m.quitting = true - return m, tea.Quit + return m, tea.Interrupt case key.Matches(msg, km.Submit): m.quitting = true return m, tea.Quit diff --git a/go.mod b/go.mod index 88df801fa..886226289 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/alecthomas/kong v1.6.0 github.com/alecthomas/mango-kong v0.1.0 github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.2.4 + github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1 github.com/charmbracelet/glamour v0.8.0 github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/log v0.4.0 @@ -42,8 +42,8 @@ require ( github.com/yuin/goldmark-emoji v1.0.4 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index 7e88dbf83..80fe2e885 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= -github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= -github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1 h1:osd3dk14DEriOrqJBWzeDE9eN2Yd00BkKzFAiLXxkS8= +github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1/go.mod h1:Hbk5+oE4a7cDyjfdPi4sHZ42aGTMYcmHnVDhsRswn7A= github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= @@ -94,12 +94,12 @@ golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0J golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.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.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= diff --git a/input/command.go b/input/command.go index 9ce22caad..55530558c 100644 --- a/input/command.go +++ b/input/command.go @@ -7,10 +7,9 @@ import ( "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/gum/cursor" - "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/internal/stdin" + "github.com/charmbracelet/gum/internal/timeout" ) // Run provides a shell script interface for the text input bubble. @@ -43,31 +42,30 @@ func (o Options) Run() error { i.EchoCharacter = '•' } - p := tea.NewProgram(model{ + m := model{ textinput: i, - aborted: false, header: o.Header, headerStyle: o.HeaderStyle.ToLipgloss(), - timeout: o.Timeout, - hasTimeout: o.Timeout > 0, autoWidth: o.Width < 1, showHelp: o.ShowHelp, help: help.New(), keymap: defaultKeymap(), - }, tea.WithOutput(os.Stderr)) + } + + ctx, cancel := timeout.Context(o.Timeout) + defer cancel() + + p := tea.NewProgram( + m, + tea.WithOutput(os.Stderr), + tea.WithContext(ctx), + ) tm, err := p.Run() if err != nil { return fmt.Errorf("failed to run input: %w", err) } - m := tm.(model) - if m.aborted { - return exit.ErrAborted - } - if m.timedOut { - return exit.ErrTimeout - } - + m = tm.(model) fmt.Println(m.textinput.Value()) return nil } diff --git a/input/input.go b/input/input.go index cb969ede3..89b88abed 100644 --- a/input/input.go +++ b/input/input.go @@ -8,13 +8,10 @@ package input import ( - "time" - "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/lipgloss" ) @@ -44,21 +41,12 @@ type model struct { headerStyle lipgloss.Style textinput textinput.Model quitting bool - timedOut bool - aborted bool - timeout time.Duration - hasTimeout bool showHelp bool help help.Model keymap keymap } -func (m model) Init() tea.Cmd { - return tea.Batch( - textinput.Blink, - timeout.Init(m.timeout, nil), - ) -} +func (m model) Init() tea.Cmd { return textinput.Blink } func (m model) View() string { if m.quitting { @@ -82,25 +70,16 @@ func (m model) View() string { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case timeout.TickTimeoutMsg: - if msg.TimeoutValue <= 0 { - m.quitting = true - m.timedOut = true - return m, tea.Quit - } - m.timeout = msg.TimeoutValue - return m, timeout.Tick(msg.TimeoutValue, msg.Data) case tea.WindowSizeMsg: if m.autoWidth { m.textinput.Width = msg.Width - lipgloss.Width(m.textinput.Prompt) - 1 } case tea.KeyMsg: switch msg.String() { - case "ctrl+c", "esc": + case "ctrl+c": m.quitting = true - m.aborted = true - return m, tea.Quit - case "enter": + return m, tea.Interrupt + case "esc", "enter": m.quitting = true return m, tea.Quit } diff --git a/internal/exit/exit.go b/internal/exit/exit.go index fb99b73d7..c4f3387ef 100644 --- a/internal/exit/exit.go +++ b/internal/exit/exit.go @@ -1,7 +1,6 @@ package exit import ( - "errors" "strconv" ) @@ -11,12 +10,6 @@ const StatusTimeout = 124 // StatusAborted is the exit code for aborted commands. const StatusAborted = 130 -// ErrAborted is the error to return when a gum command is aborted by Ctrl+C. -var ErrAborted = errors.New("user aborted") - -// ErrTimeout is the error returned when the timeout is reached. -var ErrTimeout = errors.New("timeout") - // ErrExit is a custom exit error. type ErrExit int diff --git a/internal/timeout/context.go b/internal/timeout/context.go new file mode 100644 index 000000000..99eaf3412 --- /dev/null +++ b/internal/timeout/context.go @@ -0,0 +1,15 @@ +package timeout + +import ( + "context" + "time" +) + +// Context setup a new context that times out if the given timeout is > 0. +func Context(timeout time.Duration) (context.Context, context.CancelFunc) { + ctx := context.Background() + if timeout == 0 { + return ctx, func() {} + } + return context.WithTimeout(ctx, timeout) +} diff --git a/main.go b/main.go index fd872edf6..75741e2cd 100644 --- a/main.go +++ b/main.go @@ -7,10 +7,10 @@ import ( "runtime/debug" "github.com/alecthomas/kong" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" - - "github.com/charmbracelet/gum/internal/exit" ) const shaLen = 7 @@ -76,11 +76,11 @@ func main() { if errors.As(err, &ex) { os.Exit(int(ex)) } - if errors.Is(err, exit.ErrTimeout) { - fmt.Fprintln(os.Stderr, err) + if errors.Is(err, tea.ErrProgramKilled) { + fmt.Fprintln(os.Stderr, "timed out") os.Exit(exit.StatusTimeout) } - if errors.Is(err, exit.ErrAborted) { + if errors.Is(err, tea.ErrInterrupted) { os.Exit(exit.StatusAborted) } fmt.Fprintln(os.Stderr, err) diff --git a/pager/command.go b/pager/command.go index 1bdf1537a..8df62ed35 100644 --- a/pager/command.go +++ b/pager/command.go @@ -6,8 +6,8 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/internal/stdin" + "github.com/charmbracelet/gum/internal/timeout" ) // Run provides a shell script interface for the viewport bubble. @@ -30,7 +30,7 @@ func (o Options) Run() error { } } - tm, err := tea.NewProgram(model{ + m := model{ viewport: vp, helpStyle: o.HelpStyle.ToLipgloss(), content: o.Content, @@ -42,14 +42,18 @@ func (o Options) Run() error { matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(), timeout: o.Timeout, hasTimeout: o.Timeout > 0, - }, tea.WithAltScreen()).Run() - if err != nil { - return fmt.Errorf("unable to start program: %w", err) } - m := tm.(model) - if m.timedOut { - return exit.ErrTimeout + ctx, cancel := timeout.Context(o.Timeout) + defer cancel() + + _, err := tea.NewProgram( + m, + tea.WithAltScreen(), + tea.WithContext(ctx), + ).Run() + if err != nil { + return fmt.Errorf("unable to start program: %w", err) } return nil diff --git a/pager/pager.go b/pager/pager.go index 38e88d30e..6ee2ce636 100644 --- a/pager/pager.go +++ b/pager/pager.go @@ -6,9 +6,6 @@ package pager import ( "fmt" "strings" - "time" - - "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" @@ -28,25 +25,12 @@ type model struct { matchStyle lipgloss.Style matchHighlightStyle lipgloss.Style maxWidth int - timeout time.Duration - hasTimeout bool - timedOut bool } -func (m model) Init() tea.Cmd { - return timeout.Init(m.timeout, nil) -} +func (m model) Init() tea.Cmd { return nil } 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 - return m, timeout.Tick(msg.TimeoutValue, msg.Data) - case tea.WindowSizeMsg: m.ProcessText(msg) case tea.KeyMsg: @@ -134,8 +118,10 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) { case "n": m.search.NextMatch(&m) m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width}) - case "q", "ctrl+c", "esc": + case "q", "esc": return m, tea.Quit + case "ctrl+c": + return m, tea.Interrupt } m.viewport, cmd = m.viewport.Update(key) } @@ -144,17 +130,14 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) { } func (m model) View() string { - var timeoutStr string - if m.hasTimeout { - timeoutStr = timeout.Str(m.timeout) + " " - } - helpMsg := "\n" + timeoutStr + " ↑/↓: Navigate • q: Quit • /: Search " + // TODO: use help bubble here + helpMsg := "\n ↑/↓: Navigate • q: Quit • /: Search " if m.search.query != nil { helpMsg += "• n: Next Match " helpMsg += "• N: Prev Match " } if m.search.active { - return m.viewport.View() + "\n" + timeoutStr + " " + m.search.input.View() + return m.viewport.View() + "\n " + m.search.input.View() } return m.viewport.View() + m.helpStyle.Render(helpMsg) diff --git a/spin/command.go b/spin/command.go index a706c4077..5dd04eb7a 100644 --- a/spin/command.go +++ b/spin/command.go @@ -6,9 +6,9 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/term" - "github.com/charmbracelet/gum/internal/exit" + "github.com/charmbracelet/gum/internal/timeout" + "github.com/charmbracelet/x/term" ) // Run provides a shell script interface for the spinner bubble. @@ -19,28 +19,28 @@ func (o Options) Run() error { s := spinner.New() s.Style = o.SpinnerStyle.ToLipgloss() s.Spinner = spinnerMap[o.Spinner] - tm, err := tea.NewProgram(model{ + m := model{ spinner: s, title: o.TitleStyle.ToLipgloss().Render(o.Title), command: o.Command, align: o.Align, showOutput: o.ShowOutput && isTTY, showError: o.ShowError, - timeout: o.Timeout, - hasTimeout: o.Timeout > 0, - }, 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 + ctx, cancel := timeout.Context(o.Timeout) + defer cancel() + + tm, err := tea.NewProgram( + m, + tea.WithOutput(os.Stderr), + tea.WithContext(ctx), + ).Run() + if err != nil { + return fmt.Errorf("unable to run action: %w", err) } + m = tm.(model) // 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. //nolint:nestif diff --git a/spin/spin.go b/spin/spin.go index 30fd95d95..61cc7ec73 100644 --- a/spin/spin.go +++ b/spin/spin.go @@ -20,13 +20,10 @@ import ( "os/exec" "strings" "syscall" - "time" - - "github.com/charmbracelet/gum/timeout" - "github.com/charmbracelet/x/term" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/term" ) type model struct { @@ -35,16 +32,12 @@ type model struct { align string command []string quitting bool - aborted bool - timedOut bool status int stdout string stderr string output string showOutput bool showError bool - timeout time.Duration - hasTimeout bool } var ( @@ -95,14 +88,13 @@ func commandAbort() tea.Msg { if executing != nil && executing.Process != nil { _ = executing.Process.Signal(syscall.SIGINT) } - return nil + return tea.InterruptMsg{} } func (m model) Init() tea.Cmd { return tea.Batch( m.spinner.Tick, commandStart(m.command), - timeout.Init(m.timeout, nil), ) } @@ -111,15 +103,11 @@ func (m model) View() string { return strings.TrimPrefix(errbuf.String()+"\n"+outbuf.String(), "\n") } - var str string - if m.hasTimeout { - str = timeout.Str(m.timeout) - } var header string if m.align == "left" { - header = m.spinner.View() + str + " " + m.title + header = m.spinner.View() + " " + m.title } else { - header = str + " " + m.title + " " + m.spinner.View() + header = m.title + " " + m.spinner.View() } if !m.showOutput { return header @@ -130,15 +118,6 @@ func (m model) View() string { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { - case timeout.TickTimeoutMsg: - if msg.TimeoutValue <= 0 { - // grab current output before closing for piped instances - m.stdout = outbuf.String() - m.timedOut = true - return m, tea.Quit - } - m.timeout = msg.TimeoutValue - return m, timeout.Tick(msg.TimeoutValue, msg.Data) case finishCommandMsg: m.stdout = msg.stdout m.stderr = msg.stderr @@ -149,7 +128,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.String() { case "ctrl+c": - m.aborted = true return m, commandAbort } } diff --git a/table/command.go b/table/command.go index 98b5fdcfe..2e6f8fa22 100644 --- a/table/command.go +++ b/table/command.go @@ -7,11 +7,11 @@ import ( "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - ltable "github.com/charmbracelet/lipgloss/table" - "github.com/charmbracelet/gum/internal/stdin" + "github.com/charmbracelet/gum/internal/timeout" "github.com/charmbracelet/gum/style" + "github.com/charmbracelet/lipgloss" + ltable "github.com/charmbracelet/lipgloss/table" ) // Run provides a shell script interface for rendering tabular data (CSV). @@ -111,9 +111,17 @@ func (o Options) Run() error { if o.Height > 0 { opts = append(opts, table.WithHeight(o.Height)) } + table := table.New(opts...) - tm, err := tea.NewProgram(model{table: table}, tea.WithOutput(os.Stderr)).Run() + ctx, cancel := timeout.Context(o.Timeout) + defer cancel() + + tm, err := tea.NewProgram( + model{table: table}, + tea.WithOutput(os.Stderr), + tea.WithContext(ctx), + ).Run() if err != nil { return fmt.Errorf("failed to start tea program: %w", err) } diff --git a/table/options.go b/table/options.go index fcfa5793d..853a8de7c 100644 --- a/table/options.go +++ b/table/options.go @@ -1,6 +1,10 @@ package table -import "github.com/charmbracelet/gum/style" +import ( + "time" + + "github.com/charmbracelet/gum/style" +) // Options is the customization options for the table command. type Options struct { @@ -12,9 +16,10 @@ type Options struct { File string `short:"f" help:"file path" default:""` Border string `short:"b" help:"border style" default:"rounded" enum:"rounded,thick,normal,hidden,double,none"` - BorderStyle style.Styles `embed:"" prefix:"border." envprefix:"GUM_TABLE_BORDER_"` - CellStyle style.Styles `embed:"" prefix:"cell." envprefix:"GUM_TABLE_CELL_"` - HeaderStyle style.Styles `embed:"" prefix:"header." envprefix:"GUM_TABLE_HEADER_"` - SelectedStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_TABLE_SELECTED_"` - ReturnColumn int `short:"r" help:"Which column number should be returned instead of whole row as string. Default=0 returns whole Row" default:"0"` + BorderStyle style.Styles `embed:"" prefix:"border." envprefix:"GUM_TABLE_BORDER_"` + CellStyle style.Styles `embed:"" prefix:"cell." envprefix:"GUM_TABLE_CELL_"` + HeaderStyle style.Styles `embed:"" prefix:"header." envprefix:"GUM_TABLE_HEADER_"` + SelectedStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_TABLE_SELECTED_"` + ReturnColumn int `short:"r" help:"Which column number should be returned instead of whole row as string. Default=0 returns whole Row" default:"0"` + Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0s" env:"GUM_TABLE_TIMEOUT"` } diff --git a/table/table.go b/table/table.go index d9a92ed20..7e2b03531 100644 --- a/table/table.go +++ b/table/table.go @@ -25,9 +25,7 @@ type model struct { quitting bool } -func (m model) Init() tea.Cmd { - return nil -} +func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd @@ -39,9 +37,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.selected = m.table.SelectedRow() m.quitting = true return m, tea.Quit - case "ctrl+c", "q", "esc": + case "q", "esc": m.quitting = true return m, tea.Quit + case "ctrl+c": + m.quitting = true + return m, tea.Interrupt } } diff --git a/timeout/options.go b/timeout/options.go deleted file mode 100644 index 8dae2d570..000000000 --- a/timeout/options.go +++ /dev/null @@ -1,48 +0,0 @@ -package timeout - -import ( - "fmt" - "time" - - tea "github.com/charmbracelet/bubbletea" -) - -// Tick interval. -const tickInterval = time.Second - -// TickTimeoutMsg will be dispatched for every tick. -// Containing current timeout value -// and optional parameter to be used when handling the timeout msg. -type TickTimeoutMsg struct { - TimeoutValue time.Duration - Data interface{} -} - -// Init Start Timeout ticker using with timeout in seconds and optional data. -func Init(timeout time.Duration, data interface{}) tea.Cmd { - if timeout > 0 { - return Tick(timeout, data) - } - return nil -} - -// Start ticker. -func Tick(timeoutValue time.Duration, data interface{}) tea.Cmd { - return tea.Tick(tickInterval, func(time.Time) tea.Msg { - // every tick checks if the timeout needs to be decremented - // and send as message - if timeoutValue >= 0 { - timeoutValue -= tickInterval - return TickTimeoutMsg{ - TimeoutValue: timeoutValue, - Data: data, - } - } - return nil - }) -} - -// Str produce Timeout String to be rendered. -func Str(timeout time.Duration) string { - return fmt.Sprintf(" (%d)", max(0, int(timeout.Seconds()))) -} diff --git a/write/command.go b/write/command.go index 97a806f77..736d5679d 100644 --- a/write/command.go +++ b/write/command.go @@ -8,10 +8,9 @@ import ( "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/textarea" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/gum/cursor" - "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/internal/stdin" + "github.com/charmbracelet/gum/internal/timeout" ) // Run provides a shell script interface for the text area bubble. @@ -49,7 +48,7 @@ func (o Options) Run() error { a.SetHeight(o.Height) a.SetValue(o.Value) - p := tea.NewProgram(model{ + m := model{ textarea: a, header: o.Header, headerStyle: o.HeaderStyle.ToLipgloss(), @@ -57,16 +56,22 @@ func (o Options) Run() error { help: help.New(), showHelp: o.ShowHelp, keymap: defaultKeymap(), - }, tea.WithOutput(os.Stderr), tea.WithReportFocus()) + } + + ctx, cancel := timeout.Context(o.Timeout) + defer cancel() + + p := tea.NewProgram( + m, + tea.WithOutput(os.Stderr), + tea.WithReportFocus(), + tea.WithContext(ctx), + ) tm, err := p.Run() if err != nil { return fmt.Errorf("failed to run write: %w", err) } - m := tm.(model) - if m.aborted { - return exit.ErrAborted - } - + m = tm.(model) fmt.Println(m.textarea.Value()) return nil } diff --git a/write/options.go b/write/options.go index 0e33cd5a6..575c3974c 100644 --- a/write/options.go +++ b/write/options.go @@ -1,20 +1,25 @@ package write -import "github.com/charmbracelet/gum/style" +import ( + "time" + + "github.com/charmbracelet/gum/style" +) // Options are the customization options for the textarea. type Options struct { - Width int `help:"Text area width (0 for terminal width)" default:"0" env:"GUM_WRITE_WIDTH"` - Height int `help:"Text area height" default:"5" env:"GUM_WRITE_HEIGHT"` - Header string `help:"Header value" default:"" env:"GUM_WRITE_HEADER"` - Placeholder string `help:"Placeholder value" default:"Write something..." env:"GUM_WRITE_PLACEHOLDER"` - Prompt string `help:"Prompt to display" default:"┃ " env:"GUM_WRITE_PROMPT"` - ShowCursorLine bool `help:"Show cursor line" default:"false" env:"GUM_WRITE_SHOW_CURSOR_LINE"` - ShowLineNumbers bool `help:"Show line numbers" default:"false" env:"GUM_WRITE_SHOW_LINE_NUMBERS"` - Value string `help:"Initial value (can be passed via stdin)" default:"" env:"GUM_WRITE_VALUE"` - CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"` - ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_WRITE_SHOW_HELP"` - CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"` + Width int `help:"Text area width (0 for terminal width)" default:"0" env:"GUM_WRITE_WIDTH"` + Height int `help:"Text area height" default:"5" env:"GUM_WRITE_HEIGHT"` + Header string `help:"Header value" default:"" env:"GUM_WRITE_HEADER"` + Placeholder string `help:"Placeholder value" default:"Write something..." env:"GUM_WRITE_PLACEHOLDER"` + Prompt string `help:"Prompt to display" default:"┃ " env:"GUM_WRITE_PROMPT"` + ShowCursorLine bool `help:"Show cursor line" default:"false" env:"GUM_WRITE_SHOW_CURSOR_LINE"` + ShowLineNumbers bool `help:"Show line numbers" default:"false" env:"GUM_WRITE_SHOW_LINE_NUMBERS"` + Value string `help:"Initial value (can be passed via stdin)" default:"" env:"GUM_WRITE_VALUE"` + CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"` + ShowHelp bool `help:"Show help key binds" negatable:"" default:"true" env:"GUM_WRITE_SHOW_HELP"` + CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_WRITE_CURSOR_MODE"` + Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0s" env:"GUM_WRITE_TIMEOUT"` BaseStyle style.Styles `embed:"" prefix:"base." envprefix:"GUM_WRITE_BASE_"` CursorLineNumberStyle style.Styles `embed:"" prefix:"cursor-line-number." set:"defaultForeground=7" envprefix:"GUM_WRITE_CURSOR_LINE_NUMBER_"` diff --git a/write/write.go b/write/write.go index 4b242340d..6d0925386 100644 --- a/write/write.go +++ b/write/write.go @@ -23,7 +23,7 @@ import ( type keymap struct { textarea.KeyMap Submit key.Binding - Quit key.Binding + Abort key.Binding OpenInEditor key.Binding } @@ -47,7 +47,7 @@ func defaultKeymap() keymap { ) return keymap{ KeyMap: km, - Quit: key.NewBinding( + Abort: key.NewBinding( key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "cancel"), ), @@ -64,7 +64,6 @@ func defaultKeymap() keymap { type model struct { autoWidth bool - aborted bool header string headerStyle lipgloss.Style quitting bool @@ -75,6 +74,7 @@ type model struct { } func (m model) Init() tea.Cmd { return textarea.Blink } + func (m model) View() string { if m.quitting { return "" @@ -107,18 +107,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, openEditor(msg.path, msg.lineno) case editorFinishedMsg: if msg.err != nil { - m.aborted = true m.quitting = true - return m, tea.Quit + return m, tea.Interrupt } m.textarea.SetValue(msg.content) case tea.KeyMsg: km := m.keymap switch { - case key.Matches(msg, km.Quit): - m.aborted = true + case key.Matches(msg, km.Abort): m.quitting = true - return m, tea.Quit + return m, tea.Interrupt case key.Matches(msg, km.Submit): m.quitting = true return m, tea.Quit