Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

horizontal scrolls and other improvements #791

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ variables:

milestones:
- close: true
#
# changelog:
# ai:
# use: anthropic
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,8 @@ The above example is running from a single shell script ([source](./examples/dem

## Tutorial

Gum provides highly configurable, ready-to-use utilities to help you write
useful shell scripts and dotfiles aliases with just a few lines of code.
Let's build a simple script to help you write
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
for your dotfiles.
Gum provides highly configurable, ready-to-use utilities to help you write useful shell scripts and dotfiles aliases with just a few lines of code.
Let's build a simple script to help you write [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for your dotfiles.

Ask for the commit type with gum choose:

Expand Down
67 changes: 51 additions & 16 deletions filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ func defaultKeymap() keymap {
Up: key.NewBinding(
key.WithKeys("up", "ctrl+k", "ctrl+p"),
),
Left: key.NewBinding(
key.WithKeys("left"),
),
Right: key.NewBinding(
key.WithKeys("right"),
),
NLeft: key.NewBinding(
key.WithKeys("h"),
),
NRight: key.NewBinding(
key.WithKeys("l"),
),
NDown: key.NewBinding(
key.WithKeys("j"),
),
Expand Down Expand Up @@ -93,6 +105,10 @@ type keymap struct {
Up,
NDown,
NUp,
Right,
Left,
NRight,
NLeft,
Home,
End,
ToggleAndNext,
Expand All @@ -111,8 +127,8 @@ func (k keymap) FullHelp() [][]key.Binding { return nil }
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("up", "down"),
key.WithHelp("↓↑", "navigate"),
key.WithKeys("left", "down", "up", "right"),
key.WithHelp("←↓↑→", "navigate"),
),
k.FocusInSearch,
k.FocusOutSearch,
Expand Down Expand Up @@ -187,22 +203,11 @@ func (m model) View() 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
if _, ok := m.selected[match.Str]; ok {
s.WriteString(m.selectedPrefixStyle.Render(m.selectedPrefix))
} else if m.limit > 1 {
s.WriteString(m.unselectedPrefixStyle.Render(m.unselectedPrefix))
} else {
s.WriteString(" ")
}

styledOption := m.choices[match.Str]
if len(match.MatchedIndexes) == 0 {
// No matches, just render the text.
Expand All @@ -228,7 +233,7 @@ func (m model) View() string {
s.WriteRune('\n')
}

m.viewport.SetContent(s.String())
m.viewport.SetContent(strings.TrimSpace(s.String()))

help := ""
if m.showHelp {
Expand Down Expand Up @@ -263,11 +268,34 @@ func (m model) helpView() string {
return "\n\n" + m.help.View(m.keymap)
}

func (m model) gutter(gc viewport.GutterContext) string {
selected := m.selectedPrefixStyle.Render(m.selectedPrefix)
unselected := m.unselectedPrefixStyle.Render(m.unselectedPrefix)
indicator := m.indicatorStyle.Render(m.indicator)
empty := strings.Repeat(" ", lipgloss.Width(indicator))

selectGutter := ""
if m.limit > 1 {
selectGutter = unselected
}
if gc.Index < len(m.matches) {
if _, ok := m.selected[m.matches[gc.Index].Str]; ok {
selectGutter = selected
}
}
if gc.Index == m.cursor {
return indicator + selectGutter
}
return empty + selectGutter
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd, icmd tea.Cmd
var cmd, icmd, vcmd tea.Cmd
m.textinput, icmd = m.textinput.Update(msg)
*m.viewport, vcmd = m.viewport.Update(msg)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.textinput.Width = msg.Width
if m.height == 0 || m.height > msg.Height {
m.viewport.Height = msg.Height - lipgloss.Height(m.textinput.View())
}
Expand Down Expand Up @@ -380,6 +408,12 @@ 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.viewport.KeyMap.Left.SetEnabled(!m.textinput.Focused())
m.viewport.KeyMap.Right.SetEnabled(!m.textinput.Focused())
m.keymap.Left.SetEnabled(!m.textinput.Focused())
m.keymap.Right.SetEnabled(!m.textinput.Focused())
m.keymap.NLeft.SetEnabled(!m.textinput.Focused())
m.keymap.NRight.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())
Expand All @@ -388,7 +422,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// 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)
return m, tea.Batch(cmd, icmd)
m.viewport.LeftGutterFunc = m.gutter
return m, tea.Batch(cmd, icmd, vcmd)
}

func (m *model) CursorUp() {
Expand Down
10 changes: 5 additions & 5 deletions filter/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
Options []string `arg:"" optional:"" help:"Options to filter."`

Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" set:"defaultPadding=0 1 0 0" envprefix:"GUM_FILTER_INDICATOR_"`

Check failure on line 14 in filter/options.go

View workflow job for this annotation

GitHub Actions / lint

SA5008: duplicate struct tag "set" (staticcheck)
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"`
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_"`
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" set:"defaultPadding=0 1 0 0" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`

Check failure on line 22 in filter/options.go

View workflow job for this annotation

GitHub Actions / lint

SA5008: duplicate struct tag "set" (staticcheck)
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" set:"defaultPadding=0 1 0 0" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`

Check failure on line 24 in filter/options.go

View workflow job for this annotation

GitHub Actions / lint

SA5008: duplicate struct tag "set" (staticcheck)
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=99" envprefix:"GUM_FILTER_HEADER_"`
Header string `help:"Header value" default:"" env:"GUM_FILTER_HEADER"`
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ require (
github.com/Masterminds/semver/v3 v3.3.1
github.com/alecthomas/kong v1.6.1
github.com/alecthomas/mango-kong v0.1.0
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbles v0.20.1-0.20250123134425-4ac5ac9181dc
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1
github.com/charmbracelet/glamour v0.8.0
github.com/charmbracelet/lipgloss v1.0.1-0.20250110214317-ecc1bd014d51
github.com/charmbracelet/lipgloss v1.0.1-0.20250121132900-022e96717265
github.com/charmbracelet/log v0.4.0
github.com/charmbracelet/x/ansi v0.7.0
github.com/charmbracelet/x/ansi v0.7.1-0.20250122132629-a969ddeb820d
github.com/charmbracelet/x/editor v0.1.0
github.com/charmbracelet/x/term v0.2.1
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.15.3-0.20241211131612-0d230cb6eb15
github.com/rivo/uniseg v0.4.7
Expand All @@ -40,6 +39,7 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/yuin/goldmark v1.7.4 // indirect
github.com/yuin/goldmark-emoji v1.0.4 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbles v0.20.1-0.20250123134425-4ac5ac9181dc h1:HhguCmm6PY0ee6zMZy4PN0o4Nao48CSPXb14sICK5F4=
github.com/charmbracelet/bubbles v0.20.1-0.20250123134425-4ac5ac9181dc/go.mod h1:Zotyd8CRrGE7bJVol81jumtVG2WvU/ysXnLXFCCD9Z4=
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1 h1:osd3dk14DEriOrqJBWzeDE9eN2Yd00BkKzFAiLXxkS8=
github.com/charmbracelet/bubbletea v1.2.5-0.20241207142916-e0515bc22ad1/go.mod h1:Hbk5+oE4a7cDyjfdPi4sHZ42aGTMYcmHnVDhsRswn7A=
github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
github.com/charmbracelet/lipgloss v1.0.1-0.20250110214317-ecc1bd014d51 h1:f+0mEkhorXNiBaHb4V9wyd364OH/aF7md7ZngkS+1gU=
github.com/charmbracelet/lipgloss v1.0.1-0.20250110214317-ecc1bd014d51/go.mod h1:QRGthpgH59/perglqXZC8xPHqDGZ9BB45ChJCFEWEMI=
github.com/charmbracelet/lipgloss v1.0.1-0.20250121132900-022e96717265 h1:pezCx+0ILRh9K7dj+D/DJ2rToW9ZHa7owZdgPn264gQ=
github.com/charmbracelet/lipgloss v1.0.1-0.20250121132900-022e96717265/go.mod h1:QRGthpgH59/perglqXZC8xPHqDGZ9BB45ChJCFEWEMI=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.7.0 h1:/QfFmiXOGGwN6fRbzvQaYp7fu1pkxpZ3qFBZWBsP404=
github.com/charmbracelet/x/ansi v0.7.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/charmbracelet/x/ansi v0.7.1-0.20250122132629-a969ddeb820d h1:BXeRCLyo/PS8EQXLOd7Q7uVBJ0GyqULGtY0d0yWmiCk=
github.com/charmbracelet/x/ansi v0.7.1-0.20250122132629-a969ddeb820d/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/charmbracelet/x/editor v0.1.0 h1:p69/dpvlwRTs9uYiPeAWruwsHqTFzHhTvQOd/WVSX98=
github.com/charmbracelet/x/editor v0.1.0/go.mod h1:oivrEbcP/AYt/Hpvk5pwDXXrQ933gQS6UzL6fxqAGSA=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down
34 changes: 18 additions & 16 deletions pager/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package pager

import (
"fmt"
"regexp"

"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/viewport"
Expand All @@ -18,38 +17,41 @@ func (o Options) Run() error {
vp.Style = o.Style.ToLipgloss()

if o.Content == "" {
stdin, err := stdin.Read()
stdin, err := stdin.Read(stdin.StripANSI(true))
if err != nil {
return fmt.Errorf("unable to read stdin")
}
if stdin != "" {
// Sanitize the input from stdin by removing backspace sequences.
backspace := regexp.MustCompile(".\x08")
o.Content = backspace.ReplaceAllString(stdin, "")
o.Content = stdin
} else {
return fmt.Errorf("provide some content to display")
}
}

if o.ShowLineNumbers {
vp.LeftGutterFunc = viewport.LineNumberGutter(o.LineNumberStyle.ToLipgloss())
}

vp.SoftWrap = o.SoftWrap
vp.FillHeight = o.ShowLineNumbers
vp.SetContent(o.Content)
vp.HighlightStyle = o.MatchStyle.ToLipgloss()
vp.SelectedHighlightStyle = o.MatchHighlightStyle.ToLipgloss()

m := model{
viewport: vp,
help: help.New(),
content: o.Content,
origContent: o.Content,
showLineNumbers: o.ShowLineNumbers,
lineNumberStyle: o.LineNumberStyle.ToLipgloss(),
softWrap: o.SoftWrap,
matchStyle: o.MatchStyle.ToLipgloss(),
matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(),
keymap: defaultKeymap(),
viewport: vp,
help: help.New(),
showLineNumbers: o.ShowLineNumbers,
lineNumberStyle: o.LineNumberStyle.ToLipgloss(),
keymap: defaultKeymap(),
}

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

_, err := tea.NewProgram(
m,
tea.WithAltScreen(),
// tea.WithAltScreen(),
tea.WithReportFocus(),
tea.WithContext(ctx),
).Run()
Expand Down
2 changes: 1 addition & 1 deletion pager/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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_"`
Content string `arg:"" optional:"" help:"Display content to scroll"`
ShowLineNumbers bool `help:"Show line numbers" default:"true"`
ShowLineNumbers bool `help:"Show line numbers" default:"true" negatable:""`
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:"true" negatable:""`
MatchStyle style.Styles `embed:"" prefix:"match." help:"Style the matched text" set:"defaultForeground=212" set:"defaultBold=true" envprefix:"GUM_PAGER_MATCH_"` //nolint:staticcheck
Expand Down
Loading
Loading