Add support for multi-key shortcuts

Fixes #338
This commit is contained in:
Kovid Goyal 2018-03-31 11:31:48 +05:30
parent 9edfafcac2
commit 530fd61125
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 111 additions and 18 deletions

View File

@ -19,11 +19,11 @@ 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, set_clipboard_string,
set_dpi_from_os_window, show_window, toggle_fullscreen,
viewport_for_window
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
from .keys import get_shortcut, shortcut_matches
from .remote_control import handle_cmd
from .session import create_session
from .tabs import SpecialWindow, SpecialWindowInstance, TabManager
@ -79,6 +79,7 @@ class Boss:
def __init__(self, os_window_id, opts, args, cached_values):
self.window_id_map = WeakValueDictionary()
self.pending_sequences = None
self.cached_values = cached_values
self.os_window_map = {}
self.cursor_blinking = True
@ -352,8 +353,37 @@ 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.opts.keymap, mods, key, scancode)
self.current_key_press_info = key, scancode, action, mods
return self.dispatch_action(key_action)
if key_action is None:
sequences = get_shortcut(self.opts.sequence_map, mods, key, scancode)
if sequences:
self.pending_sequences = sequences
set_in_sequence_mode(True)
return True
else:
self.current_key_press_info = key, scancode, action, mods
return self.dispatch_action(key_action)
def process_sequence(self, key, scancode, action, mods):
if not self.pending_sequences:
set_in_sequence_mode(False)
remaining = {}
matched_action = None
for seq, key_action in self.pending_sequences.items():
if shortcut_matches(seq[0], mods, key, scancode):
seq = seq[1:]
if seq:
remaining[seq] = key_action
else:
matched_action = key_action
if remaining:
self.pending_sequences = remaining
else:
self.pending_sequences = None
set_in_sequence_mode(False)
if matched_action is not None:
self.dispatch_action(matched_action)
def default_bg_changed_for(self, window_id):
w = self.window_id_map.get(window_id)

View File

@ -120,16 +120,36 @@ def parse_key_action(action):
return KeyAction(func, args)
def parse_key(val, keymap):
all_key_actions = set()
sequence_sep = '>'
def parse_key(val, keymap, sequence_map):
sc, action = val.partition(' ')[::2]
sc, action = sc.strip(), action.strip()
sc, action = sc.strip().strip(sequence_sep), action.strip()
if not sc or not action:
return
mods, key = parse_shortcut(sc)
if key is None:
log_error('Shortcut: {} has unknown key, ignoring'.format(
val))
return
is_sequence = sequence_sep in sc
if is_sequence:
trigger = None
rest = []
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))
return
if trigger is None:
trigger = mods, key
else:
rest.append((mods, key))
rest = tuple(rest)
else:
mods, key = parse_shortcut(sc)
if key is None:
log_error('Shortcut: {} has unknown key, ignoring'.format(
sc))
return
try:
paction = parse_key_action(action)
except Exception:
@ -137,7 +157,12 @@ def parse_key(val, keymap):
action))
else:
if paction is not None:
keymap[(mods, key)] = paction
all_key_actions.add(paction.func)
if is_sequence:
s = sequence_map.setdefault(trigger, {})
s[rest] = paction
else:
keymap[(mods, key)] = paction
def parse_symbol_map(val):
@ -177,7 +202,7 @@ def parse_send_text_bytes(text):
).encode('utf-8')
def parse_send_text(val, keymap):
def parse_send_text(val, keymap, sequence_map):
parts = val.split(' ')
def abort(msg):
@ -190,7 +215,7 @@ def parse_send_text(val, keymap):
mode, sc = parts[:2]
text = ' '.join(parts[2:])
key_str = '{} send_text {} {}'.format(sc, mode, text)
return parse_key(key_str, keymap)
return parse_key(key_str, keymap, sequence_map)
def to_modifiers(val):
@ -317,14 +342,14 @@ for a in ('active', 'inactive'):
def special_handling(key, val, ans):
if key == 'map':
parse_key(val, ans['keymap'])
parse_key(val, ans['keymap'], ans['sequence_map'])
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'])
parse_send_text(val, ans['keymap'], ans['sequence_map'])
return True
@ -337,6 +362,7 @@ default_config_path = os.path.join(
def parse_config(lines, check_keys=True):
ans = {
'keymap': {},
'sequence_map': {},
'symbol_map': {},
}
parse_config_base(
@ -351,7 +377,7 @@ def parse_config(lines, check_keys=True):
Options, defaults = init_config(default_config_path, parse_config)
actions = frozenset(a.func for a in defaults.keymap.values()) | frozenset(
actions = frozenset(all_key_actions) | frozenset(
'combine send_text goto_tab goto_layout set_font_size new_tab_with_cwd new_window_with_cwd new_os_window_with_cwd'.
split()
)
@ -370,6 +396,20 @@ def merge_keymaps(defaults, newvals):
return ans
def merge_sequence_maps(defaults, newvals):
ans = {t: r.copy() for t, r in defaults.items()}
for trigger, rest_map in newvals.items():
s = ans.setdefault(trigger, {})
for k, v in rest_map.items():
f = v.func
if f in no_op_actions:
s.pop(k, None)
continue
if f in actions:
s[k] = v
return {k: v for k, v in ans.items() if v}
def merge_dicts(defaults, newvals):
ans = defaults.copy()
ans.update(newvals)
@ -383,6 +423,8 @@ def merge_configs(defaults, vals):
newvals = vals.get(k, {})
if k == 'keymap':
ans['keymap'] = merge_keymaps(v, newvals)
elif k == 'sequence_map':
ans['sequence_map'] = merge_sequence_maps(v, newvals)
else:
ans[k] = merge_dicts(v, newvals)
else:

View File

@ -101,6 +101,10 @@ void
on_key_input(int key, int scancode, int action, int mods, const char* text, int state UNUSED) {
Window *w = active_window();
if (!w) return;
if (global_state.in_sequence_mode) {
if (action != GLFW_RELEASE) call_boss(process_sequence, "iiii", key, scancode, action, mods);
return;
}
Screen *screen = w->render_data.screen;
bool has_text = text && !is_ascii_control_char(text[0]);
#ifdef __APPLE__

View File

@ -251,6 +251,10 @@ def get_shortcut(keymap, mods, key, scancode):
return keymap.get((mods & 0b1111, key))
def shortcut_matches(s, mods, key, scancode):
return s[0] & 0b1111 == mods & 0b1111 and s[1] == key
def generate_key_table():
# To run this, use: python3 . -c "from kitty.keys import *; generate_key_table()"
import os

View File

@ -323,6 +323,12 @@ term xterm-kitty
# For example:
# map ctrl+shift+e combine : new_window : next_layout
# this will create a new window and switch to the next available layout
#
# You can use multi-key shortcuts using the syntax shown below:
# map key1>key2>key3 action
# For example:
# map ctrl+f>2 set_font_size 20
# this will change the font size to 20 points when you press ctrl+f and then 3
# Clipboard {{{
map ctrl+shift+v paste_from_clipboard

View File

@ -284,6 +284,7 @@ os_window_regions(OSWindow *os_window, Region *central, Region *tab_bar) {
#define KKI(name) PYWRAP1(name) { id_type a, b; unsigned int c; PA("KKI", &a, &b, &c); name(a, b, c); Py_RETURN_NONE; }
#define KKII(name) PYWRAP1(name) { id_type a, b; unsigned int c, d; PA("KKII", &a, &b, &c, &d); name(a, b, c, d); Py_RETURN_NONE; }
#define KK5I(name) PYWRAP1(name) { id_type a, b; unsigned int c, d, e, f, g; PA("KKIIIII", &a, &b, &c, &d, &e, &f, &g); name(a, b, c, d, e, f, g); Py_RETURN_NONE; }
#define BOOL_SET(name) PYWRAP1(set_##name) { global_state.name = PyObject_IsTrue(args); Py_RETURN_NONE; }
static inline color_type
color_as_int(PyObject *color) {
@ -367,6 +368,8 @@ PYWRAP1(set_options) {
GA(keymap); set_special_keys(ret);
Py_DECREF(ret); if (PyErr_Occurred()) return NULL;
GA(sequence_map); set_special_keys(ret);
Py_DECREF(ret); if (PyErr_Occurred()) return NULL;
#define read_adjust(name) { \
PyObject *al = PyObject_GetAttrString(opts, #name); \
@ -386,6 +389,8 @@ PYWRAP1(set_options) {
Py_RETURN_NONE;
}
BOOL_SET(in_sequence_mode)
PYWRAP1(set_tab_bar_render_data) {
ScreenRenderData d = {0};
id_type os_window_id;
@ -555,6 +560,7 @@ KK5I(add_borders_rect)
static PyMethodDef module_methods[] = {
MW(current_os_window, METH_NOARGS),
MW(set_options, METH_VARARGS),
MW(set_in_sequence_mode, METH_O),
MW(handle_for_window_id, METH_VARARGS),
MW(set_logical_dpi, METH_VARARGS),
MW(pt_to_px, METH_O),

View File

@ -132,6 +132,7 @@ typedef struct {
bool is_wayland;
bool debug_gl, debug_font_fallback;
bool has_pending_resizes;
bool in_sequence_mode;
} GlobalState;
extern GlobalState global_state;