mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-08-27 03:42:49 +00:00
Allow creating key mappings that depend on the state of the focused window, such as what program is running inside it
This commit is contained in:
parent
8a1571f62c
commit
f11f770011
7 changed files with 118 additions and 35 deletions
|
|
@ -121,7 +121,7 @@ from .keys import get_shortcut, shortcut_matches
|
|||
from .layout.base import set_layout_options
|
||||
from .notify import notification_activated
|
||||
from .options.types import Options
|
||||
from .options.utils import MINIMUM_FONT_SIZE, KeyMap, SubSequenceMap
|
||||
from .options.utils import MINIMUM_FONT_SIZE, KeyDefinition, KeyMap, SubSequenceMap
|
||||
from .os_window_size import initial_window_size_func
|
||||
from .rgb import color_from_int
|
||||
from .session import Session, create_sessions, get_os_window_sizing_data
|
||||
|
|
@ -390,7 +390,7 @@ class Boss:
|
|||
global_shortcuts = set_cocoa_global_shortcuts(get_options())
|
||||
else:
|
||||
global_shortcuts = {}
|
||||
self.global_shortcuts_map: KeyMap = {v: k for k, v in global_shortcuts.items()}
|
||||
self.global_shortcuts_map: KeyMap = {v: [KeyDefinition(definition=k)] for k, v in global_shortcuts.items()}
|
||||
self.global_shortcuts = global_shortcuts
|
||||
self.keymap = get_options().keymap.copy()
|
||||
for sc in self.global_shortcuts.values():
|
||||
|
|
@ -1347,14 +1347,16 @@ class Boss:
|
|||
key_action = get_shortcut(self.keymap, ev)
|
||||
if key_action is None:
|
||||
sequences = get_shortcut(get_options().sequence_map, ev)
|
||||
if sequences and not isinstance(sequences, str):
|
||||
if sequences:
|
||||
self.set_pending_sequences(sequences)
|
||||
self.current_sequence = [ev]
|
||||
return True
|
||||
if self.global_shortcuts_map and get_shortcut(self.global_shortcuts_map, ev):
|
||||
return True
|
||||
elif key_action and isinstance(key_action, str):
|
||||
return self.combine(key_action)
|
||||
elif key_action:
|
||||
final_action = self.matching_key_action(key_action)
|
||||
if final_action is not None:
|
||||
return self.combine(final_action.definition)
|
||||
return False
|
||||
|
||||
def clear_pending_sequences(self) -> None:
|
||||
|
|
@ -1378,21 +1380,23 @@ class Boss:
|
|||
# kitty bindings:
|
||||
remaining = {}
|
||||
matched_action = None
|
||||
for seq, key_action in self.pending_sequences.items():
|
||||
for seq, key_actions in self.pending_sequences.items():
|
||||
if shortcut_matches(seq[0], ev):
|
||||
seq = seq[1:]
|
||||
if seq:
|
||||
remaining[seq] = key_action
|
||||
else:
|
||||
matched_action = key_action
|
||||
key_action = self.matching_key_action(key_actions)
|
||||
if key_action is not None:
|
||||
seq = seq[1:]
|
||||
if seq:
|
||||
remaining[seq] = [key_action]
|
||||
else:
|
||||
matched_action = key_action
|
||||
|
||||
if remaining:
|
||||
self.pending_sequences = remaining
|
||||
return True
|
||||
matched_action = matched_action or self.default_pending_action
|
||||
if matched_action:
|
||||
final_action = self.default_pending_action if matched_action is None else matched_action.definition
|
||||
if final_action:
|
||||
self.clear_pending_sequences()
|
||||
self.combine(matched_action)
|
||||
self.combine(final_action)
|
||||
return True
|
||||
w = self.active_window
|
||||
if w is not None:
|
||||
|
|
@ -1400,6 +1404,21 @@ class Boss:
|
|||
self.clear_pending_sequences()
|
||||
return False
|
||||
|
||||
def matching_key_action(self, candidates: Iterable[KeyDefinition]) -> Optional[KeyDefinition]:
|
||||
w = self.active_window
|
||||
ans = None
|
||||
for x in candidates:
|
||||
if x.when_focus_on:
|
||||
try:
|
||||
if w and w in self.match_windows(x.when_focus_on):
|
||||
ans = x
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
ans = x
|
||||
return ans
|
||||
|
||||
def cancel_current_visual_select(self) -> None:
|
||||
if self.current_visual_select:
|
||||
self.current_visual_select.cancel()
|
||||
|
|
@ -1434,16 +1453,16 @@ class Boss:
|
|||
for idx, window in tab.windows.iter_windows_with_number(only_visible=True):
|
||||
if only_window_ids and window.id not in only_window_ids:
|
||||
continue
|
||||
ac = f'visual_window_select_action_trigger {window.id}'
|
||||
ac = KeyDefinition(definition=f'visual_window_select_action_trigger {window.id}')
|
||||
if idx >= len(alphanumerics):
|
||||
break
|
||||
ch = alphanumerics[idx]
|
||||
window.screen.set_window_char(ch)
|
||||
self.current_visual_select.window_ids.append(window.id)
|
||||
for mods in (0, GLFW_MOD_CONTROL, GLFW_MOD_CONTROL | GLFW_MOD_SHIFT, GLFW_MOD_SUPER, GLFW_MOD_ALT, GLFW_MOD_SHIFT):
|
||||
pending_sequences[(SingleKey(mods=mods, key=ord(ch.lower())),)] = ac
|
||||
pending_sequences[(SingleKey(mods=mods, key=ord(ch.lower())),)] = [ac]
|
||||
if ch in string.digits:
|
||||
pending_sequences[(SingleKey(mods=mods, key=fmap[f'KP_{ch}']),)] = ac
|
||||
pending_sequences[(SingleKey(mods=mods, key=fmap[f'KP_{ch}']),)] = [ac]
|
||||
if len(self.current_visual_select.window_ids) > 1:
|
||||
self.set_pending_sequences(pending_sequences, default_pending_action='visual_window_select_action_trigger 0')
|
||||
redirect_mouse_handling(True)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import json
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager, suppress
|
||||
from functools import partial
|
||||
from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple
|
||||
|
|
@ -100,17 +101,17 @@ def finalize_keys(opts: Options, accumulate_bad_lines: Optional[List[BadLine]] =
|
|||
else:
|
||||
accumulate_bad_lines.append(BadLine(d.definition_location.number, d.definition_location.line, err, d.definition_location.file))
|
||||
|
||||
keymap: KeyMap = {}
|
||||
keymap: KeyMap = defaultdict(list)
|
||||
sequence_map: SequenceMap = {}
|
||||
|
||||
for defn in defns:
|
||||
if defn.is_sequence:
|
||||
keymap.pop(defn.trigger, None)
|
||||
s = sequence_map.setdefault(defn.trigger, {})
|
||||
s[defn.rest] = defn.definition
|
||||
s = sequence_map.setdefault(defn.trigger, defaultdict(list))
|
||||
s[defn.rest].append(defn)
|
||||
else:
|
||||
sequence_map.pop(defn.trigger, None)
|
||||
keymap[defn.trigger] = defn.definition
|
||||
keymap[defn.trigger].append(defn)
|
||||
opts.keymap = keymap
|
||||
opts.sequence_map = sequence_map
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Union
|
||||
from typing import List, Optional, Union, overload
|
||||
|
||||
from .fast_data_types import GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_HYPER, GLFW_MOD_META, GLFW_MOD_SHIFT, GLFW_MOD_SUPER, KeyEvent, SingleKey
|
||||
from .options.utils import KeyMap, SequenceMap, SubSequenceMap
|
||||
from .options.utils import KeyDefinition, KeyMap, SequenceMap, SubSequenceMap
|
||||
from .typing import ScreenType
|
||||
|
||||
mod_mask = GLFW_MOD_ALT | GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER | GLFW_MOD_META | GLFW_MOD_HYPER
|
||||
|
|
@ -17,7 +17,12 @@ def keyboard_mode_name(screen: ScreenType) -> str:
|
|||
return 'application' if screen.cursor_key_mode else 'normal'
|
||||
|
||||
|
||||
def get_shortcut(keymap: Union[KeyMap, SequenceMap], ev: KeyEvent) -> Union[str, SubSequenceMap, None]:
|
||||
@overload
|
||||
def get_shortcut(keymap: KeyMap, ev: KeyEvent) -> Optional[List[KeyDefinition]]: ...
|
||||
@overload
|
||||
def get_shortcut(keymap: SequenceMap, ev: KeyEvent) -> Optional[SubSequenceMap]: ...
|
||||
|
||||
def get_shortcut(keymap: Union[KeyMap | SequenceMap], ev: KeyEvent) -> Union[List[KeyDefinition] | SubSequenceMap | None]:
|
||||
mods = ev.mods & mod_mask
|
||||
ans = keymap.get(SingleKey(mods, False, ev.key))
|
||||
if ans is None and ev.shifted_key and mods & GLFW_MOD_SHIFT:
|
||||
|
|
|
|||
|
|
@ -219,8 +219,10 @@ def set_cocoa_global_shortcuts(opts: Options) -> Dict[str, SingleKey]:
|
|||
from collections import defaultdict
|
||||
func_map = defaultdict(list)
|
||||
for k, v in opts.keymap.items():
|
||||
parts = tuple(v.split())
|
||||
func_map[parts].append(k)
|
||||
for kd in v:
|
||||
if not kd.when_focus_on:
|
||||
parts = tuple(kd.definition.split())
|
||||
func_map[parts].append(k)
|
||||
|
||||
for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab',
|
||||
'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry', 'toggle_fullscreen',
|
||||
|
|
|
|||
|
|
@ -3447,6 +3447,33 @@ For example::
|
|||
|
||||
map ctrl+f>2 set_font_size 20
|
||||
|
||||
You can create mappings that apply when the focused window matches some condition,
|
||||
such as having a particular program running. For example::
|
||||
|
||||
map --when-focus-on title:keyboard.protocol kitty_mod+t
|
||||
|
||||
This will cause :kbd:`kitty_mod+t` (the default shortcut for opening a new tab)
|
||||
to be unmapped only when the focused window
|
||||
has :code:`keyboard protocol` in its title. Run the show-key kitten as::
|
||||
|
||||
kitten show-key -m kitty
|
||||
|
||||
and press :kbd:`ctrl+shift+t` and instead of a new tab opening, you will
|
||||
see the key press being reported by the kitten. :code:`--when-focus-on` can test
|
||||
the focused window using very powerful criteria, see :ref:`search_syntax` for
|
||||
details. Note that spaces are not allowed in the argument of --when-focus-on.
|
||||
Use the . character or :code:`\\\\s` to match spaces.
|
||||
A more practical example unmaps the key when the focused window is running vim::
|
||||
|
||||
map --when-focus-on var:in_editor
|
||||
|
||||
In order to make this work, you need the following lines in your :file:`.vimrc`::
|
||||
|
||||
let &t_ti = &t_ti . "\\033]1337;SetUserVar=in_editor=MQo\\007"
|
||||
let &t_te = &t_te . "\\033]1337;SetUserVar=in_editor\\007"
|
||||
|
||||
These cause vim to set the :code:`in_editor` variable in kitty and unset it when leaving vim.
|
||||
|
||||
The full list of actions that can be mapped to key presses is available
|
||||
:doc:`here </actions>`.
|
||||
''')
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ from kitty.rgb import color_as_int
|
|||
from kitty.types import FloatEdges, MouseEvent
|
||||
from kitty.utils import expandvars, log_error, resolve_abs_or_config_path
|
||||
|
||||
KeyMap = Dict[SingleKey, str]
|
||||
KeyMap = Dict[SingleKey, List['KeyDefinition']]
|
||||
MouseMap = Dict[MouseEvent, str]
|
||||
KeySequence = Tuple[SingleKey, ...]
|
||||
SubSequenceMap = Dict[KeySequence, str]
|
||||
SubSequenceMap = Dict[KeySequence, List['KeyDefinition']]
|
||||
SequenceMap = Dict[SingleKey, SubSequenceMap]
|
||||
MINIMUM_FONT_SIZE = 4
|
||||
default_tab_separator = ' ┇'
|
||||
|
|
@ -1138,31 +1138,60 @@ class KeyDefinition(BaseDefinition):
|
|||
|
||||
def __init__(
|
||||
self, is_sequence: bool = False, trigger: SingleKey = SingleKey(),
|
||||
rest: Tuple[SingleKey, ...] = (), definition: str = ''
|
||||
rest: Tuple[SingleKey, ...] = (), definition: str = '',
|
||||
when_focus_on: str = '',
|
||||
):
|
||||
super().__init__(definition)
|
||||
self.is_sequence = is_sequence
|
||||
self.trigger = trigger
|
||||
self.rest = rest
|
||||
self.when_focus_on = when_focus_on
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.pretty_repr('is_sequence', 'trigger', 'rest')
|
||||
return self.pretty_repr('is_sequence', 'trigger', 'rest', 'when_focus_on')
|
||||
|
||||
def resolve_and_copy(self, kitty_mod: int) -> 'KeyDefinition':
|
||||
def r(k: SingleKey) -> SingleKey:
|
||||
return k.resolve_kitty_mod(kitty_mod)
|
||||
ans = KeyDefinition(
|
||||
self.is_sequence, r(self.trigger), tuple(map(r, self.rest)),
|
||||
self.definition
|
||||
self.definition, self.when_focus_on
|
||||
)
|
||||
ans.definition_location = self.definition_location
|
||||
return ans
|
||||
|
||||
|
||||
def parse_options_for_map(val: str) -> Tuple[Dict[str, str], List[str]]:
|
||||
expecting_arg = ''
|
||||
ans = {}
|
||||
parts = val.split()
|
||||
for i, x in enumerate(parts):
|
||||
if expecting_arg:
|
||||
ans[expecting_arg] = x
|
||||
expecting_arg = ''
|
||||
elif x.startswith('--'):
|
||||
expecting_arg = x[2:]
|
||||
k, sep, v = expecting_arg.partition('=')
|
||||
if sep == '=':
|
||||
ans[k] = v
|
||||
expecting_arg = ''
|
||||
else:
|
||||
return ans, parts[i:]
|
||||
return ans, []
|
||||
|
||||
|
||||
def parse_map(val: str) -> Iterable[KeyDefinition]:
|
||||
parts = val.split(maxsplit=1)
|
||||
options: Dict[str, str] = {}
|
||||
if len(parts) == 2:
|
||||
sc, action = parts
|
||||
if sc.startswith('--'):
|
||||
options, parts = parse_options_for_map(val)
|
||||
if len(parts) == 1:
|
||||
sc, action = parts[0], ''
|
||||
else:
|
||||
sc = parts[0]
|
||||
action = ' '.join(parts[1:])
|
||||
else:
|
||||
sc, action = val, ''
|
||||
sc, action = sc.strip().strip(sequence_sep), action.strip()
|
||||
|
|
@ -1197,10 +1226,10 @@ def parse_map(val: str) -> Iterable[KeyDefinition]:
|
|||
return
|
||||
if is_sequence:
|
||||
if trigger is not None:
|
||||
yield KeyDefinition(True, trigger, rest, definition=action)
|
||||
yield KeyDefinition(True, trigger, rest, definition=action, when_focus_on=options.get('when-focus-on', ''))
|
||||
else:
|
||||
assert key is not None
|
||||
yield KeyDefinition(False, SingleKey(mods, is_native, key), definition=action)
|
||||
yield KeyDefinition(False, SingleKey(mods, is_native, key), definition=action, when_focus_on=options.get('when-focus-on', ''))
|
||||
|
||||
|
||||
def parse_mouse_map(val: str) -> Iterable[MouseMapping]:
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ from .fast_data_types import (
|
|||
Screen,
|
||||
add_timer,
|
||||
add_window,
|
||||
base64_decode,
|
||||
cell_size_for_window,
|
||||
click_mouse_cmd_output,
|
||||
click_mouse_url,
|
||||
|
|
@ -947,9 +948,8 @@ class Window:
|
|||
for record in raw_data.split(';'):
|
||||
key, _, val = record.partition('=')
|
||||
if key == 'SetUserVar':
|
||||
from base64 import standard_b64decode
|
||||
ukey, has_equal, uval = val.partition('=')
|
||||
self.set_user_var(ukey, (standard_b64decode(uval) if uval else b'') if has_equal == '=' else None)
|
||||
self.set_user_var(ukey, (base64_decode(uval) if uval else b'') if has_equal == '=' else None)
|
||||
|
||||
def desktop_notify(self, osc_code: int, raw_data: str) -> None:
|
||||
if osc_code == 1337:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue