diff kitten: Make the keyboard shortcuts configurable

Fixes #563
This commit is contained in:
Kovid Goyal 2018-05-23 13:10:32 +05:30
parent c127476c4e
commit 4b259dd719
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 205 additions and 60 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

32
kitty/key_encoding.py generated
View File

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