Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into choose-in
Browse files Browse the repository at this point in the history
Signed-off-by: Carlos Alexandro Becker <[email protected]>
  • Loading branch information
caarlos0 committed Dec 17, 2024
2 parents 169642b + 0b89ff8 commit 7815c8f
Show file tree
Hide file tree
Showing 26 changed files with 280 additions and 77 deletions.
1 change: 0 additions & 1 deletion choose/choose.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ func defaultKeymap() keymap {
),
End: key.NewBinding(
key.WithKeys("G", "end"),
key.WithHelp("G", "end"),
),
ToggleAll: key.NewBinding(
key.WithKeys("a", "A", "ctrl+a"),
Expand Down
47 changes: 29 additions & 18 deletions choose/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package choose
import (
"errors"
"fmt"
"maps"
"os"
"slices"
"sort"
Expand All @@ -13,9 +14,8 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
"github.com/charmbracelet/gum/internal/tty"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/term"
)

// Run provides a shell script interface for choosing between different through
Expand All @@ -26,18 +26,35 @@ func (o Options) Run() error {
verySubduedStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"})
)

input, _ := stdin.ReadStrip()
if len(o.Options) > 0 {
o.Selected = strings.Split(input, "\n")
} else {
input, _ := stdin.Read(stdin.StripANSI(o.StripANSI))
if len(o.Options) > 0 && len(o.Selected) == 0 {
o.Selected = strings.Split(input, o.InputDelimiter)
} else if len(o.Options) == 0 {
if input == "" {
return errors.New("no options provided, see `gum choose --help`")
}
o.Options = strings.Split(input, "\n")
o.Options = strings.Split(input, o.InputDelimiter)
}

// normalize options into a map
options := map[string]string{}
for _, opt := range o.Options {
if o.LabelDelimiter == "" {
options[opt] = opt
continue
}
label, value, ok := strings.Cut(opt, o.LabelDelimiter)
if !ok {
return fmt.Errorf("invalid option format: %q", opt)
}
options[label] = value
}
if o.LabelDelimiter != "" {
o.Options = slices.Collect(maps.Keys(options))
}

if o.SelectIfOne && len(o.Options) == 1 {
fmt.Println(o.Options[0])
fmt.Println(options[o.Options[0]])
return nil
}

Expand Down Expand Up @@ -148,19 +165,13 @@ func (o Options) Run() error {
return m.items[i].order < m.items[j].order
})
}
var s strings.Builder

var out []string
for _, item := range m.items {
if item.selected {
s.WriteString(item.text)
s.WriteRune('\n')
out = append(out, options[item.text])
}
}

if term.IsTerminal(os.Stdout.Fd()) {
fmt.Print(s.String())
} else {
fmt.Print(ansi.Strip(s.String()))
}

tty.Println(strings.Join(out, o.OutputDelimiter))
return nil
}
4 changes: 4 additions & 0 deletions choose/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type Options struct {
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 (selects all if given '*')" default:"" env:"GUM_CHOOSE_SELECTED"`
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
InputDelimiter string `help:"Option delimiter when reading from STDIN" default:"\n" env:"GUM_CHOOSE_INPUT_DELIMITER"`
OutputDelimiter string `help:"Option delimiter when writing to STDOUT" default:"\n" env:"GUM_CHOOSE_OUTPUT_DELIMITER"`
LabelDelimiter string `help:"Allows to set a delimiter, so options can be set as label:value" default:"" env:"GUM_CHOOSE_LABEL_DELIMITER"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_CHOOSE_STRIP_ANSI"`

CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=99" envprefix:"GUM_CHOOSE_HEADER_"`
Expand Down
15 changes: 14 additions & 1 deletion confirm/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,25 @@ import (

"github.com/charmbracelet/bubbles/help"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/internal/timeout"
)

var errNotConfirmed = errors.New("not confirmed")

// 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 {
line, err := stdin.Read(stdin.SingleLine(true))
if err == nil {
switch line {
case "yes", "y":
return nil
default:
return errNotConfirmed
}
}

ctx, cancel := timeout.Context(o.Timeout)
defer cancel()

Expand Down Expand Up @@ -52,5 +65,5 @@ func (o Options) Run() error {
return nil
}

return errors.New("not confirmed")
return errNotConfirmed
}
63 changes: 35 additions & 28 deletions filter/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import (
"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/charmbracelet/gum/internal/tty"
"github.com/sahilm/fuzzy"
)

Expand All @@ -33,8 +32,8 @@ func (o Options) Run() error {
v := viewport.New(o.Width, o.Height)

if len(o.Options) == 0 {
if input, _ := stdin.ReadStrip(); input != "" {
o.Options = strings.Split(input, "\n")
if input, _ := stdin.Read(stdin.StripANSI(o.StripANSI)); input != "" {
o.Options = strings.Split(input, o.InputDelimiter)
} else {
o.Options = files.List()
}
Expand All @@ -44,11 +43,6 @@ func (o Options) Run() error {
return errors.New("no options provided, see `gum filter --help`")
}

if o.SelectIfOne && len(o.Options) == 1 {
fmt.Println(o.Options[0])
return nil
}

ctx, cancel := timeout.Context(o.Timeout)
defer cancel()

Expand All @@ -74,19 +68,24 @@ func (o Options) Run() error {
matches = matchAll(o.Options)
}

km := defaultKeymap()

if o.NoLimit {
o.Limit = len(o.Options)
}

if o.SelectIfOne && len(matches) == 1 {
tty.Println(matches[0].Str)
return nil
}

km := defaultKeymap()
if o.NoLimit || o.Limit > 1 {
km.Toggle.SetEnabled(true)
km.ToggleAndPrevious.SetEnabled(true)
km.ToggleAndNext.SetEnabled(true)
km.ToggleAll.SetEnabled(true)
}

p := tea.NewProgram(model{
m := model{
choices: o.Options,
indicator: o.Indicator,
matches: matches,
Expand All @@ -112,41 +111,49 @@ func (o Options) Run() error {
showHelp: o.ShowHelp,
keymap: km,
help: help.New(),
}, options...)
}

tm, err := p.Run()
for _, s := range o.Selected {
if o.NoLimit || o.Limit > 1 {
m.selected[s] = struct{}{}
}
}

if len(o.Selected) > 0 {
for i, match := range matches {
if match.Str == o.Selected[0] {
m.cursor = i
break
}
}
}

tm, err := tea.NewProgram(m, options...).Run()
if err != nil {
return fmt.Errorf("unable to run filter: %w", err)
}

m := tm.(model)
m = tm.(model)
if !m.submitted {
return errors.New("nothing selected")
}
isTTY := term.IsTerminal(os.Stdout.Fd())

// allSelections contains values only if limit is greater
// than 1 or if flag --no-limit is passed, hence there is
// no need to further checks
if len(m.selected) > 0 {
o.checkSelected(m, isTTY)
o.checkSelected(m)
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
if isTTY {
fmt.Println(m.matches[m.cursor].Str)
} else {
fmt.Println(ansi.Strip(m.matches[m.cursor].Str))
}
tty.Println(m.matches[m.cursor].Str)
}

return nil
}

func (o Options) checkSelected(m model, isTTY bool) {
func (o Options) checkSelected(m model) {
out := []string{}
for k := range m.selected {
if isTTY {
fmt.Println(k)
} else {
fmt.Println(ansi.Strip(k))
}
out = append(out, k)
}
tty.Println(strings.Join(out, o.OutputDelimiter))
}
49 changes: 47 additions & 2 deletions filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ func defaultKeymap() keymap {
Up: key.NewBinding(
key.WithKeys("up", "ctrl+k", "ctrl+p"),
),
NDown: key.NewBinding(
key.WithKeys("j"),
),
NUp: key.NewBinding(
key.WithKeys("k"),
),
Home: key.NewBinding(
key.WithKeys("g", "home"),
),
End: key.NewBinding(
key.WithKeys("G", "end"),
),
ToggleAndNext: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "toggle"),
Expand All @@ -50,6 +62,14 @@ func defaultKeymap() keymap {
key.WithHelp("ctrl+a", "select all"),
key.WithDisabled(),
),
FocusInSearch: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "search"),
),
FocusOutSearch: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "blur search"),
),
Quit: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "quit"),
Expand All @@ -66,8 +86,14 @@ func defaultKeymap() keymap {
}

type keymap struct {
FocusInSearch,
FocusOutSearch,
Down,
Up,
NDown,
NUp,
Home,
End,
ToggleAndNext,
ToggleAndPrevious,
ToggleAll,
Expand All @@ -87,6 +113,8 @@ func (k keymap) ShortHelp() []key.Binding {
key.WithKeys("up", "down"),
key.WithHelp("↓↑", "navigate"),
),
k.FocusInSearch,
k.FocusOutSearch,
k.ToggleAndNext,
k.ToggleAll,
k.Submit,
Expand Down Expand Up @@ -262,6 +290,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
km := m.keymap
switch {
case key.Matches(msg, km.FocusInSearch):
m.textinput.Focus()
case key.Matches(msg, km.FocusOutSearch):
m.textinput.Blur()
case key.Matches(msg, km.Quit):
m.quitting = true
return m, tea.Quit
Expand All @@ -272,10 +304,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.quitting = true
m.submitted = true
return m, tea.Quit
case key.Matches(msg, km.Down):
case key.Matches(msg, km.Down, km.NDown):
m.CursorDown()
case key.Matches(msg, km.Up):
case key.Matches(msg, km.Up, km.NUp):
m.CursorUp()
case key.Matches(msg, km.Home):
m.cursor = 0
m.viewport.GotoTop()
case key.Matches(msg, km.End):
m.cursor = len(m.choices) - 1
m.viewport.GotoBottom()
case key.Matches(msg, km.ToggleAndNext):
if m.limit == 1 {
break // no op
Expand Down Expand Up @@ -344,6 +382,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}

m.keymap.FocusInSearch.SetEnabled(!m.textinput.Focused())
m.keymap.FocusOutSearch.SetEnabled(m.textinput.Focused())
m.keymap.NUp.SetEnabled(!m.textinput.Focused())
m.keymap.NDown.SetEnabled(!m.textinput.Focused())
m.keymap.Home.SetEnabled(!m.textinput.Focused())
m.keymap.End.SetEnabled(!m.textinput.Focused())

// It's possible that filtering items have caused fewer matches. So, ensure
// that the selected index is within the bounds of the number of matches.
m.cursor = clamp(0, len(m.matches)-1, m.cursor)
Expand Down
4 changes: 4 additions & 0 deletions filter/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Options struct {
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"`
SelectIfOne bool `help:"Select the given option if there is only one" group:"Selection"`
Selected []string `help:"Options that should start as selected (selects all if given '*')" default:"" env:"GUM_FILTER_SELECTED"`
ShowHelp bool `help:"Show help keybinds" default:"true" negatable:"" env:"GUM_FILTER_SHOW_HELP"`
Strict bool `help:"Only returns if anything matched. Otherwise return Filter" negatable:"" default:"true" group:"Selection"`
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
Expand All @@ -37,6 +38,9 @@ type Options struct {
Fuzzy bool `help:"Enable fuzzy matching; otherwise match from start of word" default:"true" env:"GUM_FILTER_FUZZY" negatable:""`
FuzzySort bool `help:"Sort fuzzy results by their scores" default:"true" env:"GUM_FILTER_FUZZY_SORT" negatable:""`
Timeout time.Duration `help:"Timeout until filter command aborts" default:"0s" env:"GUM_FILTER_TIMEOUT"`
InputDelimiter string `help:"Option delimiter when reading from STDIN" default:"\n" env:"GUM_FILTER_INPUT_DELIMITER"`
OutputDelimiter string `help:"Option delimiter when writing to STDOUT" default:"\n" env:"GUM_FILTER_OUTPUT_DELIMITER"`
StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_FILTER_STRIP_ANSI"`

// Deprecated: use [FuzzySort]. This will be removed at some point.
Sort bool `help:"Sort fuzzy results by their scores" default:"true" env:"GUM_FILTER_FUZZY_SORT" negatable:"" hidden:""`
Expand Down
2 changes: 1 addition & 1 deletion format/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (o Options) Run() error {
if len(o.Template) > 0 {
input = strings.Join(o.Template, "\n")
} else {
input, _ = stdin.ReadStrip()
input, _ = stdin.Read(stdin.StripANSI(o.StripANSI))
}

switch o.Type {
Expand Down
Loading

0 comments on commit 7815c8f

Please sign in to comment.