from __future__ import annotations import io import os import re import sys # types if False: from typing import Protocol class Pager(Protocol): def __call__(self, text: str, title: str = "") -> None: ... def get_pager() -> Pager: """Decide what method to use for paging through text.""" if not hasattr(sys.stdin, "isatty"): return plain_pager if not hasattr(sys.stdout, "isatty"): return plain_pager if not sys.stdin.isatty() or not sys.stdout.isatty(): return plain_pager if sys.platform == "emscripten": return plain_pager use_pager = os.environ.get('MANPAGER') or os.environ.get('PAGER') if use_pager: if sys.platform == 'win32': # pipes completely broken in Windows return lambda text, title='': tempfile_pager(plain(text), use_pager) elif os.environ.get('TERM') in ('dumb', 'emacs'): return lambda text, title='': pipe_pager(plain(text), use_pager, title) else: return lambda text, title='': pipe_pager(text, use_pager, title) if os.environ.get('TERM') in ('dumb', 'emacs'): return plain_pager if sys.platform == 'win32': return lambda text, title='': tempfile_pager(plain(text), 'more <') if hasattr(os, 'system') and os.system('(pager) 2>/dev/null') == 0: return lambda text, title='': pipe_pager(text, 'pager', title) if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: return lambda text, title='': pipe_pager(text, 'less', title) import tempfile (fd, filename) = tempfile.mkstemp() os.close(fd) try: if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0: return lambda text, title='': pipe_pager(text, 'more', title) else: return tty_pager finally: os.unlink(filename) def escape_stdout(text: str) -> str: # Escape non-encodable characters to avoid encoding errors later encoding = getattr(sys.stdout, 'encoding', None) or 'utf-8' return text.encode(encoding, 'backslashreplace').decode(encoding) def escape_less(s: str) -> str: return re.sub(r'([?:.%\\])', r'\\\1', s) def plain(text: str) -> str: """Remove boldface formatting from text.""" return re.sub('.\b', '', text) def tty_pager(text: str, title: str = '') -> None: """Page through text on a text terminal.""" lines = plain(escape_stdout(text)).split('\n') has_tty = False try: import tty import termios fd = sys.stdin.fileno() old = termios.tcgetattr(fd) tty.setcbreak(fd) has_tty = True def getchar() -> str: return sys.stdin.read(1) except (ImportError, AttributeError, io.UnsupportedOperation): def getchar() -> str: return sys.stdin.readline()[:-1][:1] try: try: h = int(os.environ.get('LINES', 0)) except ValueError: h = 0 if h <= 1: h = 25 r = inc = h - 1 sys.stdout.write('\n'.join(lines[:inc]) + '\n') while lines[r:]: sys.stdout.write('-- more --') sys.stdout.flush() c = getchar() if c in ('q', 'Q'): sys.stdout.write('\r \r') break elif c in ('\r', '\n'): sys.stdout.write('\r \r' + lines[r] + '\n') r = r + 1 continue if c in ('b', 'B', '\x1b'): r = r - inc - inc if r < 0: r = 0 sys.stdout.write('\n' + '\n'.join(lines[r:r+inc]) + '\n') r = r + inc finally: if has_tty: termios.tcsetattr(fd, termios.TCSAFLUSH, old) def plain_pager(text: str, title: str = '') -> None: """Simply print unformatted text. This is the ultimate fallback.""" sys.stdout.write(plain(escape_stdout(text))) def pipe_pager(text: str, cmd: str, title: str = '') -> None: """Page through text by feeding it to another program.""" import subprocess env = os.environ.copy() if title: title += ' ' esc_title = escape_less(title) prompt_string = ( f' {esc_title}' + '?ltline %lt?L/%L.' ':byte %bB?s/%s.' '.' '?e (END):?pB %pB\\%..' ' (press h for help or q to quit)') env['LESS'] = '-RmPm{0}$PM{0}$'.format(prompt_string) proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, errors='backslashreplace', env=env) assert proc.stdin is not None try: with proc.stdin as pipe: try: pipe.write(text) except KeyboardInterrupt: # We've hereby abandoned whatever text hasn't been written, # but the pager is still in control of the terminal. pass except OSError: pass # Ignore broken pipes caused by quitting the pager program. while True: try: proc.wait() break except KeyboardInterrupt: # Ignore ctl-c like the pager itself does. Otherwise the pager is # left running and the terminal is in raw mode and unusable. pass def tempfile_pager(text: str, cmd: str, title: str = '') -> None: """Page through text by invoking a program on a temporary file.""" import tempfile with tempfile.TemporaryDirectory() as tempdir: filename = os.path.join(tempdir, 'pydoc.out') with open(filename, 'w', errors='backslashreplace', encoding=os.device_encoding(0) if sys.platform == 'win32' else None ) as file: file.write(text) os.system(cmd + ' "' + filename + '"')