Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Support kitty keyboard protocol in Unix _pyrepl for VS Code terminal compatibility#145759

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed
jechol wants to merge1 commit intopython:mainfromjechol:pyrepl-kitty-keyboard
Closed
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletionsLib/_pyrepl/unix_console.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,6 +70,8 @@ def __init__(self, message: str) -> None:
FIONREAD = getattr(termios, "FIONREAD", None)
TIOCGWINSZ = getattr(termios, "TIOCGWINSZ", None)

KITTY_KEYBOARD_FLAGS = 0b1 | 0b10 | 0b100 | 0b1000 | 0b10000

# ------------ start of baudrate definitions ------------

# Add (possibly) missing baudrates (check termios man page) to termios
Expand DownExpand Up@@ -377,12 +379,14 @@ def prepare(self):
pass

self.__enable_bracketed_paste()
self.__enable_kitty_keyboard()

def restore(self):
"""
Restore the console to the default state
"""
self.__disable_bracketed_paste()
self.__disable_kitty_keyboard()
self.__maybe_write_code(self._rmkx)
self.flushoutput()
self.__input_fd_set(self.__svtermstate)
Expand DownExpand Up@@ -598,6 +602,12 @@ def __enable_bracketed_paste(self) -> None:
def __disable_bracketed_paste(self) -> None:
os.write(self.output_fd, b"\x1b[?2004l")

def __enable_kitty_keyboard(self) -> None:
os.write(self.output_fd, f"\x1b[={KITTY_KEYBOARD_FLAGS};1u".encode("ascii"))

def __disable_kitty_keyboard(self) -> None:
os.write(self.output_fd, b"\x1b[<u")

def __setup_movement(self):
"""
Set up the movement functions based on the terminal capabilities.
Expand Down
288 changes: 288 additions & 0 deletionsLib/_pyrepl/unix_eventqueue.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,9 @@
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

from dataclasses import dataclass

from .console import Event
from .terminfo import TermInfo
from .trace import trace
from .base_eventqueue import BaseEventQueue
Expand DownExpand Up@@ -54,6 +57,95 @@
b'\033Oc': 'ctrl right',
}

KITTY_MOD_SHIFT = 0x01
KITTY_MOD_ALT = 0x02
KITTY_MOD_CTRL = 0x04
KITTY_MOD_SUPER = 0x08
KITTY_MOD_HYPER = 0x10
KITTY_MOD_META = 0x20
KITTY_MOD_CAPS_LOCK = 0x40
KITTY_MOD_NUM_LOCK = 0x80

KITTY_EVENT_PRESS = 1
KITTY_EVENT_REPEAT = 2
KITTY_EVENT_RELEASE = 3

KITTY_CSI_FINAL_BYTES = frozenset(b"~uABCDEFHPQS")
KITTY_FUNCTIONAL_TILDE_KEYS = {
2: "insert",
3: "delete",
5: "page up",
6: "page down",
7: "home",
8: "end",
11: "f1",
12: "f2",
13: "f3",
14: "f4",
15: "f5",
17: "f6",
18: "f7",
19: "f8",
20: "f9",
21: "f10",
23: "f11",
24: "f12",
57427: "begin",
}
KITTY_FUNCTIONAL_LETTER_KEYS = {
"A": "up",
"B": "down",
"C": "right",
"D": "left",
"E": "begin",
"F": "end",
"H": "home",
"P": "f1",
"Q": "f2",
"S": "f4",
}
KITTY_FUNCTIONAL_U_KEYS = {
9: "\t",
13: "\r",
27: "\033",
127: "backspace",
57358: "caps_lock",
57359: "scroll_lock",
57360: "num_lock",
}
KITTY_FUNCTIONAL_U_KEYS.update((57363 + i, f"f{i}") for i in range(13, 36))
KITTY_IGNORED_FUNCTIONAL_U_KEYS = frozenset(range(57399, 57455))
KITTY_CTRL_KEY_OVERRIDES = {
ord(" "): 0,
ord("2"): 0,
ord("@"): 0,
ord("3"): 27,
ord("["): 27,
ord("4"): 28,
ord("\\"): 28,
ord("5"): 29,
ord("]"): 29,
ord("6"): 30,
ord("^"): 30,
ord("7"): 31,
ord("-"): 31,
ord("_"): 31,
ord("/"): 31,
ord("8"): 127,
ord("?"): 127,
}


@dataclass(slots=True)
class _KittyKeyEvent:
key_code: int
shifted_key: int | None = None
base_layout_key: int | None = None
text: str = ""
modifiers: int = 0
event_type: int = KITTY_EVENT_PRESS
key_name: str | None = None

def get_terminal_keycodes(ti: TermInfo) -> dict[bytes, str]:
"""
Generates a dictionary mapping terminal keycodes to human-readable names.
Expand All@@ -75,3 +167,199 @@ def __init__(self, fd: int, encoding: str, ti: TermInfo) -> None:
backspace = tcgetattr(fd)[6][VERASE]
keycodes[backspace] = "backspace"
BaseEventQueue.__init__(self, encoding, keycodes)
self._escape_buffer = bytearray()

def push(self, char: int | bytes) -> None:
assert isinstance(char, (int, bytes))
ord_char = char if isinstance(char, int) else ord(char)

if self.keymap is not self.compiled_keymap:
super().push(ord_char)
return

if self._escape_buffer:
self._escape_buffer.append(ord_char)
self._process_escape_buffer()
return

if ord_char == 0x1b:
self._escape_buffer.append(ord_char)
return

super().push(ord_char)

def _process_escape_buffer(self) -> None:
if len(self._escape_buffer) < 2:
return

if self._escape_buffer[1] != ord("["):
self._flush_escape_buffer()
return

final = self._escape_buffer[-1]
if len(self._escape_buffer) > 2 and 0x40 <= final <= 0x7E:
seq = bytes(self._escape_buffer)
self._escape_buffer.clear()
if final in KITTY_CSI_FINAL_BYTES and self._handle_kitty_sequence(seq):
return
self._push_bytes(seq)

def _flush_escape_buffer(self) -> None:
if self._escape_buffer:
self._push_bytes(bytes(self._escape_buffer))
self._escape_buffer.clear()

def _push_bytes(self, data: bytes) -> None:
for byte in data:
super().push(byte)

def _handle_kitty_sequence(self, seq: bytes) -> bool:
params = seq[2:-1].decode("ascii", "strict")
final = chr(seq[-1])

try:
if final == "u":
event = self._parse_kitty_u(params)
elif final == "~":
event = self._parse_kitty_tilde(params)
else:
event = self._parse_kitty_letter(params, final)
except ValueError:
return False

if event is None:
return False
return self._emit_kitty_event(event, seq)

def _parse_kitty_u(self, params: str) -> _KittyKeyEvent | None:
fields = params.split(";")
if not fields or not fields[0]:
raise ValueError

key_fields = fields[0].split(":")
key_code = int(key_fields[0])
shifted_key = int(key_fields[1]) if len(key_fields) > 1 and key_fields[1] else None
base_layout_key = int(key_fields[2]) if len(key_fields) > 2 and key_fields[2] else None

modifiers = 0
event_type = KITTY_EVENT_PRESS
if len(fields) > 1 and fields[1]:
modifier_fields = fields[1].split(":")
modifiers = self._decode_kitty_modifiers(modifier_fields[0])
if len(modifier_fields) > 1 and modifier_fields[1]:
event_type = int(modifier_fields[1])

text = ""
if len(fields) > 2 and fields[2]:
text = "".join(chr(int(cp)) for cp in fields[2].split(":") if cp)

key_name = KITTY_FUNCTIONAL_U_KEYS.get(key_code)
if key_name is None and key_code in KITTY_IGNORED_FUNCTIONAL_U_KEYS:
key_name = ""

return _KittyKeyEvent(
key_code=key_code,
shifted_key=shifted_key,
base_layout_key=base_layout_key,
text=text,
modifiers=modifiers,
event_type=event_type,
key_name=key_name,
)

def _parse_kitty_tilde(self, params: str) -> _KittyKeyEvent | None:
fields = params.split(";")
if not fields or not fields[0]:
raise ValueError
key_code = int(fields[0])
key_name = KITTY_FUNCTIONAL_TILDE_KEYS.get(key_code)
if key_name is None:
return None
modifiers = self._decode_kitty_modifiers(fields[1]) if len(fields) > 1 and fields[1] else 0
return _KittyKeyEvent(key_code=key_code, modifiers=modifiers, key_name=key_name)

def _parse_kitty_letter(self, params: str, final: str) -> _KittyKeyEvent | None:
key_name = KITTY_FUNCTIONAL_LETTER_KEYS.get(final)
if key_name is None:
return None
modifiers = 0
if params:
fields = params.split(";")
if fields[0] not in ("", "1"):
return None
if len(fields) > 1 and fields[1]:
modifiers = self._decode_kitty_modifiers(fields[1])
return _KittyKeyEvent(key_code=1, modifiers=modifiers, key_name=key_name)

def _decode_kitty_modifiers(self, value: str) -> int:
modifier_value = int(value)
return max(modifier_value - 1, 0)

def _emit_kitty_event(self, event: _KittyKeyEvent, raw: bytes) -> bool:
if event.event_type == KITTY_EVENT_RELEASE:
return True

keys = self._translate_kitty_event(event)
for key in keys:
self.insert(Event("key", key, raw))
return True

def _translate_kitty_event(self, event: _KittyKeyEvent) -> list[str]:
if event.key_name == "":
return []

modifiers = event.modifiers
key_name = event.key_name

if key_name in {"caps_lock", "scroll_lock", "num_lock"}:
return []

if key_name is not None:
if modifiers & KITTY_MOD_ALT:
return self._prefix_alt([key_name])
ctrl_key = self._maybe_ctrl_special_key(key_name, modifiers)
return [ctrl_key] if ctrl_key is not None else [key_name]

text = event.text
if not text:
text = self._kitty_key_text(event)
if not text:
return []

if modifiers & KITTY_MOD_CTRL:
text = self._apply_ctrl_to_text(text, event)
if modifiers & KITTY_MOD_ALT:
return self._prefix_alt(list(text))
return list(text)

def _maybe_ctrl_special_key(self, key_name: str, modifiers: int) -> str | None:
if not modifiers & KITTY_MOD_CTRL:
return None
if key_name in {"left", "right"}:
return f"ctrl {key_name}"
return None

def _kitty_key_text(self, event: _KittyKeyEvent) -> str:
codepoint = event.base_layout_key or event.key_code
if event.modifiers & KITTY_MOD_SHIFT and event.shifted_key is not None:
codepoint = event.shifted_key
try:
return chr(codepoint)
except ValueError:
return ""

def _apply_ctrl_to_text(self, text: str, event: _KittyKeyEvent) -> str:
codepoint = event.base_layout_key or event.key_code
if codepoint in KITTY_CTRL_KEY_OVERRIDES:
return chr(KITTY_CTRL_KEY_OVERRIDES[codepoint])

if not text:
return ""

char = text[0]
if "a" <= char <= "z" or "A" <= char <= "Z":
return chr(ord(char.upper()) & 0x1F)
return char

def _prefix_alt(self, keys: list[str]) -> list[str]:
return ["\033", *keys]
Loading
Loading

[8]ページ先頭

©2009-2026 Movatter.jp