| | |
| | """ |
| | Paging capabilities for IPython.core |
| | |
| | Notes |
| | ----- |
| | |
| | For now this uses IPython hooks, so it can't be in IPython.utils. If we can get |
| | rid of that dependency, we could move it there. |
| | ----- |
| | """ |
| |
|
| | |
| | |
| |
|
| |
|
| | import os |
| | import io |
| | import re |
| | import sys |
| | import tempfile |
| | import subprocess |
| |
|
| | from io import UnsupportedOperation |
| | from pathlib import Path |
| |
|
| | from IPython import get_ipython |
| | from IPython.display import display |
| | from IPython.core.error import TryNext |
| | from IPython.utils.data import chop |
| | from IPython.utils.process import system |
| | from IPython.utils.terminal import get_terminal_size |
| | from IPython.utils import py3compat |
| |
|
| |
|
| | def display_page(strng, start=0, screen_lines=25): |
| | """Just display, no paging. screen_lines is ignored.""" |
| | if isinstance(strng, dict): |
| | data = strng |
| | else: |
| | if start: |
| | strng = u'\n'.join(strng.splitlines()[start:]) |
| | data = { 'text/plain': strng } |
| | display(data, raw=True) |
| |
|
| |
|
| | def as_hook(page_func): |
| | """Wrap a pager func to strip the `self` arg |
| | |
| | so it can be called as a hook. |
| | """ |
| | return lambda self, *args, **kwargs: page_func(*args, **kwargs) |
| |
|
| |
|
| | esc_re = re.compile(r"(\x1b[^m]+m)") |
| |
|
| | def page_dumb(strng, start=0, screen_lines=25): |
| | """Very dumb 'pager' in Python, for when nothing else works. |
| | |
| | Only moves forward, same interface as page(), except for pager_cmd and |
| | mode. |
| | """ |
| | if isinstance(strng, dict): |
| | strng = strng.get('text/plain', '') |
| | out_ln = strng.splitlines()[start:] |
| | screens = chop(out_ln,screen_lines-1) |
| | if len(screens) == 1: |
| | print(os.linesep.join(screens[0])) |
| | else: |
| | last_escape = "" |
| | for scr in screens[0:-1]: |
| | hunk = os.linesep.join(scr) |
| | print(last_escape + hunk) |
| | if not page_more(): |
| | return |
| | esc_list = esc_re.findall(hunk) |
| | if len(esc_list) > 0: |
| | last_escape = esc_list[-1] |
| | print(last_escape + os.linesep.join(screens[-1])) |
| |
|
| | def _detect_screen_size(screen_lines_def): |
| | """Attempt to work out the number of lines on the screen. |
| | |
| | This is called by page(). It can raise an error (e.g. when run in the |
| | test suite), so it's separated out so it can easily be called in a try block. |
| | """ |
| | TERM = os.environ.get('TERM',None) |
| | if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'): |
| | |
| | |
| | return screen_lines_def |
| | |
| | try: |
| | import termios |
| | import curses |
| | except ImportError: |
| | return screen_lines_def |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | try: |
| | term_flags = termios.tcgetattr(sys.stdout) |
| | except termios.error as err: |
| | |
| | raise TypeError('termios error: {0}'.format(err)) from err |
| |
|
| | try: |
| | scr = curses.initscr() |
| | except AttributeError: |
| | |
| | return screen_lines_def |
| | |
| | screen_lines_real,screen_cols = scr.getmaxyx() |
| | curses.endwin() |
| |
|
| | |
| | termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags) |
| | |
| | return screen_lines_real |
| | |
| | |
| |
|
| | def pager_page(strng, start=0, screen_lines=0, pager_cmd=None): |
| | """Display a string, piping through a pager after a certain length. |
| | |
| | strng can be a mime-bundle dict, supplying multiple representations, |
| | keyed by mime-type. |
| | |
| | The screen_lines parameter specifies the number of *usable* lines of your |
| | terminal screen (total lines minus lines you need to reserve to show other |
| | information). |
| | |
| | If you set screen_lines to a number <=0, page() will try to auto-determine |
| | your screen size and will only use up to (screen_size+screen_lines) for |
| | printing, paging after that. That is, if you want auto-detection but need |
| | to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for |
| | auto-detection without any lines reserved simply use screen_lines = 0. |
| | |
| | If a string won't fit in the allowed lines, it is sent through the |
| | specified pager command. If none given, look for PAGER in the environment, |
| | and ultimately default to less. |
| | |
| | If no system pager works, the string is sent through a 'dumb pager' |
| | written in python, very simplistic. |
| | """ |
| | |
| | |
| | if isinstance(strng, dict): |
| | strng = strng['text/plain'] |
| |
|
| | |
| | TERM = os.environ.get('TERM','dumb') |
| | if TERM in ['dumb','emacs'] and os.name != 'nt': |
| | print(strng) |
| | return |
| | |
| | str_lines = strng.splitlines()[start:] |
| | str_toprint = os.linesep.join(str_lines) |
| | num_newlines = len(str_lines) |
| | len_str = len(str_toprint) |
| |
|
| | |
| | |
| | |
| | numlines = max(num_newlines,int(len_str/80)+1) |
| |
|
| | screen_lines_def = get_terminal_size()[1] |
| |
|
| | |
| | if screen_lines <= 0: |
| | try: |
| | screen_lines += _detect_screen_size(screen_lines_def) |
| | except (TypeError, UnsupportedOperation): |
| | print(str_toprint) |
| | return |
| |
|
| | |
| | if numlines <= screen_lines : |
| | |
| | print(str_toprint) |
| | else: |
| | |
| | |
| | |
| | |
| | pager_cmd = get_pager_cmd(pager_cmd) |
| | pager_cmd += ' ' + get_pager_start(pager_cmd,start) |
| | if os.name == 'nt': |
| | if pager_cmd.startswith('type'): |
| | |
| | retval = 1 |
| | else: |
| | fd, tmpname = tempfile.mkstemp('.txt') |
| | tmppath = Path(tmpname) |
| | try: |
| | os.close(fd) |
| | with tmppath.open("wt", encoding="utf-8") as tmpfile: |
| | tmpfile.write(strng) |
| | cmd = "%s < %s" % (pager_cmd, tmppath) |
| | |
| | if os.system(cmd): |
| | retval = 1 |
| | else: |
| | retval = None |
| | finally: |
| | Path.unlink(tmppath) |
| | else: |
| | try: |
| | retval = None |
| | |
| | proc = subprocess.Popen( |
| | pager_cmd, |
| | shell=True, |
| | stdin=subprocess.PIPE, |
| | stderr=subprocess.DEVNULL, |
| | ) |
| | pager = os._wrap_close( |
| | io.TextIOWrapper(proc.stdin, encoding="utf-8"), proc |
| | ) |
| | try: |
| | pager_encoding = pager.encoding or sys.stdout.encoding |
| | pager.write(strng) |
| | finally: |
| | retval = pager.close() |
| | except IOError as msg: |
| | if msg.args == (32, 'Broken pipe'): |
| | retval = None |
| | else: |
| | retval = 1 |
| | except OSError: |
| | |
| | retval = 1 |
| | if retval is not None: |
| | page_dumb(strng,screen_lines=screen_lines) |
| |
|
| |
|
| | def page(data, start=0, screen_lines=0, pager_cmd=None): |
| | """Display content in a pager, piping through a pager after a certain length. |
| | |
| | data can be a mime-bundle dict, supplying multiple representations, |
| | keyed by mime-type, or text. |
| | |
| | Pager is dispatched via the `show_in_pager` IPython hook. |
| | If no hook is registered, `pager_page` will be used. |
| | """ |
| | |
| | |
| | start = max(0, start) |
| |
|
| | |
| | ip = get_ipython() |
| | if ip: |
| | try: |
| | ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines) |
| | return |
| | except TryNext: |
| | pass |
| | |
| | |
| | return pager_page(data, start, screen_lines, pager_cmd) |
| |
|
| |
|
| | def page_file(fname, start=0, pager_cmd=None): |
| | """Page a file, using an optional pager command and starting line. |
| | """ |
| |
|
| | pager_cmd = get_pager_cmd(pager_cmd) |
| | pager_cmd += ' ' + get_pager_start(pager_cmd,start) |
| |
|
| | try: |
| | if os.environ['TERM'] in ['emacs','dumb']: |
| | raise EnvironmentError |
| | system(pager_cmd + ' ' + fname) |
| | except: |
| | try: |
| | if start > 0: |
| | start -= 1 |
| | page(open(fname, encoding="utf-8").read(), start) |
| | except: |
| | print('Unable to show file',repr(fname)) |
| |
|
| |
|
| | def get_pager_cmd(pager_cmd=None): |
| | """Return a pager command. |
| | |
| | Makes some attempts at finding an OS-correct one. |
| | """ |
| | if os.name == 'posix': |
| | default_pager_cmd = 'less -R' |
| | elif os.name in ['nt','dos']: |
| | default_pager_cmd = 'type' |
| |
|
| | if pager_cmd is None: |
| | try: |
| | pager_cmd = os.environ['PAGER'] |
| | except: |
| | pager_cmd = default_pager_cmd |
| | |
| | if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower(): |
| | pager_cmd += ' -R' |
| | |
| | return pager_cmd |
| |
|
| |
|
| | def get_pager_start(pager, start): |
| | """Return the string for paging files with an offset. |
| | |
| | This is the '+N' argument which less and more (under Unix) accept. |
| | """ |
| |
|
| | if pager in ['less','more']: |
| | if start: |
| | start_string = '+' + str(start) |
| | else: |
| | start_string = '' |
| | else: |
| | start_string = '' |
| | return start_string |
| |
|
| |
|
| | |
| | if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs': |
| | import msvcrt |
| | def page_more(): |
| | """ Smart pausing between pages |
| | |
| | @return: True if need print more lines, False if quit |
| | """ |
| | sys.stdout.write('---Return to continue, q to quit--- ') |
| | ans = msvcrt.getwch() |
| | if ans in ("q", "Q"): |
| | result = False |
| | else: |
| | result = True |
| | sys.stdout.write("\b"*37 + " "*37 + "\b"*37) |
| | return result |
| | else: |
| | def page_more(): |
| | ans = py3compat.input('---Return to continue, q to quit--- ') |
| | if ans.lower().startswith('q'): |
| | return False |
| | else: |
| | return True |
| |
|