mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 16:37:27 +00:00
Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com> Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/85fbf706-4688-4901-9a23-907cebc91da3
403 lines
9 KiB
Go
403 lines
9 KiB
Go
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
|
"github.com/kovidgoyal/kitty/tools/utils"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
func ParseStrDict(val, record_sep, field_sep string) (map[string]string, error) {
|
|
ans := make(map[string]string)
|
|
for record := range strings.SplitSeq(val, record_sep) {
|
|
key, val, found := strings.Cut(record, field_sep)
|
|
if found {
|
|
ans[key] = val
|
|
}
|
|
}
|
|
return ans, nil
|
|
}
|
|
|
|
func PositiveFloat(val string) (ans float64, err error) {
|
|
ans, err = strconv.ParseFloat(val, 64)
|
|
if err == nil {
|
|
ans = max(0, ans)
|
|
}
|
|
return
|
|
}
|
|
|
|
func UnitFloat(val string) (ans float64, err error) {
|
|
ans, err = strconv.ParseFloat(val, 64)
|
|
if err == nil {
|
|
ans = max(0, min(ans, 1))
|
|
}
|
|
return
|
|
}
|
|
|
|
func StringLiteral(val string) (string, error) {
|
|
ans := strings.Builder{}
|
|
ans.Grow(len(val))
|
|
var buf [8]rune
|
|
bufcount := 0
|
|
buflimit := 0
|
|
var prefix rune
|
|
type State int
|
|
const (
|
|
normal State = iota
|
|
backslash
|
|
octal
|
|
hex
|
|
)
|
|
var state State
|
|
decode := func(base int) {
|
|
text := string(buf[:bufcount])
|
|
if num, err := strconv.ParseUint(text, base, 32); err == nil && num <= utf8.MaxRune {
|
|
ans.WriteRune(rune(num))
|
|
}
|
|
state = normal
|
|
bufcount = 0
|
|
buflimit = 0
|
|
prefix = 0
|
|
}
|
|
|
|
write_invalid_buf := func() {
|
|
ans.WriteByte('\\')
|
|
ans.WriteRune(prefix)
|
|
for _, r := range buf[:bufcount] {
|
|
ans.WriteRune(r)
|
|
}
|
|
state = normal
|
|
bufcount = 0
|
|
buflimit = 0
|
|
prefix = 0
|
|
}
|
|
|
|
var dispatch_ch_recurse func(rune)
|
|
|
|
dispatch_ch := func(ch rune) {
|
|
switch state {
|
|
case normal:
|
|
switch ch {
|
|
case '\\':
|
|
state = backslash
|
|
default:
|
|
ans.WriteRune(ch)
|
|
}
|
|
case octal:
|
|
switch ch {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7':
|
|
if bufcount >= buflimit {
|
|
decode(8)
|
|
dispatch_ch_recurse(ch)
|
|
} else {
|
|
buf[bufcount] = ch
|
|
bufcount++
|
|
}
|
|
default:
|
|
decode(8)
|
|
dispatch_ch_recurse(ch)
|
|
}
|
|
case hex:
|
|
switch ch {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'E', 'f', 'F':
|
|
buf[bufcount] = ch
|
|
bufcount++
|
|
if bufcount >= buflimit {
|
|
decode(16)
|
|
}
|
|
default:
|
|
write_invalid_buf()
|
|
dispatch_ch_recurse(ch)
|
|
}
|
|
case backslash:
|
|
switch ch {
|
|
case '\n':
|
|
case '\\':
|
|
ans.WriteRune('\\')
|
|
state = normal
|
|
case '\'', '"':
|
|
ans.WriteRune(ch)
|
|
state = normal
|
|
case 'a':
|
|
ans.WriteRune('\a')
|
|
state = normal
|
|
case 'b':
|
|
ans.WriteRune('\b')
|
|
state = normal
|
|
case 'f':
|
|
ans.WriteRune('\f')
|
|
state = normal
|
|
case 'n':
|
|
ans.WriteRune('\n')
|
|
state = normal
|
|
case 'r':
|
|
ans.WriteRune('\r')
|
|
state = normal
|
|
case 't':
|
|
ans.WriteRune('\t')
|
|
state = normal
|
|
case 'v':
|
|
ans.WriteRune('\v')
|
|
state = normal
|
|
case '0', '1', '2', '3', '4', '5', '6', '7':
|
|
buf[0] = ch
|
|
bufcount = 1
|
|
buflimit = 3
|
|
state = octal
|
|
case 'x':
|
|
bufcount = 0
|
|
buflimit = 2
|
|
state = hex
|
|
prefix = ch
|
|
case 'u':
|
|
bufcount = 0
|
|
buflimit = 4
|
|
state = hex
|
|
prefix = ch
|
|
case 'U':
|
|
bufcount = 0
|
|
buflimit = 8
|
|
state = hex
|
|
prefix = ch
|
|
default:
|
|
ans.WriteByte('\\')
|
|
ans.WriteRune(ch)
|
|
state = normal
|
|
}
|
|
}
|
|
}
|
|
dispatch_ch_recurse = dispatch_ch
|
|
for _, ch := range val {
|
|
dispatch_ch(ch)
|
|
}
|
|
switch state {
|
|
case octal:
|
|
decode(8)
|
|
case hex:
|
|
write_invalid_buf()
|
|
case backslash:
|
|
ans.WriteRune('\\')
|
|
}
|
|
return ans.String(), nil
|
|
}
|
|
|
|
var ModMap = sync.OnceValue(func() map[string]string {
|
|
return map[string]string{
|
|
"shift": "shift",
|
|
"⇧": "shift",
|
|
"alt": "alt",
|
|
"option": "alt",
|
|
"opt": "alt",
|
|
"⌥": "alt",
|
|
"super": "super",
|
|
"command": "super",
|
|
"cmd": "super",
|
|
"⌘": "super",
|
|
"control": "ctrl",
|
|
"ctrl": "ctrl",
|
|
"⌃": "ctrl",
|
|
"hyper": "hyper",
|
|
"meta": "meta",
|
|
"num_lock": "num_lock",
|
|
"caps_lock": "caps_lock",
|
|
}
|
|
})
|
|
|
|
var ShortcutSpecPat = sync.OnceValue(func() *regexp.Regexp {
|
|
return regexp.MustCompile(`([^+])>`)
|
|
})
|
|
|
|
func NormalizeShortcut(spec string) string {
|
|
parts := strings.Split(strings.ToLower(spec), "+")
|
|
key := parts[len(parts)-1]
|
|
if len(parts) == 1 {
|
|
return key
|
|
}
|
|
mods := parts[:len(parts)-1]
|
|
mmap := ModMap()
|
|
mods = utils.Map(func(x string) string {
|
|
ans := mmap[x]
|
|
if ans == "" {
|
|
ans = x
|
|
}
|
|
return ans
|
|
}, mods)
|
|
slices.Sort(mods)
|
|
return strings.Join(mods, "+") + "+" + key
|
|
}
|
|
|
|
func NormalizeShortcuts(spec string) []string {
|
|
if strings.HasSuffix(spec, "+") {
|
|
spec = spec[:len(spec)-1] + "plus"
|
|
}
|
|
spec = strings.ReplaceAll(spec, "++", "+plus")
|
|
spec = ShortcutSpecPat().ReplaceAllString(spec, "$1\x00")
|
|
return utils.Map(NormalizeShortcut, strings.Split(spec, "\x00"))
|
|
}
|
|
|
|
type KeyAction struct {
|
|
Normalized_keys []string
|
|
Name string
|
|
Args string
|
|
AllowFallback string
|
|
}
|
|
|
|
func (self *KeyAction) String() string {
|
|
return fmt.Sprintf("map %#v %#v %#v\n", strings.Join(self.Normalized_keys, ">"), self.Name, self.Args)
|
|
}
|
|
|
|
func validateAllowFallback(value string) error {
|
|
if value == "" || value == "none" {
|
|
return nil
|
|
}
|
|
for part := range strings.SplitSeq(value, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part != "shifted" && part != "ascii" {
|
|
return fmt.Errorf("Invalid allow-fallback value %#v, allowed values: shifted, ascii, none", part)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ParseMap(val string) (*KeyAction, error) {
|
|
allow_fallback := "shifted"
|
|
for strings.HasPrefix(val, "--") {
|
|
flag, rest, found := strings.Cut(val, " ")
|
|
if !found {
|
|
return nil, fmt.Errorf("No key specified after flag %s", flag)
|
|
}
|
|
rest = strings.TrimSpace(rest)
|
|
if name, value, ok := strings.Cut(flag, "="); ok {
|
|
// --flag=value form
|
|
name = strings.ReplaceAll(name[2:], "-", "_")
|
|
switch name {
|
|
case "allow_fallback":
|
|
if err := validateAllowFallback(value); err != nil {
|
|
return nil, err
|
|
}
|
|
if value == "none" {
|
|
allow_fallback = ""
|
|
} else {
|
|
allow_fallback = value
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("Unknown map option: %s", flag)
|
|
}
|
|
} else {
|
|
// --flag value form (space-separated)
|
|
name := strings.ReplaceAll(flag[2:], "-", "_")
|
|
switch name {
|
|
case "allow_fallback":
|
|
value, after, has_more := strings.Cut(rest, " ")
|
|
if !has_more {
|
|
return nil, fmt.Errorf("No key specified after %s %s", flag, value)
|
|
}
|
|
if err := validateAllowFallback(value); err != nil {
|
|
return nil, err
|
|
}
|
|
if value == "none" {
|
|
allow_fallback = ""
|
|
} else {
|
|
allow_fallback = value
|
|
}
|
|
rest = strings.TrimSpace(after)
|
|
default:
|
|
return nil, fmt.Errorf("Unknown map option: %s", flag)
|
|
}
|
|
}
|
|
val = rest
|
|
}
|
|
spec, action, found := strings.Cut(val, " ")
|
|
if !found {
|
|
return nil, fmt.Errorf("No action specified for shortcut %s", val)
|
|
}
|
|
action = strings.TrimSpace(action)
|
|
action_name, action_args, _ := strings.Cut(action, " ")
|
|
action_args = strings.TrimSpace(action_args)
|
|
return &KeyAction{Name: action_name, Args: action_args, Normalized_keys: NormalizeShortcuts(spec), AllowFallback: allow_fallback}, nil
|
|
}
|
|
|
|
type partialMatch struct {
|
|
action *KeyAction
|
|
priority int
|
|
}
|
|
|
|
type ShortcutTracker struct {
|
|
partial_matches []partialMatch
|
|
partial_num_consumed int
|
|
}
|
|
|
|
func (self *ShortcutTracker) Match(ev *loop.KeyEvent, all_actions []*KeyAction) *KeyAction {
|
|
if self.partial_num_consumed > 0 {
|
|
ev.Handled = true
|
|
new_matches := self.partial_matches[:0]
|
|
for _, pm := range self.partial_matches {
|
|
if self.partial_num_consumed >= len(pm.action.Normalized_keys) {
|
|
continue
|
|
}
|
|
p := ev.MatchesPressOrRepeatPriorityWithFallback(pm.action.Normalized_keys[self.partial_num_consumed], pm.action.AllowFallback)
|
|
if p >= 0 {
|
|
if p > pm.priority {
|
|
pm.priority = p
|
|
}
|
|
new_matches = append(new_matches, pm)
|
|
}
|
|
}
|
|
self.partial_matches = new_matches
|
|
if len(self.partial_matches) == 0 {
|
|
self.partial_num_consumed = 0
|
|
return nil
|
|
}
|
|
} else {
|
|
new_matches := self.partial_matches[:0]
|
|
for _, ac := range all_actions {
|
|
p := ev.MatchesPressOrRepeatPriorityWithFallback(ac.Normalized_keys[0], ac.AllowFallback)
|
|
if p >= 0 {
|
|
new_matches = append(new_matches, partialMatch{action: ac, priority: p})
|
|
}
|
|
}
|
|
self.partial_matches = new_matches
|
|
if len(self.partial_matches) == 0 {
|
|
return nil
|
|
}
|
|
ev.Handled = true
|
|
}
|
|
self.partial_num_consumed++
|
|
var best *partialMatch
|
|
for i := range self.partial_matches {
|
|
pm := &self.partial_matches[i]
|
|
if self.partial_num_consumed >= len(pm.action.Normalized_keys) {
|
|
if best == nil || pm.priority < best.priority {
|
|
best = pm
|
|
}
|
|
}
|
|
}
|
|
if best != nil {
|
|
self.partial_num_consumed = 0
|
|
self.partial_matches = nil
|
|
return best.action
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ResolveShortcuts(actions []*KeyAction) []*KeyAction {
|
|
action_map := make(map[string]*KeyAction, len(actions))
|
|
for _, ac := range actions {
|
|
key := strings.Join(ac.Normalized_keys, "\x00")
|
|
if ac.Name == "no_op" || ac.Name == "no-op" {
|
|
delete(action_map, key)
|
|
} else {
|
|
action_map[key] = ac
|
|
}
|
|
}
|
|
return utils.Values(action_map)
|
|
}
|