| 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'] = '-RcmPm{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 + '"') |