744 lines
26 KiB
Go
744 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type KeyBindings struct {
|
|
General GeneralKeyBindings `toml:"general"`
|
|
Navigation NavigationKeyBindings `toml:"navigation"`
|
|
Views ViewKeyBindings `toml:"views"`
|
|
Threads ThreadKeyBindings `toml:"threads"`
|
|
Input InputKeyBindings `toml:"input"`
|
|
Vim VimKeyBindings `toml:"vim"`
|
|
}
|
|
|
|
type GeneralKeyBindings struct {
|
|
Quit []string `toml:"quit"`
|
|
Help []string `toml:"help"`
|
|
Refresh []string `toml:"refresh"`
|
|
Back []string `toml:"back"`
|
|
Confirm []string `toml:"confirm"`
|
|
Reject []string `toml:"reject"`
|
|
}
|
|
|
|
type NavigationKeyBindings struct {
|
|
Down []string `toml:"down"`
|
|
Up []string `toml:"up"`
|
|
Left []string `toml:"left"`
|
|
Right []string `toml:"right"`
|
|
First []string `toml:"first"`
|
|
Last []string `toml:"last"`
|
|
PageDown []string `toml:"page_down"`
|
|
PageUp []string `toml:"page_up"`
|
|
}
|
|
|
|
type ViewKeyBindings struct {
|
|
Open []string `toml:"open"`
|
|
Dashboard []string `toml:"dashboard"`
|
|
Health []string `toml:"health"`
|
|
Edit []string `toml:"edit"`
|
|
AutoMerge []string `toml:"auto_merge"`
|
|
MergeNow []string `toml:"merge_now"`
|
|
ToggleList []string `toml:"toggle_list"`
|
|
AI []string `toml:"ai"`
|
|
}
|
|
|
|
type ThreadKeyBindings struct {
|
|
Search []string `toml:"search"`
|
|
ClearFilter []string `toml:"clear_filter"`
|
|
NextUnread []string `toml:"next_unread"`
|
|
PreviousUnread []string `toml:"previous_unread"`
|
|
MarkRead []string `toml:"mark_read"`
|
|
Reply []string `toml:"reply"`
|
|
Resolve []string `toml:"resolve"`
|
|
Toggle []string `toml:"toggle"`
|
|
FoldPrefix []string `toml:"fold_prefix"`
|
|
FoldToggle []string `toml:"fold_toggle"`
|
|
}
|
|
|
|
type InputKeyBindings struct {
|
|
Cancel []string `toml:"cancel"`
|
|
Submit []string `toml:"submit"`
|
|
Newline []string `toml:"newline"`
|
|
DeleteBackward []string `toml:"delete_backward"`
|
|
DeleteForward []string `toml:"delete_forward"`
|
|
Clear []string `toml:"clear"`
|
|
NextField []string `toml:"next_field"`
|
|
PreviousField []string `toml:"previous_field"`
|
|
NextCompletion []string `toml:"next_completion"`
|
|
PreviousCompletion []string `toml:"previous_completion"`
|
|
LineStart []string `toml:"line_start"`
|
|
LineEnd []string `toml:"line_end"`
|
|
}
|
|
|
|
type VimKeyBindings struct {
|
|
Insert []string `toml:"insert"`
|
|
Append []string `toml:"append"`
|
|
InsertLineStart []string `toml:"insert_line_start"`
|
|
AppendLineEnd []string `toml:"append_line_end"`
|
|
OpenBelow []string `toml:"open_below"`
|
|
OpenAbove []string `toml:"open_above"`
|
|
ReplaceCharacter []string `toml:"replace_character"`
|
|
Visual []string `toml:"visual"`
|
|
VisualLine []string `toml:"visual_line"`
|
|
SelectionOtherEnd []string `toml:"selection_other_end"`
|
|
Yank []string `toml:"yank"`
|
|
Delete []string `toml:"delete"`
|
|
DeleteBefore []string `toml:"delete_before"`
|
|
Paste []string `toml:"paste"`
|
|
LineStart []string `toml:"line_start"`
|
|
FirstNonBlank []string `toml:"first_non_blank"`
|
|
LineEnd []string `toml:"line_end"`
|
|
WordForward []string `toml:"word_forward"`
|
|
WORDForward []string `toml:"big_word_forward"`
|
|
WordBackward []string `toml:"word_backward"`
|
|
WORDBackward []string `toml:"big_word_backward"`
|
|
WordEnd []string `toml:"word_end"`
|
|
WORDEnd []string `toml:"big_word_end"`
|
|
GoPrefix []string `toml:"go_prefix"`
|
|
FindForward []string `toml:"find_forward"`
|
|
FindBackward []string `toml:"find_backward"`
|
|
TillForward []string `toml:"till_forward"`
|
|
TillBackward []string `toml:"till_backward"`
|
|
RepeatFind []string `toml:"repeat_find"`
|
|
RepeatFindReverse []string `toml:"repeat_find_reverse"`
|
|
}
|
|
|
|
func defaultKeyBindings() KeyBindings {
|
|
return KeyBindings{
|
|
General: GeneralKeyBindings{
|
|
Quit: []string{"q", "ctrl+c"}, Help: []string{"?", "f1"},
|
|
Refresh: []string{"r"}, Back: []string{"b", "esc"},
|
|
Confirm: []string{"y"}, Reject: []string{"n", "esc"},
|
|
},
|
|
Navigation: NavigationKeyBindings{
|
|
Down: []string{"j", "down"}, Up: []string{"k", "up"},
|
|
Left: []string{"h", "left"}, Right: []string{"l", "right"},
|
|
First: []string{"g"}, Last: []string{"G"},
|
|
PageDown: []string{"ctrl+d", "pgdown"}, PageUp: []string{"ctrl+u", "pgup"},
|
|
},
|
|
Views: ViewKeyBindings{
|
|
Open: []string{"enter", "l"}, Dashboard: []string{"d"},
|
|
Health: []string{"H"}, Edit: []string{"e"}, ToggleList: []string{"tab"},
|
|
AutoMerge: []string{"a"}, MergeNow: []string{"M"},
|
|
AI: []string{"A"},
|
|
},
|
|
Threads: ThreadKeyBindings{
|
|
Search: []string{"/"}, ClearFilter: []string{"F"},
|
|
NextUnread: []string{"n"}, PreviousUnread: []string{"N"},
|
|
MarkRead: []string{"m"},
|
|
Reply: []string{"c"}, Resolve: []string{"R"}, Toggle: []string{"enter"},
|
|
FoldPrefix: []string{"z"}, FoldToggle: []string{"a"},
|
|
},
|
|
Input: InputKeyBindings{
|
|
Cancel: []string{"esc"}, Submit: []string{"ctrl+s"}, Newline: []string{"enter"},
|
|
DeleteBackward: []string{"backspace"}, DeleteForward: []string{"delete"},
|
|
Clear: []string{"ctrl+u"}, NextField: []string{"tab"},
|
|
PreviousField: []string{"shift+tab"}, NextCompletion: []string{"ctrl+n"},
|
|
PreviousCompletion: []string{"ctrl+p"},
|
|
LineStart: []string{"home", "ctrl+a"}, LineEnd: []string{"end", "ctrl+e"},
|
|
},
|
|
Vim: VimKeyBindings{
|
|
Insert: []string{"i"}, Append: []string{"a"},
|
|
InsertLineStart: []string{"I"}, AppendLineEnd: []string{"A"},
|
|
OpenBelow: []string{"o"}, OpenAbove: []string{"O"},
|
|
ReplaceCharacter: []string{"s"}, Visual: []string{"v"}, VisualLine: []string{"V"},
|
|
SelectionOtherEnd: []string{"o"}, Yank: []string{"y"},
|
|
Delete: []string{"d", "x", "delete"}, DeleteBefore: []string{"X", "backspace"},
|
|
Paste: []string{"p"}, LineStart: []string{"0", "home"},
|
|
FirstNonBlank: []string{"^"}, LineEnd: []string{"$", "end"},
|
|
WordForward: []string{"w"}, WORDForward: []string{"W"},
|
|
WordBackward: []string{"b"}, WORDBackward: []string{"B"},
|
|
WordEnd: []string{"e"}, WORDEnd: []string{"E"}, GoPrefix: []string{"g"},
|
|
FindForward: []string{"f"}, FindBackward: []string{"F"},
|
|
TillForward: []string{"t"}, TillBackward: []string{"T"},
|
|
RepeatFind: []string{";"}, RepeatFindReverse: []string{","},
|
|
},
|
|
}
|
|
}
|
|
|
|
func keyMatches(key string, bindings []string) bool {
|
|
for _, binding := range bindings {
|
|
if key == binding {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func keyLabel(bindings []string) string {
|
|
return strings.Join(bindings, " / ")
|
|
}
|
|
|
|
func primaryKeyLabel(bindings []string) string {
|
|
if len(bindings) == 0 {
|
|
return ""
|
|
}
|
|
return bindings[0]
|
|
}
|
|
|
|
func primaryCombinedKeyLabel(groups ...[]string) string {
|
|
keys := make([]string, 0, len(groups))
|
|
for _, group := range groups {
|
|
if key := primaryKeyLabel(group); key != "" {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
return strings.Join(keys, " / ")
|
|
}
|
|
|
|
func primarySequenceKeyLabel(prefixes, suffixes []string) string {
|
|
return primaryKeyLabel(prefixes) + primaryKeyLabel(suffixes)
|
|
}
|
|
|
|
func combinedKeyLabel(groups ...[]string) string {
|
|
var keys []string
|
|
for _, group := range groups {
|
|
keys = append(keys, group...)
|
|
}
|
|
return keyLabel(keys)
|
|
}
|
|
|
|
func sequenceKeyLabel(prefixes, suffixes []string) string {
|
|
var sequences []string
|
|
for _, prefix := range prefixes {
|
|
for _, suffix := range suffixes {
|
|
sequences = append(sequences, prefix+suffix)
|
|
}
|
|
}
|
|
return keyLabel(sequences)
|
|
}
|
|
|
|
func (k KeyBindings) canonicalHelpKey(key string) string {
|
|
switch {
|
|
case keyMatches(key, k.General.Quit):
|
|
return "ctrl+c"
|
|
case keyMatches(key, k.General.Help), keyMatches(key, k.General.Back):
|
|
return "esc"
|
|
case keyMatches(key, k.Navigation.Down):
|
|
return "j"
|
|
case keyMatches(key, k.Navigation.Up):
|
|
return "k"
|
|
case keyMatches(key, k.Navigation.First):
|
|
return "g"
|
|
case keyMatches(key, k.Navigation.Last):
|
|
return "G"
|
|
case keyMatches(key, k.Navigation.PageDown):
|
|
return "ctrl+d"
|
|
case keyMatches(key, k.Navigation.PageUp):
|
|
return "ctrl+u"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (k KeyBindings) canonicalSearchKey(key string) string {
|
|
switch {
|
|
case keyMatches(key, k.Input.Cancel):
|
|
return "esc"
|
|
case keyMatches(key, k.Input.Newline):
|
|
return "enter"
|
|
case keyMatches(key, k.Input.PreviousCompletion):
|
|
return "up"
|
|
case keyMatches(key, k.Input.NextCompletion):
|
|
return "down"
|
|
case keyMatches(key, k.Input.DeleteBackward):
|
|
return "backspace"
|
|
case keyMatches(key, k.Input.Clear):
|
|
return "ctrl+u"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (k KeyBindings) canonicalWriteKey(key string) string {
|
|
switch {
|
|
case keyMatches(key, k.Input.Cancel):
|
|
return "esc"
|
|
case keyMatches(key, k.General.Confirm):
|
|
return "y"
|
|
case keyMatches(key, k.General.Reject):
|
|
return "n"
|
|
case keyMatches(key, k.Input.Submit):
|
|
return "ctrl+s"
|
|
case keyMatches(key, k.Input.Newline):
|
|
return "enter"
|
|
case keyMatches(key, k.Input.DeleteBackward):
|
|
return "backspace"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (k KeyBindings) canonicalMainKey(key string, current screen) string {
|
|
switch {
|
|
case keyMatches(key, k.General.Quit):
|
|
return "q"
|
|
case keyMatches(key, k.General.Help):
|
|
return "?"
|
|
case keyMatches(key, k.General.Refresh):
|
|
return "r"
|
|
case keyMatches(key, k.General.Back):
|
|
return "esc"
|
|
case keyMatches(key, k.Navigation.Down):
|
|
return "j"
|
|
case keyMatches(key, k.Navigation.Up):
|
|
return "k"
|
|
case keyMatches(key, k.Navigation.First):
|
|
return "g"
|
|
case keyMatches(key, k.Navigation.Last):
|
|
return "G"
|
|
case keyMatches(key, k.Navigation.PageDown):
|
|
return "ctrl+d"
|
|
case keyMatches(key, k.Navigation.PageUp):
|
|
return "ctrl+u"
|
|
}
|
|
if current == prScreen || current == dashboardScreen {
|
|
if keyMatches(key, k.Views.Open) {
|
|
return "enter"
|
|
}
|
|
}
|
|
if current == dashboardScreen {
|
|
switch {
|
|
case keyMatches(key, k.Views.AutoMerge):
|
|
return "a"
|
|
case keyMatches(key, k.Views.MergeNow):
|
|
return "M"
|
|
}
|
|
}
|
|
if current == threadScreen {
|
|
switch {
|
|
case keyMatches(key, k.Navigation.Left):
|
|
return "h"
|
|
case keyMatches(key, k.Navigation.Right):
|
|
return "l"
|
|
case keyMatches(key, k.Views.ToggleList):
|
|
return "tab"
|
|
case keyMatches(key, k.Threads.Search):
|
|
return "/"
|
|
case keyMatches(key, k.Threads.ClearFilter):
|
|
return "F"
|
|
case keyMatches(key, k.Threads.NextUnread):
|
|
return "n"
|
|
case keyMatches(key, k.Threads.PreviousUnread):
|
|
return "N"
|
|
case keyMatches(key, k.Threads.MarkRead):
|
|
return "m"
|
|
case keyMatches(key, k.Threads.Reply):
|
|
return "c"
|
|
case keyMatches(key, k.Threads.Resolve):
|
|
return "R"
|
|
case keyMatches(key, k.Threads.Toggle):
|
|
return "enter"
|
|
case keyMatches(key, k.Threads.FoldPrefix):
|
|
return "z"
|
|
}
|
|
}
|
|
switch {
|
|
case keyMatches(key, k.Views.Dashboard):
|
|
return "d"
|
|
case keyMatches(key, k.Views.Health):
|
|
return "H"
|
|
case keyMatches(key, k.Views.Edit):
|
|
return "e"
|
|
case (current == dashboardScreen || current == threadScreen) && keyMatches(key, k.Views.AI):
|
|
return "A"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (k KeyBindings) canonicalPREditKey(key string, field int, confirming bool) string {
|
|
if confirming {
|
|
switch {
|
|
case keyMatches(key, k.General.Confirm):
|
|
return "y"
|
|
case keyMatches(key, k.General.Reject), keyMatches(key, k.Input.Cancel):
|
|
return "esc"
|
|
case keyMatches(key, k.Navigation.Down):
|
|
return "down"
|
|
case keyMatches(key, k.Navigation.Up):
|
|
return "up"
|
|
case keyMatches(key, k.Navigation.PageDown):
|
|
return "ctrl+d"
|
|
case keyMatches(key, k.Navigation.PageUp):
|
|
return "ctrl+u"
|
|
case keyMatches(key, k.Navigation.First):
|
|
return "g"
|
|
case keyMatches(key, k.Navigation.Last):
|
|
return "G"
|
|
}
|
|
}
|
|
switch {
|
|
case keyMatches(key, k.Input.Cancel):
|
|
return "esc"
|
|
case keyMatches(key, k.Input.Submit):
|
|
return "ctrl+s"
|
|
case keyMatches(key, k.Input.NextField):
|
|
return "tab"
|
|
case keyMatches(key, k.Input.PreviousField):
|
|
return "shift+tab"
|
|
case keyMatches(key, k.Input.NextCompletion):
|
|
return "ctrl+n"
|
|
case keyMatches(key, k.Input.PreviousCompletion):
|
|
return "ctrl+p"
|
|
case keyMatches(key, k.Navigation.PageDown):
|
|
return "ctrl+d"
|
|
case keyMatches(key, k.Navigation.PageUp):
|
|
return "ctrl+u"
|
|
}
|
|
if field != prEditBodyField {
|
|
switch {
|
|
case keyMatches(key, k.Input.Newline):
|
|
return "enter"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func validateKeyBindings(bindings KeyBindings) error {
|
|
groups := []struct {
|
|
name string
|
|
values map[string][]string
|
|
}{
|
|
{"keybindings.general", map[string][]string{
|
|
"quit": bindings.General.Quit, "help": bindings.General.Help,
|
|
"refresh": bindings.General.Refresh, "back": bindings.General.Back,
|
|
"confirm": bindings.General.Confirm, "reject": bindings.General.Reject,
|
|
}},
|
|
{"keybindings.navigation", map[string][]string{
|
|
"down": bindings.Navigation.Down, "up": bindings.Navigation.Up,
|
|
"left": bindings.Navigation.Left, "right": bindings.Navigation.Right,
|
|
"first": bindings.Navigation.First, "last": bindings.Navigation.Last,
|
|
"page_down": bindings.Navigation.PageDown, "page_up": bindings.Navigation.PageUp,
|
|
}},
|
|
{"keybindings.views", map[string][]string{
|
|
"open": bindings.Views.Open, "dashboard": bindings.Views.Dashboard,
|
|
"health": bindings.Views.Health, "edit": bindings.Views.Edit,
|
|
"auto_merge": bindings.Views.AutoMerge, "merge_now": bindings.Views.MergeNow,
|
|
"toggle_list": bindings.Views.ToggleList,
|
|
"ai": bindings.Views.AI,
|
|
}},
|
|
{"keybindings.threads", map[string][]string{
|
|
"search": bindings.Threads.Search, "clear_filter": bindings.Threads.ClearFilter,
|
|
"next_unread": bindings.Threads.NextUnread,
|
|
"previous_unread": bindings.Threads.PreviousUnread,
|
|
"mark_read": bindings.Threads.MarkRead,
|
|
"reply": bindings.Threads.Reply, "resolve": bindings.Threads.Resolve,
|
|
"toggle": bindings.Threads.Toggle, "fold_prefix": bindings.Threads.FoldPrefix,
|
|
"fold_toggle": bindings.Threads.FoldToggle,
|
|
}},
|
|
{"keybindings.input", map[string][]string{
|
|
"cancel": bindings.Input.Cancel,
|
|
"submit": bindings.Input.Submit, "newline": bindings.Input.Newline,
|
|
"delete_backward": bindings.Input.DeleteBackward,
|
|
"delete_forward": bindings.Input.DeleteForward, "clear": bindings.Input.Clear,
|
|
"next_field": bindings.Input.NextField, "previous_field": bindings.Input.PreviousField,
|
|
"next_completion": bindings.Input.NextCompletion,
|
|
"previous_completion": bindings.Input.PreviousCompletion,
|
|
"line_start": bindings.Input.LineStart, "line_end": bindings.Input.LineEnd,
|
|
}},
|
|
{"keybindings.vim", map[string][]string{
|
|
"insert": bindings.Vim.Insert, "append": bindings.Vim.Append,
|
|
"insert_line_start": bindings.Vim.InsertLineStart,
|
|
"append_line_end": bindings.Vim.AppendLineEnd,
|
|
"open_below": bindings.Vim.OpenBelow, "open_above": bindings.Vim.OpenAbove,
|
|
"replace_character": bindings.Vim.ReplaceCharacter,
|
|
"visual": bindings.Vim.Visual, "visual_line": bindings.Vim.VisualLine,
|
|
"selection_other_end": bindings.Vim.SelectionOtherEnd,
|
|
"yank": bindings.Vim.Yank, "delete": bindings.Vim.Delete,
|
|
"delete_before": bindings.Vim.DeleteBefore, "paste": bindings.Vim.Paste,
|
|
"line_start": bindings.Vim.LineStart,
|
|
"first_non_blank": bindings.Vim.FirstNonBlank, "line_end": bindings.Vim.LineEnd,
|
|
"word_forward": bindings.Vim.WordForward, "big_word_forward": bindings.Vim.WORDForward,
|
|
"word_backward": bindings.Vim.WordBackward, "big_word_backward": bindings.Vim.WORDBackward,
|
|
"word_end": bindings.Vim.WordEnd, "big_word_end": bindings.Vim.WORDEnd,
|
|
"go_prefix": bindings.Vim.GoPrefix,
|
|
"find_forward": bindings.Vim.FindForward, "find_backward": bindings.Vim.FindBackward,
|
|
"till_forward": bindings.Vim.TillForward, "till_backward": bindings.Vim.TillBackward,
|
|
"repeat_find": bindings.Vim.RepeatFind,
|
|
"repeat_find_reverse": bindings.Vim.RepeatFindReverse,
|
|
}},
|
|
}
|
|
for _, group := range groups {
|
|
for action, keys := range group.values {
|
|
if len(keys) == 0 {
|
|
return fmt.Errorf("%s.%s must contain at least one key", group.name, action)
|
|
}
|
|
seen := make(map[string]bool)
|
|
for _, key := range keys {
|
|
if strings.TrimSpace(key) == "" {
|
|
return fmt.Errorf("%s.%s contains an empty key", group.name, action)
|
|
}
|
|
if seen[key] {
|
|
return fmt.Errorf("%s.%s contains duplicate key %q", group.name, action, key)
|
|
}
|
|
seen[key] = true
|
|
}
|
|
}
|
|
}
|
|
return validateKeyBindingContexts(bindings)
|
|
}
|
|
|
|
type contextBinding struct {
|
|
action string
|
|
keys []string
|
|
}
|
|
|
|
func validateKeyBindingContexts(bindings KeyBindings) error {
|
|
navigation := bindings.Navigation
|
|
general := bindings.General
|
|
views := bindings.Views
|
|
threads := bindings.Threads
|
|
input := bindings.Input
|
|
vim := bindings.Vim
|
|
|
|
screenCommon := []contextBinding{
|
|
{"quit", general.Quit}, {"help", general.Help}, {"refresh", general.Refresh},
|
|
{"back", general.Back}, {"down", navigation.Down}, {"up", navigation.Up},
|
|
{"first", navigation.First}, {"last", navigation.Last},
|
|
{"page_down", navigation.PageDown}, {"page_up", navigation.PageUp},
|
|
{"health", views.Health},
|
|
}
|
|
if err := validateKeyContext("pull request list", append(screenCommon,
|
|
contextBinding{"open", views.Open},
|
|
contextBinding{"dashboard", views.Dashboard},
|
|
)...); err != nil {
|
|
return err
|
|
}
|
|
if err := validateKeyContext("dashboard", append(screenCommon,
|
|
contextBinding{"open", views.Open},
|
|
contextBinding{"edit", views.Edit},
|
|
contextBinding{"auto_merge", views.AutoMerge},
|
|
contextBinding{"merge_now", views.MergeNow},
|
|
contextBinding{"ai", views.AI},
|
|
)...); err != nil {
|
|
return err
|
|
}
|
|
if err := validateKeyContext("health screen", screenCommon...); err != nil {
|
|
return err
|
|
}
|
|
if err := validateKeyContext("review threads", append(screenCommon,
|
|
contextBinding{"left", navigation.Left},
|
|
contextBinding{"right", navigation.Right},
|
|
contextBinding{"toggle_list", views.ToggleList},
|
|
contextBinding{"dashboard", views.Dashboard},
|
|
contextBinding{"search", threads.Search},
|
|
contextBinding{"clear_filter", threads.ClearFilter},
|
|
contextBinding{"next_unread", threads.NextUnread},
|
|
contextBinding{"previous_unread", threads.PreviousUnread},
|
|
contextBinding{"mark_read", threads.MarkRead},
|
|
contextBinding{"reply", threads.Reply},
|
|
contextBinding{"resolve", threads.Resolve},
|
|
contextBinding{"toggle", threads.Toggle},
|
|
contextBinding{"fold_prefix", threads.FoldPrefix},
|
|
contextBinding{"ai", views.AI},
|
|
)...); err != nil {
|
|
return err
|
|
}
|
|
if err := validateKeyContext("help popup",
|
|
contextBinding{"quit", general.Quit},
|
|
contextBinding{"close_help", appendCopy(general.Help, general.Back...)},
|
|
contextBinding{"down", navigation.Down},
|
|
contextBinding{"up", navigation.Up},
|
|
contextBinding{"first", navigation.First},
|
|
contextBinding{"last", navigation.Last},
|
|
contextBinding{"page_down", navigation.PageDown},
|
|
contextBinding{"page_up", navigation.PageUp},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validateKeyContext("search input",
|
|
contextBinding{"quit", nonTextBindings(general.Quit)},
|
|
contextBinding{"cancel", input.Cancel},
|
|
contextBinding{"apply", input.Newline},
|
|
contextBinding{"previous_completion", input.PreviousCompletion},
|
|
contextBinding{"next_completion", input.NextCompletion},
|
|
contextBinding{"delete_backward", input.DeleteBackward},
|
|
contextBinding{"clear", input.Clear},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := validateKeyContext("reply input",
|
|
contextBinding{"quit", nonTextBindings(general.Quit)},
|
|
contextBinding{"cancel", input.Cancel},
|
|
contextBinding{"submit", input.Submit},
|
|
contextBinding{"newline", input.Newline},
|
|
contextBinding{"delete_backward", input.DeleteBackward},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := validateKeyContext("confirmation",
|
|
contextBinding{"quit", nonTextBindings(general.Quit)},
|
|
contextBinding{"confirm", general.Confirm},
|
|
contextBinding{"cancel", appendCopy(general.Reject, input.Cancel...)},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := validateKeyContext("AI menu",
|
|
contextBinding{"quit", nonTextBindings(general.Quit)},
|
|
contextBinding{"cancel", appendCopy(general.Back, input.Cancel...)},
|
|
contextBinding{"down", navigation.Down},
|
|
contextBinding{"up", navigation.Up},
|
|
contextBinding{"select", appendCopy(views.Open, input.Newline...)},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := validateKeyContext("AI confirmation",
|
|
contextBinding{"quit", nonTextBindings(general.Quit)},
|
|
contextBinding{"confirm", general.Confirm},
|
|
contextBinding{"cancel", appendCopy(general.Reject, input.Cancel...)},
|
|
contextBinding{"down", navigation.Down},
|
|
contextBinding{"up", navigation.Up},
|
|
contextBinding{"page_down", navigation.PageDown},
|
|
contextBinding{"page_up", navigation.PageUp},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
editorOuter := []contextBinding{
|
|
{"help", general.Help},
|
|
{"cancel", input.Cancel},
|
|
{"submit", input.Submit},
|
|
{"next_field", input.NextField},
|
|
{"previous_field", input.PreviousField},
|
|
{"page_down", navigation.PageDown},
|
|
{"page_up", navigation.PageUp},
|
|
}
|
|
normalEditor := append(append([]contextBinding(nil), editorOuter...),
|
|
contextBinding{"left", navigation.Left},
|
|
contextBinding{"down", navigation.Down},
|
|
contextBinding{"up", navigation.Up},
|
|
contextBinding{"right", navigation.Right},
|
|
contextBinding{"last", navigation.Last},
|
|
contextBinding{"insert", vim.Insert},
|
|
contextBinding{"append", vim.Append},
|
|
contextBinding{"insert_line_start", vim.InsertLineStart},
|
|
contextBinding{"append_line_end", vim.AppendLineEnd},
|
|
contextBinding{"open_below", vim.OpenBelow},
|
|
contextBinding{"open_above", vim.OpenAbove},
|
|
contextBinding{"replace_character", vim.ReplaceCharacter},
|
|
contextBinding{"visual", vim.Visual},
|
|
contextBinding{"visual_line", vim.VisualLine},
|
|
contextBinding{"paste", vim.Paste},
|
|
contextBinding{"line_start", vim.LineStart},
|
|
contextBinding{"first_non_blank", vim.FirstNonBlank},
|
|
contextBinding{"line_end", vim.LineEnd},
|
|
contextBinding{"word_forward", vim.WordForward},
|
|
contextBinding{"big_word_forward", vim.WORDForward},
|
|
contextBinding{"word_backward", vim.WordBackward},
|
|
contextBinding{"big_word_backward", vim.WORDBackward},
|
|
contextBinding{"word_end", vim.WordEnd},
|
|
contextBinding{"big_word_end", vim.WORDEnd},
|
|
contextBinding{"go_prefix", vim.GoPrefix},
|
|
contextBinding{"find_forward", vim.FindForward},
|
|
contextBinding{"find_backward", vim.FindBackward},
|
|
contextBinding{"till_forward", vim.TillForward},
|
|
contextBinding{"till_backward", vim.TillBackward},
|
|
contextBinding{"repeat_find", vim.RepeatFind},
|
|
contextBinding{"repeat_find_reverse", vim.RepeatFindReverse},
|
|
contextBinding{"delete", vim.Delete},
|
|
contextBinding{"delete_before", vim.DeleteBefore},
|
|
)
|
|
if err := validateKeyContext("Vim Normal mode", normalEditor...); err != nil {
|
|
return err
|
|
}
|
|
visualEditor := append(append([]contextBinding(nil), editorOuter...),
|
|
contextBinding{"left", navigation.Left},
|
|
contextBinding{"down", navigation.Down},
|
|
contextBinding{"up", navigation.Up},
|
|
contextBinding{"right", navigation.Right},
|
|
contextBinding{"last", navigation.Last},
|
|
contextBinding{"visual", vim.Visual},
|
|
contextBinding{"visual_line", vim.VisualLine},
|
|
contextBinding{"selection_other_end", vim.SelectionOtherEnd},
|
|
contextBinding{"yank", vim.Yank},
|
|
contextBinding{"delete", vim.Delete},
|
|
contextBinding{"paste", vim.Paste},
|
|
contextBinding{"line_start", vim.LineStart},
|
|
contextBinding{"first_non_blank", vim.FirstNonBlank},
|
|
contextBinding{"line_end", vim.LineEnd},
|
|
contextBinding{"word_forward", vim.WordForward},
|
|
contextBinding{"big_word_forward", vim.WORDForward},
|
|
contextBinding{"word_backward", vim.WordBackward},
|
|
contextBinding{"big_word_backward", vim.WORDBackward},
|
|
contextBinding{"word_end", vim.WordEnd},
|
|
contextBinding{"big_word_end", vim.WORDEnd},
|
|
contextBinding{"go_prefix", vim.GoPrefix},
|
|
contextBinding{"find_forward", vim.FindForward},
|
|
contextBinding{"find_backward", vim.FindBackward},
|
|
contextBinding{"till_forward", vim.TillForward},
|
|
contextBinding{"till_backward", vim.TillBackward},
|
|
contextBinding{"repeat_find", vim.RepeatFind},
|
|
contextBinding{"repeat_find_reverse", vim.RepeatFindReverse},
|
|
)
|
|
if err := validateKeyContext("Vim Visual mode", visualEditor...); err != nil {
|
|
return err
|
|
}
|
|
insertEditor := append(append([]contextBinding(nil), editorOuter...),
|
|
contextBinding{"left", nonTextBindings(navigation.Left)},
|
|
contextBinding{"down", nonTextBindings(navigation.Down)},
|
|
contextBinding{"up", nonTextBindings(navigation.Up)},
|
|
contextBinding{"right", nonTextBindings(navigation.Right)},
|
|
contextBinding{"newline", input.Newline},
|
|
contextBinding{"delete_backward", input.DeleteBackward},
|
|
contextBinding{"delete_forward", input.DeleteForward},
|
|
contextBinding{"line_start", input.LineStart},
|
|
contextBinding{"line_end", input.LineEnd},
|
|
)
|
|
if err := validateKeyContext("editor Insert mode", insertEditor...); err != nil {
|
|
return err
|
|
}
|
|
return validateKeyContext("single-line editor input",
|
|
contextBinding{"help", nonTextBindings(general.Help)},
|
|
contextBinding{"cancel", input.Cancel},
|
|
contextBinding{"submit", input.Submit},
|
|
contextBinding{"next_field", input.NextField},
|
|
contextBinding{"previous_field", input.PreviousField},
|
|
contextBinding{"previous_completion", input.PreviousCompletion},
|
|
contextBinding{"next_completion", input.NextCompletion},
|
|
contextBinding{"newline", input.Newline},
|
|
contextBinding{"delete_backward", input.DeleteBackward},
|
|
contextBinding{"delete_forward", input.DeleteForward},
|
|
contextBinding{"line_start", input.LineStart},
|
|
contextBinding{"line_end", input.LineEnd},
|
|
contextBinding{"up", nonTextBindings(navigation.Up)},
|
|
contextBinding{"down", nonTextBindings(navigation.Down)},
|
|
)
|
|
}
|
|
|
|
func validateKeyContext(context string, bindings ...contextBinding) error {
|
|
assigned := make(map[string]string)
|
|
for _, binding := range bindings {
|
|
for _, key := range binding.keys {
|
|
if previous, exists := assigned[key]; exists && previous != binding.action {
|
|
return fmt.Errorf(
|
|
"keybinding conflict in %s: key %q is assigned to both %s and %s",
|
|
context, key, previous, binding.action,
|
|
)
|
|
}
|
|
assigned[key] = binding.action
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func nonTextBindings(bindings []string) []string {
|
|
var filtered []string
|
|
for _, binding := range bindings {
|
|
if utf8.RuneCountInString(binding) != 1 {
|
|
filtered = append(filtered, binding)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func appendCopy(bindings []string, more ...string) []string {
|
|
result := append([]string(nil), bindings...)
|
|
return append(result, more...)
|
|
}
|