mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-28 03:01:57 +02:00
Add timeout option to key mappings
This commit is contained in:
@@ -167,6 +167,33 @@ the program running in the terminal, map it to :ac:`discard_event`::
|
|||||||
|
|
||||||
.. _conditional_mappings:
|
.. _conditional_mappings:
|
||||||
|
|
||||||
|
Configuring a timeout
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
You can also set a timeout for keyboard modes and multi-key mappings. If a
|
||||||
|
timeout is set and you don't complete the key sequence or exit the mode within
|
||||||
|
the specified time, the mode will be automatically cancelled. This is useful
|
||||||
|
for multi-key mappings where you might accidentally press the first key and
|
||||||
|
then change your mind. The timeout is specified in seconds and can be set
|
||||||
|
globally using the :opt:`map_timeout` option or per-mode using ``--timeout``::
|
||||||
|
|
||||||
|
# Set a global 2 second timeout for all multi-key and modal mappings
|
||||||
|
map_timeout 2.0
|
||||||
|
|
||||||
|
# This mode will have a 5 second timeout (overrides global setting)
|
||||||
|
map --new-mode resize --timeout 5.0 kitty_mod+r
|
||||||
|
map --mode resize h resize_window narrower
|
||||||
|
map --mode resize l resize_window wider
|
||||||
|
# ... more mappings
|
||||||
|
|
||||||
|
# Multi-key mapping with the global timeout
|
||||||
|
map ctrl+a>h new_window
|
||||||
|
|
||||||
|
When a timeout occurs, the mode is exited and any buffered keys are discarded.
|
||||||
|
A timeout value of zero disables the timeout. For multi-key sequences, the
|
||||||
|
timeout is restarted after each valid key press in the sequence.
|
||||||
|
|
||||||
|
|
||||||
Conditional mappings depending on the state of the focused window
|
Conditional mappings depending on the state of the focused window
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ def finalize_keys(opts: Options, accumulate_bad_lines: list[BadLine] | None = No
|
|||||||
modes[defn.options.new_mode] = nm = KeyboardMode(defn.options.new_mode)
|
modes[defn.options.new_mode] = nm = KeyboardMode(defn.options.new_mode)
|
||||||
nm.on_unknown = defn.options.on_unknown
|
nm.on_unknown = defn.options.on_unknown
|
||||||
nm.on_action = defn.options.on_action
|
nm.on_action = defn.options.on_action
|
||||||
|
nm.timeout = defn.options.timeout if defn.options.timeout is not None else opts.map_timeout
|
||||||
defn.definition = f'push_keyboard_mode {defn.options.new_mode}'
|
defn.definition = f'push_keyboard_mode {defn.options.new_mode}'
|
||||||
try:
|
try:
|
||||||
m = modes[defn.options.mode]
|
m = modes[defn.options.mode]
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ def shortcut_matches(s: SingleKey, ev: KeyEvent) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
class Mappings:
|
class Mappings:
|
||||||
|
"Manage all keyboard mappings"
|
||||||
|
|
||||||
' Manage all keyboard mappings '
|
def __init__(self, global_shortcuts: dict[str, SingleKey] | None = None, callback_on_mode_change: Callable[[], Any] = lambda: None) -> None:
|
||||||
|
|
||||||
def __init__(self, global_shortcuts:dict[str, SingleKey] | None = None, callback_on_mode_change: Callable[[], Any] = lambda: None) -> None:
|
|
||||||
self.keyboard_mode_stack: list[KeyboardMode] = []
|
self.keyboard_mode_stack: list[KeyboardMode] = []
|
||||||
|
self.mode_timeout_timer_id: int | None = None
|
||||||
self.update_keymap(global_shortcuts)
|
self.update_keymap(global_shortcuts)
|
||||||
self.callback_on_mode_change = callback_on_mode_change
|
self.callback_on_mode_change = callback_on_mode_change
|
||||||
|
|
||||||
@@ -87,6 +87,7 @@ class Mappings:
|
|||||||
|
|
||||||
def clear_keyboard_modes(self) -> None:
|
def clear_keyboard_modes(self) -> None:
|
||||||
had_mode = bool(self.keyboard_mode_stack)
|
had_mode = bool(self.keyboard_mode_stack)
|
||||||
|
self._cancel_mode_timeout()
|
||||||
self.keyboard_mode_stack = []
|
self.keyboard_mode_stack = []
|
||||||
self.set_ignore_os_keyboard_processing(False)
|
self.set_ignore_os_keyboard_processing(False)
|
||||||
if had_mode:
|
if had_mode:
|
||||||
@@ -95,6 +96,7 @@ class Mappings:
|
|||||||
def pop_keyboard_mode(self) -> bool:
|
def pop_keyboard_mode(self) -> bool:
|
||||||
passthrough = True
|
passthrough = True
|
||||||
if self.keyboard_mode_stack:
|
if self.keyboard_mode_stack:
|
||||||
|
self._cancel_mode_timeout()
|
||||||
self.keyboard_mode_stack.pop()
|
self.keyboard_mode_stack.pop()
|
||||||
if not self.keyboard_mode_stack:
|
if not self.keyboard_mode_stack:
|
||||||
self.set_ignore_os_keyboard_processing(False)
|
self.set_ignore_os_keyboard_processing(False)
|
||||||
@@ -110,12 +112,38 @@ class Mappings:
|
|||||||
def _push_keyboard_mode(self, mode: KeyboardMode) -> None:
|
def _push_keyboard_mode(self, mode: KeyboardMode) -> None:
|
||||||
self.keyboard_mode_stack.append(mode)
|
self.keyboard_mode_stack.append(mode)
|
||||||
self.set_ignore_os_keyboard_processing(True)
|
self.set_ignore_os_keyboard_processing(True)
|
||||||
|
self._start_mode_timeout(mode)
|
||||||
self.callback_on_mode_change()
|
self.callback_on_mode_change()
|
||||||
|
|
||||||
def push_keyboard_mode(self, new_mode: str) -> None:
|
def push_keyboard_mode(self, new_mode: str) -> None:
|
||||||
mode = self.keyboard_modes[new_mode]
|
mode = self.keyboard_modes[new_mode]
|
||||||
self._push_keyboard_mode(mode)
|
self._push_keyboard_mode(mode)
|
||||||
|
|
||||||
|
def _start_mode_timeout(self, mode: KeyboardMode) -> None:
|
||||||
|
if mode.timeout > 0:
|
||||||
|
from .fast_data_types import add_timer
|
||||||
|
|
||||||
|
self._cancel_mode_timeout()
|
||||||
|
self.mode_timeout_timer_id = add_timer(self._on_mode_timeout, mode.timeout, False)
|
||||||
|
mode.timeout_timer_id = self.mode_timeout_timer_id
|
||||||
|
|
||||||
|
def _cancel_mode_timeout(self) -> None:
|
||||||
|
if self.mode_timeout_timer_id is not None:
|
||||||
|
from .fast_data_types import remove_timer
|
||||||
|
|
||||||
|
remove_timer(self.mode_timeout_timer_id)
|
||||||
|
self.mode_timeout_timer_id = None
|
||||||
|
|
||||||
|
def _on_mode_timeout(self, timer_id: int | None) -> None:
|
||||||
|
self.mode_timeout_timer_id = None
|
||||||
|
if self.keyboard_mode_stack:
|
||||||
|
self.pop_keyboard_mode()
|
||||||
|
|
||||||
|
def _get_effective_timeout(self, key_def: KeyDefinition) -> float:
|
||||||
|
if key_def.options.timeout is not None:
|
||||||
|
return key_def.options.timeout
|
||||||
|
return self.get_options().map_timeout
|
||||||
|
|
||||||
def matching_key_actions(self, candidates: Iterable[KeyDefinition]) -> list[KeyDefinition]:
|
def matching_key_actions(self, candidates: Iterable[KeyDefinition]) -> list[KeyDefinition]:
|
||||||
w = self.get_active_window()
|
w = self.get_active_window()
|
||||||
matches = []
|
matches = []
|
||||||
@@ -128,8 +156,9 @@ class Mappings:
|
|||||||
is_applicable = True
|
is_applicable = True
|
||||||
except Exception:
|
except Exception:
|
||||||
self.clear_keyboard_modes()
|
self.clear_keyboard_modes()
|
||||||
self.show_error(_('Invalid key mapping'), _(
|
self.show_error(
|
||||||
'The match expression {0} is not valid for {1}').format(x.options.when_focus_on, '--when-focus-on'))
|
_('Invalid key mapping'), _('The match expression {0} is not valid for {1}').format(x.options.when_focus_on, '--when-focus-on')
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
is_applicable = True
|
is_applicable = True
|
||||||
@@ -143,10 +172,10 @@ class Mappings:
|
|||||||
if not x.rest:
|
if not x.rest:
|
||||||
last_terminal_idx = i
|
last_terminal_idx = i
|
||||||
if last_terminal_idx > -1:
|
if last_terminal_idx > -1:
|
||||||
if last_terminal_idx == len(matches) -1:
|
if last_terminal_idx == len(matches) - 1:
|
||||||
matches = matches[last_terminal_idx:]
|
matches = matches[last_terminal_idx:]
|
||||||
else:
|
else:
|
||||||
matches = matches[last_terminal_idx+1:]
|
matches = matches[last_terminal_idx + 1 :]
|
||||||
q = matches[-1].options.when_focus_on
|
q = matches[-1].options.when_focus_on
|
||||||
matches = [x for x in matches if x.options.when_focus_on == q]
|
matches = [x for x in matches if x.options.when_focus_on == q]
|
||||||
elif matches:
|
elif matches:
|
||||||
@@ -192,6 +221,7 @@ class Mappings:
|
|||||||
sm = KeyboardMode('__sequence__')
|
sm = KeyboardMode('__sequence__')
|
||||||
sm.on_action = 'end'
|
sm.on_action = 'end'
|
||||||
sm.sequence_keys = [ev]
|
sm.sequence_keys = [ev]
|
||||||
|
sm.timeout = self._get_effective_timeout(final_actions[0])
|
||||||
for fa in final_actions:
|
for fa in final_actions:
|
||||||
sm.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy())
|
sm.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy())
|
||||||
self._push_keyboard_mode(sm)
|
self._push_keyboard_mode(sm)
|
||||||
@@ -206,6 +236,7 @@ class Mappings:
|
|||||||
w.send_key_sequence(*mode.sequence_keys)
|
w.send_key_sequence(*mode.sequence_keys)
|
||||||
return consumed
|
return consumed
|
||||||
mode.sequence_keys.append(ev)
|
mode.sequence_keys.append(ev)
|
||||||
|
self._start_mode_timeout(mode)
|
||||||
self.debug_print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='')
|
self.debug_print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='')
|
||||||
mode.keymap.clear()
|
mode.keymap.clear()
|
||||||
for fa in final_actions:
|
for fa in final_actions:
|
||||||
@@ -219,6 +250,8 @@ class Mappings:
|
|||||||
self.callback_on_mode_change()
|
self.callback_on_mode_change()
|
||||||
if not self.keyboard_mode_stack:
|
if not self.keyboard_mode_stack:
|
||||||
self.set_ignore_os_keyboard_processing(False)
|
self.set_ignore_os_keyboard_processing(False)
|
||||||
|
elif not is_root_mode and mode_pos < len(self.keyboard_mode_stack) and self.keyboard_mode_stack[mode_pos] is mode:
|
||||||
|
self._start_mode_timeout(mode)
|
||||||
return consumed
|
return consumed
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -252,5 +285,7 @@ class Mappings:
|
|||||||
|
|
||||||
def set_cocoa_global_shortcuts(self, opts: Options) -> dict[str, SingleKey]:
|
def set_cocoa_global_shortcuts(self, opts: Options) -> dict[str, SingleKey]:
|
||||||
from .main import set_cocoa_global_shortcuts
|
from .main import set_cocoa_global_shortcuts
|
||||||
|
|
||||||
return set_cocoa_global_shortcuts(opts)
|
return set_cocoa_global_shortcuts(opts)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|||||||
@@ -3847,6 +3847,24 @@ remove the default shortcuts.
|
|||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
opt('map_timeout', '0.0',
|
||||||
|
option_type='positive_float', ctype='time',
|
||||||
|
long_text='''
|
||||||
|
The default timeout (in seconds) for multi-key mappings and modal keyboard modes.
|
||||||
|
If you press the first key(s) of a multi-key mapping and don't press the next
|
||||||
|
key within this timeout, the mapping is cancelled and the mode is exited. A value
|
||||||
|
of zero disables the timeout. This can be overridden for specific modes using the
|
||||||
|
:code:`--timeout` option when creating a keyboard mode with :code:`--new-mode`.
|
||||||
|
For example::
|
||||||
|
|
||||||
|
# 2 second timeout for all mappings
|
||||||
|
map_timeout 2.0
|
||||||
|
|
||||||
|
# This mode will have a 5 second timeout (overrides the global 2 second timeout)
|
||||||
|
map --new-mode resize --timeout 5.0 kitty_mod+r
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
opt('+action_alias', 'launch_tab launch --type=tab --cwd=current',
|
opt('+action_alias', 'launch_tab launch --type=tab --cwd=current',
|
||||||
option_type='action_alias',
|
option_type='action_alias',
|
||||||
add_to_default=False,
|
add_to_default=False,
|
||||||
|
|||||||
3
kitty/options/parse.py
generated
3
kitty/options/parse.py
generated
@@ -1060,6 +1060,9 @@ class Parser:
|
|||||||
def kitty_mod(self, val: str, ans: dict[str, typing.Any]) -> None:
|
def kitty_mod(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||||
ans['kitty_mod'] = to_modifiers(val)
|
ans['kitty_mod'] = to_modifiers(val)
|
||||||
|
|
||||||
|
def map_timeout(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||||
|
ans['map_timeout'] = positive_float(val)
|
||||||
|
|
||||||
def linux_bell_theme(self, val: str, ans: dict[str, typing.Any]) -> None:
|
def linux_bell_theme(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||||
ans['linux_bell_theme'] = str(val)
|
ans['linux_bell_theme'] = str(val)
|
||||||
|
|
||||||
|
|||||||
2
kitty/options/types.py
generated
2
kitty/options/types.py
generated
@@ -390,6 +390,7 @@ option_names = (
|
|||||||
'macos_traditional_fullscreen',
|
'macos_traditional_fullscreen',
|
||||||
'macos_window_resizable',
|
'macos_window_resizable',
|
||||||
'map',
|
'map',
|
||||||
|
'map_timeout',
|
||||||
'mark1_background',
|
'mark1_background',
|
||||||
'mark1_foreground',
|
'mark1_foreground',
|
||||||
'mark2_background',
|
'mark2_background',
|
||||||
@@ -572,6 +573,7 @@ class Options:
|
|||||||
input_delay: int = 3
|
input_delay: int = 3
|
||||||
italic_font: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='auto', axes=(), variable_name=None, features=(), created_from_string='auto')
|
italic_font: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='auto', axes=(), variable_name=None, features=(), created_from_string='auto')
|
||||||
kitty_mod: int = 5
|
kitty_mod: int = 5
|
||||||
|
map_timeout: float = 0.0
|
||||||
linux_bell_theme: str = '__custom'
|
linux_bell_theme: str = '__custom'
|
||||||
linux_display_server: choices_for_linux_display_server = 'auto'
|
linux_display_server: choices_for_linux_display_server = 'auto'
|
||||||
listen_on: str = 'none'
|
listen_on: str = 'none'
|
||||||
|
|||||||
@@ -1290,6 +1290,7 @@ class KeyMapOptions:
|
|||||||
mode: str = ''
|
mode: str = ''
|
||||||
on_unknown: LiteralField[OnUnknown] = LiteralField[OnUnknown](get_args(OnUnknown))
|
on_unknown: LiteralField[OnUnknown] = LiteralField[OnUnknown](get_args(OnUnknown))
|
||||||
on_action: LiteralField[OnAction] = LiteralField[OnAction](get_args(OnAction))
|
on_action: LiteralField[OnAction] = LiteralField[OnAction](get_args(OnAction))
|
||||||
|
timeout: float | None = None
|
||||||
|
|
||||||
|
|
||||||
default_key_map_options = KeyMapOptions()
|
default_key_map_options = KeyMapOptions()
|
||||||
@@ -1349,6 +1350,8 @@ class KeyboardMode:
|
|||||||
on_unknown: OnUnknown = get_args(OnUnknown)[0]
|
on_unknown: OnUnknown = get_args(OnUnknown)[0]
|
||||||
on_action : OnAction = get_args(OnAction)[0]
|
on_action : OnAction = get_args(OnAction)[0]
|
||||||
sequence_keys: list[defines.KeyEvent] | None = None
|
sequence_keys: list[defines.KeyEvent] | None = None
|
||||||
|
timeout: float = 0.0
|
||||||
|
timeout_timer_id: int | None = None
|
||||||
|
|
||||||
def __init__(self, name: str = '') -> None:
|
def __init__(self, name: str = '') -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -1361,11 +1364,15 @@ KeyboardModeMap = dict[str, KeyboardMode]
|
|||||||
def parse_options_for_map(val: str) -> tuple[KeyMapOptions, str]:
|
def parse_options_for_map(val: str) -> tuple[KeyMapOptions, str]:
|
||||||
expecting_arg = ''
|
expecting_arg = ''
|
||||||
ans = KeyMapOptions()
|
ans = KeyMapOptions()
|
||||||
|
key_map_option_converters: dict[str, Callable[[str], object]] = {
|
||||||
|
'timeout': float,
|
||||||
|
}
|
||||||
s = Shlex(val)
|
s = Shlex(val)
|
||||||
while (tok := s.next_word())[0] > -1:
|
while (tok := s.next_word())[0] > -1:
|
||||||
x = tok[1]
|
x = tok[1]
|
||||||
if expecting_arg:
|
if expecting_arg:
|
||||||
object.__setattr__(ans, expecting_arg, x)
|
convert = key_map_option_converters.get(expecting_arg)
|
||||||
|
object.__setattr__(ans, expecting_arg, convert(x) if convert else x)
|
||||||
expecting_arg = ''
|
expecting_arg = ''
|
||||||
elif x.startswith('--'):
|
elif x.startswith('--'):
|
||||||
expecting_arg = x[2:]
|
expecting_arg = x[2:]
|
||||||
@@ -1375,7 +1382,8 @@ def parse_options_for_map(val: str) -> tuple[KeyMapOptions, str]:
|
|||||||
if expecting_arg not in allowed_key_map_options:
|
if expecting_arg not in allowed_key_map_options:
|
||||||
raise KeyError(f'The map option {x} is unknown. Allowed options: {", ".join(allowed_key_map_options)}')
|
raise KeyError(f'The map option {x} is unknown. Allowed options: {", ".join(allowed_key_map_options)}')
|
||||||
if sep == '=':
|
if sep == '=':
|
||||||
object.__setattr__(ans, k, v)
|
convert = key_map_option_converters.get(k)
|
||||||
|
object.__setattr__(ans, k, convert(v) if convert else v)
|
||||||
expecting_arg = ''
|
expecting_arg = ''
|
||||||
else:
|
else:
|
||||||
return ans, val[tok[0]:]
|
return ans, val[tok[0]:]
|
||||||
|
|||||||
Reference in New Issue
Block a user