mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-25 22:52:05 +00:00
Simplify remap_modifier making it a single option
This commit is contained in:
parent
f5fba5e584
commit
58828dd893
7 changed files with 75 additions and 80 deletions
|
|
@ -3568,39 +3568,35 @@ this option to alter all default shortcuts that use :opt:`kitty_mod`.
|
|||
)
|
||||
|
||||
opt(
|
||||
'+remap_modifier',
|
||||
'ctrl super',
|
||||
option_type='remap_modifier',
|
||||
ctype='!remap_modifier',
|
||||
'remap_modifiers',
|
||||
'',
|
||||
option_type='remap_modifiers',
|
||||
ctype='!remap_modifiers',
|
||||
add_to_default=False,
|
||||
long_text="""
|
||||
Present one modifier key to kitty as if it were another. The syntax is::
|
||||
Remap modifiers, making one modifier act as another. The syntax is::
|
||||
|
||||
remap_modifier <from> <to>
|
||||
remap_modifiers <from>:<to> <from>:<to> ...
|
||||
|
||||
for example :code:`remap_modifier ctrl super` makes every :kbd:`Ctrl+key` press
|
||||
for example :code:`remap_modifiers ctrl:super` makes every :kbd:`Ctrl+key` press
|
||||
arrive as :kbd:`Super+key`. Only :code:`shift`, :code:`alt`, :code:`ctrl`,
|
||||
:code:`super`, :code:`hyper` and :code:`meta` can be named. The source must be
|
||||
exactly one modifier; the destination may name more than one, in which case
|
||||
holding the source is indistinguishable from holding all of them.
|
||||
|
||||
This option can be specified multiple times, and the declarations are applied as
|
||||
a single simultaneous permutation, once, as each event arrives. So::
|
||||
To swap two modifiers, specify remap rules for both, for example, to swap ctrl and super::
|
||||
|
||||
remap_modifier ctrl super
|
||||
remap_modifier super ctrl
|
||||
remap_modifiers ctrl:super super:ctrl
|
||||
|
||||
exchanges the two rather than collapsing to no change. Ordering among
|
||||
:opt:`remap_modifier` declarations matters only in that the last declaration for
|
||||
a given source wins; where they sit relative to other options in
|
||||
:file:`kitty.conf` makes no difference.
|
||||
Ordering among :opt:`remap_modifiers` items matters only in that the last item for
|
||||
a given source wins.
|
||||
|
||||
The remapping happens before anything else looks at the event, so it is
|
||||
application wide: kitty's own keyboard shortcuts, :opt:`kitty_mod`,
|
||||
:opt:`mouse_map` and the keys sent to the program running in the terminal all
|
||||
see the remapped modifier. Every mapping in :file:`kitty.conf` is therefore
|
||||
written in terms of the modifier a key *becomes*, not the one printed on the
|
||||
keycap — including the built-in shortcuts, so :code:`remap_modifier ctrl super`
|
||||
keycap — including the built-in shortcuts, so :code:`remap_modifiers ctrl super`
|
||||
moves every default :code:`ctrl+shift+…` binding onto the physical Super key
|
||||
unless you also change :opt:`kitty_mod`.
|
||||
|
||||
|
|
@ -3625,8 +3621,7 @@ silently losing the modifier.
|
|||
On Wayland, kitty only detects the :kbd:`hyper` and :kbd:`meta` modifiers when
|
||||
:envvar:`KITTY_WAYLAND_DETECT_MODIFIERS` is set in the environment. Without it
|
||||
those two never appear on key events at all, so remapping to or from them has
|
||||
nothing to act on there. This is pre-existing behaviour, not a limitation of
|
||||
this option.
|
||||
nothing to act on there.
|
||||
|
||||
This is useful for keyboard layouts that move Control somewhere more comfortable
|
||||
— for example placing a Hyper key on Caps Lock to use for readline and TUI
|
||||
|
|
|
|||
8
kitty/options/parse.py
generated
8
kitty/options/parse.py
generated
|
|
@ -17,7 +17,7 @@ from kitty.options.utils import (
|
|||
deprecated_send_text, disable_ligatures, edge_width, env, filter_notification, font_features,
|
||||
hide_window_decorations, macos_option_as_alt, macos_titlebar_color, menu_map, modify_font,
|
||||
mouse_hide_wait, narrow_symbols, notify_on_cmd_finish, optional_edge_width, parse_font_spec,
|
||||
parse_map, parse_mouse_map, paste_actions, pointer_shape_when_dragging, remap_modifier,
|
||||
parse_map, parse_mouse_map, paste_actions, pointer_shape_when_dragging, remap_modifiers,
|
||||
remote_control_password, resize_debounce_time, scrollback_lines, scrollback_pager_history_size,
|
||||
scrollbar_color, shell_integration, show_hyperlink_targets, store_multiple, symbol_map,
|
||||
tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade,
|
||||
|
|
@ -1242,9 +1242,8 @@ class Parser:
|
|||
|
||||
choices_for_progress_bar = frozenset(('left', 'right', 'top', 'bottom', 'hidden'))
|
||||
|
||||
def remap_modifier(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||
for k, v in remap_modifier(val):
|
||||
ans["remap_modifier"][k] = v
|
||||
def remap_modifiers(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||
ans['remap_modifiers'] = remap_modifiers(val)
|
||||
|
||||
def remember_window_position(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||
ans['remember_window_position'] = to_bool(val)
|
||||
|
|
@ -1644,7 +1643,6 @@ def create_result_dict() -> dict[str, typing.Any]:
|
|||
'menu_map': {},
|
||||
'modify_font': {},
|
||||
'narrow_symbols': {},
|
||||
'remap_modifier': {},
|
||||
'remote_control_password': {},
|
||||
'symbol_map': {},
|
||||
'watcher': {},
|
||||
|
|
|
|||
12
kitty/options/to-c-generated.h
generated
12
kitty/options/to-c-generated.h
generated
|
|
@ -1539,15 +1539,15 @@ convert_from_opts_wayland_enable_ime(PyObject *py_opts, Options *opts) {
|
|||
}
|
||||
|
||||
static void
|
||||
convert_from_python_remap_modifier(PyObject *val, Options *opts) {
|
||||
remap_modifier(val, opts);
|
||||
convert_from_python_remap_modifiers(PyObject *val, Options *opts) {
|
||||
remap_modifiers(val, opts);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_opts_remap_modifier(PyObject *py_opts, Options *opts) {
|
||||
PyObject *ret = PyObject_GetAttrString(py_opts, "remap_modifier");
|
||||
convert_from_opts_remap_modifiers(PyObject *py_opts, Options *opts) {
|
||||
PyObject *ret = PyObject_GetAttrString(py_opts, "remap_modifiers");
|
||||
if (ret == NULL) return;
|
||||
convert_from_python_remap_modifier(ret, opts);
|
||||
convert_from_python_remap_modifiers(ret, opts);
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
|
|
@ -1789,7 +1789,7 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
|||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_wayland_enable_ime(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_remap_modifier(py_opts, opts);
|
||||
convert_from_opts_remap_modifiers(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -555,7 +555,7 @@ box_drawing_scale(PyObject *val, Options *opts) {
|
|||
}
|
||||
|
||||
static inline void
|
||||
remap_modifier(PyObject *val, Options *opts) {
|
||||
remap_modifiers(PyObject *val, Options *opts) {
|
||||
memset(opts->modifier_remap, 0, sizeof(opts->modifier_remap));
|
||||
opts->modifier_remap_mask = 0;
|
||||
if (!PyDict_Check(val)) {
|
||||
|
|
|
|||
5
kitty/options/types.py
generated
5
kitty/options/types.py
generated
|
|
@ -426,7 +426,7 @@ option_names = (
|
|||
'pointer_shape_when_dragging',
|
||||
'pointer_shape_when_grabbed',
|
||||
'progress_bar',
|
||||
'remap_modifier',
|
||||
'remap_modifiers',
|
||||
'remember_window_position',
|
||||
'remember_window_size',
|
||||
'remote_control_password',
|
||||
|
|
@ -642,6 +642,7 @@ class Options:
|
|||
pointer_shape_when_dragging: tuple[str, str] = ('beam', 'crosshair')
|
||||
pointer_shape_when_grabbed: choices_for_pointer_shape_when_grabbed = 'arrow'
|
||||
progress_bar: choices_for_progress_bar = 'top'
|
||||
remap_modifiers: dict[int, int] = {}
|
||||
remember_window_position: bool = False
|
||||
remember_window_size: bool = True
|
||||
repaint_delay: int = 10
|
||||
|
|
@ -745,7 +746,6 @@ class Options:
|
|||
menu_map: dict[tuple[str, ...], str] = {}
|
||||
modify_font: dict[str, kitty.fonts.FontModification] = {}
|
||||
narrow_symbols: dict[tuple[int, int], int] = {}
|
||||
remap_modifier: dict[int, int] = {}
|
||||
remote_control_password: dict[str, collections.abc.Sequence[str]] = {}
|
||||
symbol_map: dict[tuple[int, int], str] = {}
|
||||
watcher: dict[str, str] = {}
|
||||
|
|
@ -875,7 +875,6 @@ defaults.kitten_alias = {}
|
|||
defaults.menu_map = {}
|
||||
defaults.modify_font = {}
|
||||
defaults.narrow_symbols = {}
|
||||
defaults.remap_modifier = {}
|
||||
defaults.remote_control_password = {}
|
||||
defaults.symbol_map = {}
|
||||
defaults.watcher = {}
|
||||
|
|
|
|||
|
|
@ -524,44 +524,49 @@ def to_modifiers(val: str) -> int:
|
|||
return parse_mods(val.split('+'), val) or 0
|
||||
|
||||
|
||||
def remap_modifier(val: str) -> Iterable[tuple[int, int]]:
|
||||
def remap_modifiers(val: str) -> dict[int, int]:
|
||||
# Only the real modifiers may be remapped. parse_mods() also accepts
|
||||
# kitty_mod (a parse-time placeholder resolved out of keymaps before any key
|
||||
# event exists) and the lock modifiers, neither of which can be meaningfully
|
||||
# carried on an event, so they are rejected here rather than silently
|
||||
# mangling events later.
|
||||
ans = {}
|
||||
if not val:
|
||||
return ans
|
||||
remappable = (
|
||||
defines.GLFW_MOD_SHIFT | defines.GLFW_MOD_ALT | defines.GLFW_MOD_CONTROL | defines.GLFW_MOD_SUPER | defines.GLFW_MOD_HYPER | defines.GLFW_MOD_META
|
||||
)
|
||||
|
||||
def bad(msg: str) -> None:
|
||||
log_error(f'Ignoring invalid remap_modifier "{val}": {msg}')
|
||||
log_error(f'Ignoring invalid remap_modifiers "{val}": {msg}')
|
||||
|
||||
parts = val.split()
|
||||
if len(parts) != 2:
|
||||
bad('must be two modifier names separated by whitespace')
|
||||
return
|
||||
# parse_mods() returns None both for an unknown name (already logged) and for
|
||||
# "none" (deliberately not logged), so check the names before trusting it
|
||||
for part in parts:
|
||||
for name in part.split('+'):
|
||||
if name.upper() in ('NONE', ''):
|
||||
bad('none is not a modifier')
|
||||
return
|
||||
src = parse_mods(parts[0].split('+'), val)
|
||||
dest = parse_mods(parts[1].split('+'), val)
|
||||
if src is None or dest is None:
|
||||
return # parse_mods() has already logged the unknown modifier name
|
||||
if src & ~remappable or dest & ~remappable:
|
||||
bad('only shift, alt, ctrl, super, hyper and meta can be remapped')
|
||||
return
|
||||
if src & (src - 1):
|
||||
bad('the source must name exactly one modifier')
|
||||
return
|
||||
if src == dest:
|
||||
bad('the source and destination are the same')
|
||||
return
|
||||
yield src, dest
|
||||
for token in val.split():
|
||||
parts = token.split(':', 1)
|
||||
if len(parts) != 2:
|
||||
bad('must be two modifier names separated by a colon')
|
||||
return {}
|
||||
# parse_mods() returns None both for an unknown name (already logged) and for
|
||||
# "none" (deliberately not logged), so check the names before trusting it
|
||||
for part in parts:
|
||||
for name in part.split('+'):
|
||||
if name.upper() in ('NONE', ''):
|
||||
bad('none is not a modifier')
|
||||
return {}
|
||||
src = parse_mods(parts[0].split('+'), val)
|
||||
dest = parse_mods(parts[1].split('+'), val)
|
||||
if src is None or dest is None:
|
||||
return {} # parse_mods() has already logged the unknown modifier name
|
||||
if src & ~remappable or dest & ~remappable:
|
||||
bad('only shift, alt, ctrl, super, hyper and meta can be remapped')
|
||||
return {}
|
||||
if src & (src - 1):
|
||||
bad('the source must name exactly one modifier')
|
||||
return {}
|
||||
if src == dest:
|
||||
bad('the source and destination are the same')
|
||||
return {}
|
||||
ans[src] = dest
|
||||
return ans
|
||||
|
||||
|
||||
def parse_shortcut(sc: str) -> SingleKey:
|
||||
|
|
|
|||
|
|
@ -466,32 +466,30 @@ def conf_parsing(self):
|
|||
# sequential implementation would produce an identical one. That property is
|
||||
# covered end to end by tests/test-remap-e2e.sh, which asserts that the
|
||||
# encoded bytes actually exchange.
|
||||
self.ae(p().remap_modifier, {})
|
||||
self.ae(p().remap_modifiers, {})
|
||||
ctrl, hyper, sup = to_modifiers('ctrl'), to_modifiers('hyper'), to_modifiers('super')
|
||||
self.ae(p('remap_modifier ctrl hyper').remap_modifier, {ctrl: hyper})
|
||||
self.ae(p('remap_modifiers ctrl:hyper').remap_modifiers, {ctrl: hyper})
|
||||
# both directions of a swap are recorded
|
||||
self.ae(p('remap_modifier ctrl hyper', 'remap_modifier hyper ctrl').remap_modifier, {ctrl: hyper, hyper: ctrl})
|
||||
self.ae(p('remap_modifiers ctrl:hyper hyper:ctrl').remap_modifiers, {ctrl: hyper, hyper: ctrl})
|
||||
# the last declaration for a given source wins, wherever it appears
|
||||
self.ae(p('remap_modifier ctrl hyper', 'kitty_mod ctrl', 'remap_modifier ctrl super').remap_modifier, {ctrl: sup})
|
||||
# unrelated options in between do not disturb it
|
||||
self.ae(p('remap_modifier ctrl hyper', 'font_size 13', 'remap_modifier super alt').remap_modifier, {ctrl: hyper, sup: to_modifiers('alt')})
|
||||
self.ae(p('remap_modifiers ctrl:hyper', 'kitty_mod ctrl', 'remap_modifiers ctrl:super').remap_modifiers, {ctrl: sup})
|
||||
# the destination may name more than one modifier
|
||||
self.ae(p('remap_modifier ctrl ctrl+shift').remap_modifier, {ctrl: to_modifiers('ctrl+shift')})
|
||||
self.ae(p('remap_modifiers ctrl:ctrl+shift').remap_modifiers, {ctrl: to_modifiers('ctrl+shift')})
|
||||
# every rejection must be REPORTED, never silently ignored: wrong arity, unknown
|
||||
# or non-remappable modifier names, a source naming more than one modifier, and
|
||||
# a mapping that does nothing
|
||||
for bad in (
|
||||
'remap_modifier ctrl',
|
||||
'remap_modifier ctrl hyper super',
|
||||
'remap_modifier ctrl nosuchmod',
|
||||
'remap_modifier nosuchmod ctrl',
|
||||
'remap_modifier ctrl+shift hyper',
|
||||
'remap_modifier ctrl none',
|
||||
'remap_modifier none ctrl',
|
||||
'remap_modifier ctrl kitty_mod',
|
||||
'remap_modifier kitty_mod ctrl',
|
||||
'remap_modifier caps_lock ctrl',
|
||||
'remap_modifier ctrl num_lock',
|
||||
'remap_modifier ctrl ctrl',
|
||||
'remap_modifiers ctrl',
|
||||
'remap_modifiers ctrl:hyper super',
|
||||
'remap_modifiers ctrl:nosuchmod',
|
||||
'remap_modifiers nosuchmod:ctrl',
|
||||
'remap_modifiers ctrl+shift:hyper',
|
||||
'remap_modifiers ctrl:none',
|
||||
'remap_modifiers none:ctrl',
|
||||
'remap_modifiers ctrl:kitty_mod',
|
||||
'remap_modifiers kitty_mod:ctrl',
|
||||
'remap_modifiers caps_lock:ctrl',
|
||||
'remap_modifiers ctrl:num_lock',
|
||||
'remap_modifiers ctrl:ctrl',
|
||||
):
|
||||
self.ae(p(bad, num_err=1).remap_modifier, {}, f'not rejected: {bad}')
|
||||
self.ae(p(bad, num_err=1).remap_modifiers, {}, f'not rejected: {bad}')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue