From 6b42e765ce5316cee2c675bacd1d4162bdf35b2a Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 2 Sep 2021 14:00:37 -0400 Subject: [PATCH 1/3] Viewport now has customizable keybindings --- viewport/keymap.go | 42 ++++++++++++++++++++++++++ viewport/viewport.go | 72 ++++++++++++++++++++++++++++---------------- 2 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 viewport/keymap.go diff --git a/viewport/keymap.go b/viewport/keymap.go new file mode 100644 index 00000000..f410f480 --- /dev/null +++ b/viewport/keymap.go @@ -0,0 +1,42 @@ +package viewport + +import "github.com/charmbracelet/bubbles/key" + +const spacebar = " " + +// KeyMap defines the keybindings for the viewport. Note that you don't +// necessary need to use keybindings at all; the viewport can be controlled +// programmatically with methods like Model.LineDown(1). See the GoDocs for +// details. +type KeyMap struct { + PageDown key.Binding + PageUp key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding + Down key.Binding + Up key.Binding +} + +// DefaultKeyMap returns a set of pager-like default keybindings. +func DefaultKeyMap() KeyMap { + return KeyMap{ + PageDown: key.NewBinding( + key.WithKeys("pgdown", spacebar, "f"), + ), + PageUp: key.NewBinding( + key.WithKeys("pgup", "b"), + ), + HalfPageUp: key.NewBinding( + key.WithKeys("u", "ctrl+u"), + ), + HalfPageDown: key.NewBinding( + key.WithKeys("d", "ctrl+d"), + ), + Up: key.NewBinding( + key.WithKeys("up", "k"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + ), + } +} diff --git a/viewport/viewport.go b/viewport/viewport.go index 0d82ab93..48b3c548 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -4,18 +4,34 @@ import ( "math" "strings" + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" ) -const ( - spacebar = " " - mouseWheelDelta = 3 -) +// NewModel returns a new model with the given width and height as well as +// default keymappings. +func NewModel(width, height int) Model { + return Model{ + Width: width, + Height: height, + KeyMap: DefaultKeyMap(), + MouseWheelEnabled: true, + MouseWheelDelta: 3, + } +} // Model is the Bubble Tea model for this viewport element. type Model struct { Width int Height int + KeyMap KeyMap + + // Whether or not to respond to the mouse. The mouse must be enabled in + // Bubble Tea for this to work. For details, see the Bubble Tea docs. + MouseWheelEnabled bool + + // The number of lines the mouse wheel will scroll. By default, this is 3. + MouseWheelDelta int // YOffset is the vertical scroll position. YOffset int @@ -37,6 +53,11 @@ type Model struct { lines []string } +// Init exists to satisfy the tea.Model interface for composability purposes. +func (m Model) Init() tea.Cmd { + return nil +} + // AtTop returns whether or not the viewport is in the very top position. func (m Model) AtTop() bool { return m.YOffset <= 0 @@ -227,54 +248,52 @@ func ViewUp(m Model, lines []string) tea.Cmd { return tea.ScrollUp(lines, top, bottom) } -// UPDATE - -// Update runs the update loop with default keybindings similar to popular -// pagers. To define your own keybindings use the methods on Model (i.e. -// Model.LineDown()) and define your own update function. +// Update handles standard message-based viewport updates. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd + m, cmd = m.updateAsModel(msg) + return m, cmd +} + +// Author's note: this method has been broken out to make it easier to +// potentially transition Update to satisfy tea.Model. +func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) { + var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: - switch msg.String() { - // Down one page - case "pgdown", spacebar, "f": + switch { + case key.Matches(msg, m.KeyMap.PageDown): lines := m.ViewDown() if m.HighPerformanceRendering { cmd = ViewDown(m, lines) } - // Up one page - case "pgup", "b": + case key.Matches(msg, m.KeyMap.PageUp): lines := m.ViewUp() if m.HighPerformanceRendering { cmd = ViewUp(m, lines) } - // Down half page - case "d", "ctrl+d": + case key.Matches(msg, m.KeyMap.HalfPageDown): lines := m.HalfViewDown() if m.HighPerformanceRendering { cmd = ViewDown(m, lines) } - // Up half page - case "u", "ctrl+u": + case key.Matches(msg, m.KeyMap.HalfPageUp): lines := m.HalfViewUp() if m.HighPerformanceRendering { cmd = ViewUp(m, lines) } - // Down one line - case "down", "j": + case key.Matches(msg, m.KeyMap.Down): lines := m.LineDown(1) if m.HighPerformanceRendering { cmd = ViewDown(m, lines) } - // Up one line - case "up", "k": + case key.Matches(msg, m.KeyMap.Up): lines := m.LineUp(1) if m.HighPerformanceRendering { cmd = ViewUp(m, lines) @@ -282,15 +301,18 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } case tea.MouseMsg: + if !m.MouseWheelEnabled { + break + } switch msg.Type { case tea.MouseWheelUp: - lines := m.LineUp(mouseWheelDelta) + lines := m.LineUp(m.MouseWheelDelta) if m.HighPerformanceRendering { cmd = ViewUp(m, lines) } case tea.MouseWheelDown: - lines := m.LineDown(mouseWheelDelta) + lines := m.LineDown(m.MouseWheelDelta) if m.HighPerformanceRendering { cmd = ViewDown(m, lines) } @@ -300,8 +322,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, cmd } -// VIEW - // View renders the viewport into a string. func (m Model) View() string { if m.HighPerformanceRendering { From cafc23d254e5d9b61c98eddb408a49001dacc97a Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 2 Sep 2021 16:13:22 -0400 Subject: [PATCH 2/3] Add a lipgloss style to the viewport for borders, margins, and padding --- viewport/viewport.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index 48b3c548..d6b07a25 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) // NewModel returns a new model with the given width and height as well as @@ -40,6 +41,10 @@ type Model struct { // window. It's used in high performance rendering only. YPosition int + // Style applies a lipgloss style to the viewport. Realistically, it's most + // useful for setting borders, margins and padding. + Style lipgloss.Style + // HighPerformanceRendering bypasses the normal Bubble Tea renderer to // provide higher performance rendering. Most of the time the normal Bubble // Tea rendering methods will suffice, but if you're passing content with @@ -340,7 +345,7 @@ func (m Model) View() string { extraLines = strings.Repeat("\n", max(0, m.Height-len(lines))) } - return strings.Join(lines, "\n") + extraLines + return m.Style.Render(strings.Join(lines, "\n") + extraLines) } // ETC From a559cfc4386de6d6e48e1235137901ce7afb19e4 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Mon, 10 Jan 2022 18:02:49 -0500 Subject: [PATCH 3/3] Viewport New() is now optional to ease the upgrade process --- viewport/viewport.go | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index d6b07a25..ee89208f 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -9,16 +9,13 @@ import ( "github.com/charmbracelet/lipgloss" ) -// NewModel returns a new model with the given width and height as well as -// default keymappings. -func NewModel(width, height int) Model { - return Model{ - Width: width, - Height: height, - KeyMap: DefaultKeyMap(), - MouseWheelEnabled: true, - MouseWheelDelta: 3, - } +// New returns a new model with the given width and height as well as default +// keymappings. +func New(width, height int) (m Model) { + m.Width = width + m.Height = height + m.setInitialValues() + return m } // Model is the Bubble Tea model for this viewport element. @@ -55,7 +52,15 @@ type Model struct { // which is usually via the alternate screen buffer. HighPerformanceRendering bool - lines []string + initialized bool + lines []string +} + +func (m *Model) setInitialValues() { + m.KeyMap = DefaultKeyMap() + m.MouseWheelEnabled = true + m.MouseWheelDelta = 3 + m.initialized = true } // Init exists to satisfy the tea.Model interface for composability purposes. @@ -263,6 +268,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // Author's note: this method has been broken out to make it easier to // potentially transition Update to satisfy tea.Model. func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) { + if !m.initialized { + m.setInitialValues() + } + var cmd tea.Cmd switch msg := msg.(type) {