From 2e693c31df6d9486b29ae2957df85994bf683589 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 27 Apr 2018 11:16:58 +0530 Subject: [PATCH] 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. --- kitty/boss.py | 12 ++--- kitty/cli.py | 75 +++++++++++++++++--------- kitty/config.py | 139 ++++++++++++++++++++++++++---------------------- kitty/keys.py | 2 +- kitty/state.c | 13 +++-- kitty/state.h | 1 - 6 files changed, 138 insertions(+), 104 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index 70b119b88..c6e1c7615 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -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) diff --git a/kitty/cli.py b/kitty/cli.py index bb85be967..f6753be8d 100644 --- a/kitty/cli.py +++ b/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): diff --git a/kitty/config.py b/kitty/config.py index 31ab292db..1df554877 100644 --- a/kitty/config.py +++ b/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 diff --git a/kitty/keys.py b/kitty/keys.py index cc40f27c4..f84788ba8 100644 --- a/kitty/keys.py +++ b/kitty/keys.py @@ -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(): diff --git a/kitty/state.c b/kitty/state.c index ca76723ef..d991582c7 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -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), diff --git a/kitty/state.h b/kitty/state.h index 710914af9..ead642d51 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -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 {