diff --git a/kittens/diff/config.py b/kittens/diff/config.py index ff2df471e..324ced793 100644 --- a/kittens/diff/config.py +++ b/kittens/diff/config.py @@ -5,8 +5,9 @@ import os from kitty.config_utils import ( - init_config, load_config as _load_config, merge_dicts, parse_config_base, - python_string, resolve_config, to_color + init_config, key_func, load_config as _load_config, merge_dicts, + parse_config_base, parse_kittens_key, python_string, resolve_config, + to_color ) from kitty.constants import config_dir from kitty.rgb import color_as_sgr @@ -58,14 +59,46 @@ for name in ( ' highlight_removed_bg highlight_added_bg' ).split(): type_map[name] = to_color +func_with_args, args_funcs = key_func() -def special_handling(*a): - pass +@func_with_args('scroll_by') +def parse_scroll_by(func, rest): + try: + return func, int(rest) + except Exception: + return func, 1 + + +@func_with_args('scroll_to') +def parse_scroll_to(func, rest): + rest = rest.lower() + if rest not in {'start', 'end', 'next-change', 'prev-change'}: + rest = 'start' + return func, rest + + +@func_with_args('change_context') +def parse_change_context(func, rest): + rest = rest.lower() + if rest in {'all', 'default'}: + return func, rest + try: + amount = int(rest) + except Exception: + amount = 5 + return func, amount + + +def special_handling(key, val, ans): + if key == 'map': + action, *key_def = parse_kittens_key(val, args_funcs) + ans['key_definitions'][tuple(key_def)] = action + return True def parse_config(lines, check_keys=True): - ans = {} + ans = {'key_definitions': {}} parse_config_base( lines, defaults, diff --git a/kittens/diff/diff.conf b/kittens/diff/diff.conf index 58104e513..088834e2b 100644 --- a/kittens/diff/diff.conf +++ b/kittens/diff/diff.conf @@ -36,3 +36,20 @@ added_margin_bg #cdffd8 filler_bg #fafbfc hunk_margin_bg #dbedff hunk_bg #f1f8ff + + +# Keyboard shortcuts +map q quit +map esc quit +map j scroll_by 1 +map k scroll_by -1 +map down scroll_by 1 +map up scroll_by -1 +map home scroll_to start +map end scroll_to end +map n scroll_to next-change +map p scroll_to prev-change +map a change_context all +map = change_context default +map + change_context 5 +map - change_context -5 diff --git a/kittens/diff/main.py b/kittens/diff/main.py index 03e8eacd0..acd456518 100644 --- a/kittens/diff/main.py +++ b/kittens/diff/main.py @@ -11,9 +11,7 @@ from gettext import gettext as _ from kitty.cli import CONFIG_HELP, appname, parse_args from kitty.fast_data_types import wcswidth -from kitty.key_encoding import ( - DOWN, END, ESCAPE, HOME, PAGE_DOWN, PAGE_UP, RELEASE, UP -) +from kitty.key_encoding import RELEASE from ..tui.handler import Handler from ..tui.images import ImageManager @@ -63,6 +61,33 @@ class DiffHandler(Handler): self.current_context_count = self.original_context_count = self.opts.num_context_lines self.highlighting_done = False self.restore_position = None + for key_def, action in self.opts.key_definitions.items(): + self.add_shortcut(action, *key_def) + + def perform_action(self, action): + func, args = action + if func == 'quit': + self.quit_loop(0) + return + if self.state <= DIFFED: + if func == 'scroll_by': + return self.scroll_lines(*args) + if func == 'scroll_to': + where = args[0] + if 'change' in where: + return self.scroll_to_next_change(backwards='prev' in where) + amt = len(self.diff_lines) * (1 if 'end' in where else -1) + return self.scroll_lines(amt) + if func == 'change_context': + new_ctx = self.current_context_count + to = args[0] + if to == 'all': + new_ctx = 100000 + elif to == 'default': + new_ctx = self.original_context_count + else: + new_ctx += to + return self.change_context_count(new_ctx) def create_collection(self): self.start_job('collect', create_collection, self.left, self.right) @@ -258,46 +283,16 @@ class DiffHandler(Handler): self.draw_screen() def on_text(self, text, in_bracketed_paste=False): - if text == 'q': - if self.state <= DIFFED: - self.quit_loop(0) - return - if self.state is DIFFED: - if text in 'jk': - self.scroll_lines(1 if text == 'j' else -1) - return - if text in 'a+-=': - new_ctx = self.current_context_count - if text == 'a': - new_ctx = 100000 - elif text == '=': - new_ctx = self.original_context_count - else: - new_ctx += (-1 if text == '-' else 1) * 5 - self.change_context_count(new_ctx) - if text in 'np': - self.scroll_to_next_change(backwards=text == 'p') - return + action = self.shortcut_action(text) + if action is not None: + return self.perform_action(action) def on_key(self, key_event): if key_event.type is RELEASE: return - if key_event.key is ESCAPE: - if self.state <= DIFFED: - self.quit_loop(0) - return - if self.state is DIFFED: - if key_event.key is UP or key_event.key is DOWN: - self.scroll_lines(1 if key_event.key is DOWN else -1) - return - if key_event.key is PAGE_UP or key_event.key is PAGE_DOWN: - amt = self.num_lines * (1 if key_event.key is PAGE_DOWN else -1) - self.scroll_lines(amt) - return - if key_event.key is HOME or key_event.key is END: - amt = len(self.diff_lines) * (1 if key_event.key is END else -1) - self.scroll_lines(amt) - return + action = self.shortcut_action(key_event) + if action is not None: + return self.perform_action(action) def on_resize(self, screen_size): self.screen_size = screen_size diff --git a/kittens/tui/handler.py b/kittens/tui/handler.py index c552dd38c..f388745e5 100644 --- a/kittens/tui/handler.py +++ b/kittens/tui/handler.py @@ -18,6 +18,19 @@ class Handler: self.cmd = commander(self) self.image_manager = image_manager + def add_shortcut(self, action, key, mods=None, is_text=False): + if not hasattr(self, '_text_shortcuts'): + self._text_shortcuts, self._key_shortcuts = {}, {} + if is_text: + self._text_shortcuts[key] = action + else: + self._key_shortcuts[(key, mods or 0)] = action + + def shortcut_action(self, key_event_or_text): + if isinstance(key_event_or_text, str): + return self._text_shortcuts.get(key_event_or_text) + return self._key_shortcuts.get((key_event_or_text.key, key_event_or_text.mods)) + def __enter__(self): if self.image_manager is not None: self.image_manager.__enter__() diff --git a/kitty/config.py b/kitty/config.py index e47042f02..2ce059c5d 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -11,9 +11,9 @@ from contextlib import contextmanager from . import fast_data_types as defines from .config_utils import ( - init_config, load_config as _load_config, merge_dicts, parse_config_base, - positive_float, positive_int, python_string, to_bool, to_cmdline, to_color, - unit_float + init_config, key_func, load_config as _load_config, merge_dicts, + parse_config_base, positive_float, positive_int, python_string, to_bool, + to_cmdline, to_color, unit_float ) from .constants import cache_dir, defconf from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE @@ -92,18 +92,7 @@ def parse_shortcut(sc): KeyAction = namedtuple('KeyAction', 'func args') -args_funcs = {} - - -def func_with_args(*names): - - def w(f): - for name in names: - if args_funcs.setdefault(name, f) is not f: - raise ValueError('the args_func {} is being redefined'.format(name)) - return f - - return w +func_with_args, args_funcs = key_func() @func_with_args( diff --git a/kitty/config_utils.py b/kitty/config_utils.py index fce2f0106..7518decfb 100644 --- a/kitty/config_utils.py +++ b/kitty/config_utils.py @@ -170,3 +170,71 @@ def init_config(defaults_path, parse_config): Options = create_options_class(defaults.keys()) defaults = Options(defaults) return Options, defaults + + +def key_func(): + ans = {} + + def func_with_args(*names): + + def w(f): + for name in names: + if ans.setdefault(name, f) is not f: + raise ValueError('the args_func {} is being redefined'.format(name)) + return f + + return w + return func_with_args, ans + + +def parse_kittens_shortcut(sc): + from kitty.key_encoding import config_key_map, config_mod_map, text_match + if sc.endswith('+'): + parts = list(filter(None, sc.rstrip('+').split('+') + ['+'])) + else: + parts = sc.split('+') + mods = parts[:-1] or None + if mods is not None: + resolved_mods = 0 + for mod in mods: + m = config_mod_map.get(mod.upper()) + if m is None: + raise ValueError('Unknown shortcut modifiers: {}'.format(sc)) + resolved_mods |= m + mods = resolved_mods + is_text = False + rkey = parts[-1] + if text_match(rkey) is None: + rkey = rkey.upper() + rkey = config_key_map.get(rkey) + if rkey is None: + raise ValueError('Unknown shortcut key: {}'.format(sc)) + else: + is_text = True + return mods, rkey, is_text + + +def parse_kittens_func_args(action, args_funcs): + parts = action.split(' ', 1) + func = parts[0] + if len(parts) == 1: + return func, () + rest = parts[1] + parser = args_funcs.get(func) + if parser is not None: + try: + func, args = parser(func, rest) + except Exception: + raise ValueError('Unknown key action: {}'.format(action)) + if not isinstance(args, (list, tuple)): + args = (args,) + return func, tuple(args) + + +def parse_kittens_key(val, funcs_with_args): + sc, action = val.partition(' ')[::2] + if not sc or not action: + return + mods, key, is_text = parse_kittens_shortcut(sc) + action = parse_kittens_func_args(action, funcs_with_args) + return action, key, mods, is_text diff --git a/kitty/key_encoding.py b/kitty/key_encoding.py index 5146d6c54..d88a828d1 100644 --- a/kitty/key_encoding.py +++ b/kitty/key_encoding.py @@ -254,6 +254,15 @@ KEY_MAP = { # END_ENCODING }}} +text_keys = string.ascii_uppercase + string.ascii_lowercase + string.digits + '`~!@#$%^&*()_-+=[{]}\\|<,>./?;:\'" ' + + +def text_match(key): + if key not in text_keys: + return + return key + + def encode( integer, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits + @@ -312,10 +321,31 @@ type_map = {'p': PRESS, 't': REPEAT, 'r': RELEASE} mod_map = {c: i for i, c in enumerate('ABCDEFGHIJKLMNOP')} key_rmap = {} g = globals() +config_key_map = {} +config_mod_map = {'SHIFT': SHIFT, 'ALT': ALT, 'OPTION': ALT, '⌥': ALT, '⌘': SUPER, 'CMD': SUPER, 'SUPER': SUPER, 'CTRL': CTRL, 'CONTROL': CTRL} for key_name, enc in ENCODING.items(): key_name = key_name.replace(' ', '_') - g[key_name] = key_name + g[key_name] = config_key_map[key_name] = key_name key_rmap[enc] = key_name +config_key_map.update({ + '`': g['GRAVE_ACCENT'], + '-': g['MINUS'], + '=': g['EQUAL'], + + '[': g['LEFT_BRACKET'], + ']': g['RIGHT_BRACKET'], + '\\': g['BACKSLASH'], + + ';': g['SEMICOLON'], + "'": g['APOSTROPHE'], + + ',': g['COMMA'], + '.': g['PERIOD'], + '/': g['SLASH'], + + 'ESC': g['ESCAPE'], +}) + del key_name, enc, g enter_key = KeyEvent(PRESS, 0, ENCODING['ENTER']) backspace_key = KeyEvent(PRESS, 0, ENCODING['BACKSPACE'])