diff --git a/pager/command.go b/pager/command.go index 7e849851a..414bee74d 100644 --- a/pager/command.go +++ b/pager/command.go @@ -4,6 +4,7 @@ import ( "fmt" "regexp" + "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/gum/internal/stdin" @@ -32,7 +33,7 @@ func (o Options) Run() error { m := model{ viewport: vp, - helpStyle: o.HelpStyle.ToLipgloss(), + help: help.New(), content: o.Content, origContent: o.Content, showLineNumbers: o.ShowLineNumbers, @@ -40,6 +41,7 @@ func (o Options) Run() error { softWrap: o.SoftWrap, matchStyle: o.MatchStyle.ToLipgloss(), matchHighlightStyle: o.MatchHighlightStyle.ToLipgloss(), + keymap: defaultKeymap(), } ctx, cancel := timeout.Context(o.Timeout) diff --git a/pager/options.go b/pager/options.go index 15e479a74..c9a0a5505 100644 --- a/pager/options.go +++ b/pager/options.go @@ -10,7 +10,6 @@ import ( 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_"` @@ -18,4 +17,7 @@ type Options struct { 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:"0s" env:"GUM_PAGER_TIMEOUT"` + + // Deprecated: this has no effect anymore. + HelpStyle style.Styles `embed:"" prefix:"help." help:"Style the help text" set:"defaultForeground=241" envprefix:"GUM_PAGER_HELP_" hidden:""` } diff --git a/pager/pager.go b/pager/pager.go index 26acf06cd..ea689bc94 100644 --- a/pager/pager.go +++ b/pager/pager.go @@ -7,6 +7,8 @@ import ( "fmt" "strings" + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" @@ -14,11 +16,83 @@ import ( "github.com/muesli/reflow/truncate" ) +type keymap struct { + Home, + End, + Search, + NextMatch, + PrevMatch, + Abort, + Quit, + ConfirmSearch, + CancelSearch key.Binding +} + +// FullHelp implements help.KeyMap. +func (k keymap) FullHelp() [][]key.Binding { + return nil +} + +// ShortHelp implements help.KeyMap. +func (k keymap) ShortHelp() []key.Binding { + return []key.Binding{ + key.NewBinding( + key.WithKeys("up", "down"), + key.WithHelp("↑/↓", "navigate"), + ), + k.Quit, + k.Search, + k.NextMatch, + k.PrevMatch, + } +} + +func defaultKeymap() keymap { + return keymap{ + Home: key.NewBinding( + key.WithKeys("g", "home"), + key.WithHelp("h", "home"), + ), + End: key.NewBinding( + key.WithKeys("G", "end"), + key.WithHelp("G", "end"), + ), + Search: key.NewBinding( + key.WithKeys("/"), + key.WithHelp("/", "search"), + ), + PrevMatch: key.NewBinding( + key.WithKeys("p", "N"), + key.WithHelp("N", "previous match"), + ), + NextMatch: key.NewBinding( + key.WithKeys("n"), + key.WithHelp("n", "next match"), + ), + Abort: key.NewBinding( + key.WithKeys("ctrl+c"), + key.WithHelp("ctrl+c", "abort"), + ), + Quit: key.NewBinding( + key.WithKeys("q", "esc"), + key.WithHelp("esc", "quit"), + ), + ConfirmSearch: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "confirm"), + ), + CancelSearch: key.NewBinding( + key.WithKeys("ctrl+c", "ctrl+d", "esc"), + key.WithHelp("ctrl+c", "cancel"), + ), + } +} + type model struct { content string origContent string viewport viewport.Model - helpStyle lipgloss.Style + help help.Model showLineNumbers bool lineNumberStyle lipgloss.Style softWrap bool @@ -26,6 +100,7 @@ type model struct { matchStyle lipgloss.Style matchHighlightStyle lipgloss.Style maxWidth int + keymap keymap } func (m model) Init() tea.Cmd { return nil } @@ -38,13 +113,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.keyHandler(msg) } + m.keymap.PrevMatch.SetEnabled(m.search.query != nil) + m.keymap.NextMatch.SetEnabled(m.search.query != nil) + var cmd tea.Cmd m.search.input, cmd = m.search.input.Update(msg) return m, cmd } +func (m *model) helpView() string { + return "\n" + m.help.View(m.keymap) +} + func (m *model) processText(msg tea.WindowSizeMsg) { - m.viewport.Height = msg.Height - lipgloss.Height(m.helpStyle.Render("?")) - 1 + m.viewport.Height = msg.Height - lipgloss.Height(m.helpView()) m.viewport.Width = msg.Width textStyle := lipgloss.NewStyle().Width(m.viewport.Width) var text strings.Builder @@ -87,11 +169,12 @@ func (m *model) processText(msg tea.WindowSizeMsg) { const heightOffset = 2 -func (m model) keyHandler(key tea.KeyMsg) (model, tea.Cmd) { +func (m model) keyHandler(msg tea.KeyMsg) (model, tea.Cmd) { + km := m.keymap var cmd tea.Cmd if m.search.active { - switch key.String() { - case "enter": + switch { + case key.Matches(msg, km.ConfirmSearch): if m.search.input.Value() != "" { m.content = m.origContent m.search.Execute(&m) @@ -102,47 +185,41 @@ func (m model) keyHandler(key tea.KeyMsg) (model, tea.Cmd) { } else { m.search.Done() } - case "ctrl+d", "ctrl+c", "esc": + case key.Matches(msg, km.CancelSearch): m.search.Done() default: - m.search.input, cmd = m.search.input.Update(key) + m.search.input, cmd = m.search.input.Update(msg) } } else { - switch key.String() { - case "g", "home": + switch { + case key.Matches(msg, km.Home): m.viewport.GotoTop() - case "G", "end": + case key.Matches(msg, km.End): m.viewport.GotoBottom() - case "/": + case key.Matches(msg, km.Search): m.search.Begin() return m, textinput.Blink - case "p", "N": + case key.Matches(msg, km.PrevMatch): m.search.PrevMatch(&m) m.processText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width}) - case "n": + case key.Matches(msg, km.NextMatch): m.search.NextMatch(&m) m.processText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width}) - case "q", "esc": + case key.Matches(msg, km.Quit): return m, tea.Quit - case "ctrl+c": + case key.Matches(msg, km.Abort): return m, tea.Interrupt } - m.viewport, cmd = m.viewport.Update(key) + m.viewport, cmd = m.viewport.Update(msg) } return m, cmd } func (m model) View() string { - // TODO: use help bubble here - helpMsg := "\n ↑/↓: 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() + m.helpStyle.Render(helpMsg) + return m.viewport.View() + m.helpView() }