Skip to content

Commit 8b2921b

Browse files
committed
Add Windows REPL virtual terminal queue
To support virtual terminal mode in Windows PYREPL, we need a scanner to read over the supported escaped VT sequences. Signed-off-by: y5c4l3 <y5c4l3@proton.me>
1 parent 4cb85fa commit 8b2921b

File tree

1 file changed

+119
-0
lines changed

1 file changed

+119
-0
lines changed

Lib/_pyrepl/windows_eventqueue.py

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Windows event and VT sequence scanner, similar to `unix_eventqueue.py`
3+
"""
4+
5+
from collections import deque
6+
7+
from . import keymap
8+
from .console import Event
9+
from .trace import trace
10+
import os
11+
12+
# Reference: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#input-sequences
13+
VT_MAP: dict[bytes, str] = {
14+
b'\x1b[A': 'up',
15+
b'\x1b[B': 'down',
16+
b'\x1b[C': 'right',
17+
b'\x1b[D': 'left',
18+
b'\x1b[1;5D': 'ctrl left',
19+
b'\x1b[1;5C': 'ctrl right',
20+
21+
b'\x1b[H': 'home',
22+
b'\x1b[F': 'end',
23+
24+
b'\x7f': 'backspace',
25+
b'\x1b[2~': 'insert',
26+
b'\x1b[3~': 'delete',
27+
b'\x1b[5~': 'page up',
28+
b'\x1b[6~': 'page down',
29+
30+
b'\x1bOP': 'f1',
31+
b'\x1bOQ': 'f2',
32+
b'\x1bOR': 'f3',
33+
b'\x1bOS': 'f4',
34+
b'\x1b[15~': 'f5',
35+
b'\x1b[17~]': 'f6',
36+
b'\x1b[18~]': 'f7',
37+
b'\x1b[19~]': 'f8',
38+
b'\x1b[20~]': 'f9',
39+
b'\x1b[21~]': 'f10',
40+
b'\x1b[23~]': 'f11',
41+
b'\x1b[24~]': 'f12',
42+
}
43+
44+
class EventQueue:
45+
def __init__(self, encoding: str) -> None:
46+
self.compiled_keymap = keymap.compile_keymap(VT_MAP)
47+
self.keymap = self.compiled_keymap
48+
trace("keymap {k!r}", k=self.keymap)
49+
self.encoding = encoding
50+
self.events: deque[Event] = deque()
51+
self.buf = bytearray()
52+
53+
def get(self) -> Event | None:
54+
"""
55+
Retrieves the next event from the queue.
56+
"""
57+
if self.events:
58+
return self.events.popleft()
59+
else:
60+
return None
61+
62+
def empty(self) -> bool:
63+
"""
64+
Checks if the queue is empty.
65+
"""
66+
return not self.events
67+
68+
def flush_buf(self) -> bytearray:
69+
"""
70+
Flushes the buffer and returns its contents.
71+
"""
72+
old = self.buf
73+
self.buf = bytearray()
74+
return old
75+
76+
def insert(self, event: Event) -> None:
77+
"""
78+
Inserts an event into the queue.
79+
"""
80+
trace('added event {event}', event=event)
81+
self.events.append(event)
82+
83+
def push(self, char: int | bytes) -> None:
84+
"""
85+
Processes a character by updating the buffer and handling special key mappings.
86+
"""
87+
ord_char = char if isinstance(char, int) else ord(char)
88+
char = bytes(bytearray((ord_char,)))
89+
self.buf.append(ord_char)
90+
if char in self.keymap:
91+
if self.keymap is self.compiled_keymap:
92+
#sanity check, buffer is empty when a special key comes
93+
assert len(self.buf) == 1
94+
k = self.keymap[char]
95+
trace('found map {k!r}', k=k)
96+
if isinstance(k, dict):
97+
self.keymap = k
98+
else:
99+
self.insert(Event('key', k, self.flush_buf()))
100+
self.keymap = self.compiled_keymap
101+
102+
elif self.buf and self.buf[0] == 27: # escape
103+
# escape sequence not recognized by our keymap: propagate it
104+
# outside so that i can be recognized as an M-... key (see also
105+
# the docstring in keymap.py
106+
trace('unrecognized escape sequence, propagating...')
107+
self.keymap = self.compiled_keymap
108+
self.insert(Event('key', '\033', bytearray(b'\033')))
109+
for _c in self.flush_buf()[1:]:
110+
self.push(_c)
111+
112+
else:
113+
try:
114+
decoded = bytes(self.buf).decode(self.encoding)
115+
except UnicodeError:
116+
return
117+
else:
118+
self.insert(Event('key', decoded, self.flush_buf()))
119+
self.keymap = self.compiled_keymap

0 commit comments

Comments
 (0)