From fd11b787e0779b0979f93f1921fb92cb2239d795 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 05:03:45 +0000 Subject: [PATCH 01/16] feat(deps): bump github.com/alecthomas/kong from 0.7.1 to 0.8.0 Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 0.7.1 to 0.8.0. - [Commits](https://github.com/alecthomas/kong/compare/v0.7.1...v0.8.0) --- updated-dependencies: - dependency-name: github.com/alecthomas/kong dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 492f4961b..9f72c64b4 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/charmbracelet/gum go 1.18 require ( - github.com/alecthomas/kong v0.7.1 + github.com/alecthomas/kong v0.8.0 github.com/alecthomas/mango-kong v0.1.0 github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 diff --git a/go.sum b/go.sum index 034560300..acabc752c 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhyLIViI= github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= -github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4= -github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/kong v0.8.0 h1:ryDCzutfIqJPnNn0omnrgHLbAggDQM2VWHikE1xqK7s= +github.com/alecthomas/kong v0.8.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= github.com/alecthomas/mango-kong v0.1.0 h1:iFVfP1k1K4qpml3JUQmD5I8MCQYfIvsD9mRdrw7jJC4= github.com/alecthomas/mango-kong v0.1.0/go.mod h1:t+TYVdsONUolf/BwVcm+15eqcdAj15h4Qe9MMFAwwT4= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= From f1b99f0aa4b2bc45d8d1da8463572c124c2e8d36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 15:34:12 +0000 Subject: [PATCH 02/16] feat(deps): bump github.com/mattn/go-isatty from 0.0.18 to 0.0.19 Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.18 to 0.0.19. - [Commits](https://github.com/mattn/go-isatty/compare/v0.0.18...v0.0.19) --- updated-dependencies: - dependency-name: github.com/mattn/go-isatty dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9f72c64b4..01fef0abb 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/bubbletea v0.24.2 github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d github.com/charmbracelet/lipgloss v0.7.2-0.20230316100548-06dd20ee5707 - github.com/mattn/go-isatty v0.0.18 + github.com/mattn/go-isatty v0.0.19 github.com/muesli/reflow v0.3.0 github.com/muesli/roff v0.1.0 github.com/muesli/termenv v0.15.2-0.20230323153104-73a40463ff25 diff --git a/go.sum b/go.sum index acabc752c..e086d8991 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= From f048bd8d87b19afa6860655d6dbe7a1f17ee1cc0 Mon Sep 17 00:00:00 2001 From: Rose Thatcher <97619538+hopefulTex@users.noreply.github.com> Date: Tue, 27 Jun 2023 07:31:54 -0700 Subject: [PATCH 03/16] feat(Spin): Option to show live output (#303) * Added live output buffer and option flag * Update Spin on README.md * Returned output formatting to previous version. * Separated the showOutput and liveOutput flags. Both flags can now be used at once. * Removed liveOutout flag showOutput flag is now realtime * (spin) Consolodated stderr and stdout * (spin) Consolodated stdout and stderr * (spin) If being piped, writes to stdout * Added error check and did some housekeeping * No longer outputs to tea.View if piped * Cleaned up the combining of stderr and stdout * Fixed spinner alignment. Updated Readme --- README.md | 2 ++ spin/command.go | 31 ++++++++++++++++++++++--------- spin/options.go | 2 +- spin/spin.go | 36 ++++++++++++++++++++---------------- 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 2252aa899..9ef4493e2 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,8 @@ gum pager < README.md Display a spinner while running a script or command. The spinner will automatically stop after the given command exits. +To view or pipe the command's output, use the `--show-output` flag. + ```bash gum spin --spinner dot --title "Buying Bubble Gum..." -- sleep 5 ``` diff --git a/spin/command.go b/spin/command.go index 46ca5d590..4c3987d3f 100644 --- a/spin/command.go +++ b/spin/command.go @@ -15,14 +15,19 @@ import ( // Run provides a shell script interface for the spinner bubble. // https://github.com/charmbracelet/bubbles/spinner func (o Options) Run() error { + var isTTY bool + info, err := os.Stdout.Stat() + isTTY = info.Mode()&os.ModeCharDevice == os.ModeCharDevice + s := spinner.New() s.Style = o.SpinnerStyle.ToLipgloss() s.Spinner = spinnerMap[o.Spinner] m := model{ - spinner: s, - title: o.TitleStyle.ToLipgloss().Render(o.Title), - command: o.Command, - align: o.Align, + spinner: s, + title: o.TitleStyle.ToLipgloss().Render(o.Title), + command: o.Command, + align: o.Align, + showOutput: o.ShowOutput && isTTY, } p := tea.NewProgram(m, tea.WithOutput(os.Stderr)) mm, err := p.Run() @@ -32,15 +37,23 @@ func (o Options) Run() error { return fmt.Errorf("failed to run spin: %w", err) } - if o.ShowOutput { - fmt.Fprint(os.Stdout, m.stdout) - fmt.Fprint(os.Stderr, m.stderr) - } - if m.aborted { return exit.ErrAborted } + if err != nil { + return fmt.Errorf("failed to access stdout: %w", err) + } + + if o.ShowOutput { + if !isTTY { + _, err := os.Stdout.WriteString(m.stdout) + if err != nil { + return fmt.Errorf("failed to write to stdout: %w", err) + } + } + } + os.Exit(m.status) return nil } diff --git a/spin/options.go b/spin/options.go index 920e788bb..e277fb5d0 100644 --- a/spin/options.go +++ b/spin/options.go @@ -6,7 +6,7 @@ import "github.com/charmbracelet/gum/style" type Options struct { Command []string `arg:"" help:"Command to run"` - ShowOutput bool `help:"Show output of command" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"` + ShowOutput bool `help:"Show or pipe output of command during execution" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"` Spinner string `help:"Spinner type" short:"s" type:"spinner" enum:"line,dot,minidot,jump,pulse,points,globe,moon,monkey,meter,hamburger" default:"dot" env:"GUM_SPIN_SPINNER"` SpinnerStyle style.Styles `embed:"" prefix:"spinner." set:"defaultForeground=212" envprefix:"GUM_SPIN_SPINNER_"` Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"` diff --git a/spin/spin.go b/spin/spin.go index f1dffeae5..4f3624695 100644 --- a/spin/spin.go +++ b/spin/spin.go @@ -20,23 +20,25 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) type model struct { - spinner spinner.Model - title string - align string - command []string - aborted bool - - status int - stdout string - stderr string + spinner spinner.Model + title string + align string + command []string + aborted bool + status int + stdout string + showOutput bool } +var outbuf strings.Builder +var errbuf strings.Builder + type finishCommandMsg struct { stdout string - stderr string status int } @@ -48,7 +50,6 @@ func commandStart(command []string) tea.Cmd { } cmd := exec.Command(command[0], args...) //nolint:gosec - var outbuf, errbuf strings.Builder cmd.Stdout = &outbuf cmd.Stderr = &errbuf @@ -62,7 +63,6 @@ func commandStart(command []string) tea.Cmd { return finishCommandMsg{ stdout: outbuf.String(), - stderr: errbuf.String(), status: status, } } @@ -75,11 +75,16 @@ func (m model) Init() tea.Cmd { ) } func (m model) View() string { + var header string if m.align == "left" { - return m.spinner.View() + " " + m.title + header = m.spinner.View() + " " + m.title + } else { + header = m.title + " " + m.spinner.View() } - - return m.title + " " + m.spinner.View() + if !m.showOutput { + return header + } + return lipgloss.JoinVertical(lipgloss.Top, header, errbuf.String(), outbuf.String()) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -87,7 +92,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case finishCommandMsg: m.stdout = msg.stdout - m.stderr = msg.stderr m.status = msg.status return m, tea.Quit case tea.KeyMsg: From 6aac40560fdd90d779b732dc37f882143e1f4885 Mon Sep 17 00:00:00 2001 From: Maas Lalani Date: Tue, 27 Jun 2023 10:36:33 -0400 Subject: [PATCH 04/16] fix: isTTY & no new line --- spin/command.go | 5 ++--- spin/spin.go | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/spin/command.go b/spin/command.go index 4c3987d3f..082ef3788 100644 --- a/spin/command.go +++ b/spin/command.go @@ -7,6 +7,7 @@ import ( "github.com/alecthomas/kong" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/mattn/go-isatty" "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/style" @@ -15,9 +16,7 @@ import ( // Run provides a shell script interface for the spinner bubble. // https://github.com/charmbracelet/bubbles/spinner func (o Options) Run() error { - var isTTY bool - info, err := os.Stdout.Stat() - isTTY = info.Mode()&os.ModeCharDevice == os.ModeCharDevice + isTTY := isatty.IsTerminal(os.Stdout.Fd()) s := spinner.New() s.Style = o.SpinnerStyle.ToLipgloss() diff --git a/spin/spin.go b/spin/spin.go index 4f3624695..2648b9a76 100644 --- a/spin/spin.go +++ b/spin/spin.go @@ -20,7 +20,6 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) type model struct { @@ -84,7 +83,7 @@ func (m model) View() string { if !m.showOutput { return header } - return lipgloss.JoinVertical(lipgloss.Top, header, errbuf.String(), outbuf.String()) + return header + errbuf.String() + "\n" + outbuf.String() } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { From 99f1348a451c8d567876e0a26a78cf74528435ef Mon Sep 17 00:00:00 2001 From: ROMAIN GUISSET Date: Wed, 24 May 2023 12:11:20 +0000 Subject: [PATCH 05/16] feat(filter): add cursor text line styling --- filter/command.go | 1 + filter/filter.go | 10 ++++++++-- filter/options.go | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/filter/command.go b/filter/command.go index af6292098..492c90432 100644 --- a/filter/command.go +++ b/filter/command.go @@ -84,6 +84,7 @@ func (o Options) Run() error { matchStyle: o.MatchStyle.ToLipgloss(), headerStyle: o.HeaderStyle.ToLipgloss(), textStyle: o.TextStyle.ToLipgloss(), + cursorTextStyle: o.CursorTextStyle.ToLipgloss(), height: o.Height, selected: make(map[string]struct{}), limit: o.Limit, diff --git a/filter/filter.go b/filter/filter.go index 69d62f5f1..22f4fdfdf 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -39,6 +39,7 @@ type model struct { headerStyle lipgloss.Style matchStyle lipgloss.Style textStyle lipgloss.Style + cursorTextStyle lipgloss.Style indicatorStyle lipgloss.Style selectedPrefixStyle lipgloss.Style unselectedPrefixStyle lipgloss.Style @@ -54,6 +55,7 @@ func (m model) View() string { } var s strings.Builder + var lineTextStyle lipgloss.Style // For reverse layout, if the number of matches is less than the viewport // height, we need to offset the matches so that the first match is at the @@ -74,10 +76,14 @@ func (m model) View() string { // If this is the current selected index, we add a small indicator to // represent it. Otherwise, simply pad the string. + // The line's text style is set depending on whether or not the cursor + // points to this line. if i == m.cursor { s.WriteString(m.indicatorStyle.Render(m.indicator)) + lineTextStyle = m.cursorTextStyle } else { s.WriteString(strings.Repeat(" ", lipgloss.Width(m.indicator))) + lineTextStyle = m.textStyle } // If there are multiple selections mark them, otherwise leave an empty space @@ -99,7 +105,7 @@ func (m model) View() string { // index. If so, color the character to indicate a match. if mi < len(match.MatchedIndexes) && ci == match.MatchedIndexes[mi] { // Flush text buffer. - s.WriteString(m.textStyle.Render(buf.String())) + s.WriteString(lineTextStyle.Render(buf.String())) buf.Reset() s.WriteString(m.matchStyle.Render(string(c))) @@ -112,7 +118,7 @@ func (m model) View() string { } } // Flush text buffer. - s.WriteString(m.textStyle.Render(buf.String())) + s.WriteString(lineTextStyle.Render(buf.String())) // We have finished displaying the match with all of it's matched // characters highlighted and the rest filled in. diff --git a/filter/options.go b/filter/options.go index 1392d91f2..b578d2929 100644 --- a/filter/options.go +++ b/filter/options.go @@ -16,6 +16,7 @@ type Options struct { HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_FILTER_HEADER_"` Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"` TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"` + CursorTextStyle style.Styles `embed:"" prefix:"cursor-text." envprefix:"GUM_FILTER_CURSOR_TEXT_"` MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"` Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"` Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"` From 93ffc250e7bb2f67079c2dc65d855ae2eb5b73a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Jun 2023 05:06:53 +0000 Subject: [PATCH 06/16] feat(deps): bump github.com/muesli/termenv Bumps [github.com/muesli/termenv](https://github.com/muesli/termenv) from 0.15.2-0.20230323153104-73a40463ff25 to 0.15.2. - [Release notes](https://github.com/muesli/termenv/releases) - [Commits](https://github.com/muesli/termenv/commits/v0.15.2) --- updated-dependencies: - dependency-name: github.com/muesli/termenv dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 01fef0abb..8aa4010d7 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/mattn/go-isatty v0.0.19 github.com/muesli/reflow v0.3.0 github.com/muesli/roff v0.1.0 - github.com/muesli/termenv v0.15.2-0.20230323153104-73a40463ff25 + github.com/muesli/termenv v0.15.2 github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f ) @@ -38,7 +38,7 @@ require ( github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/sys v0.7.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index e086d8991..bd4f6e51e 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= -github.com/muesli/termenv v0.15.2-0.20230323153104-73a40463ff25 h1:bgCNxFKF+mM5GxpNvkGleUFt12xOzLOzmMOytttpeK4= -github.com/muesli/termenv v0.15.2-0.20230323153104-73a40463ff25/go.mod h1:puu7Fg2fBjAuOzC9hb6zDO/s86uLSYrBlPkIplp2EiA= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -72,8 +72,9 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= From ae1da5d32961fb1026f0f82d9478e5292fc1a608 Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Thu, 29 Jun 2023 22:35:03 +0200 Subject: [PATCH 07/16] Feature/218/adding timeout option (#379) * feat: Adding timeout option in preparation for coming timeout features in all commands * feat: Adding timeout option in preparation for coming timeout features in all commands * feat: Adding timeout option in preparation for coming timeout features in all commands * chore: Linter issues --- confirm/command.go | 5 ++++- go.sum | 4 ++++ pager/pager.go | 7 +++--- timeout/options.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 timeout/options.go diff --git a/confirm/command.go b/confirm/command.go index 3597150c4..88d7db4b9 100644 --- a/confirm/command.go +++ b/confirm/command.go @@ -10,6 +10,9 @@ import ( "github.com/charmbracelet/gum/style" ) +// Aborted is the exit code when the user aborts the confirmation. +const Aborted = 130 + // 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 { @@ -31,7 +34,7 @@ func (o Options) Run() error { } if m.(model).aborted { - os.Exit(130) + os.Exit(Aborted) } else if m.(model).confirmation { os.Exit(0) } else { diff --git a/go.sum b/go.sum index bd4f6e51e..6e4c40eca 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06 github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d h1:S4Ejl/M2VrryIgDrDbiuvkwMUDa67/t/H3Wz3i2/vUw= github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d/go.mod h1:swCB3CXFsh22H1ESDYdY1tirLiNqCziulDyJ1B6Nt7Q= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/lipgloss v0.7.2-0.20230316100548-06dd20ee5707 h1:dXv2HjaDlJZj7wLpTjg1P4B68bdvoXfx7+VXF2/RelY= github.com/charmbracelet/lipgloss v0.7.2-0.20230316100548-06dd20ee5707/go.mod h1:BDceYFEeE5FBoGZeuApZ+V4wSgi8AOIHoryyjYbCTHM= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= @@ -56,6 +57,7 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= @@ -67,6 +69,7 @@ github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -79,3 +82,4 @@ golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= diff --git a/pager/pager.go b/pager/pager.go index 5091ea3d3..49235f147 100644 --- a/pager/pager.go +++ b/pager/pager.go @@ -86,6 +86,7 @@ func (m *model) ProcessText(msg tea.WindowSizeMsg) { func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) { var cmd tea.Cmd + const HeightOffset = 2 if m.search.active { switch key.String() { case "enter": @@ -95,7 +96,7 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) { // Trigger a view update to highlight the found matches. m.search.NextMatch(&m) - m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + 2, Width: m.viewport.Width}) + m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + HeightOffset, Width: m.viewport.Width}) } else { m.search.Done() } @@ -114,10 +115,10 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) { m.search.Begin() case "p", "N": m.search.PrevMatch(&m) - m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + 2, Width: m.viewport.Width}) + m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + HeightOffset, Width: m.viewport.Width}) case "n": m.search.NextMatch(&m) - m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + 2, Width: m.viewport.Width}) + m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + HeightOffset, Width: m.viewport.Width}) case "q", "ctrl+c", "esc": return m, tea.Quit } diff --git a/timeout/options.go b/timeout/options.go new file mode 100644 index 000000000..998683506 --- /dev/null +++ b/timeout/options.go @@ -0,0 +1,55 @@ +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()))) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} From b6f739d7d184cfe69efdb9a3d7226ac71606f7b0 Mon Sep 17 00:00:00 2001 From: Maas Lalani Date: Thu, 29 Jun 2023 16:36:47 -0400 Subject: [PATCH 08/16] refactor: use exit.StatusAborted, unexport heightOffset --- confirm/command.go | 6 ++---- pager/pager.go | 9 +++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/confirm/command.go b/confirm/command.go index 88d7db4b9..4ea454733 100644 --- a/confirm/command.go +++ b/confirm/command.go @@ -7,12 +7,10 @@ import ( "github.com/alecthomas/kong" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/style" ) -// Aborted is the exit code when the user aborts the confirmation. -const Aborted = 130 - // 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 { @@ -34,7 +32,7 @@ func (o Options) Run() error { } if m.(model).aborted { - os.Exit(Aborted) + os.Exit(exit.StatusAborted) } else if m.(model).confirmation { os.Exit(0) } else { diff --git a/pager/pager.go b/pager/pager.go index 49235f147..a3eed042b 100644 --- a/pager/pager.go +++ b/pager/pager.go @@ -84,9 +84,10 @@ func (m *model) ProcessText(msg tea.WindowSizeMsg) { m.viewport.SetContent(text.String()) } +const heightOffset = 2 + func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) { var cmd tea.Cmd - const HeightOffset = 2 if m.search.active { switch key.String() { case "enter": @@ -96,7 +97,7 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) { // Trigger a view update to highlight the found matches. m.search.NextMatch(&m) - m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + HeightOffset, Width: m.viewport.Width}) + m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width}) } else { m.search.Done() } @@ -115,10 +116,10 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) { m.search.Begin() case "p", "N": m.search.PrevMatch(&m) - m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + HeightOffset, Width: m.viewport.Width}) + m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width}) case "n": m.search.NextMatch(&m) - m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + HeightOffset, Width: m.viewport.Width}) + m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width}) case "q", "ctrl+c", "esc": return m, tea.Quit } From abae6fd80c802f347cd096484c788c135e5fa807 Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Thu, 29 Jun 2023 23:27:25 +0200 Subject: [PATCH 09/16] feat: Adding timeout option to Pager command (#381) --- pager/command.go | 2 ++ pager/options.go | 23 ++++++++++++++--------- pager/pager.go | 22 +++++++++++++++++++--- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/pager/command.go b/pager/command.go index b64a41e26..69de89e73 100644 --- a/pager/command.go +++ b/pager/command.go @@ -41,6 +41,8 @@ func (o Options) Run() error { softWrap: o.SoftWrap, matchStyle: o.MatchStyle.ToLipgloss(), matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(), + timeout: o.Timeout, + hasTimeout: o.Timeout > 0, } _, err := tea.NewProgram(model, tea.WithAltScreen()).Run() if err != nil { diff --git a/pager/options.go b/pager/options.go index 53b6c7caf..f257cb0e1 100644 --- a/pager/options.go +++ b/pager/options.go @@ -1,16 +1,21 @@ package pager -import "github.com/charmbracelet/gum/style" +import ( + "time" + + "github.com/charmbracelet/gum/style" +) // Options are the options for the pager. type Options struct { //nolint:staticcheck - Style style.Styles `embed:"" help:"Style the pager" set:"defaultBorder=rounded" set:"defaultPadding=0 1" set:"defaultBorderForeground=212" envprefix:"GUM_PAGER_"` - HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_"` - Content string `arg:"" optional:"" help:"Display content to scroll"` - ShowLineNumbers bool `help:"Show line numbers" default:"true"` - LineNumberStyle style.Styles `embed:"" prefix:"line-number." help:"Style the line numbers" set:"defaultForeground=237" envprefix:"GUM_PAGER_LINE_NUMBER_"` - SoftWrap bool `help:"Soft wrap lines" default:"false"` - MatchStyle style.Styles `embed:"" prefix:"match." help:"Style the matched text" set:"defaultForeground=212" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_"` //nolint:staticcheck - MatchHighlightStyle style.Styles `embed:"" prefix:"match-highlight." help:"Style the matched highlight text" set:"defaultForeground=235" set:"defaultBackground=225" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_HIGH_"` //nolint:staticcheck + Style style.Styles `embed:"" help:"Style the pager" set:"defaultBorder=rounded" set:"defaultPadding=0 1" set:"defaultBorderForeground=212" envprefix:"GUM_PAGER_"` + HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_"` + Content string `arg:"" optional:"" help:"Display content to scroll"` + ShowLineNumbers bool `help:"Show line numbers" default:"true"` + LineNumberStyle style.Styles `embed:"" prefix:"line-number." help:"Style the line numbers" set:"defaultForeground=237" envprefix:"GUM_PAGER_LINE_NUMBER_"` + SoftWrap bool `help:"Soft wrap lines" default:"false"` + MatchStyle style.Styles `embed:"" prefix:"match." help:"Style the matched text" set:"defaultForeground=212" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_"` //nolint:staticcheck + MatchHighlightStyle style.Styles `embed:"" prefix:"match-highlight." help:"Style the matched highlight text" set:"defaultForeground=235" set:"defaultBackground=225" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_HIGH_"` //nolint:staticcheck + Timeout time.Duration `help:"Timeout until command exits" default:"0" env:"GUM_PAGER_TIMEOUT"` } diff --git a/pager/pager.go b/pager/pager.go index a3eed042b..ac8c3a2f7 100644 --- a/pager/pager.go +++ b/pager/pager.go @@ -6,6 +6,9 @@ package pager import ( "fmt" "strings" + "time" + + "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" @@ -25,14 +28,23 @@ type model struct { matchStyle lipgloss.Style matchHighlightStyle lipgloss.Style maxWidth int + timeout time.Duration + hasTimeout bool } func (m model) Init() tea.Cmd { - return nil + return timeout.Init(m.timeout, nil) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case timeout.TickTimeoutMsg: + if msg.TimeoutValue <= 0 { + 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: @@ -130,13 +142,17 @@ func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) { } func (m model) View() string { - helpMsg := "\n ↑/↓: Navigate • q: Quit • /: Search " + var timeoutStr string + if m.hasTimeout { + timeoutStr = timeout.Str(m.timeout) + " " + } + helpMsg := "\n"+timeoutStr+" ↑/↓: 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 " + m.search.input.View() + return m.viewport.View() + "\n"+timeoutStr+ " "+ m.search.input.View() } return m.viewport.View() + m.helpStyle.Render(helpMsg) From 7e71c4d664d28fabd935d6c302970583fcf39a17 Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Thu, 29 Jun 2023 23:29:46 +0200 Subject: [PATCH 10/16] feat: Adding timeout option to Filter command (#380) --- filter/command.go | 20 +++++++++++------- filter/filter.go | 18 +++++++++++++++- filter/options.go | 53 ++++++++++++++++++++++++++--------------------- 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/filter/command.go b/filter/command.go index 492c90432..9cb614aa8 100644 --- a/filter/command.go +++ b/filter/command.go @@ -90,6 +90,8 @@ func (o Options) Run() error { limit: o.Limit, reverse: o.Reverse, fuzzy: o.Fuzzy, + timeout: o.Timeout, + hasTimeout: o.Timeout > 0, sort: o.Sort, }, options...) @@ -108,13 +110,7 @@ func (o Options) Run() error { // than 1 or if flag --no-limit is passed, hence there is // no need to further checks if len(m.selected) > 0 { - for k := range m.selected { - if isTTY { - fmt.Println(k) - } else { - fmt.Println(ansi.Strip(k)) - } - } + o.checkSelected(m, isTTY) } else if len(m.matches) > m.cursor && m.cursor >= 0 { if isTTY { fmt.Println(m.matches[m.cursor].Str) @@ -129,6 +125,16 @@ func (o Options) Run() error { return nil } +func (o Options) checkSelected(m model, isTTY bool) { + for k := range m.selected { + if isTTY { + fmt.Println(k) + } else { + fmt.Println(ansi.Strip(k)) + } + } +} + // BeforeReset hook. Used to unclutter style flags. func (o Options) BeforeReset(ctx *kong.Context) error { style.HideFlags(ctx) diff --git a/filter/filter.go b/filter/filter.go index 22f4fdfdf..38c25f92f 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -12,6 +12,9 @@ package filter import ( "strings" + "time" + + "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" @@ -46,9 +49,13 @@ type model struct { reverse bool fuzzy bool sort bool + timeout time.Duration + hasTimeout bool } -func (m model) Init() tea.Cmd { return nil } +func (m model) Init() tea.Cmd { + return timeout.Init(m.timeout, nil) +} func (m model) View() string { if m.quitting { return "" @@ -149,6 +156,15 @@ 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 { + m.quitting = true + m.aborted = 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()) diff --git a/filter/options.go b/filter/options.go index b578d2929..366d75786 100644 --- a/filter/options.go +++ b/filter/options.go @@ -1,30 +1,35 @@ package filter -import "github.com/charmbracelet/gum/style" +import ( + "time" + + "github.com/charmbracelet/gum/style" +) // Options is the customization options for the filter command. type Options struct { - Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"` - IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"` - Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"` - NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"` - Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"` - SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"` - SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"` - UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"` - UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"` - HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_FILTER_HEADER_"` - Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"` - TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"` - CursorTextStyle style.Styles `embed:"" prefix:"cursor-text." envprefix:"GUM_FILTER_CURSOR_TEXT_"` - MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"` - Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"` - Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"` - PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"` - Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"` - Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"` - Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"` - Reverse bool `help:"Display from the bottom of the screen" env:"GUM_FILTER_REVERSE"` - Fuzzy bool `help:"Enable fuzzy matching" default:"true" env:"GUM_FILTER_FUZZY" negatable:""` - Sort bool `help:"Sort the results" default:"true" env:"GUM_FILTER_SORT" negatable:""` + Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"` + IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"` + Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"` + NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"` + Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"true" default:"true" group:"Selection"` + SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"` + SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"` + UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"` + UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"` + HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_FILTER_HEADER_"` + Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"` + TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"` + CursorTextStyle style.Styles `embed:"" prefix:"cursor-text." envprefix:"GUM_FILTER_CURSOR_TEXT_"` + MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"` + Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"` + Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"` + PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"` + Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"` + Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"` + Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"` + Reverse bool `help:"Display from the bottom of the screen" env:"GUM_FILTER_REVERSE"` + Fuzzy bool `help:"Enable fuzzy matching" default:"true" env:"GUM_FILTER_FUZZY" negatable:""` + Sort bool `help:"Sort the results" default:"true" env:"GUM_FILTER_SORT" negatable:""` + Timeout time.Duration `help:"Timeout until filter command aborts" default:"0" env:"GUM_FILTER_TIMEOUT"` } From 0c1cc8e669ac34114f910353dddad57d1f873b88 Mon Sep 17 00:00:00 2001 From: Maas Lalani Date: Thu, 29 Jun 2023 17:43:58 -0400 Subject: [PATCH 11/16] fix(pager): lint --- pager/pager.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pager/pager.go b/pager/pager.go index ac8c3a2f7..3685da068 100644 --- a/pager/pager.go +++ b/pager/pager.go @@ -146,13 +146,13 @@ func (m model) View() string { if m.hasTimeout { timeoutStr = timeout.Str(m.timeout) + " " } - helpMsg := "\n"+timeoutStr+" ↑/↓: Navigate • q: Quit • /: Search " + helpMsg := "\n" + timeoutStr + " ↑/↓: 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" + timeoutStr + " " + m.search.input.View() } return m.viewport.View() + m.helpStyle.Render(helpMsg) From f8caeef1958d0c6e098e6e16e942f46aa69df4ec Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Fri, 30 Jun 2023 15:17:09 +0200 Subject: [PATCH 12/16] feat: Timeout for Spin Command (#385) * feat: Timeout for Spin Command * fix: spin timeout --------- Co-authored-by: Maas Lalani --- spin/command.go | 3 ++- spin/options.go | 19 ++++++++++++------- spin/spin.go | 22 ++++++++++++++++++++-- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/spin/command.go b/spin/command.go index 082ef3788..9482b7fae 100644 --- a/spin/command.go +++ b/spin/command.go @@ -26,7 +26,8 @@ func (o Options) Run() error { title: o.TitleStyle.ToLipgloss().Render(o.Title), command: o.Command, align: o.Align, - showOutput: o.ShowOutput && isTTY, + timeout: o.Timeout, + hasTimeout: o.Timeout > 0, } p := tea.NewProgram(m, tea.WithOutput(os.Stderr)) mm, err := p.Run() diff --git a/spin/options.go b/spin/options.go index e277fb5d0..9b55c4aa5 100644 --- a/spin/options.go +++ b/spin/options.go @@ -1,15 +1,20 @@ package spin -import "github.com/charmbracelet/gum/style" +import ( + "time" + + "github.com/charmbracelet/gum/style" +) // Options is the customization options for the spin command. type Options struct { Command []string `arg:"" help:"Command to run"` - ShowOutput bool `help:"Show or pipe output of command during execution" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"` - Spinner string `help:"Spinner type" short:"s" type:"spinner" enum:"line,dot,minidot,jump,pulse,points,globe,moon,monkey,meter,hamburger" default:"dot" env:"GUM_SPIN_SPINNER"` - SpinnerStyle style.Styles `embed:"" prefix:"spinner." set:"defaultForeground=212" envprefix:"GUM_SPIN_SPINNER_"` - Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"` - TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_SPIN_TITLE_"` - Align string `help:"Alignment of spinner with regard to the title" short:"a" type:"align" enum:"left,right" default:"left" env:"GUM_SPIN_ALIGN"` + ShowOutput bool `help:"Show or pipe output of command during execution" default:"false" env:"GUM_SPIN_SHOW_OUTPUT"` + Spinner string `help:"Spinner type" short:"s" type:"spinner" enum:"line,dot,minidot,jump,pulse,points,globe,moon,monkey,meter,hamburger" default:"dot" env:"GUM_SPIN_SPINNER"` + SpinnerStyle style.Styles `embed:"" prefix:"spinner." set:"defaultForeground=212" envprefix:"GUM_SPIN_SPINNER_"` + Title string `help:"Text to display to user while spinning" default:"Loading..." env:"GUM_SPIN_TITLE"` + TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_SPIN_TITLE_"` + Align string `help:"Alignment of spinner with regard to the title" short:"a" type:"align" enum:"left,right" default:"left" env:"GUM_SPIN_ALIGN"` + Timeout time.Duration `help:"Timeout until spin command aborts" default:"0" env:"GUM_SPIN_TIMEOUT"` } diff --git a/spin/spin.go b/spin/spin.go index 2648b9a76..48f6f7faf 100644 --- a/spin/spin.go +++ b/spin/spin.go @@ -17,6 +17,10 @@ package spin import ( "os/exec" "strings" + "time" + + "github.com/charmbracelet/gum/internal/exit" + "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" @@ -31,6 +35,8 @@ type model struct { status int stdout string showOutput bool + timeout time.Duration + hasTimeout bool } var outbuf strings.Builder @@ -71,14 +77,19 @@ func (m model) Init() tea.Cmd { return tea.Batch( m.spinner.Tick, commandStart(m.command), + timeout.Init(m.timeout, nil), ) } func (m model) View() string { + var str string + if m.hasTimeout { + str = timeout.Str(m.timeout) + } var header string if m.align == "left" { - header = m.spinner.View() + " " + m.title + header = m.spinner.View() + str + " " + m.title } else { - header = m.title + " " + m.spinner.View() + header = str + " " + m.title + " " + m.spinner.View() } if !m.showOutput { return header @@ -89,6 +100,13 @@ 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 { + m.status = exit.StatusAborted + return m, tea.Quit + } + m.timeout = msg.TimeoutValue + return m, timeout.Tick(msg.TimeoutValue, msg.Data) case finishCommandMsg: m.stdout = msg.stdout m.status = msg.status From 6bf79aa899b36e60cd59a3d2a49c8e0673a9d64a Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Fri, 30 Jun 2023 15:18:02 +0200 Subject: [PATCH 13/16] feat: Timeout for Filter Command (#382) --- input/command.go | 2 ++ input/input.go | 21 +++++++++++++++++++-- input/options.go | 29 +++++++++++++++++------------ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/input/command.go b/input/command.go index 4d3edb2f1..4094ddb4c 100644 --- a/input/command.go +++ b/input/command.go @@ -43,6 +43,8 @@ func (o Options) Run() error { aborted: false, header: o.Header, headerStyle: o.HeaderStyle.ToLipgloss(), + timeout: o.Timeout, + hasTimeout: o.Timeout > 0, autoWidth: o.Width < 1, }, tea.WithOutput(os.Stderr)) tm, err := p.Run() diff --git a/input/input.go b/input/input.go index 0ad2e54a8..9db2306bc 100644 --- a/input/input.go +++ b/input/input.go @@ -8,8 +8,11 @@ package input import ( + "time" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/lipgloss" ) @@ -20,14 +23,20 @@ type model struct { textinput textinput.Model quitting bool aborted bool + timeout time.Duration + hasTimeout bool } -func (m model) Init() tea.Cmd { return textinput.Blink } +func (m model) Init() tea.Cmd { + return tea.Batch( + textinput.Blink, + timeout.Init(m.timeout, nil), + ) +} func (m model) View() string { if m.quitting { return "" } - if m.header != "" { header := m.headerStyle.Render(m.header) return lipgloss.JoinVertical(lipgloss.Left, header, m.textinput.View()) @@ -38,6 +47,14 @@ 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.aborted = 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 diff --git a/input/options.go b/input/options.go index 77cc683a5..82ba50895 100644 --- a/input/options.go +++ b/input/options.go @@ -1,18 +1,23 @@ package input -import "github.com/charmbracelet/gum/style" +import ( + "time" + + "github.com/charmbracelet/gum/style" +) // Options are the customization options for the input. type Options struct { - Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"` - Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"` - PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_INPUT_PROMPT_"` - CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_INPUT_CURSOR_"` - CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_INPUT_CURSOR_MODE"` - Value string `help:"Initial value (can also be passed via stdin)" default:""` - CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"` - Width int `help:"Input width (0 for terminal width)" default:"40" env:"GUM_INPUT_WIDTH"` - Password bool `help:"Mask input characters" default:"false"` - Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"` - HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"` + Placeholder string `help:"Placeholder value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"` + Prompt string `help:"Prompt to display" default:"> " env:"GUM_INPUT_PROMPT"` + PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_INPUT_PROMPT_"` + CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_INPUT_CURSOR_"` + CursorMode string `prefix:"cursor." name:"mode" help:"Cursor mode" default:"blink" enum:"blink,hide,static" env:"GUM_INPUT_CURSOR_MODE"` + Value string `help:"Initial value (can also be passed via stdin)" default:""` + CharLimit int `help:"Maximum value length (0 for no limit)" default:"400"` + Width int `help:"Input width (0 for terminal width)" default:"40" env:"GUM_INPUT_WIDTH"` + Password bool `help:"Mask input characters" default:"false"` + Header string `help:"Header value" default:"" env:"GUM_INPUT_HEADER"` + HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_INPUT_HEADER_"` + Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_INPUT_TIMEOUT"` } From eef6431d7cb0c7e9e6243f25c1d626cee2e4cb25 Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Fri, 30 Jun 2023 15:20:33 +0200 Subject: [PATCH 14/16] feat: Timeout for Confirm Command (#383) * feat: Timeout for Confirm Command * fix: comment --------- Co-authored-by: Maas Lalani --- confirm/command.go | 3 ++- confirm/confirm.go | 64 ++++++++++++++++------------------------------ confirm/options.go | 14 +++++----- 3 files changed, 31 insertions(+), 50 deletions(-) diff --git a/confirm/command.go b/confirm/command.go index 4ea454733..5d42d9c17 100644 --- a/confirm/command.go +++ b/confirm/command.go @@ -4,10 +4,11 @@ import ( "fmt" "os" + "github.com/charmbracelet/gum/internal/exit" + "github.com/alecthomas/kong" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/style" ) diff --git a/confirm/confirm.go b/confirm/confirm.go index 250fc2608..c148da3a0 100644 --- a/confirm/confirm.go +++ b/confirm/confirm.go @@ -11,9 +11,10 @@ package confirm import ( - "fmt" "time" + "github.com/charmbracelet/gum/timeout" + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) @@ -37,21 +38,8 @@ type model struct { unselectedStyle lipgloss.Style } -const tickInterval = time.Second - -type tickMsg struct{} - -func tick() tea.Cmd { - return tea.Tick(tickInterval, func(time.Time) tea.Msg { - return tickMsg{} - }) -} - func (m model) Init() tea.Cmd { - if m.timeout > 0 { - return tick() - } - return nil + return timeout.Init(m.timeout, m.defaultSelection) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -61,6 +49,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.String() { case "ctrl+c": + m.confirmation = false m.aborted = true fallthrough case "esc": @@ -85,14 +74,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.confirmation = true return m, tea.Quit } - case tickMsg: - if m.timeout <= 0 { + case timeout.TickTimeoutMsg: + + if msg.TimeoutValue <= 0 { m.quitting = true m.confirmation = m.defaultSelection return m, tea.Quit } - m.timeout -= tickInterval - return m, tick() + + m.timeout = msg.TimeoutValue + return m, timeout.Tick(msg.TimeoutValue, msg.Data) } return m, nil } @@ -102,27 +93,23 @@ func (m model) View() string { return "" } - var aff, neg, timeout, affirmativeTimeout, negativeTimeout string - + var aff, neg, timeoutStrYes, timeoutStrNo string + timeoutStrNo = "" + timeoutStrYes = "" if m.hasTimeout { - timeout = fmt.Sprintf(" (%d)", max(0, int(m.timeout.Seconds()))) - } - - // set timer based on defaultSelection - if m.defaultSelection { - affirmativeTimeout = m.affirmative + timeout - negativeTimeout = m.negative - } else { - affirmativeTimeout = m.affirmative - negativeTimeout = m.negative + timeout + if m.defaultSelection { + timeoutStrYes = timeout.Str(m.timeout) + } else { + timeoutStrNo = timeout.Str(m.timeout) + } } if m.confirmation { - aff = m.selectedStyle.Render(affirmativeTimeout) - neg = m.unselectedStyle.Render(negativeTimeout) + aff = m.selectedStyle.Render(m.affirmative + timeoutStrYes) + neg = m.unselectedStyle.Render(m.negative + timeoutStrNo) } else { - aff = m.unselectedStyle.Render(affirmativeTimeout) - neg = m.selectedStyle.Render(negativeTimeout) + aff = m.unselectedStyle.Render(m.affirmative + timeoutStrYes) + neg = m.selectedStyle.Render(m.negative + timeoutStrNo) } // If the option is intentionally empty, do not show it. @@ -132,10 +119,3 @@ func (m model) View() string { return lipgloss.JoinVertical(lipgloss.Center, m.promptStyle.Render(m.prompt), lipgloss.JoinHorizontal(lipgloss.Left, aff, neg)) } - -func max(a, b int) int { - if a > b { - return a - } - return b -} diff --git a/confirm/options.go b/confirm/options.go index 3c0881c81..672c646e3 100644 --- a/confirm/options.go +++ b/confirm/options.go @@ -8,14 +8,14 @@ import ( // Options is the customization options for the confirm command. type Options struct { - Affirmative string `help:"The title of the affirmative action" default:"Yes"` - Negative string `help:"The title of the negative action" default:"No"` - Default bool `help:"Default confirmation action" default:"true"` - Timeout time.Duration `help:"Timeout for confirmation" default:"0" env:"GUM_CONFIRM_TIMEOUT"` - Prompt string `arg:"" help:"Prompt to display." default:"Are you sure?"` - PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=1 0 0 0" envprefix:"GUM_CONFIRM_PROMPT_"` + Default bool `help:"Default confirmation action" default:"true"` + Affirmative string `help:"The title of the affirmative action" default:"Yes"` + Negative string `help:"The title of the negative action" default:"No"` + Prompt string `arg:"" help:"Prompt to display." default:"Are you sure?"` + PromptStyle style.Styles `embed:"" prefix:"prompt." help:"The style of the prompt" set:"defaultMargin=1 0 0 0" envprefix:"GUM_CONFIRM_PROMPT_"` //nolint:staticcheck SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style of the selected action" set:"defaultBackground=212" set:"defaultForeground=230" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_SELECTED_"` //nolint:staticcheck - UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_UNSELECTED_"` + UnselectedStyle style.Styles `embed:"" prefix:"unselected." help:"The style of the unselected action" set:"defaultBackground=235" set:"defaultForeground=254" set:"defaultPadding=0 3" set:"defaultMargin=1 1" envprefix:"GUM_CONFIRM_UNSELECTED_"` + Timeout time.Duration `help:"Timeout until confirm returns selected value or default if provided" default:"0" env:"GUM_CONFIRM_TIMEOUT"` } From f73341a56c0540b7202ae83397f6ba7db4cd8f58 Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Fri, 30 Jun 2023 15:22:45 +0200 Subject: [PATCH 15/16] feat: Timeout for File Command (#386) --- file/command.go | 10 ++++++++-- file/file.go | 18 +++++++++++++++++- file/options.go | 9 +++++++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/file/command.go b/file/command.go index e7daa14a7..28b8fc6f4 100644 --- a/file/command.go +++ b/file/command.go @@ -6,10 +6,11 @@ import ( "os" "path/filepath" + "github.com/charmbracelet/gum/internal/exit" + "github.com/alecthomas/kong" "github.com/charmbracelet/bubbles/filepicker" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/gum/internal/exit" "github.com/charmbracelet/gum/style" ) @@ -46,7 +47,12 @@ func (o Options) Run() error { fp.Styles.Selected = o.SelectedStyle.ToLipgloss() fp.Styles.FileSize = o.FileSizeStyle.ToLipgloss() - m := model{filepicker: fp} + m := model{ + filepicker: fp, + timeout: o.Timeout, + hasTimeout: o.Timeout > 0, + aborted: false, + } tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run() if err != nil { diff --git a/file/file.go b/file/file.go index eb2de380e..3328d7990 100644 --- a/file/file.go +++ b/file/file.go @@ -13,8 +13,11 @@ package file import ( + "time" + "github.com/charmbracelet/bubbles/filepicker" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/gum/timeout" ) type model struct { @@ -22,10 +25,15 @@ type model struct { selectedPath string aborted bool quitting bool + timeout time.Duration + hasTimeout bool } func (m model) Init() tea.Cmd { - return m.filepicker.Init() + return tea.Batch( + timeout.Init(m.timeout, nil), + m.filepicker.Init(), + ) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -37,6 +45,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.quitting = true return m, tea.Quit } + case timeout.TickTimeoutMsg: + if msg.TimeoutValue <= 0 { + m.quitting = true + m.aborted = 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/file/options.go b/file/options.go index 340943713..85ca87f0e 100644 --- a/file/options.go +++ b/file/options.go @@ -1,6 +1,10 @@ package file -import "github.com/charmbracelet/gum/style" +import ( + "time" + + "github.com/charmbracelet/gum/style" +) // Options are the options for the file command. type Options struct { @@ -21,5 +25,6 @@ type Options struct { //nolint:staticcheck SelectedStyle style.Styles `embed:"" prefix:"selected." help:"The style to use for the selected item" set:"defaultBold=true" set:"defaultForeground=212" envprefix:"GUM_FILE_SELECTED_"` //nolint:staticcheck - FileSizeStyle style.Styles `embed:"" prefix:"file-size." help:"The style to use for file sizes" set:"defaultWidth=8" set:"defaultAlign=right" set:"defaultForeground=240" envprefix:"GUM_FILE_FILE_SIZE_"` + FileSizeStyle style.Styles `embed:"" prefix:"file-size." help:"The style to use for file sizes" set:"defaultWidth=8" set:"defaultAlign=right" set:"defaultForeground=240" envprefix:"GUM_FILE_FILE_SIZE_"` + Timeout time.Duration `help:"Timeout until command aborts without a selection" default:"0" env:"GUM_FILE_TIMEOUT"` } From d1ad453ce61aeb1181e5cf86ceb28c888ab92ff2 Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Fri, 30 Jun 2023 15:28:46 +0200 Subject: [PATCH 16/16] feat: Timeout for Choose Command (#384) --- choose/choose.go | 29 ++++++++++++++++++++++++++--- choose/command.go | 2 ++ choose/options.go | 38 +++++++++++++++++++++----------------- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/choose/choose.go b/choose/choose.go index a694a7a84..b32030b8a 100644 --- a/choose/choose.go +++ b/choose/choose.go @@ -12,6 +12,9 @@ package choose import ( "strings" + "time" + + "github.com/charmbracelet/gum/timeout" "github.com/charmbracelet/bubbles/paginator" tea "github.com/charmbracelet/bubbletea" @@ -39,6 +42,8 @@ type model struct { headerStyle lipgloss.Style itemStyle lipgloss.Style selectedItemStyle lipgloss.Style + hasTimeout bool + timeout time.Duration } type item struct { @@ -47,13 +52,27 @@ type item struct { order int } -func (m model) Init() tea.Cmd { return nil } +func (m model) Init() tea.Cmd { + return timeout.Init(m.timeout, 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 + // 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)) switch keypress := msg.String(); keypress { @@ -151,6 +170,7 @@ 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] { @@ -161,7 +181,10 @@ func (m model) View() string { } if item.selected { - s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text)) + if m.hasTimeout { + timeoutStr = timeout.Str(m.timeout) + } + s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text + timeoutStr)) } 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 3c9322127..7407147aa 100644 --- a/choose/command.go +++ b/choose/command.go @@ -112,6 +112,8 @@ func (o Options) Run() error { itemStyle: o.ItemStyle.ToLipgloss(), selectedItemStyle: o.SelectedItemStyle.ToLipgloss(), numSelected: currentSelected, + hasTimeout: o.Timeout > 0, + timeout: o.Timeout, }, tea.WithOutput(os.Stderr)).Run() if err != nil { diff --git a/choose/options.go b/choose/options.go index 064718c1c..52e3a50d9 100644 --- a/choose/options.go +++ b/choose/options.go @@ -1,23 +1,27 @@ package choose -import "github.com/charmbracelet/gum/style" +import ( + "time" + + "github.com/charmbracelet/gum/style" +) // Options is the customization options for the choose command. type Options struct { - Options []string `arg:"" optional:"" help:"Options to choose from."` - - Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"` - NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"` - Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"` - Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"` - Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"` - Header string `help:"Header value" default:"" env:"GUM_CHOOSE_HEADER"` - CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_CURSOR_PREFIX"` - SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"◉ " env:"GUM_CHOOSE_SELECTED_PREFIX"` - UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_UNSELECTED_PREFIX"` - Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"` - CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"` - HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_CHOOSE_HEADER_"` - ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"` - SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"` + Options []string `arg:"" optional:"" help:"Options to choose from."` + Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"` + NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"` + Ordered bool `help:"Maintain the order of the selected options" env:"GUM_CHOOSE_ORDERED"` + Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"` + Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"` + Header string `help:"Header value" default:"" env:"GUM_CHOOSE_HEADER"` + CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_CURSOR_PREFIX"` + SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"◉ " env:"GUM_CHOOSE_SELECTED_PREFIX"` + UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"○ " env:"GUM_CHOOSE_UNSELECTED_PREFIX"` + Selected []string `help:"Options that should start as selected" default:"" env:"GUM_CHOOSE_SELECTED"` + CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"` + HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_CHOOSE_HEADER_"` + ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"` + SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"` + Timeout time.Duration `help:"Timeout until choose returns selected element" default:"0" env:"GUM_CCHOOSE_TIMEOUT"` // including timeout command options [Timeout,...] }