mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-06-24 18:07:01 +00:00
Implement modal keyboard handling
This commit is contained in:
parent
cb418a0040
commit
9e815212dc
11 changed files with 100 additions and 114 deletions
125
kitty/boss.py
125
kitty/boss.py
|
|
@ -66,7 +66,6 @@ from .fast_data_types import (
|
|||
GLFW_MOD_SUPER,
|
||||
GLFW_MOUSE_BUTTON_LEFT,
|
||||
GLFW_PRESS,
|
||||
GLFW_RELEASE,
|
||||
IMPERATIVE_CLOSE_REQUESTED,
|
||||
NO_CLOSE_REQUESTED,
|
||||
ChildMonitor,
|
||||
|
|
@ -92,7 +91,6 @@ from .fast_data_types import (
|
|||
get_options,
|
||||
get_os_window_size,
|
||||
global_font_size,
|
||||
is_modifier_key,
|
||||
last_focused_os_window_id,
|
||||
mark_os_window_for_close,
|
||||
os_window_font_size,
|
||||
|
|
@ -105,7 +103,7 @@ from .fast_data_types import (
|
|||
set_application_quit_request,
|
||||
set_background_image,
|
||||
set_boss,
|
||||
set_in_sequence_mode,
|
||||
set_ignore_os_keyboard_processing,
|
||||
set_options,
|
||||
set_os_window_chrome,
|
||||
set_os_window_size,
|
||||
|
|
@ -117,11 +115,11 @@ from .fast_data_types import (
|
|||
wrapped_kitten_names,
|
||||
)
|
||||
from .key_encoding import get_name_to_functional_number_map
|
||||
from .keys import get_shortcut, shortcut_matches
|
||||
from .keys import get_shortcut
|
||||
from .layout.base import set_layout_options
|
||||
from .notify import notification_activated
|
||||
from .options.types import Options
|
||||
from .options.utils import MINIMUM_FONT_SIZE, KeyDefinition, KeyMap, SubSequenceMap
|
||||
from .options.utils import MINIMUM_FONT_SIZE, KeyboardMode, KeyDefinition, KeyMap
|
||||
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
|
||||
|
|
@ -297,7 +295,7 @@ class VisualSelect:
|
|||
set_os_window_title(self.os_window_id, '')
|
||||
boss = get_boss()
|
||||
redirect_mouse_handling(False)
|
||||
boss.clear_pending_sequences()
|
||||
boss.keyboard_mode_stack = []
|
||||
for wid in self.window_ids:
|
||||
w = boss.window_id_map.get(wid)
|
||||
if w is not None:
|
||||
|
|
@ -343,10 +341,7 @@ class Boss:
|
|||
self.startup_colors = {k: opts[k] for k in opts if isinstance(opts[k], Color)}
|
||||
self.current_visual_select: Optional[VisualSelect] = None
|
||||
self.startup_cursor_text_color = opts.cursor_text_color
|
||||
self.pending_sequences: Optional[SubSequenceMap] = None
|
||||
# A list of events received so far that are potentially part of a sequence keybinding.
|
||||
self.current_sequence: List[KeyEvent] = []
|
||||
self.default_pending_action: str = ''
|
||||
self.cached_values = cached_values
|
||||
self.os_window_map: Dict[int, TabManager] = {}
|
||||
self.os_window_death_actions: Dict[int, Callable[[], None]] = {}
|
||||
|
|
@ -378,6 +373,7 @@ class Boss:
|
|||
set_boss(self)
|
||||
self.args = args
|
||||
self.mouse_handler: Optional[Callable[[WindowSystemMouseEvent], None]] = None
|
||||
self.keyboard_mode_stack: List[KeyboardMode] = []
|
||||
self.update_keymap(global_shortcuts)
|
||||
if is_macos:
|
||||
from .fast_data_types import cocoa_set_notification_activated_callback
|
||||
|
|
@ -392,9 +388,11 @@ class Boss:
|
|||
global_shortcuts = {}
|
||||
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()
|
||||
self.keyboard_modes = get_options().keyboard_modes.copy()
|
||||
km = self.keyboard_modes[''].keymap
|
||||
self.keyboard_modes[''].keymap = km = km.copy()
|
||||
for sc in self.global_shortcuts.values():
|
||||
self.keymap.pop(sc, None)
|
||||
km.pop(sc, None)
|
||||
|
||||
def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Iterable[Session] = ()) -> None:
|
||||
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
||||
|
|
@ -1337,71 +1335,64 @@ class Boss:
|
|||
t = self.active_tab
|
||||
return None if t is None else t.active_window
|
||||
|
||||
def set_pending_sequences(self, sequences: SubSequenceMap, default_pending_action: str = '') -> None:
|
||||
self.pending_sequences = sequences
|
||||
self.default_pending_action = default_pending_action
|
||||
set_in_sequence_mode(True)
|
||||
@ac('misc', '''
|
||||
End the current keyboard mode switching to the previous mode.
|
||||
''')
|
||||
def pop_keyboard_mode(self) -> bool:
|
||||
if self.keyboard_mode_stack:
|
||||
self.keyboard_mode_stack.pop()
|
||||
if not self.keyboard_mode_stack:
|
||||
set_ignore_os_keyboard_processing(False)
|
||||
return True
|
||||
return False
|
||||
|
||||
@ac('misc', '''
|
||||
Switch to the specified keyboard mode, pushing it onto the stack of keyboard modes.
|
||||
''')
|
||||
def push_keyboard_mode(self, new_mode: str) -> None:
|
||||
mode = self.keyboard_modes[new_mode]
|
||||
self._push_keyboard_mode(mode)
|
||||
|
||||
def _push_keyboard_mode(self, mode: KeyboardMode) -> None:
|
||||
self.keyboard_mode_stack.append(mode)
|
||||
set_ignore_os_keyboard_processing(True)
|
||||
|
||||
def dispatch_possible_special_key(self, ev: KeyEvent) -> bool:
|
||||
# Handles shortcuts, return True if the key was consumed
|
||||
key_action = get_shortcut(self.keymap, ev)
|
||||
is_root_mode = not self.keyboard_mode_stack
|
||||
mode = self.keyboard_modes[''] if is_root_mode else self.keyboard_mode_stack[-1]
|
||||
key_action = get_shortcut(mode.keymap, ev)
|
||||
if key_action is None:
|
||||
sequences = get_shortcut(get_options().sequence_map, ev)
|
||||
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:
|
||||
if self.pop_keyboard_mode():
|
||||
return True
|
||||
else:
|
||||
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:
|
||||
self.pending_sequences = None
|
||||
self.current_sequence = []
|
||||
self.default_pending_action = ''
|
||||
set_in_sequence_mode(False)
|
||||
|
||||
def process_sequence(self, ev: KeyEvent) -> bool:
|
||||
# Process an event as part of a sequence. Returns whether the key
|
||||
# is consumed as part of a kitty sequence keybinding.
|
||||
if not self.pending_sequences:
|
||||
set_in_sequence_mode(False)
|
||||
return False
|
||||
|
||||
if self.current_sequence:
|
||||
self.current_sequence.append(ev)
|
||||
if ev.action == GLFW_RELEASE or is_modifier_key(ev.key):
|
||||
return True
|
||||
# For a press/repeat event that's not a modifier, try matching with
|
||||
# kitty bindings:
|
||||
remaining = {}
|
||||
matched_action = None
|
||||
for seq, key_actions in self.pending_sequences.items():
|
||||
if shortcut_matches(seq[0], ev):
|
||||
key_action = self.matching_key_action(key_actions)
|
||||
if key_action is not None:
|
||||
seq = seq[1:]
|
||||
if seq:
|
||||
remaining[seq] = [key_action]
|
||||
mode_pos = len(self.keyboard_mode_stack) - 1
|
||||
if final_action.is_sequence:
|
||||
if mode.sequence_left is None:
|
||||
sm = KeyboardMode('__sequence__')
|
||||
sm.end_on_action = True
|
||||
sm.sequence_left = final_action.rest[1:]
|
||||
sm.keymap[final_action.rest[0]].append(final_action)
|
||||
self._push_keyboard_mode(sm)
|
||||
elif mode.sequence_left:
|
||||
mode.keymap.clear()
|
||||
mode.keymap[mode.sequence_left[0]].append(final_action)
|
||||
mode.sequence_left = mode.sequence_left[1:]
|
||||
else:
|
||||
matched_action = key_action
|
||||
|
||||
if remaining:
|
||||
self.pending_sequences = remaining
|
||||
return True
|
||||
final_action = self.default_pending_action if matched_action is None else matched_action.definition
|
||||
if final_action:
|
||||
self.clear_pending_sequences()
|
||||
self.combine(final_action)
|
||||
return True
|
||||
w = self.active_window
|
||||
if w is not None:
|
||||
w.write_to_child(b''.join(w.encoded_key(ev) for ev in self.current_sequence))
|
||||
self.clear_pending_sequences()
|
||||
self.pop_keyboard_mode()
|
||||
return self.combine(final_action.definition)
|
||||
return True
|
||||
consumed = self.combine(final_action.definition)
|
||||
if consumed and not is_root_mode and mode.end_on_action:
|
||||
if mode_pos < len(self.keyboard_mode_stack) and self.keyboard_mode_stack[mode_pos] is mode:
|
||||
del self.keyboard_mode_stack[mode_pos]
|
||||
if not self.keyboard_mode_stack:
|
||||
set_ignore_os_keyboard_processing(False)
|
||||
return consumed
|
||||
return False
|
||||
|
||||
def matching_key_action(self, candidates: Iterable[KeyDefinition]) -> Optional[KeyDefinition]:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
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
|
||||
|
|
@ -12,7 +11,7 @@ from .conf.utils import BadLine, parse_config_base
|
|||
from .conf.utils import load_config as _load_config
|
||||
from .constants import cache_dir, defconf
|
||||
from .options.types import Options, defaults, option_names
|
||||
from .options.utils import KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap, build_action_aliases
|
||||
from .options.utils import KeyboardMode, KeyboardModeMap, KeyDefinition, MouseMap, MouseMapping, build_action_aliases
|
||||
from .typing import TypedDict
|
||||
from .utils import log_error
|
||||
|
||||
|
|
@ -101,19 +100,21 @@ 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 = defaultdict(list)
|
||||
sequence_map: SequenceMap = {}
|
||||
modes: KeyboardModeMap = {'': KeyboardMode()}
|
||||
|
||||
for defn in defns:
|
||||
if defn.is_sequence:
|
||||
keymap.pop(defn.trigger, None)
|
||||
s = sequence_map.setdefault(defn.trigger, defaultdict(list))
|
||||
s[defn.rest].append(defn)
|
||||
else:
|
||||
sequence_map.pop(defn.trigger, None)
|
||||
keymap[defn.trigger].append(defn)
|
||||
opts.keymap = keymap
|
||||
opts.sequence_map = sequence_map
|
||||
if defn.options.new_mode:
|
||||
modes[defn.options.new_mode] = nm = KeyboardMode(defn.options.new_mode)
|
||||
nm.passthrough_unknown = defn.options.passthrough_unknown
|
||||
nm.end_on_action = defn.options.end_on_action
|
||||
defn.definition = f'push_keyboard_mode {defn.options.new_mode}'
|
||||
try:
|
||||
m = modes[defn.options.mode]
|
||||
except KeyError:
|
||||
log_error(f'The keyboard mode {defn.options.mode} is unknown, ignoring the mapping: {defn}')
|
||||
continue
|
||||
m.keymap[defn.trigger].append(defn)
|
||||
opts.keyboard_modes = modes
|
||||
|
||||
|
||||
def finalize_mouse_mappings(opts: Options, accumulate_bad_lines: Optional[List[BadLine]] = None) -> None:
|
||||
|
|
|
|||
|
|
@ -604,7 +604,7 @@ def thread_write(fd: int, data: bytes) -> None:
|
|||
pass
|
||||
|
||||
|
||||
def set_in_sequence_mode(yes: bool) -> None:
|
||||
def set_ignore_os_keyboard_processing(yes: bool) -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
12
kitty/keys.c
12
kitty/keys.c
|
|
@ -9,7 +9,6 @@
|
|||
#include "keys.h"
|
||||
#include "screen.h"
|
||||
#include "glfw-wrapper.h"
|
||||
#include "control-codes.h"
|
||||
#include <structmember.h>
|
||||
|
||||
// python KeyEvent object {{{
|
||||
|
|
@ -200,17 +199,6 @@ on_key_input(GLFWkeyevent *ev) {
|
|||
else { consumed = ret == Py_True; Py_CLEAR(ret); } \
|
||||
w = window_for_window_id(active_window_id); \
|
||||
}
|
||||
if (global_state.in_sequence_mode) {
|
||||
debug("in sequence mode, handling as a potential shortcut\n");
|
||||
dispatch_key_event(process_sequence);
|
||||
if (dispatch_ok) {
|
||||
if (consumed && action != GLFW_RELEASE && w && !is_modifier_key(key)) {
|
||||
w->last_special_key_pressed = key;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
|
||||
w->last_special_key_pressed = 0;
|
||||
dispatch_key_event(dispatch_possible_special_key);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import List, Optional, Union, overload
|
||||
from typing import List, Optional
|
||||
|
||||
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 KeyDefinition, KeyMap, SequenceMap, SubSequenceMap
|
||||
from .options.utils import KeyDefinition, KeyMap
|
||||
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,12 +17,7 @@ def keyboard_mode_name(screen: ScreenType) -> str:
|
|||
return 'application' if screen.cursor_key_mode else 'normal'
|
||||
|
||||
|
||||
@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]:
|
||||
def get_shortcut(keymap: KeyMap, ev: KeyEvent) -> Optional[List[KeyDefinition]]:
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ def set_cocoa_global_shortcuts(opts: Options) -> Dict[str, SingleKey]:
|
|||
func_map = defaultdict(list)
|
||||
for k, v in opts.keymap.items():
|
||||
for kd in v:
|
||||
if not kd.options.when_focus_on:
|
||||
if not kd.options.when_focus_on and not kd.options.mode and not kd.options.new_mode:
|
||||
parts = tuple(kd.definition.split())
|
||||
func_map[parts].append(k)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from kitty.constants import website_url
|
|||
|
||||
definition = Definition(
|
||||
'kitty',
|
||||
Action('map', 'parse_map', {'keymap': 'KeyMap', 'sequence_map': 'SequenceMap', 'alias_map': 'AliasMap'},
|
||||
Action('map', 'parse_map', {'keyboard_modes': 'KeyboardModeMap', 'alias_map': 'AliasMap'},
|
||||
['KeyDefinition', 'kitty.fast_data_types.SingleKey']),
|
||||
Action('mouse_map', 'parse_mouse_map', {'mousemap': 'MouseMap'}, ['MouseMapping']),
|
||||
has_color_table=True,
|
||||
|
|
|
|||
5
kitty/options/types.py
generated
5
kitty/options/types.py
generated
|
|
@ -9,7 +9,7 @@ from kitty.fast_data_types import Color, SingleKey
|
|||
import kitty.fast_data_types
|
||||
import kitty.fonts
|
||||
from kitty.options.utils import (
|
||||
AliasMap, KeyDefinition, KeyMap, MouseMap, MouseMapping, NotifyOnCmdFinish, SequenceMap,
|
||||
AliasMap, KeyDefinition, KeyboardModeMap, MouseMap, MouseMapping, NotifyOnCmdFinish,
|
||||
TabBarMarginHeight
|
||||
)
|
||||
import kitty.options.utils
|
||||
|
|
@ -628,8 +628,7 @@ class Options:
|
|||
symbol_map: typing.Dict[typing.Tuple[int, int], str] = {}
|
||||
watcher: typing.Dict[str, str] = {}
|
||||
map: typing.List[kitty.options.utils.KeyDefinition] = []
|
||||
keymap: KeyMap = {}
|
||||
sequence_map: SequenceMap = {}
|
||||
keyboard_modes: KeyboardModeMap = {}
|
||||
alias_map: AliasMap = AliasMap()
|
||||
mouse_map: typing.List[kitty.options.utils.MouseMapping] = []
|
||||
mousemap: MouseMap = {}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import enum
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, fields
|
||||
from functools import lru_cache
|
||||
from typing import Any, Callable, Container, Dict, FrozenSet, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Tuple, Union
|
||||
|
|
@ -35,8 +36,6 @@ from kitty.utils import expandvars, log_error, resolve_abs_or_config_path
|
|||
KeyMap = Dict[SingleKey, List['KeyDefinition']]
|
||||
MouseMap = Dict[MouseEvent, str]
|
||||
KeySequence = Tuple[SingleKey, ...]
|
||||
SubSequenceMap = Dict[KeySequence, List['KeyDefinition']]
|
||||
SequenceMap = Dict[SingleKey, SubSequenceMap]
|
||||
MINIMUM_FONT_SIZE = 4
|
||||
default_tab_separator = ' ┇'
|
||||
mod_map = {'⌃': 'CONTROL', 'CTRL': 'CONTROL', '⇧': 'SHIFT', '⌥': 'ALT', 'OPTION': 'ALT', 'OPT': 'ALT',
|
||||
|
|
@ -142,7 +141,7 @@ def detach_tab_parse(func: str, rest: str) -> FuncArgsType:
|
|||
return func, (rest,)
|
||||
|
||||
|
||||
@func_with_args('set_background_opacity', 'goto_layout', 'toggle_layout', 'kitty_shell', 'show_kitty_doc', 'set_tab_title')
|
||||
@func_with_args('set_background_opacity', 'goto_layout', 'toggle_layout', 'kitty_shell', 'show_kitty_doc', 'set_tab_title', 'push_keyboard_mode')
|
||||
def simple_parse(func: str, rest: str) -> FuncArgsType:
|
||||
return func, [rest]
|
||||
|
||||
|
|
@ -1157,6 +1156,7 @@ class KeyMapOptions:
|
|||
new_mode: str = ''
|
||||
mode: str = ''
|
||||
passthrough_unknown: BoolField = BoolField(False)
|
||||
end_on_action: BoolField = BoolField(False)
|
||||
|
||||
|
||||
default_key_map_options = KeyMapOptions()
|
||||
|
|
@ -1190,6 +1190,20 @@ class KeyDefinition(BaseDefinition):
|
|||
return ans
|
||||
|
||||
|
||||
class KeyboardMode:
|
||||
|
||||
passthrough_unknown: bool = False
|
||||
end_on_action: bool = False
|
||||
sequence_left: Optional[Sequence[SingleKey]] = None
|
||||
|
||||
def __init__(self, name: str = '') -> None:
|
||||
self.name = name
|
||||
self.keymap: KeyMap = defaultdict(list)
|
||||
|
||||
|
||||
KeyboardModeMap = Dict[str, KeyboardMode]
|
||||
|
||||
|
||||
def parse_options_for_map(val: str) -> Tuple[KeyMapOptions, List[str]]:
|
||||
expecting_arg = ''
|
||||
ans = KeyMapOptions()
|
||||
|
|
|
|||
|
|
@ -746,9 +746,8 @@ PYWRAP1(set_options) {
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PYWRAP1(set_in_sequence_mode) {
|
||||
global_state.in_sequence_mode = PyObject_IsTrue(args);
|
||||
set_ignore_os_keyboard_processing(global_state.in_sequence_mode);
|
||||
PYWRAP1(set_ignore_os_keyboard_processing) {
|
||||
set_ignore_os_keyboard_processing(PyObject_IsTrue(args));
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
|
@ -1343,7 +1342,7 @@ static PyMethodDef module_methods[] = {
|
|||
MW(redirect_mouse_handling, METH_O),
|
||||
MW(mouse_selection, METH_VARARGS),
|
||||
MW(set_window_logo, METH_VARARGS),
|
||||
MW(set_in_sequence_mode, METH_O),
|
||||
MW(set_ignore_os_keyboard_processing, METH_O),
|
||||
MW(handle_for_window_id, METH_VARARGS),
|
||||
MW(update_ime_position_for_window, METH_VARARGS),
|
||||
MW(pt_to_px, METH_VARARGS),
|
||||
|
|
|
|||
|
|
@ -265,7 +265,6 @@ typedef struct {
|
|||
bool has_render_frames;
|
||||
bool debug_rendering, debug_font_fallback;
|
||||
bool has_pending_resizes, has_pending_closes;
|
||||
bool in_sequence_mode;
|
||||
bool check_for_active_animated_images;
|
||||
struct { double x, y; } default_dpi;
|
||||
id_type active_drag_in_window, tracked_drag_in_window;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue