make keybinds configurable and consolidate

This commit is contained in:
2026-07-28 14:55:38 +02:00
parent bb8e91039f
commit 948f3e1a79
12 changed files with 1469 additions and 158 deletions

View File

@@ -39,12 +39,13 @@ type textEditor struct {
err error
hardwareCursor bool
highlightMarkdown bool
keys KeyBindings
}
func newTextEditor(text string, modal bool) textEditor {
editor := textEditor{
Text: text, Modal: modal, Mode: textEditorInsert,
clipboard: systemTextClipboard{},
clipboard: systemTextClipboard{}, keys: defaultKeyBindings(),
}
editor.Cursor = len([]rune(text))
if modal {
@@ -63,7 +64,7 @@ func (e *textEditor) handleKeyAtWidth(key tea.KeyMsg, multiline bool, wrapWidth
return e.handleStandardKey(key, multiline, wrapWidth)
}
if e.Mode == textEditorInsert {
if key.String() == "esc" {
if keyMatches(key.String(), e.keys.Input.Cancel) {
e.Mode = textEditorNormal
e.clearPending()
start, _ := editorLineBounds(e.Text, e.Cursor, wrapWidth)
@@ -87,30 +88,35 @@ func (e *textEditor) movePage(delta, wrapWidth int) {
}
func (e *textEditor) handleStandardKey(key tea.KeyMsg, multiline bool, wrapWidth int) bool {
switch key.String() {
case "left":
k := key.String()
switch {
case key.Type != tea.KeyRunes && key.Type != tea.KeySpace &&
keyMatches(k, e.keys.Navigation.Left):
e.Cursor = max(0, e.Cursor-1)
case "right":
case key.Type != tea.KeyRunes && key.Type != tea.KeySpace &&
keyMatches(k, e.keys.Navigation.Right):
e.Cursor = min(len([]rune(e.Text)), e.Cursor+1)
case "up":
case key.Type != tea.KeyRunes && key.Type != tea.KeySpace &&
keyMatches(k, e.keys.Navigation.Up):
if !multiline {
return false
}
e.Cursor = moveEditorCursorLine(e.Text, e.Cursor, -1, wrapWidth, false)
case "down":
case key.Type != tea.KeyRunes && key.Type != tea.KeySpace &&
keyMatches(k, e.keys.Navigation.Down):
if !multiline {
return false
}
e.Cursor = moveEditorCursorLine(e.Text, e.Cursor, 1, wrapWidth, false)
case "home", "ctrl+a":
case keyMatches(k, e.keys.Input.LineStart):
e.Cursor, _ = editorLineBounds(e.Text, e.Cursor, wrapWidth)
case "end", "ctrl+e":
case keyMatches(k, e.keys.Input.LineEnd):
_, e.Cursor = editorLineBounds(e.Text, e.Cursor, wrapWidth)
case "backspace":
case keyMatches(k, e.keys.Input.DeleteBackward):
e.deleteBefore()
case "delete":
case keyMatches(k, e.keys.Input.DeleteForward):
e.deleteAt()
case "enter":
case keyMatches(k, e.keys.Input.Newline):
if !multiline {
return false
}
@@ -145,36 +151,37 @@ func (e *textEditor) handleNormalKey(key tea.KeyMsg, multiline bool, wrapWidth i
}
if e.pendingG {
e.pendingG = false
if key.String() == "g" {
if keyMatches(key.String(), e.keys.Vim.GoPrefix) {
e.Cursor = firstNonBlank(e.Text, 0)
return true
}
}
switch key.String() {
case "esc":
k := key.String()
switch {
case keyMatches(k, e.keys.Input.Cancel):
e.clearPending()
return false
case "i":
case keyMatches(k, e.keys.Vim.Insert):
e.Mode = textEditorInsert
case "a":
case keyMatches(k, e.keys.Vim.Append):
_, end := editorLineBounds(e.Text, e.Cursor, wrapWidth)
e.Cursor = min(end, e.Cursor+1)
e.Mode = textEditorInsert
case "I":
case keyMatches(k, e.keys.Vim.InsertLineStart):
e.Cursor = firstNonBlankAtWidth(e.Text, e.Cursor, wrapWidth)
e.Mode = textEditorInsert
case "A":
case keyMatches(k, e.keys.Vim.AppendLineEnd):
_, e.Cursor = editorLineBounds(e.Text, e.Cursor, wrapWidth)
e.Mode = textEditorInsert
case "o":
case keyMatches(k, e.keys.Vim.OpenBelow):
if !multiline {
return false
}
_, e.Cursor = editorLineBounds(e.Text, e.Cursor, wrapWidth)
e.insert("\n")
e.Mode = textEditorInsert
case "O":
case keyMatches(k, e.keys.Vim.OpenAbove):
if !multiline {
return false
}
@@ -183,69 +190,75 @@ func (e *textEditor) handleNormalKey(key tea.KeyMsg, multiline bool, wrapWidth i
e.insert("\n")
e.Cursor = start
e.Mode = textEditorInsert
case "h", "left":
case keyMatches(k, e.keys.Navigation.Left):
start, _ := editorLineBounds(e.Text, e.Cursor, wrapWidth)
e.Cursor = max(start, e.Cursor-1)
case "l", "right":
case keyMatches(k, e.keys.Navigation.Right):
e.Cursor = min(normalEditorLineLast(e.Text, e.Cursor, wrapWidth), e.Cursor+1)
case "j", "down":
case keyMatches(k, e.keys.Navigation.Down):
if multiline {
e.Cursor = moveEditorCursorLine(e.Text, e.Cursor, 1, wrapWidth, true)
}
case "k", "up":
case keyMatches(k, e.keys.Navigation.Up):
if multiline {
e.Cursor = moveEditorCursorLine(e.Text, e.Cursor, -1, wrapWidth, true)
}
case "0", "home":
case keyMatches(k, e.keys.Vim.LineStart):
e.Cursor, _ = editorLineBounds(e.Text, e.Cursor, wrapWidth)
case "^":
case keyMatches(k, e.keys.Vim.FirstNonBlank):
e.Cursor = firstNonBlankAtWidth(e.Text, e.Cursor, wrapWidth)
case "$", "end":
case keyMatches(k, e.keys.Vim.LineEnd):
e.Cursor = normalEditorLineLast(e.Text, e.Cursor, wrapWidth)
case "w":
case keyMatches(k, e.keys.Vim.WordForward):
e.Cursor = nextWordStart(e.Text, e.Cursor, false)
case "W":
case keyMatches(k, e.keys.Vim.WORDForward):
e.Cursor = nextWordStart(e.Text, e.Cursor, true)
case "b":
case keyMatches(k, e.keys.Vim.WordBackward):
e.Cursor = previousWordStart(e.Text, e.Cursor, false)
case "B":
case keyMatches(k, e.keys.Vim.WORDBackward):
e.Cursor = previousWordStart(e.Text, e.Cursor, true)
case "e":
case keyMatches(k, e.keys.Vim.WordEnd):
e.Cursor = wordEndAtWidth(e.Text, e.Cursor, false, wrapWidth)
case "E":
case keyMatches(k, e.keys.Vim.WORDEnd):
e.Cursor = wordEndAtWidth(e.Text, e.Cursor, true, wrapWidth)
case "g":
case keyMatches(k, e.keys.Vim.GoPrefix):
e.pendingG = true
case "G":
case keyMatches(k, e.keys.Navigation.Last):
e.Cursor = firstNonBlank(e.Text, len([]rune(e.Text)))
case "v":
case keyMatches(k, e.keys.Vim.Visual):
e.startVisual(false)
case "V":
case keyMatches(k, e.keys.Vim.VisualLine):
e.startVisual(true)
case "p":
case keyMatches(k, e.keys.Vim.Paste):
e.pasteClipboard(false, wrapWidth)
case "s":
case keyMatches(k, e.keys.Vim.ReplaceCharacter):
_, end := editorLineBounds(e.Text, e.Cursor, wrapWidth)
if e.Cursor < end {
e.deleteAt()
}
e.Mode = textEditorInsert
case "f", "F", "t", "T":
e.pendingFind = []rune(key.String())[0]
case ";":
case keyMatches(k, e.keys.Vim.FindForward):
e.pendingFind = 'f'
case keyMatches(k, e.keys.Vim.FindBackward):
e.pendingFind = 'F'
case keyMatches(k, e.keys.Vim.TillForward):
e.pendingFind = 't'
case keyMatches(k, e.keys.Vim.TillBackward):
e.pendingFind = 'T'
case keyMatches(k, e.keys.Vim.RepeatFind):
if e.lastFind.valid {
e.performFind(e.lastFind.command, e.lastFind.target, false, wrapWidth)
}
case ",":
case keyMatches(k, e.keys.Vim.RepeatFindReverse):
if e.lastFind.valid {
e.performFind(reverseFind(e.lastFind.command), e.lastFind.target, false, wrapWidth)
}
case "x", "delete":
case keyMatches(k, e.keys.Vim.Delete):
_, end := editorLineBounds(e.Text, e.Cursor, wrapWidth)
if e.Cursor < end {
e.deleteAt()
}
case "X", "backspace":
case keyMatches(k, e.keys.Vim.DeleteBefore):
start, _ := editorLineBounds(e.Text, e.Cursor, wrapWidth)
if e.Cursor > start {
e.deleteBefore()
@@ -263,25 +276,26 @@ func (e *textEditor) handleVisualKey(key tea.KeyMsg, multiline bool, wrapWidth i
e.Mode = textEditorVisual
return handled
}
switch key.String() {
case "esc", "v":
k := key.String()
switch {
case keyMatches(k, e.keys.Input.Cancel), keyMatches(k, e.keys.Vim.Visual):
e.stopVisual()
case "V":
case keyMatches(k, e.keys.Vim.VisualLine):
if e.visualLine {
e.stopVisual()
} else {
e.visualLine = true
}
case "o":
case keyMatches(k, e.keys.Vim.SelectionOtherEnd):
e.Cursor, e.visualAnchor = e.visualAnchor, e.Cursor
case "y":
case keyMatches(k, e.keys.Vim.Yank):
e.yankSelection(wrapWidth)
case "d", "x", "delete":
case keyMatches(k, e.keys.Vim.Delete):
e.deleteSelection(wrapWidth)
case "p":
case keyMatches(k, e.keys.Vim.Paste):
e.pasteClipboard(true, wrapWidth)
default:
if !isVisualMotion(key.String()) {
if !e.isVisualMotion(k) {
return false
}
e.Mode = textEditorNormal
@@ -292,16 +306,24 @@ func (e *textEditor) handleVisualKey(key tea.KeyMsg, multiline bool, wrapWidth i
return true
}
func isVisualMotion(key string) bool {
switch key {
case "h", "j", "k", "l", "left", "down", "up", "right",
"0", "^", "$", "home", "end",
"w", "W", "b", "B", "e", "E", "g", "G",
"f", "F", "t", "T", ";", ",":
return true
default:
return false
func (e textEditor) isVisualMotion(key string) bool {
groups := [][]string{
e.keys.Navigation.Left, e.keys.Navigation.Down, e.keys.Navigation.Up,
e.keys.Navigation.Right, e.keys.Navigation.Last,
e.keys.Vim.LineStart, e.keys.Vim.FirstNonBlank, e.keys.Vim.LineEnd,
e.keys.Vim.WordForward, e.keys.Vim.WORDForward,
e.keys.Vim.WordBackward, e.keys.Vim.WORDBackward,
e.keys.Vim.WordEnd, e.keys.Vim.WORDEnd, e.keys.Vim.GoPrefix,
e.keys.Vim.FindForward, e.keys.Vim.FindBackward,
e.keys.Vim.TillForward, e.keys.Vim.TillBackward,
e.keys.Vim.RepeatFind, e.keys.Vim.RepeatFindReverse,
}
for _, group := range groups {
if keyMatches(key, group) {
return true
}
}
return false
}
func (e *textEditor) startVisual(linewise bool) {