feat: support key bindings in textinput like textarea

This commit is contained in:
Raphael 'kena' Poss 2022-10-07 14:26:23 +02:00 committed by Maas Lalani
parent b2b7040ccf
commit 9a48dca003

View File

@ -7,6 +7,7 @@ import (
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/cursor" "github.com/charmbracelet/bubbles/cursor"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
rw "github.com/mattn/go-runewidth" rw "github.com/mattn/go-runewidth"
@ -37,6 +38,41 @@ const (
// ValidateFunc is a function that returns an error if the input is invalid. // ValidateFunc is a function that returns an error if the input is invalid.
type ValidateFunc func(string) error type ValidateFunc func(string) error
// KeyMap is the key bindings for different actions within the textinput.
type KeyMap struct {
CharacterForward key.Binding
CharacterBackward key.Binding
WordForward key.Binding
WordBackward key.Binding
DeleteWordBackward key.Binding
DeleteWordForward key.Binding
DeleteAfterCursor key.Binding
DeleteBeforeCursor key.Binding
DeleteCharacterBackward key.Binding
DeleteCharacterForward key.Binding
LineStart key.Binding
LineEnd key.Binding
Paste key.Binding
}
// DefaultKeyMap is the default set of key bindings for navigating and acting
// upon the textinput.
var DefaultKeyMap = KeyMap{
CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f")),
CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b")),
WordForward: key.NewBinding(key.WithKeys("alt+right", "alt+f")),
WordBackward: key.NewBinding(key.WithKeys("alt+left", "alt+b")),
DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w")),
DeleteWordForward: key.NewBinding(key.WithKeys("alte+delete", "alt+d")),
DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k")),
DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u")),
DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h")),
DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d")),
LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")),
LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")),
Paste: key.NewBinding(key.WithKeys("ctrl+v")),
}
// Model is the Bubble Tea model for this text input element. // Model is the Bubble Tea model for this text input element.
type Model struct { type Model struct {
Err error Err error
@ -71,6 +107,9 @@ type Model struct {
// viewport. If 0 or less this setting is ignored. // viewport. If 0 or less this setting is ignored.
Width int Width int
// KeyMap encodes the keybindings recognized by the widget.
KeyMap KeyMap
// Underlying text value. // Underlying text value.
value []rune value []rune
@ -101,6 +140,7 @@ func New() Model {
CharLimit: 0, CharLimit: 0,
PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
Cursor: cursor.New(), Cursor: cursor.New(),
KeyMap: DefaultKeyMap,
value: nil, value: nil,
focus: false, focus: false,
@ -300,8 +340,8 @@ func (m *Model) deleteAfterCursor() {
m.SetCursor(len(m.value)) m.SetCursor(len(m.value))
} }
// deleteWordLeft deletes the word left to the cursor. // deleteWordBackward deletes the word left to the cursor.
func (m *Model) deleteWordLeft() { func (m *Model) deleteWordBackward() {
if m.pos == 0 || len(m.value) == 0 { if m.pos == 0 || len(m.value) == 0 {
return return
} }
@ -344,10 +384,10 @@ func (m *Model) deleteWordLeft() {
} }
} }
// deleteWordRight deletes the word right to the cursor If input is masked // deleteWordForward deletes the word right to the cursor If input is masked
// delete everything after the cursor so as not to reveal word breaks in the // delete everything after the cursor so as not to reveal word breaks in the
// masked input. // masked input.
func (m *Model) deleteWordRight() { func (m *Model) deleteWordForward() {
if m.pos >= len(m.value) || len(m.value) == 0 { if m.pos >= len(m.value) || len(m.value) == 0 {
return return
} }
@ -385,9 +425,9 @@ func (m *Model) deleteWordRight() {
m.SetCursor(oldPos) m.SetCursor(oldPos)
} }
// wordLeft moves the cursor one word to the left. If input is masked, move // wordBackward moves the cursor one word to the left. If input is masked, move
// input to the start so as not to reveal word breaks in the masked input. // input to the start so as not to reveal word breaks in the masked input.
func (m *Model) wordLeft() { func (m *Model) wordBackward() {
if m.pos == 0 || len(m.value) == 0 { if m.pos == 0 || len(m.value) == 0 {
return return
} }
@ -417,9 +457,9 @@ func (m *Model) wordLeft() {
} }
} }
// wordRight moves the cursor one word to the right. If the input is masked, // wordForward moves the cursor one word to the right. If the input is masked,
// move input to the end so as not to reveal word breaks in the masked input. // move input to the end so as not to reveal word breaks in the masked input.
func (m *Model) wordRight() { func (m *Model) wordForward() {
if m.pos >= len(m.value) || len(m.value) == 0 { if m.pos >= len(m.value) || len(m.value) == 0 {
return return
} }
@ -473,68 +513,49 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.Type { switch {
case tea.KeyBackspace, tea.KeyCtrlH: // delete character before cursor case key.Matches(msg, m.KeyMap.DeleteWordBackward):
m.Err = nil m.Err = nil
m.deleteWordBackward()
if msg.Alt { case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
m.deleteWordLeft() m.Err = nil
} else { if len(m.value) > 0 {
if len(m.value) > 0 { m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...) if m.pos > 0 {
if m.pos > 0 { m.SetCursor(m.pos - 1)
m.SetCursor(m.pos - 1)
}
} }
} }
case tea.KeyLeft, tea.KeyCtrlB: case key.Matches(msg, m.KeyMap.WordBackward):
if msg.Alt { // alt+left arrow, back one word m.wordBackward()
m.wordLeft() case key.Matches(msg, m.KeyMap.CharacterBackward):
break if m.pos > 0 {
}
if m.pos > 0 { // left arrow, ^F, back one character
m.SetCursor(m.pos - 1) m.SetCursor(m.pos - 1)
} }
case tea.KeyRight, tea.KeyCtrlF: case key.Matches(msg, m.KeyMap.WordForward):
if msg.Alt { // alt+right arrow, forward one word m.wordForward()
m.wordRight() case key.Matches(msg, m.KeyMap.CharacterForward):
break if m.pos < len(m.value) {
}
if m.pos < len(m.value) { // right arrow, ^F, forward one character
m.SetCursor(m.pos + 1) m.SetCursor(m.pos + 1)
} }
case tea.KeyCtrlW: // ^W, delete word left of cursor case key.Matches(msg, m.KeyMap.DeleteWordBackward):
m.deleteWordLeft() m.deleteWordBackward()
case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning case key.Matches(msg, m.KeyMap.LineStart):
m.CursorStart() m.CursorStart()
case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
if len(m.value) > 0 && m.pos < len(m.value) { if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...) m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
} }
case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end case key.Matches(msg, m.KeyMap.LineEnd):
m.CursorEnd() m.CursorEnd()
case tea.KeyCtrlK: // ^K, kill text after cursor case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
m.deleteAfterCursor() m.deleteAfterCursor()
case tea.KeyCtrlU: // ^U, kill text before cursor case key.Matches(msg, m.KeyMap.DeleteBeforeCursor):
m.deleteBeforeCursor() m.deleteBeforeCursor()
case tea.KeyCtrlV: // ^V paste case key.Matches(msg, m.KeyMap.Paste):
return m, Paste return m, Paste
case tea.KeyRunes, tea.KeySpace: // input regular characters case key.Matches(msg, m.KeyMap.DeleteWordForward):
if msg.Alt && len(msg.Runes) == 1 { m.deleteWordForward()
if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor default:
m.deleteWordRight()
break
}
if msg.Runes[0] == 'b' { // alt+b, back one word
m.wordLeft()
break
}
if msg.Runes[0] == 'f' { // alt+f, forward one word
m.wordRight()
break
}
}
// Input a regular character // Input a regular character
if m.CharLimit <= 0 || len(m.value) < m.CharLimit { if m.CharLimit <= 0 || len(m.value) < m.CharLimit {
runes := msg.Runes runes := msg.Runes