diff --git a/kitty/boss.py b/kitty/boss.py index 522a1f9fe..ca609423b 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -28,7 +28,7 @@ from .fonts.render import set_font_family from .borders import BordersProgram from .char_grid import cursor_shader, cell_shader from .constants import is_key_pressed -from .keys import interpret_text_event, interpret_key_event, get_shortcut +from .keys import interpret_text_event, interpret_key_event, get_shortcut, get_sent_data from .session import create_session from .shaders import Sprites, ShaderProgram from .tabs import TabManager, SpecialWindow @@ -308,7 +308,7 @@ class Boss(Thread): return if window.char_grid.scrolled_by and key not in MODIFIER_KEYS and action == GLFW_PRESS: window.scroll_end() - data = interpret_key_event(key, scancode, mods, window, action) + data = get_sent_data(self.opts.send_text_map, key, scancode, mods, window, action) or interpret_key_event(key, scancode, mods, window, action) if data: window.write_to_child(data) diff --git a/kitty/config.py b/kitty/config.py index 60d55b995..f3d8fef07 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -2,6 +2,7 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal +import ast import json import os import re @@ -84,16 +85,22 @@ named_keys = { } -def parse_key(val, keymap): - sc, action = val.partition(' ')[::2] - action = action.strip() - sc = sc.strip() - if not sc or not action: - return +def parse_shortcut(sc): parts = sc.split('+') mods = parse_mods(parts[:-1]) 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 + + +def parse_key(val, keymap): + sc, action = val.partition(' ')[::2] + sc, action = sc.strip(), action.strip() + if not sc or not action: + return + mods, key = parse_shortcut(sc) if key is None: safe_print( 'Shortcut: {} has an unknown key, ignoring'.format(val), @@ -137,6 +144,39 @@ def parse_symbol_map(val): return symbol_map +def parse_send_text(val): + parts = val.split(' ') + + def abort(msg): + safe_print( + 'Send text: {} is invalid ({}), ignoring'.format(val, msg), file=sys.stderr + ) + return {} + + if len(parts) < 3: + return abort('Incomplete') + + text = ' '.join(parts[2:]) + mode, sc = parts[:2] + mods, key = parse_shortcut(sc.strip()) + if key is None: + return abort('Invalid shortcut') + text = ast.literal_eval("'''" + text + "'''").encode('utf-8') + if not text: + return abort('Empty text') + + if mode in ('all', '*'): + modes = parse_send_text.all_modes + else: + modes = frozenset(mode.split(',')).intersection(parse_send_text.all_modes) + if not modes: + return abort('Invalid keyboard modes') + return {mode: {(mods, key): text} for mode in modes} + + +parse_send_text.all_modes = frozenset({'normal', 'application', 'kitty'}) + + def to_open_url_modifiers(val): return parse_mods(val.split('+')) @@ -199,7 +239,7 @@ for a in ('active', 'inactive'): def parse_config(lines): - ans = {'keymap': {}, 'symbol_map': {}} + ans = {'keymap': {}, 'symbol_map': {}, 'send_text_map': {'kitty': {}, 'normal': {}, 'application': {}}} for line in lines: line = line.strip() if not line or line.startswith('#'): @@ -213,6 +253,11 @@ def parse_config(lines): if key == 'symbol_map': ans['symbol_map'].update(parse_symbol_map(val)) continue + if key == 'send_text': + stvals = parse_send_text(val) + for k, v in ans['send_text_map'].items(): + v.update(stvals.get(k, {})) + continue tm = type_map.get(key) if tm is not None: val = tm(val) @@ -236,7 +281,7 @@ def update_dict(a, b): def merge_dicts(vals, defaults): return { - k: update_dict(v, vals.get(k, {})) + k: merge_dicts(v, vals.get(k, {})) if isinstance(v, dict) else vals.get(k, v) for k, v in defaults.items() } diff --git a/kitty/keys.py b/kitty/keys.py index 336f121e6..02283c0d9 100644 --- a/kitty/keys.py +++ b/kitty/keys.py @@ -105,6 +105,12 @@ def get_key_map(screen): return cursor_key_mode_map[screen.cursor_key_mode] +def keyboard_mode_name(screen): + if screen.extended_keyboard: + return 'kitty' + return 'application' if screen.cursor_key_mode else 'normal' + + valid_localized_key_names = { k: getattr(defines, 'GLFW_KEY_' + k) for k in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' @@ -195,3 +201,11 @@ def interpret_text_event(codepoint, mods, window): def get_shortcut(keymap, mods, key, scancode): key = get_localized_key(key, scancode) return keymap.get((mods & 0b1111, key)) + + +def get_sent_data(send_text_map, key, scancode, mods, window, action): + if action in (defines.GLFW_PRESS, defines.GLFW_REPEAT): + key = get_localized_key(key, scancode) + m = keyboard_mode_name(window.screen) + keymap = send_text_map[m] + return keymap.get((mods & 0b1111, key)) diff --git a/kitty/kitty.conf b/kitty/kitty.conf index 8a6bcd54d..4e7a65049 100644 --- a/kitty/kitty.conf +++ b/kitty/kitty.conf @@ -231,6 +231,23 @@ map ctrl+shift+equal increase_font_size map ctrl+shift+minus decrease_font_size map ctrl+shift+backspace restore_font_size +# Sending arbitrary text on shortcut key presses +# You can tell kitty to send arbitrary (UTF-8) encoded text to +# the client program when pressing specified shortcut keys. For example: +# send_text all ctrl+alt+a Special text +# This will send "Special text" when you press the Ctrl+Alt+a key combination. +# The text to be sent is a python string literal so you can use escapes like +# \x1b to send control codes or \u21fb to send unicode characters (or you can +# just input the unicode characters directly as UTF-8 text). The first argument +# to send_text is the keyboard modes in which to activate the shortcut. The possible +# values are normal or application or kitty or a comma separated combination of them. +# The special keyword all means all modes. The modes normal and application refer to +# the DECCKM cursor key mode for terminals, and kitty refers to the special kitty +# extended keyboard protocol. Another example, that outputs a word and then moves the cursor +# to the start of the line (same as pressing the Home key): +# send_text normal ctrl+alt+a Word\x1b[H +# send_text application ctrl+alt+a Word\x1bOH + # Symbol mapping (special font for specified unicode code points). Map the # specified unicode codepoints to a particular font. Useful if you need special # rendering for some symbols, such as for Powerline. Avoids the need for