Skip to content

Commit 781eda5

Browse files
committed
fix: Disable menu filtering on legacy Windows console
Calling terminal.get_cursor_pos() when using the legacy Windows Console Host resulted in the program hanging waiting for a response on stdin to an escape sequence that the terminal didn't respond to. Enabling VT mode temporarily made it respond and provide a cursor position, but either the return values differed from how a modern terminal would report them or this interacted strangely with menu rendering in some other way, as this resulted in parts of menus being rendered to the wrong locations on any menu with a filter text field. To work around this, we now simply detect whether VT mode is enabled, and if it isn't, we disable the filter text field. This allows menus to mostly work, just without the ability to search by typing.
1 parent 4d31d6f commit 781eda5

File tree

2 files changed

+39
-40
lines changed

2 files changed

+39
-40
lines changed

zmk/menu.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,17 @@ def __init__(
133133
self._num_title_lines = 0
134134
self._last_title_line_len = 0
135135

136-
if self._get_display_count() == self._max_items_per_page:
137-
self._top_row = 1
136+
if terminal.cursor_control_supported():
137+
if self._get_display_count() == self._max_items_per_page:
138+
self._top_row = 1
139+
else:
140+
_, y = terminal.get_cursor_pos()
141+
self._top_row = min(y, self.console.height - self._get_menu_height())
138142
else:
139-
_, y = terminal.get_cursor_pos()
140-
self._top_row = min(y, self.console.height - self._get_menu_height())
143+
# If get_cursor_pos() is unsupported, then we can't move the cursor
144+
# accurately between the end of the menu and the filter text. Disable
145+
# the filter feature so the menu still mostly works.
146+
self._filter_func = None
141147

142148
self._apply_filter()
143149

@@ -381,21 +387,24 @@ def _handle_input(self):
381387
return False
382388

383389
def _handle_backspace(self):
384-
if self._cursor_index == 0:
390+
if not self.has_filter or self._cursor_index == 0:
385391
return
386392

387393
self._filter_text = splice(self._filter_text, self._cursor_index - 1, count=1)
388394
self._cursor_index -= 1
389395
self._apply_filter()
390396

391397
def _handle_delete(self):
392-
if self._cursor_index == len(self._filter_text):
398+
if not self.has_filter or self._cursor_index == len(self._filter_text):
393399
return
394400

395401
self._filter_text = splice(self._filter_text, self._cursor_index, count=1)
396402
self._apply_filter()
397403

398404
def _handle_text(self, key: bytes):
405+
if not self.has_filter:
406+
return
407+
399408
text = key.decode()
400409
self._filter_text = splice(
401410
self._filter_text, self._cursor_index, insert_text=text

zmk/terminal.py

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,7 @@
3535
_STD_INPUT_HANDLE = -10
3636
_STD_OUTPUT_HANDLE = -11
3737

38-
_ENABLE_PROCESSED_OUTPUT = 1
39-
_ENABLE_WRAP_AT_EOL_OUTPUT = 2
4038
_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
41-
_VT_FLAGS = (
42-
_ENABLE_PROCESSED_OUTPUT
43-
| _ENABLE_WRAP_AT_EOL_OUTPUT
44-
| _ENABLE_VIRTUAL_TERMINAL_PROCESSING
45-
)
4639

4740
_WINDOWS_SPECIAL_KEYS = {
4841
71: HOME,
@@ -76,25 +69,6 @@ def read_key() -> bytes:
7669

7770
return key
7871

79-
@contextmanager
80-
def enable_vt_mode() -> Generator[None, None, None]:
81-
"""
82-
Context manager which enables virtual terminal processing.
83-
"""
84-
kernel32 = windll.kernel32
85-
stdout_handle = kernel32.GetStdHandle(_STD_OUTPUT_HANDLE)
86-
87-
old_stdout_mode = wintypes.DWORD()
88-
kernel32.GetConsoleMode(stdout_handle, byref(old_stdout_mode))
89-
90-
new_stdout_mode = old_stdout_mode.value | _VT_FLAGS
91-
92-
try:
93-
kernel32.SetConsoleMode(stdout_handle, new_stdout_mode)
94-
yield
95-
finally:
96-
kernel32.SetConsoleMode(stdout_handle, old_stdout_mode)
97-
9872
@contextmanager
9973
def disable_echo() -> Generator[None, None, None]:
10074
"""
@@ -112,16 +86,22 @@ def disable_echo() -> Generator[None, None, None]:
11286
finally:
11387
kernel32.SetConsoleMode(stdin_handle, old_stdin_mode)
11488

115-
except ImportError:
116-
import termios
117-
118-
@contextmanager
119-
def enable_vt_mode() -> Generator[None, None, None]:
89+
def cursor_control_supported() -> bool:
12090
"""
121-
Context manager which enables virtual terminal processing.
91+
Gets whether this terminal supports the virtual terminal escape sequence
92+
for getting the cursor position.
12293
"""
123-
# Assume that Unix terminals support VT escape sequences by default.
124-
yield
94+
kernel32 = windll.kernel32
95+
stdout_handle = kernel32.GetStdHandle(_STD_OUTPUT_HANDLE)
96+
97+
stdout_mode = wintypes.DWORD()
98+
kernel32.GetConsoleMode(stdout_handle, byref(stdout_mode))
99+
100+
return bool(stdout_mode.value & _ENABLE_VIRTUAL_TERMINAL_PROCESSING)
101+
102+
103+
except ImportError:
104+
import termios
125105

126106
@contextmanager
127107
def disable_echo() -> Generator[None, None, None]:
@@ -147,10 +127,20 @@ def read_key() -> bytes:
147127
with disable_echo():
148128
return os.read(sys.stdin.fileno(), 4)
149129

130+
def cursor_control_supported() -> bool:
131+
"""
132+
Gets whether this terminal supports the virtual terminal escape sequence
133+
for getting the cursor position.
134+
"""
135+
# Assume that Unix terminals support VT escape sequences by default.
136+
return True
137+
150138

151139
def get_cursor_pos() -> tuple[int, int]:
152140
"""
153141
Returns the cursor position as a tuple (x, y). Positions are 0-based.
142+
143+
This function may not work properly if cursor_control_supported() returns False.
154144
"""
155145
with disable_echo():
156146
sys.stdout.write("\x1b[6n")

0 commit comments

Comments
 (0)