Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into confirm-yes
Browse files Browse the repository at this point in the history
  • Loading branch information
caarlos0 committed Dec 17, 2024
2 parents 0b53eef + 4cedf9f commit f9fa23b
Show file tree
Hide file tree
Showing 25 changed files with 262 additions and 82 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
41 changes: 26 additions & 15 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 @@ -27,15 +27,32 @@ func (o Options) Run() error {
)

if len(o.Options) <= 0 {
input, _ := stdin.ReadStrip()
input, _ := stdin.Read(stdin.StripANSI(o.StripANSI))
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 @@ -146,19 +163,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
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
2 changes: 2 additions & 0 deletions format/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ type Options struct {
Theme string `help:"Glamour theme to use for markdown formatting" default:"pink" env:"GUM_FORMAT_THEME"`
Language string `help:"Programming language to parse code" short:"l" default:"" env:"GUM_FORMAT_LANGUAGE"`

StripANSI bool `help:"Strip ANSI sequences when reading from STDIN" default:"true" negatable:"" env:"GUM_FORMAT_STRIP_ANSI"`

Type string `help:"Format to use (markdown,template,code,emoji)" enum:"markdown,template,code,emoji" short:"t" default:"markdown" env:"GUM_FORMAT_TYPE"`
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module github.com/charmbracelet/gum

go 1.21
go 1.23.0

require (
github.com/Masterminds/semver/v3 v3.3.1
github.com/alecthomas/kong v1.6.0
github.com/alecthomas/mango-kong v0.1.0
github.com/charmbracelet/bubbles v0.20.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
Expand Down
Loading

0 comments on commit f9fa23b

Please sign in to comment.