page.py
377 lines
| 12.5 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2205 | # encoding: utf-8 | ||
""" | ||||
Paging capabilities for IPython.core | ||||
Notes | ||||
----- | ||||
Min RK
|
r19387 | For now this uses IPython hooks, so it can't be in IPython.utils. If we can get | ||
Brian Granger
|
r2205 | rid of that dependency, we could move it there. | ||
----- | ||||
""" | ||||
Min RK
|
r19387 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
Brian Granger
|
r2205 | |||
Matthias BUSSONNIER
|
r7817 | from __future__ import print_function | ||
Brian Granger
|
r2205 | |||
import os | ||||
import re | ||||
import sys | ||||
Brian Granger
|
r2498 | import tempfile | ||
Brian Granger
|
r2205 | |||
Thomas Kluyver
|
r4923 | from io import UnsupportedOperation | ||
MinRK
|
r10581 | from IPython import get_ipython | ||
Min RK
|
r19387 | from IPython.core.display import display | ||
Brian Granger
|
r2205 | from IPython.core.error import TryNext | ||
Brian Granger
|
r2498 | from IPython.utils.data import chop | ||
MinRK
|
r3800 | from IPython.utils import io | ||
Fernando Perez
|
r2908 | from IPython.utils.process import system | ||
Brian Granger
|
r2498 | from IPython.utils.terminal import get_terminal_size | ||
Michael Droettboom
|
r8966 | from IPython.utils import py3compat | ||
Brian Granger
|
r2205 | |||
Min RK
|
r19387 | 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) | ||||
Brian Granger
|
r2205 | |||
esc_re = re.compile(r"(\x1b[^m]+m)") | ||||
Brian Granger
|
r2498 | def page_dumb(strng, start=0, screen_lines=25): | ||
Brian Granger
|
r2205 | """Very dumb 'pager' in Python, for when nothing else works. | ||
Only moves forward, same interface as page(), except for pager_cmd and | ||||
mode.""" | ||||
out_ln = strng.splitlines()[start:] | ||||
screens = chop(out_ln,screen_lines-1) | ||||
if len(screens) == 1: | ||||
Matthias BUSSONNIER
|
r7817 | print(os.linesep.join(screens[0]), file=io.stdout) | ||
Brian Granger
|
r2205 | else: | ||
last_escape = "" | ||||
for scr in screens[0:-1]: | ||||
hunk = os.linesep.join(scr) | ||||
Matthias BUSSONNIER
|
r7817 | print(last_escape + hunk, file=io.stdout) | ||
Brian Granger
|
r2205 | if not page_more(): | ||
return | ||||
esc_list = esc_re.findall(hunk) | ||||
if len(esc_list) > 0: | ||||
last_escape = esc_list[-1] | ||||
Matthias BUSSONNIER
|
r7817 | print(last_escape + os.linesep.join(screens[-1]), file=io.stdout) | ||
Brian Granger
|
r2205 | |||
Thomas Kluyver
|
r9388 | def _detect_screen_size(screen_lines_def): | ||
Thomas Kluyver
|
r4920 | """Attempt to work out the number of lines on the screen. | ||
Michael Droettboom
|
r8966 | |||
Thomas Kluyver
|
r4920 | 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. | ||||
""" | ||||
Thomas Kluyver
|
r4921 | TERM = os.environ.get('TERM',None) | ||
Thomas Kluyver
|
r9388 | if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'): | ||
Thomas Kluyver
|
r4897 | # curses causes problems on many terminals other than xterm, and | ||
# some termios calls lock up on Sun OS5. | ||||
Thomas Kluyver
|
r9388 | return screen_lines_def | ||
try: | ||||
Thomas Kluyver
|
r4897 | import termios | ||
import curses | ||||
Thomas Kluyver
|
r9388 | except ImportError: | ||
return screen_lines_def | ||||
# There is a bug in curses, where *sometimes* it fails to properly | ||||
# initialize, and then after the endwin() call is made, the | ||||
# terminal is left in an unusable state. Rather than trying to | ||||
# check everytime for this (by requesting and comparing termios | ||||
# flags each time), we just save the initial terminal state and | ||||
# unconditionally reset it every time. It's cheaper than making | ||||
# the checks. | ||||
term_flags = termios.tcgetattr(sys.stdout) | ||||
# Curses modifies the stdout buffer size by default, which messes | ||||
# up Python's normal stdout buffering. This would manifest itself | ||||
# to IPython users as delayed printing on stdout after having used | ||||
# the pager. | ||||
# | ||||
# We can prevent this by manually setting the NCURSES_NO_SETBUF | ||||
# environment variable. For more details, see: | ||||
# http://bugs.python.org/issue10144 | ||||
NCURSES_NO_SETBUF = os.environ.get('NCURSES_NO_SETBUF', None) | ||||
os.environ['NCURSES_NO_SETBUF'] = '' | ||||
# Proceed with curses initialization | ||||
try: | ||||
Thomas Kluyver
|
r4897 | scr = curses.initscr() | ||
Thomas Kluyver
|
r9388 | except AttributeError: | ||
# Curses on Solaris may not be complete, so we can't use it there | ||||
Thomas Kluyver
|
r4897 | return screen_lines_def | ||
Thomas Kluyver
|
r9388 | |||
screen_lines_real,screen_cols = scr.getmaxyx() | ||||
curses.endwin() | ||||
# Restore environment | ||||
if NCURSES_NO_SETBUF is None: | ||||
del os.environ['NCURSES_NO_SETBUF'] | ||||
else: | ||||
os.environ['NCURSES_NO_SETBUF'] = NCURSES_NO_SETBUF | ||||
# Restore terminal state in case endwin() didn't. | ||||
termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags) | ||||
# Now we have what we needed: the screen size in rows/columns | ||||
return screen_lines_real | ||||
#print '***Screen size:',screen_lines_real,'lines x',\ | ||||
#screen_cols,'columns.' # dbg | ||||
Brian Granger
|
r2498 | |||
Min RK
|
r19387 | def pager_page(strng, start=0, screen_lines=0, pager_cmd=None): | ||
MinRK
|
r16665 | """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. | ||||
Brian Granger
|
r2205 | |||
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. | ||||
""" | ||||
MinRK
|
r16586 | |||
# for compatibility with mime-bundle form: | ||||
if isinstance(strng, dict): | ||||
strng = strng['text/plain'] | ||||
Brian Granger
|
r2205 | |||
# Ugly kludge, but calling curses.initscr() flat out crashes in emacs | ||||
TERM = os.environ.get('TERM','dumb') | ||||
if TERM in ['dumb','emacs'] and os.name != 'nt': | ||||
Matthias BUSSONNIER
|
r7817 | print(strng) | ||
Brian Granger
|
r2205 | return | ||
# chop off the topmost part of the string we don't want to see | ||||
MinRK
|
r3739 | str_lines = strng.splitlines()[start:] | ||
Brian Granger
|
r2205 | str_toprint = os.linesep.join(str_lines) | ||
num_newlines = len(str_lines) | ||||
len_str = len(str_toprint) | ||||
# Dumb heuristics to guesstimate number of on-screen lines the string | ||||
# takes. Very basic, but good enough for docstrings in reasonable | ||||
# terminals. If someone later feels like refining it, it's not hard. | ||||
numlines = max(num_newlines,int(len_str/80)+1) | ||||
Brian Granger
|
r2498 | screen_lines_def = get_terminal_size()[1] | ||
Brian Granger
|
r2205 | |||
# auto-determine screen size | ||||
if screen_lines <= 0: | ||||
Thomas Kluyver
|
r4897 | try: | ||
Thomas Kluyver
|
r9388 | screen_lines += _detect_screen_size(screen_lines_def) | ||
Thomas Kluyver
|
r4923 | except (TypeError, UnsupportedOperation): | ||
Matthias BUSSONNIER
|
r7817 | print(str_toprint, file=io.stdout) | ||
Thomas Kluyver
|
r4897 | return | ||
Brian Granger
|
r2205 | |||
#print 'numlines',numlines,'screenlines',screen_lines # dbg | ||||
if numlines <= screen_lines : | ||||
#print '*** normal print' # dbg | ||||
Matthias BUSSONNIER
|
r7817 | print(str_toprint, file=io.stdout) | ||
Brian Granger
|
r2205 | else: | ||
# Try to open pager and default to internal one if that fails. | ||||
# All failure modes are tagged as 'retval=1', to match the return | ||||
# value of a failed system command. If any intermediate attempt | ||||
# sets retval to 1, at the end we resort to our own page_dumb() pager. | ||||
pager_cmd = get_pager_cmd(pager_cmd) | ||||
pager_cmd += ' ' + get_pager_start(pager_cmd,start) | ||||
if os.name == 'nt': | ||||
if pager_cmd.startswith('type'): | ||||
# The default WinXP 'type' command is failing on complex strings. | ||||
retval = 1 | ||||
else: | ||||
Julian Taylor
|
r15372 | fd, tmpname = tempfile.mkstemp('.txt') | ||
try: | ||||
os.close(fd) | ||||
with open(tmpname, 'wt') as tmpfile: | ||||
tmpfile.write(strng) | ||||
cmd = "%s < %s" % (pager_cmd, tmpname) | ||||
# tmpfile needs to be closed for windows | ||||
if os.system(cmd): | ||||
retval = 1 | ||||
else: | ||||
retval = None | ||||
finally: | ||||
os.remove(tmpname) | ||||
Brian Granger
|
r2205 | else: | ||
try: | ||||
retval = None | ||||
# if I use popen4, things hang. No idea why. | ||||
#pager,shell_out = os.popen4(pager_cmd) | ||||
Michael Droettboom
|
r8966 | pager = os.popen(pager_cmd, 'w') | ||
try: | ||||
pager_encoding = pager.encoding or sys.stdout.encoding | ||||
pager.write(py3compat.cast_bytes_py2( | ||||
strng, encoding=pager_encoding)) | ||||
finally: | ||||
retval = pager.close() | ||||
Matthias BUSSONNIER
|
r7787 | except IOError as msg: # broken pipe when user quits | ||
Michael Droettboom
|
r8966 | if msg.args == (32, 'Broken pipe'): | ||
Brian Granger
|
r2205 | retval = None | ||
else: | ||||
retval = 1 | ||||
except OSError: | ||||
# Other strange problems, sometimes seen in Win2k/cygwin | ||||
retval = 1 | ||||
if retval is not None: | ||||
page_dumb(strng,screen_lines=screen_lines) | ||||
Brian Granger
|
r2498 | |||
Min RK
|
r19387 | 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. | ||||
""" | ||||
# Some routines may auto-compute start offsets incorrectly and pass a | ||||
# negative value. Offset to 0 for robustness. | ||||
start = max(0, start) | ||||
# first, try the hook | ||||
ip = get_ipython() | ||||
if ip: | ||||
try: | ||||
ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines) | ||||
return | ||||
except TryNext: | ||||
pass | ||||
# fallback on default pager | ||||
return pager_page(data, start, screen_lines, pager_cmd) | ||||
Brian Granger
|
r2498 | def page_file(fname, start=0, pager_cmd=None): | ||
Brian Granger
|
r2205 | """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 | ||||
Fernando Perez
|
r2908 | system(pager_cmd + ' ' + fname) | ||
Brian Granger
|
r2205 | except: | ||
try: | ||||
if start > 0: | ||||
start -= 1 | ||||
page(open(fname).read(),start) | ||||
except: | ||||
Matthias BUSSONNIER
|
r7817 | print('Unable to show file',repr(fname)) | ||
Brian Granger
|
r2205 | |||
Brian Granger
|
r2498 | def get_pager_cmd(pager_cmd=None): | ||
"""Return a pager command. | ||||
Brian Granger
|
r2205 | |||
Brian Granger
|
r2498 | Makes some attempts at finding an OS-correct one. | ||
""" | ||||
Brian Granger
|
r2205 | if os.name == 'posix': | ||
default_pager_cmd = 'less -r' # -r for color control sequences | ||||
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 | ||||
return pager_cmd | ||||
Brian Granger
|
r2498 | |||
def get_pager_start(pager, start): | ||||
Brian Granger
|
r2205 | """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 | ||||
Brian Granger
|
r2498 | |||
# (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch() | ||||
Brian Granger
|
r2205 | 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 | ||||
""" | ||||
MinRK
|
r3800 | io.stdout.write('---Return to continue, q to quit--- ') | ||
Thomas Kluyver
|
r12260 | ans = msvcrt.getwch() | ||
Brian Granger
|
r2205 | if ans in ("q", "Q"): | ||
result = False | ||||
else: | ||||
result = True | ||||
MinRK
|
r3800 | io.stdout.write("\b"*37 + " "*37 + "\b"*37) | ||
Brian Granger
|
r2205 | return result | ||
else: | ||||
def page_more(): | ||||
Thomas Kluyver
|
r13355 | ans = py3compat.input('---Return to continue, q to quit--- ') | ||
Brian Granger
|
r2205 | if ans.lower().startswith('q'): | ||
return False | ||||
else: | ||||
return True | ||||
Brian Granger
|
r2498 | |||
Brian Granger
|
r2205 | def snip_print(str,width = 75,print_full = 0,header = ''): | ||
"""Print a string snipping the midsection to fit in width. | ||||
print_full: mode control: | ||||
Thomas Kluyver
|
r12553 | |||
Brian Granger
|
r2205 | - 0: only snip long strings | ||
- 1: send to page() directly. | ||||
- 2: snip long strings and ask for full length viewing with page() | ||||
Thomas Kluyver
|
r12553 | |||
Brian Granger
|
r2205 | Return 1 if snipping was necessary, 0 otherwise.""" | ||
if print_full == 1: | ||||
page(header+str) | ||||
return 0 | ||||
Matthias BUSSONNIER
|
r7817 | print(header, end=' ') | ||
Brian Granger
|
r2205 | if len(str) < width: | ||
Matthias BUSSONNIER
|
r7817 | print(str) | ||
Brian Granger
|
r2205 | snip = 0 | ||
else: | ||||
whalf = int((width -5)/2) | ||||
Matthias BUSSONNIER
|
r7817 | print(str[:whalf] + ' <...> ' + str[-whalf:]) | ||
Brian Granger
|
r2205 | snip = 1 | ||
if snip and print_full == 2: | ||||
Thomas Kluyver
|
r13355 | if py3compat.input(header+' Snipped. View (y/n)? [N]').lower() == 'y': | ||
Brian Granger
|
r2205 | page(str) | ||
Brian Granger
|
r2498 | return snip | ||