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

SUP-1755 Add keybindings type #195

Merged
merged 5 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions internal/keys/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Package keys provides types and function for defining user-configurable keymappings in bubbletea components.
// Example:
//
// var quit = NewBinding(
// WithKeys("q"),
// WithAction(func(m tea.Model) any {
// return tea.Quit()
// }),
// WithHelp("q", "quit"),
// )
package keys

import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
)

// Action is a callback function executed when a keybinding is pressed
type Action func(tea.Model) any

// Help is help information for a given keybinding.
type Help struct {
Key string
Desc string
}

// Binding describes a set of keybindings and a callback to execute when pressed, along with their associated help text.
type Binding struct {
*key.Binding
action Action
}

// BindingOpt is an initialization option for a keybinding. It's used as an
// argument to NewBinding.
type BindingOpt func(*Binding)

// NewBinding returns a new keybinding from a set of BindingOpt options.
func NewBinding(opts ...BindingOpt) Binding {
innerBind := key.NewBinding()
b := Binding{
Binding: &innerBind,
}
for _, opt := range opts {
opt(&b)
}
return b
}

// WithAction initializes a keybinding with the given callback to execute when pressed.
func WithAction(act Action) BindingOpt {
return func(b *Binding) {
b.action = act
}
}

// WithKeys initializes a keybinding with the given keystrokes.
func WithKeys(keys ...string) BindingOpt {
return func(b *Binding) {
key.WithKeys(keys...)(b.Binding)
}
}

// WithHelp initializes a keybinding with the given help text.
func WithHelp(k, desc string) BindingOpt {
return func(b *Binding) {
key.WithHelp(k, desc)(b.Binding)
}
}

// WithDisabled initializes a disabled keybinding.
func WithDisabled() BindingOpt {
return func(b *Binding) {
key.WithDisabled()(b.Binding)
}
}

// ExecuteAction calls the action for the keybinding
func (b *Binding) ExecuteAction(model tea.Model) any {
return b.action(model)
}
90 changes: 90 additions & 0 deletions internal/list/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package list

import (
"github.com/buildkite/cli/v3/internal/keys"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
)

type KeyMap []keys.Binding

// FullHelp implements help.KeyMap.
func (km KeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
km.AsBindings(),
}
}

// ShortHelp implements help.KeyMap.
func (km KeyMap) ShortHelp() []key.Binding {
return km.AsBindings()
}

type selectable interface {
MoveDown()
MoveTop()
MoveBottom()
MoveUp()
}

func (km KeyMap) AsBindings() []key.Binding {
bindings := make([]key.Binding, len(km))

for i, b := range km {
bindings[i] = *b.Binding
}

return bindings
}

func DefaultKeyMap() KeyMap {
return KeyMap{
keys.NewBinding(
keys.WithKeys("q"),
keys.WithHelp("q", "quit"),
keys.WithAction(func(tea.Model) any {
return tea.Quit()
}),
),
keys.NewBinding(
keys.WithKeys("k", "up"),
keys.WithHelp("↑/k", "up"),
keys.WithAction(func(m tea.Model) any {
if l, ok := m.(selectable); ok {
l.MoveUp()
}
return nil
}),
),
keys.NewBinding(
keys.WithKeys("j", "down"),
keys.WithHelp("↓/j", "down"),
keys.WithAction(func(m tea.Model) any {
if l, ok := m.(selectable); ok {
l.MoveDown()
}
return nil
}),
),
keys.NewBinding(
keys.WithKeys("home", "g"),
keys.WithHelp("g/home", "go to start"),
keys.WithAction(func(m tea.Model) any {
if l, ok := m.(selectable); ok {
l.MoveTop()
}
return nil
}),
),
keys.NewBinding(
keys.WithKeys("end", "G"),
keys.WithHelp("G/end", "go to end"),
keys.WithAction(func(m tea.Model) any {
if l, ok := m.(selectable); ok {
l.MoveBottom()
}
return nil
}),
),
}
}
58 changes: 58 additions & 0 deletions internal/list/keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package list_test

import (
"testing"

"github.com/buildkite/cli/v3/internal/keys"
"github.com/buildkite/cli/v3/internal/list"
"github.com/charmbracelet/bubbles/help"
)

func TestKeyMapHelpMenu(t *testing.T) {
t.Parallel()

t.Run("it renders a help menu", func(t *testing.T) {
t.Parallel()

keymap := list.DefaultKeyMap()
help := help.New()

got := help.View(keymap)
want := "q quit • ↑/k up • ↓/j down • g/home go to start • G/end go to end"

if got != want {
t.Fatalf("Output does not match expected. %s != %s", got, want)
}
})

t.Run("it adds more menu items", func(t *testing.T) {
t.Parallel()

keymap := list.KeyMap([]keys.Binding{keys.NewBinding(keys.WithKeys("v"), keys.WithHelp("v", "view"))})
help := help.New()

got := help.View(keymap)
want := "v view"

if got != want {
t.Fatalf("Output does not match expected. %s != %s", got, want)
}
})

t.Run("it doesnt show disabled bindings", func(t *testing.T) {
t.Parallel()

keymap := list.KeyMap([]keys.Binding{
keys.NewBinding(keys.WithKeys("v"), keys.WithHelp("v", "view")),
keys.NewBinding(keys.WithKeys("b"), keys.WithHelp("w", "open in browser"), keys.WithDisabled()),
})
help := help.New()

got := help.View(keymap)
want := "v view"

if got != want {
t.Fatalf("Output does not match expected. %s != %s", got, want)
}
})
}