Skip to content

Commit 0b9b626

Browse files
committed
debugger: leave output screen with single key press
Introduce support code to get single key presses from a console. Support Windows (msvcrt) and POSIX supporting Unix platforms (select, termios). Fall back to Python's input() routine for Webassembly and Emscripten. Avoid the accumulation of repeated prompts on platforms that support single key presses. Only the backwards compatible fallback on minor platforms involves a prompt, and users can tell when they face that situation. This change increases usability, a single key press leaves the output screen after it was entered by pressing 'o'. Does not require a config item, platform detection is automatic. The approach uses Python core libraries, no external dependencies are involved. The implementation is considered maintainable, lends itself to future extension for more platforms as the need gets identified.
1 parent f0b2aa5 commit 0b9b626

File tree

3 files changed

+88
-3
lines changed

3 files changed

+88
-3
lines changed

.pylintrc-local.yml

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
- IPython
55
- py.test
66
- pytest
7+
- msvcrt

pudb/debugger.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
import urwid
3737

38-
from pudb.lowlevel import decode_lines, ui_log
38+
from pudb.lowlevel import ConsoleSingleKeyReader, decode_lines, ui_log
3939
from pudb.settings import get_save_config_path, load_config, save_config
4040

4141

@@ -769,10 +769,15 @@ def __init__(self, screen):
769769

770770
def __enter__(self):
771771
self.screen.stop()
772+
return self
772773

773774
def __exit__(self, exc_type, exc_value, exc_traceback):
774775
self.screen.start()
775776

777+
def press_key_to_return(self):
778+
with ConsoleSingleKeyReader() as key_reader:
779+
key_reader.get_single_key()
780+
776781

777782
class DebuggerUI(FrameVarInfoKeeper):
778783
# {{{ constructor
@@ -2082,8 +2087,8 @@ def shrink_sidebar(w, size, key):
20822087
# {{{ top-level listeners
20832088

20842089
def show_output(w, size, key):
2085-
with StoppedScreen(self.screen):
2086-
input("Hit Enter to return:")
2090+
with StoppedScreen(self.screen) as s:
2091+
s.press_key_to_return()
20872092

20882093
def reload_breakpoints_and_redisplay():
20892094
reload_breakpoints()

pudb/lowlevel.py

+79
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
__copyright__ = """
22
Copyright (C) 2009-2017 Andreas Kloeckner
33
Copyright (C) 2014-2017 Aaron Meurer
4+
Copyright (C) 2024 Gerhard Sittig
45
"""
56

67
__license__ = """
@@ -27,6 +28,7 @@
2728
import logging
2829
import sys
2930
from datetime import datetime
31+
from enum import Enum, auto
3032

3133

3234
logfile = [None]
@@ -283,4 +285,81 @@ def decode_lines(lines):
283285

284286
# }}}
285287

288+
289+
# {{{ get single key press from console outside of curses
290+
291+
class KeyReadImpl(Enum):
292+
INPUT = auto()
293+
GETCH = auto()
294+
SELECT = auto()
295+
296+
297+
_keyread_impl = KeyReadImpl.INPUT
298+
if sys.platform in ("emscripten", "wasi"):
299+
pass
300+
elif sys.platform in ("win32",):
301+
_keyread_impl = KeyReadImpl.GETCH
302+
else:
303+
_keyread_impl = KeyReadImpl.SELECT
304+
305+
306+
class ConsoleSingleKeyReader:
307+
"""
308+
Get a single key press from a terminal without a prompt.
309+
310+
Eliminates the necessity to press ENTER before other input also
311+
becomes available. Avoids the accumulation of prompts on the screen
312+
as was the case with Python's input() call. Is used in situations
313+
where urwid is disabled and curses calls are not available.
314+
315+
Supports major desktop platforms with special cases (msvcrt getch(),
316+
termios and select). Transparently falls back to Python's input()
317+
method. Call sites remain simple and straight forward.
318+
"""
319+
320+
def __enter__(self):
321+
if _keyread_impl == KeyReadImpl.SELECT:
322+
import termios
323+
import tty
324+
self.prev_settings = termios.tcgetattr(sys.stdin)
325+
tty.setcbreak(sys.stdin.fileno())
326+
return self
327+
328+
def __exit__(self, type, value, traceback):
329+
if _keyread_impl == KeyReadImpl.SELECT:
330+
import termios
331+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.prev_settings)
332+
333+
def get_single_key(self):
334+
if _keyread_impl == KeyReadImpl.GETCH:
335+
import msvcrt
336+
# https://docs.python.org/3/library/msvcrt.html#msvcrt.getch
337+
# Most keys are returned in the first getch() call. Some
338+
# special keys (function keys, cursor, keypad) require
339+
# another call when the first returned '\0' or '\xe0'.
340+
c = msvcrt.getch()
341+
if c in ("\x00", "\xe0"):
342+
c = msvcrt.getch()
343+
return c
344+
345+
elif _keyread_impl == KeyReadImpl.SELECT:
346+
import select
347+
rset, _, _ = select.select([sys.stdin], [], [], None)
348+
assert sys.stdin in rset
349+
return sys.stdin.read(1)
350+
351+
# Strictly speaking putting the fallback here which requires
352+
# pressing ENTER is not correct, this is the "non buffered"
353+
# console support code. But it simplifies call sites. And is
354+
# easy to tell by users because a prompt is provided. This is
355+
# the most portable approach, and backwards compatible with
356+
# earlier PuDB releases. It's a most appropriate default for
357+
# otherwise unsupported platforms. Or when users choose to
358+
# not accept single key presses, or keys other than ENTER.
359+
else:
360+
input("Hit Enter to return:")
361+
return None
362+
363+
# }}}
364+
286365
# vim: foldmethod=marker

0 commit comments

Comments
 (0)