Refactor shortcut config parsing
Resolution of kitty_mod and creation of the maps now happens in one central place after all config is parsed. Fixes #496 Also have --debug-config output multi-key shortcuts nicely as well.
This commit is contained in:
parent
0f06bf1bdb
commit
2e693c31df
@ -20,9 +20,9 @@ from .constants import appname, config_dir, editor, set_boss
|
||||
from .fast_data_types import (
|
||||
ChildMonitor, create_os_window, current_os_window, destroy_global_data,
|
||||
destroy_sprite_map, get_clipboard_string, glfw_post_empty_event,
|
||||
layout_sprite_map, mark_os_window_for_close, resolve_key_mods,
|
||||
set_clipboard_string, set_dpi_from_os_window, set_in_sequence_mode,
|
||||
show_window, toggle_fullscreen, viewport_for_window
|
||||
layout_sprite_map, mark_os_window_for_close, set_clipboard_string,
|
||||
set_dpi_from_os_window, set_in_sequence_mode, show_window,
|
||||
toggle_fullscreen, viewport_for_window
|
||||
)
|
||||
from .fonts.render import prerender, resize_fonts, set_font_family
|
||||
from .keys import get_shortcut, shortcut_matches
|
||||
@ -105,8 +105,6 @@ class Boss:
|
||||
initialize_renderer()
|
||||
startup_session = create_session(opts, args)
|
||||
self.add_os_window(startup_session, os_window_id=os_window_id)
|
||||
self.resolved_keymap = {(resolve_key_mods(mods), key): v for (mods, key), v in self.opts.keymap.items()}
|
||||
self.resolved_sequence_map = {(resolve_key_mods(mods), key): v for (mods, key), v in self.opts.sequence_map.items()}
|
||||
|
||||
def add_os_window(self, startup_session, os_window_id=None, wclass=None, wname=None, size=None, startup_id=None):
|
||||
dpi_changed = False
|
||||
@ -370,9 +368,9 @@ class Boss:
|
||||
|
||||
def dispatch_special_key(self, key, scancode, action, mods):
|
||||
# Handles shortcuts, return True if the key was consumed
|
||||
key_action = get_shortcut(self.resolved_keymap, mods, key, scancode)
|
||||
key_action = get_shortcut(self.opts.keymap, mods, key, scancode)
|
||||
if key_action is None:
|
||||
sequences = get_shortcut(self.resolved_sequence_map, mods, key, scancode)
|
||||
sequences = get_shortcut(self.opts.sequence_map, mods, key, scancode)
|
||||
if sequences:
|
||||
self.pending_sequences = sequences
|
||||
set_in_sequence_mode(True)
|
||||
|
||||
75
kitty/cli.py
75
kitty/cli.py
@ -500,7 +500,7 @@ def parse_args(args=None, ospec=options_spec, usage=None, message=None, appname=
|
||||
SYSTEM_CONF = '/etc/xdg/kitty/kitty.conf'
|
||||
|
||||
|
||||
def print_shortcut(key, action):
|
||||
def print_shortcut(key_or_sequence, action):
|
||||
if not getattr(print_shortcut, 'maps', None):
|
||||
from kitty.keys import defines
|
||||
v = vars(defines)
|
||||
@ -509,42 +509,67 @@ def print_shortcut(key, action):
|
||||
krmap = {v: k for k, v in kmap.items()}
|
||||
print_shortcut.maps = mmap, krmap
|
||||
mmap, krmap = print_shortcut.maps
|
||||
names = []
|
||||
mods, key = key
|
||||
for name, val in mmap.items():
|
||||
if mods & val:
|
||||
names.append(name)
|
||||
if key:
|
||||
names.append(krmap[key])
|
||||
print('\t', '+'.join(names), action)
|
||||
keys = []
|
||||
if isinstance(key_or_sequence[0], int):
|
||||
key_or_sequence = (key_or_sequence,)
|
||||
for key in key_or_sequence:
|
||||
names = []
|
||||
mods, key = key
|
||||
for name, val in mmap.items():
|
||||
if mods & val:
|
||||
names.append(name)
|
||||
if key:
|
||||
names.append(krmap[key])
|
||||
keys.append('+'.join(names))
|
||||
|
||||
print('\t', ' > '.join(keys), action)
|
||||
|
||||
|
||||
def print_shortcut_changes(defns, text, changes):
|
||||
if changes:
|
||||
print(title(text))
|
||||
|
||||
def k(x):
|
||||
if isinstance(x[0], int):
|
||||
x = (x,)
|
||||
return x
|
||||
|
||||
for k in sorted(changes, key=k):
|
||||
print_shortcut(k, defns[k])
|
||||
|
||||
|
||||
def compare_keymaps(final, initial):
|
||||
added = set(final) - set(initial)
|
||||
removed = set(initial) - set(final)
|
||||
changed = {k for k in set(final) & set(initial) if final[k] != initial[k]}
|
||||
if added:
|
||||
print(title('Added shortcuts:'))
|
||||
for k in added:
|
||||
print_shortcut(k, final[k])
|
||||
if removed:
|
||||
print(title('Removed shortcuts:'))
|
||||
for k in removed:
|
||||
print_shortcut(k, initial[k])
|
||||
if changed:
|
||||
print(title('Changed shortcuts:'))
|
||||
for k in changed:
|
||||
print_shortcut(k, final[k])
|
||||
print_shortcut_changes(final, 'Added shortcuts:', added)
|
||||
print_shortcut_changes(initial, 'Removed shortcuts:', removed)
|
||||
print_shortcut_changes(final, 'Changed shortcuts:', changed)
|
||||
|
||||
|
||||
def flatten_sequence_map(m):
|
||||
ans = {}
|
||||
for k, rest_map in m.items():
|
||||
for r, action in rest_map.items():
|
||||
ans[(k,) + (r)] = action
|
||||
return ans
|
||||
|
||||
|
||||
def compare_opts(opts):
|
||||
print('\nConfig options different from defaults:')
|
||||
default_opts = load_config()
|
||||
|
||||
for f in sorted(defaults._fields):
|
||||
if getattr(opts, f) != getattr(defaults, f):
|
||||
if f == 'keymap':
|
||||
compare_keymaps(opts.keymap, defaults.keymap)
|
||||
else:
|
||||
print(title('{:20s}'.format(f)), getattr(opts, f))
|
||||
if f in ('key_definitions', 'keymap', 'sequence_map'):
|
||||
continue
|
||||
print(title('{:20s}'.format(f)), getattr(opts, f))
|
||||
|
||||
final, initial = opts.keymap, default_opts.keymap
|
||||
final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map))
|
||||
final.update(final_s)
|
||||
initial.update(initial_s)
|
||||
compare_keymaps(final, initial)
|
||||
|
||||
|
||||
def create_opts(args, debug_config=False):
|
||||
|
||||
139
kitty/config.py
139
kitty/config.py
@ -50,7 +50,7 @@ def to_cursor_shape(x):
|
||||
mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER', '⌥': 'ALT', 'OPTION': 'ALT', 'KITTY_MOD': 'KITTY'}
|
||||
|
||||
|
||||
def parse_mods(parts):
|
||||
def parse_mods(parts, sc):
|
||||
|
||||
def map_mod(m):
|
||||
return mod_map.get(m, m)
|
||||
@ -60,8 +60,7 @@ def parse_mods(parts):
|
||||
try:
|
||||
mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper()))
|
||||
except AttributeError:
|
||||
log_error('Shortcut: {} has unknown modifier, ignoring'.format(
|
||||
parts.join('+')))
|
||||
log_error('Shortcut: {} has unknown modifier, ignoring'.format(sc))
|
||||
return
|
||||
|
||||
return mods
|
||||
@ -83,12 +82,14 @@ named_keys = {
|
||||
|
||||
def parse_shortcut(sc):
|
||||
parts = sc.split('+')
|
||||
mods = parse_mods(parts[:-1])
|
||||
mods = parse_mods(parts[:-1], sc)
|
||||
if mods is None:
|
||||
return None, None
|
||||
key = parts[-1].upper()
|
||||
key = getattr(defines, 'GLFW_KEY_' + named_keys.get(key, key), None)
|
||||
if key is not None:
|
||||
return mods, key
|
||||
return None, None
|
||||
return mods, None
|
||||
|
||||
|
||||
KeyAction = namedtuple('KeyAction', 'func args')
|
||||
@ -129,7 +130,20 @@ all_key_actions = set()
|
||||
sequence_sep = '>'
|
||||
|
||||
|
||||
def parse_key(val, keymap, sequence_map):
|
||||
class KeyDefinition:
|
||||
|
||||
def __init__(self, is_sequence, action, mods, key, rest=()):
|
||||
self.is_sequence = is_sequence
|
||||
self.action = action
|
||||
self.trigger = mods, key
|
||||
self.rest = rest
|
||||
|
||||
def resolve(self, kitty_mod):
|
||||
self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1]
|
||||
self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), key) for mods, key in self.rest)
|
||||
|
||||
|
||||
def parse_key(val, key_definitions):
|
||||
sc, action = val.partition(' ')[::2]
|
||||
sc, action = sc.strip().strip(sequence_sep), action.strip()
|
||||
if not sc or not action:
|
||||
@ -141,8 +155,8 @@ def parse_key(val, keymap, sequence_map):
|
||||
for part in sc.split(sequence_sep):
|
||||
mods, key = parse_shortcut(part)
|
||||
if key is None:
|
||||
log_error('Shortcut: {} has unknown key, ignoring'.format(
|
||||
sc))
|
||||
if mods is not None:
|
||||
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
|
||||
return
|
||||
if trigger is None:
|
||||
trigger = mods, key
|
||||
@ -152,8 +166,8 @@ def parse_key(val, keymap, sequence_map):
|
||||
else:
|
||||
mods, key = parse_shortcut(sc)
|
||||
if key is None:
|
||||
log_error('Shortcut: {} has unknown key, ignoring'.format(
|
||||
sc))
|
||||
if mods is not None:
|
||||
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
|
||||
return
|
||||
try:
|
||||
paction = parse_key_action(action)
|
||||
@ -164,10 +178,9 @@ def parse_key(val, keymap, sequence_map):
|
||||
if paction is not None:
|
||||
all_key_actions.add(paction.func)
|
||||
if is_sequence:
|
||||
s = sequence_map.setdefault(trigger, {})
|
||||
s[rest] = paction
|
||||
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], rest))
|
||||
else:
|
||||
keymap[(mods, key)] = paction
|
||||
key_definitions.append(KeyDefinition(False, paction, mods, key))
|
||||
|
||||
|
||||
def parse_symbol_map(val):
|
||||
@ -207,7 +220,7 @@ def parse_send_text_bytes(text):
|
||||
).encode('utf-8')
|
||||
|
||||
|
||||
def parse_send_text(val, keymap, sequence_map):
|
||||
def parse_send_text(val, key_definitions):
|
||||
parts = val.split(' ')
|
||||
|
||||
def abort(msg):
|
||||
@ -220,11 +233,11 @@ def parse_send_text(val, keymap, sequence_map):
|
||||
mode, sc = parts[:2]
|
||||
text = ' '.join(parts[2:])
|
||||
key_str = '{} send_text {} {}'.format(sc, mode, text)
|
||||
return parse_key(key_str, keymap, sequence_map)
|
||||
return parse_key(key_str, key_definitions)
|
||||
|
||||
|
||||
def to_modifiers(val):
|
||||
return parse_mods(val.split('+'))
|
||||
return parse_mods(val.split('+'), val) or 0
|
||||
|
||||
|
||||
def to_layout_names(raw):
|
||||
@ -349,25 +362,20 @@ for a in ('active', 'inactive'):
|
||||
type_map['%s_tab_%s' % (a, b)] = to_color
|
||||
|
||||
|
||||
def init_shortcut_maps(ans):
|
||||
ans['keymap'] = {}
|
||||
ans['sequence_map'] = {}
|
||||
|
||||
|
||||
def special_handling(key, val, ans):
|
||||
if key == 'map':
|
||||
parse_key(val, ans['keymap'], ans['sequence_map'])
|
||||
parse_key(val, ans['key_definitions'])
|
||||
return True
|
||||
if key == 'symbol_map':
|
||||
ans['symbol_map'].update(parse_symbol_map(val))
|
||||
return True
|
||||
if key == 'send_text':
|
||||
# For legacy compatibility
|
||||
parse_send_text(val, ans['keymap'], ans['sequence_map'])
|
||||
parse_send_text(val, ans['key_definitions'])
|
||||
return True
|
||||
if key == 'clear_all_shortcuts':
|
||||
if to_bool(val):
|
||||
init_shortcut_maps(ans)
|
||||
ans['key_definitions'] = [None]
|
||||
return
|
||||
|
||||
|
||||
@ -378,8 +386,7 @@ default_config_path = os.path.join(
|
||||
|
||||
|
||||
def parse_config(lines, check_keys=True):
|
||||
ans = {'symbol_map': {}}
|
||||
init_shortcut_maps(ans)
|
||||
ans = {'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': []}
|
||||
parse_config_base(
|
||||
lines,
|
||||
defaults,
|
||||
@ -409,53 +416,20 @@ actions = frozenset(all_key_actions) | frozenset(
|
||||
no_op_actions = frozenset({'noop', 'no-op', 'no_op'})
|
||||
|
||||
|
||||
def merge_keys(ans, defaults, newvals):
|
||||
ans['keymap'] = defaults['keymap'].copy()
|
||||
ans['sequence_map'] = {t: r.copy() for t, r in defaults['sequence_map'].items()}
|
||||
# Merge the keymap
|
||||
for k, v in newvals['keymap'].items():
|
||||
ans['sequence_map'].pop(k, None)
|
||||
f = v.func
|
||||
if f in no_op_actions:
|
||||
ans['keymap'].pop(k, None)
|
||||
elif f in actions:
|
||||
ans['keymap'][k] = v
|
||||
# Merge the sequence map
|
||||
for trigger, rest_map in newvals['sequence_map'].items():
|
||||
ans['keymap'].pop(trigger, None)
|
||||
if trigger in newvals['keymap']:
|
||||
log_error('The shortcut for {} has conflicting definitions'.format(newvals['keymap'][trigger].func))
|
||||
s = ans['sequence_map'].setdefault(trigger, {})
|
||||
for k, v in rest_map.items():
|
||||
f = v.func
|
||||
if f in no_op_actions:
|
||||
s.pop(k, None)
|
||||
elif f in actions:
|
||||
s[k] = v
|
||||
ans['sequence_map'] = {k: v for k, v in ans['sequence_map'].items() if v}
|
||||
|
||||
|
||||
def merge_configs(defaults, vals):
|
||||
ans = {}
|
||||
for k, v in defaults.items():
|
||||
if isinstance(v, dict):
|
||||
if k not in ('keymap', 'sequence_map'):
|
||||
newvals = vals.get(k, {})
|
||||
ans[k] = merge_dicts(v, newvals)
|
||||
newvals = vals.get(k, {})
|
||||
ans[k] = merge_dicts(v, newvals)
|
||||
elif k == 'key_definitions':
|
||||
ans['key_definitions'] = v + vals.get('key_definitions', [])
|
||||
else:
|
||||
ans[k] = vals.get(k, v)
|
||||
defvals = {'keymap': defaults.get('keymap', {}), 'sequence_map': defaults.get('sequence_map', {})}
|
||||
if vals.get('clear_all_shortcuts'):
|
||||
init_shortcut_maps(defvals)
|
||||
merge_keys(
|
||||
ans,
|
||||
defvals,
|
||||
{'keymap': vals.get('keymap', {}), 'sequence_map': vals.get('sequence_map', {})}
|
||||
)
|
||||
return ans
|
||||
|
||||
|
||||
def build_ansi_color_table(opts: Options = defaults):
|
||||
def build_ansi_color_table(opts=defaults):
|
||||
|
||||
def as_int(x):
|
||||
return (x[0] << 16) | (x[1] << 8) | x[2]
|
||||
@ -540,5 +514,40 @@ def prepare_config_file_for_editing():
|
||||
return defconf
|
||||
|
||||
|
||||
def finalize_keys(opts):
|
||||
defns = []
|
||||
for d in opts.key_definitions:
|
||||
if d is None: # clear_all_shortcuts
|
||||
defns = []
|
||||
else:
|
||||
defns.append(d)
|
||||
for d in defns:
|
||||
d.resolve(opts.kitty_mod)
|
||||
keymap = {}
|
||||
sequence_map = {}
|
||||
|
||||
for defn in defns:
|
||||
is_no_op = defn.action.func in no_op_actions
|
||||
if defn.is_sequence:
|
||||
keymap.pop(defn.trigger, None)
|
||||
s = sequence_map.setdefault(defn.trigger, {})
|
||||
if is_no_op:
|
||||
s.pop(defn.rest, None)
|
||||
if not s:
|
||||
del sequence_map[defn.trigger]
|
||||
else:
|
||||
s[defn.rest] = defn.action
|
||||
else:
|
||||
sequence_map.pop(defn.trigger, None)
|
||||
if is_no_op:
|
||||
keymap.pop(defn.trigger, None)
|
||||
else:
|
||||
keymap[defn.trigger] = defn.action
|
||||
opts.keymap = keymap
|
||||
opts.sequence_map = sequence_map
|
||||
|
||||
|
||||
def load_config(*paths, overrides=None):
|
||||
return _load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=overrides)
|
||||
opts = _load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=overrides)
|
||||
finalize_keys(opts)
|
||||
return opts
|
||||
|
||||
@ -257,7 +257,7 @@ def get_shortcut(keymap, mods, key, scancode):
|
||||
|
||||
|
||||
def shortcut_matches(s, mods, key, scancode):
|
||||
return defines.resolve_key_mods(s[0]) & 0b1111 == mods & 0b1111 and s[1] == key
|
||||
return s[0] & 0b1111 == mods & 0b1111 and s[1] == key
|
||||
|
||||
|
||||
def generate_key_table():
|
||||
|
||||
@ -303,10 +303,12 @@ repaint_delay(PyObject *val) {
|
||||
PyObject *key, *value; Py_ssize_t pos = 0; \
|
||||
while (PyDict_Next(d, &pos, &key, &value))
|
||||
|
||||
static int kitty_mod = 0;
|
||||
|
||||
static inline int
|
||||
resolve_mods(int mods) {
|
||||
if (mods & GLFW_MOD_KITTY) {
|
||||
mods = (mods & ~GLFW_MOD_KITTY) | OPT(kitty_mod);
|
||||
mods = (mods & ~GLFW_MOD_KITTY) | kitty_mod;
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
@ -320,7 +322,7 @@ static inline void
|
||||
set_special_keys(PyObject *dict) {
|
||||
dict_iter(dict) {
|
||||
if (!PyTuple_Check(key)) { PyErr_SetString(PyExc_TypeError, "dict keys for special keys must be tuples"); return; }
|
||||
int mods = resolve_mods(PyLong_AsLong(PyTuple_GET_ITEM(key, 0)));
|
||||
int mods = PyLong_AsLong(PyTuple_GET_ITEM(key, 0));
|
||||
int glfw_key = PyLong_AsLong(PyTuple_GET_ITEM(key, 1));
|
||||
set_special_key_combo(glfw_key, mods);
|
||||
}}
|
||||
@ -345,7 +347,8 @@ PYWRAP1(set_options) {
|
||||
global_state.debug_font_fallback = debug_font_fallback ? true : false;
|
||||
#define GA(name) ret = PyObject_GetAttrString(opts, #name); if (ret == NULL) return NULL;
|
||||
#define S(name, convert) { GA(name); global_state.opts.name = convert(ret); Py_DECREF(ret); if (PyErr_Occurred()) return NULL; }
|
||||
S(kitty_mod, PyLong_AsLong);
|
||||
GA(kitty_mod);
|
||||
kitty_mod = PyLong_AsLong(ret); Py_CLEAR(ret); if (PyErr_Occurred()) return NULL;
|
||||
S(visual_bell_duration, PyFloat_AsDouble);
|
||||
S(enable_audio_bell, PyObject_IsTrue);
|
||||
S(focus_follows_mouse, PyObject_IsTrue);
|
||||
@ -560,7 +563,7 @@ PYWRAP1(set_display_state) {
|
||||
|
||||
THREE_ID_OBJ(update_window_title)
|
||||
THREE_ID(remove_window)
|
||||
PYWRAP1(resolve_key_mods) { return PyLong_FromLong(convert_mods(args)); }
|
||||
PYWRAP1(resolve_key_mods) { int mods; PA("ii", &kitty_mod, &mods); return PyLong_FromLong(resolve_mods(mods)); }
|
||||
PYWRAP1(add_tab) { return PyLong_FromUnsignedLongLong(add_tab(PyLong_AsUnsignedLongLong(args))); }
|
||||
PYWRAP1(add_window) { PyObject *title; id_type a, b; PA("KKO", &a, &b, &title); return PyLong_FromUnsignedLongLong(add_window(a, b, title)); }
|
||||
PYWRAP0(current_os_window) { OSWindow *w = current_os_window(); if (!w) Py_RETURN_NONE; return PyLong_FromUnsignedLongLong(w->id); }
|
||||
@ -578,7 +581,7 @@ static PyMethodDef module_methods[] = {
|
||||
MW(current_os_window, METH_NOARGS),
|
||||
MW(set_options, METH_VARARGS),
|
||||
MW(set_in_sequence_mode, METH_O),
|
||||
MW(resolve_key_mods, METH_O),
|
||||
MW(resolve_key_mods, METH_VARARGS),
|
||||
MW(handle_for_window_id, METH_VARARGS),
|
||||
MW(set_logical_dpi, METH_VARARGS),
|
||||
MW(pt_to_px, METH_O),
|
||||
|
||||
@ -32,7 +32,6 @@ typedef struct {
|
||||
Edge tab_bar_edge;
|
||||
bool sync_to_monitor;
|
||||
bool close_on_child_death;
|
||||
int kitty_mod;
|
||||
} Options;
|
||||
|
||||
typedef struct {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user