Skip to content

Commit

Permalink
debugger: leave output screen with single key press
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gsigh committed Jan 23, 2025
1 parent f0b2aa5 commit 0b9b626
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 3 deletions.
1 change: 1 addition & 0 deletions .pylintrc-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
- IPython
- py.test
- pytest
- msvcrt
11 changes: 8 additions & 3 deletions pudb/debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

import urwid

from pudb.lowlevel import decode_lines, ui_log
from pudb.lowlevel import ConsoleSingleKeyReader, decode_lines, ui_log
from pudb.settings import get_save_config_path, load_config, save_config


Expand Down Expand Up @@ -769,10 +769,15 @@ def __init__(self, screen):

def __enter__(self):
self.screen.stop()
return self

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

def press_key_to_return(self):
with ConsoleSingleKeyReader() as key_reader:
key_reader.get_single_key()


class DebuggerUI(FrameVarInfoKeeper):
# {{{ constructor
Expand Down Expand Up @@ -2082,8 +2087,8 @@ def shrink_sidebar(w, size, key):
# {{{ top-level listeners

def show_output(w, size, key):
with StoppedScreen(self.screen):
input("Hit Enter to return:")
with StoppedScreen(self.screen) as s:
s.press_key_to_return()

def reload_breakpoints_and_redisplay():
reload_breakpoints()
Expand Down
79 changes: 79 additions & 0 deletions pudb/lowlevel.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__copyright__ = """
Copyright (C) 2009-2017 Andreas Kloeckner
Copyright (C) 2014-2017 Aaron Meurer
Copyright (C) 2024 Gerhard Sittig
"""

__license__ = """
Expand All @@ -27,6 +28,7 @@
import logging
import sys
from datetime import datetime
from enum import Enum, auto


logfile = [None]
Expand Down Expand Up @@ -283,4 +285,81 @@ def decode_lines(lines):

# }}}


# {{{ get single key press from console outside of curses

class KeyReadImpl(Enum):
INPUT = auto()
GETCH = auto()
SELECT = auto()


_keyread_impl = KeyReadImpl.INPUT
if sys.platform in ("emscripten", "wasi"):
pass
elif sys.platform in ("win32",):
_keyread_impl = KeyReadImpl.GETCH
else:
_keyread_impl = KeyReadImpl.SELECT


class ConsoleSingleKeyReader:
"""
Get a single key press from a terminal without a prompt.
Eliminates the necessity to press ENTER before other input also
becomes available. Avoids the accumulation of prompts on the screen
as was the case with Python's input() call. Is used in situations
where urwid is disabled and curses calls are not available.
Supports major desktop platforms with special cases (msvcrt getch(),
termios and select). Transparently falls back to Python's input()
method. Call sites remain simple and straight forward.
"""

def __enter__(self):
if _keyread_impl == KeyReadImpl.SELECT:
import termios
import tty
self.prev_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno())
return self

def __exit__(self, type, value, traceback):
if _keyread_impl == KeyReadImpl.SELECT:
import termios
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.prev_settings)

def get_single_key(self):
if _keyread_impl == KeyReadImpl.GETCH:
import msvcrt
# https://docs.python.org/3/library/msvcrt.html#msvcrt.getch
# Most keys are returned in the first getch() call. Some
# special keys (function keys, cursor, keypad) require
# another call when the first returned '\0' or '\xe0'.
c = msvcrt.getch()
if c in ("\x00", "\xe0"):
c = msvcrt.getch()
return c

elif _keyread_impl == KeyReadImpl.SELECT:
import select
rset, _, _ = select.select([sys.stdin], [], [], None)
assert sys.stdin in rset
return sys.stdin.read(1)

# Strictly speaking putting the fallback here which requires
# pressing ENTER is not correct, this is the "non buffered"
# console support code. But it simplifies call sites. And is
# easy to tell by users because a prompt is provided. This is
# the most portable approach, and backwards compatible with
# earlier PuDB releases. It's a most appropriate default for
# otherwise unsupported platforms. Or when users choose to
# not accept single key presses, or keys other than ENTER.
else:
input("Hit Enter to return:")
return None

# }}}

# vim: foldmethod=marker

0 comments on commit 0b9b626

Please sign in to comment.