aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Lib/_pyrepl/pager.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/_pyrepl/pager.py')
-rw-r--r--Lib/_pyrepl/pager.py169
1 files changed, 169 insertions, 0 deletions
diff --git a/Lib/_pyrepl/pager.py b/Lib/_pyrepl/pager.py
new file mode 100644
index 00000000000..ecf5ddc79a5
--- /dev/null
+++ b/Lib/_pyrepl/pager.py
@@ -0,0 +1,169 @@
+from __future__ import annotations
+
+import io
+import os
+import re
+import sys
+
+
+# types
+if False:
+ from typing import Protocol, Any
+ 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 plainpager
+ 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='': tempfilepager(plain(text), 'more <')
+ 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)
+ getchar = lambda: sys.stdin.read(1)
+ has_tty = True
+ except (ImportError, AttributeError, io.UnsupportedOperation):
+ getchar = lambda: 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 + '"')