Merge branch 'main' into feature/218/Adding_timeout_to_most_commands

This commit is contained in:
Maas Lalani 2023-06-27 10:43:08 -04:00 committed by GitHub
commit 2743d58f2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 47 deletions

View file

@ -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
```

View file

@ -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,

View file

@ -42,6 +42,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
@ -61,6 +62,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
@ -81,10 +83,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
@ -106,7 +112,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)))
@ -119,7 +125,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.

View file

@ -8,27 +8,28 @@ import (
// 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_"`
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:""`
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"`
Sort bool `help:"Sort the results" default:"true" env:"GUM_FILTER_SORT" negatable:""`
}
}

4
go.mod
View file

@ -3,13 +3,13 @@ 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
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

6
go.sum
View file

@ -64,8 +64,8 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
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.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
@ -391,6 +391,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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=

View file

@ -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,6 +16,8 @@ import (
// Run provides a shell script interface for the spinner bubble.
// https://github.com/charmbracelet/bubbles/spinner
func (o Options) Run() error {
isTTY := isatty.IsTerminal(os.Stdout.Fd())
s := spinner.New()
s.Style = o.SpinnerStyle.ToLipgloss()
s.Spinner = spinnerMap[o.Spinner]
@ -25,6 +28,7 @@ func (o Options) Run() error {
align: o.Align,
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
showOutput: o.ShowOutput && isTTY,
}
p := tea.NewProgram(m, tea.WithOutput(os.Stderr))
mm, err := p.Run()
@ -34,15 +38,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
}

View file

@ -34,14 +34,16 @@ type model struct {
status int
stdout string
stderr string
timeout time.Duration
hasTimeout bool
showOutput bool
}
var outbuf strings.Builder
var errbuf strings.Builder
type finishCommandMsg struct {
stdout string
stderr string
status int
}
@ -53,7 +55,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
@ -67,7 +68,6 @@ func commandStart(command []string) tea.Cmd {
return finishCommandMsg{
stdout: outbuf.String(),
stderr: errbuf.String(),
status: status,
}
}
@ -80,17 +80,18 @@ func (m model) Init() tea.Cmd {
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" {
return m.spinner.View() + " " + m.title + str
header = m.spinner.View() + " " + m.title
} else {
header = m.title + " " + m.spinner.View()
}
return str + " " + m.title + " " + m.spinner.View()
if !m.showOutput {
return header
}
return header + errbuf.String() + "\n" + outbuf.String()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@ -105,7 +106,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
case finishCommandMsg:
m.stdout = msg.stdout
m.stderr = msg.stderr
m.status = msg.status
return m, tea.Quit
case tea.KeyMsg: