• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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