1from __future__ import annotations 2 3import io 4import os 5import re 6import sys 7 8 9# types 10if False: 11 from typing import Protocol 12 class Pager(Protocol): 13 def __call__(self, text: str, title: str = "") -> None: 14 ... 15 16 17def get_pager() -> Pager: 18 """Decide what method to use for paging through text.""" 19 if not hasattr(sys.stdin, "isatty"): 20 return plain_pager 21 if not hasattr(sys.stdout, "isatty"): 22 return plain_pager 23 if not sys.stdin.isatty() or not sys.stdout.isatty(): 24 return plain_pager 25 if sys.platform == "emscripten": 26 return plain_pager 27 use_pager = os.environ.get('MANPAGER') or os.environ.get('PAGER') 28 if use_pager: 29 if sys.platform == 'win32': # pipes completely broken in Windows 30 return lambda text, title='': tempfile_pager(plain(text), use_pager) 31 elif os.environ.get('TERM') in ('dumb', 'emacs'): 32 return lambda text, title='': pipe_pager(plain(text), use_pager, title) 33 else: 34 return lambda text, title='': pipe_pager(text, use_pager, title) 35 if os.environ.get('TERM') in ('dumb', 'emacs'): 36 return plain_pager 37 if sys.platform == 'win32': 38 return lambda text, title='': tempfile_pager(plain(text), 'more <') 39 if hasattr(os, 'system') and os.system('(pager) 2>/dev/null') == 0: 40 return lambda text, title='': pipe_pager(text, 'pager', title) 41 if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: 42 return lambda text, title='': pipe_pager(text, 'less', title) 43 44 import tempfile 45 (fd, filename) = tempfile.mkstemp() 46 os.close(fd) 47 try: 48 if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0: 49 return lambda text, title='': pipe_pager(text, 'more', title) 50 else: 51 return tty_pager 52 finally: 53 os.unlink(filename) 54 55 56def escape_stdout(text: str) -> str: 57 # Escape non-encodable characters to avoid encoding errors later 58 encoding = getattr(sys.stdout, 'encoding', None) or 'utf-8' 59 return text.encode(encoding, 'backslashreplace').decode(encoding) 60 61 62def escape_less(s: str) -> str: 63 return re.sub(r'([?:.%\\])', r'\\\1', s) 64 65 66def plain(text: str) -> str: 67 """Remove boldface formatting from text.""" 68 return re.sub('.\b', '', text) 69 70 71def tty_pager(text: str, title: str = '') -> None: 72 """Page through text on a text terminal.""" 73 lines = plain(escape_stdout(text)).split('\n') 74 has_tty = False 75 try: 76 import tty 77 import termios 78 fd = sys.stdin.fileno() 79 old = termios.tcgetattr(fd) 80 tty.setcbreak(fd) 81 has_tty = True 82 83 def getchar() -> str: 84 return sys.stdin.read(1) 85 86 except (ImportError, AttributeError, io.UnsupportedOperation): 87 def getchar() -> str: 88 return sys.stdin.readline()[:-1][:1] 89 90 try: 91 try: 92 h = int(os.environ.get('LINES', 0)) 93 except ValueError: 94 h = 0 95 if h <= 1: 96 h = 25 97 r = inc = h - 1 98 sys.stdout.write('\n'.join(lines[:inc]) + '\n') 99 while lines[r:]: 100 sys.stdout.write('-- more --') 101 sys.stdout.flush() 102 c = getchar() 103 104 if c in ('q', 'Q'): 105 sys.stdout.write('\r \r') 106 break 107 elif c in ('\r', '\n'): 108 sys.stdout.write('\r \r' + lines[r] + '\n') 109 r = r + 1 110 continue 111 if c in ('b', 'B', '\x1b'): 112 r = r - inc - inc 113 if r < 0: r = 0 114 sys.stdout.write('\n' + '\n'.join(lines[r:r+inc]) + '\n') 115 r = r + inc 116 117 finally: 118 if has_tty: 119 termios.tcsetattr(fd, termios.TCSAFLUSH, old) 120 121 122def plain_pager(text: str, title: str = '') -> None: 123 """Simply print unformatted text. This is the ultimate fallback.""" 124 sys.stdout.write(plain(escape_stdout(text))) 125 126 127def pipe_pager(text: str, cmd: str, title: str = '') -> None: 128 """Page through text by feeding it to another program.""" 129 import subprocess 130 env = os.environ.copy() 131 if title: 132 title += ' ' 133 esc_title = escape_less(title) 134 prompt_string = ( 135 f' {esc_title}' + 136 '?ltline %lt?L/%L.' 137 ':byte %bB?s/%s.' 138 '.' 139 '?e (END):?pB %pB\\%..' 140 ' (press h for help or q to quit)') 141 env['LESS'] = '-RmPm{0}$PM{0}$'.format(prompt_string) 142 proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, 143 errors='backslashreplace', env=env) 144 assert proc.stdin is not None 145 try: 146 with proc.stdin as pipe: 147 try: 148 pipe.write(text) 149 except KeyboardInterrupt: 150 # We've hereby abandoned whatever text hasn't been written, 151 # but the pager is still in control of the terminal. 152 pass 153 except OSError: 154 pass # Ignore broken pipes caused by quitting the pager program. 155 while True: 156 try: 157 proc.wait() 158 break 159 except KeyboardInterrupt: 160 # Ignore ctl-c like the pager itself does. Otherwise the pager is 161 # left running and the terminal is in raw mode and unusable. 162 pass 163 164 165def tempfile_pager(text: str, cmd: str, title: str = '') -> None: 166 """Page through text by invoking a program on a temporary file.""" 167 import tempfile 168 with tempfile.TemporaryDirectory() as tempdir: 169 filename = os.path.join(tempdir, 'pydoc.out') 170 with open(filename, 'w', errors='backslashreplace', 171 encoding=os.device_encoding(0) if 172 sys.platform == 'win32' else None 173 ) as file: 174 file.write(text) 175 os.system(cmd + ' "' + filename + '"') 176