This commit is contained in:
Kovid Goyal
2026-02-25 19:39:47 +05:30
7 changed files with 103 additions and 9 deletions

View File

@@ -167,6 +167,33 @@ the program running in the terminal, map it to :ac:`discard_event`::
.. _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
----------------------------------------------------------------------

View File

@@ -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)
nm.on_unknown = defn.options.on_unknown
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}'
try:
m = modes[defn.options.mode]

View File

@@ -62,11 +62,11 @@ def shortcut_matches(s: SingleKey, ev: KeyEvent) -> bool:
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.mode_timeout_timer_id: int | None = None
self.update_keymap(global_shortcuts)
self.callback_on_mode_change = callback_on_mode_change
@@ -87,6 +87,7 @@ class Mappings:
def clear_keyboard_modes(self) -> None:
had_mode = bool(self.keyboard_mode_stack)
self._cancel_mode_timeout()
self.keyboard_mode_stack = []
self.set_ignore_os_keyboard_processing(False)
if had_mode:
@@ -95,6 +96,7 @@ class Mappings:
def pop_keyboard_mode(self) -> bool:
passthrough = True
if self.keyboard_mode_stack:
self._cancel_mode_timeout()
self.keyboard_mode_stack.pop()
if not self.keyboard_mode_stack:
self.set_ignore_os_keyboard_processing(False)
@@ -110,12 +112,38 @@ class Mappings:
def _push_keyboard_mode(self, mode: KeyboardMode) -> None:
self.keyboard_mode_stack.append(mode)
self.set_ignore_os_keyboard_processing(True)
self._start_mode_timeout(mode)
self.callback_on_mode_change()
def push_keyboard_mode(self, new_mode: str) -> None:
mode = self.keyboard_modes[new_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]:
w = self.get_active_window()
matches = []
@@ -128,8 +156,9 @@ class Mappings:
is_applicable = True
except Exception:
self.clear_keyboard_modes()
self.show_error(_('Invalid key mapping'), _(
'The match expression {0} is not valid for {1}').format(x.options.when_focus_on, '--when-focus-on'))
self.show_error(
_('Invalid key mapping'), _('The match expression {0} is not valid for {1}').format(x.options.when_focus_on, '--when-focus-on')
)
return []
else:
is_applicable = True
@@ -143,10 +172,10 @@ class Mappings:
if not x.rest:
last_terminal_idx = i
if last_terminal_idx > -1:
if last_terminal_idx == len(matches) -1:
if last_terminal_idx == len(matches) - 1:
matches = matches[last_terminal_idx:]
else:
matches = matches[last_terminal_idx+1:]
matches = matches[last_terminal_idx + 1 :]
q = matches[-1].options.when_focus_on
matches = [x for x in matches if x.options.when_focus_on == q]
elif matches:
@@ -192,6 +221,7 @@ class Mappings:
sm = KeyboardMode('__sequence__')
sm.on_action = 'end'
sm.sequence_keys = [ev]
sm.timeout = self._get_effective_timeout(final_actions[0])
for fa in final_actions:
sm.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy())
self._push_keyboard_mode(sm)
@@ -206,6 +236,7 @@ class Mappings:
w.send_key_sequence(*mode.sequence_keys)
return consumed
mode.sequence_keys.append(ev)
self._start_mode_timeout(mode)
self.debug_print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='')
mode.keymap.clear()
for fa in final_actions:
@@ -219,6 +250,8 @@ class Mappings:
self.callback_on_mode_change()
if not self.keyboard_mode_stack:
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 False
@@ -252,5 +285,7 @@ class Mappings:
def set_cocoa_global_shortcuts(self, opts: Options) -> dict[str, SingleKey]:
from .main import set_cocoa_global_shortcuts
return set_cocoa_global_shortcuts(opts)
# }}}

View File

@@ -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',
option_type='action_alias',
add_to_default=False,

View File

@@ -1060,6 +1060,9 @@ class Parser:
def kitty_mod(self, val: str, ans: dict[str, typing.Any]) -> None:
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:
ans['linux_bell_theme'] = str(val)

View File

@@ -390,6 +390,7 @@ option_names = (
'macos_traditional_fullscreen',
'macos_window_resizable',
'map',
'map_timeout',
'mark1_background',
'mark1_foreground',
'mark2_background',
@@ -572,6 +573,7 @@ class Options:
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')
kitty_mod: int = 5
map_timeout: float = 0.0
linux_bell_theme: str = '__custom'
linux_display_server: choices_for_linux_display_server = 'auto'
listen_on: str = 'none'

View File

@@ -1290,6 +1290,7 @@ class KeyMapOptions:
mode: str = ''
on_unknown: LiteralField[OnUnknown] = LiteralField[OnUnknown](get_args(OnUnknown))
on_action: LiteralField[OnAction] = LiteralField[OnAction](get_args(OnAction))
timeout: float | None = None
default_key_map_options = KeyMapOptions()
@@ -1349,6 +1350,8 @@ class KeyboardMode:
on_unknown: OnUnknown = get_args(OnUnknown)[0]
on_action : OnAction = get_args(OnAction)[0]
sequence_keys: list[defines.KeyEvent] | None = None
timeout: float = 0.0
timeout_timer_id: int | None = None
def __init__(self, name: str = '') -> None:
self.name = name
@@ -1361,11 +1364,15 @@ KeyboardModeMap = dict[str, KeyboardMode]
def parse_options_for_map(val: str) -> tuple[KeyMapOptions, str]:
expecting_arg = ''
ans = KeyMapOptions()
key_map_option_converters: dict[str, Callable[[str], object]] = {
'timeout': float,
}
s = Shlex(val)
while (tok := s.next_word())[0] > -1:
x = tok[1]
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 = ''
elif x.startswith('--'):
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:
raise KeyError(f'The map option {x} is unknown. Allowed options: {", ".join(allowed_key_map_options)}')
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 = ''
else:
return ans, val[tok[0]:]