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:
Kovid Goyal 2018-04-27 11:16:58 +05:30
parent 0f06bf1bdb
commit 2e693c31df
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 138 additions and 104 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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():

View File

@ -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),

View File

@ -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 {