kitty/tools/config/utils.go
2024-06-24 07:54:13 +05:30

312 lines
6.6 KiB
Go

// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package config
import (
"fmt"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"regexp"
"slices"
"strconv"
"strings"
"sync"
)
var _ = fmt.Print
func ParseStrDict(val, record_sep, field_sep string) (map[string]string, error) {
ans := make(map[string]string)
for _, record := range strings.Split(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])
num, _ := strconv.ParseUint(text, base, 32)
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
}
func (self *KeyAction) String() string {
return fmt.Sprintf("map %#v %#v %#v\n", strings.Join(self.Normalized_keys, ">"), self.Name, self.Args)
}
func ParseMap(val string) (*KeyAction, error) {
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)}, nil
}
type ShortcutTracker struct {
partial_matches []*KeyAction
partial_num_consumed int
}
func (self *ShortcutTracker) Match(ev *loop.KeyEvent, all_actions []*KeyAction) *KeyAction {
if self.partial_num_consumed > 0 {
ev.Handled = true
self.partial_matches = utils.Filter(self.partial_matches, func(ac *KeyAction) bool {
return self.partial_num_consumed < len(ac.Normalized_keys) && ev.MatchesPressOrRepeat(ac.Normalized_keys[self.partial_num_consumed])
})
if len(self.partial_matches) == 0 {
self.partial_num_consumed = 0
return nil
}
} else {
self.partial_matches = utils.Filter(all_actions, func(ac *KeyAction) bool {
return ev.MatchesPressOrRepeat(ac.Normalized_keys[0])
})
if len(self.partial_matches) == 0 {
return nil
}
ev.Handled = true
}
self.partial_num_consumed++
for _, x := range self.partial_matches {
if self.partial_num_consumed >= len(x.Normalized_keys) {
self.partial_num_consumed = 0
return x
}
}
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)
}