page.py
345 lines
| 11.7 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2205 | # encoding: utf-8 | ||
""" | ||||
Paging capabilities for IPython.core | ||||
Authors: | ||||
* Brian Granger | ||||
* Fernando Perez | ||||
Notes | ||||
----- | ||||
For now this uses ipapi, so it can't be in IPython.utils. If we can get | ||||
rid of that dependency, we could move it there. | ||||
----- | ||||
""" | ||||
#----------------------------------------------------------------------------- | ||||
Matthias BUSSONNIER
|
r5390 | # Copyright (C) 2008-2011 The IPython Development Team | ||
Brian Granger
|
r2205 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Matthias BUSSONNIER
|
r7817 | from __future__ import print_function | ||
Brian Granger
|
r2205 | |||
import os | ||||
import re | ||||
Michael Droettboom
|
r8966 | import subprocess | ||
Brian Granger
|
r2205 | import sys | ||
Brian Granger
|
r2498 | import tempfile | ||
Brian Granger
|
r2205 | |||
Thomas Kluyver
|
r4923 | from io import UnsupportedOperation | ||
Brian Granger
|
r2205 | from IPython.core import ipapi | ||
from IPython.core.error import TryNext | ||||
Brian Granger
|
r2498 | from IPython.utils.cursesimport import use_curses | ||
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 | |||
#----------------------------------------------------------------------------- | ||||
# Classes and functions | ||||
#----------------------------------------------------------------------------- | ||||
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
|
r4897 | def _detect_screen_size(use_curses, 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
|
r4897 | if (TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5': | ||
local_use_curses = use_curses | ||||
else: | ||||
# curses causes problems on many terminals other than xterm, and | ||||
# some termios calls lock up on Sun OS5. | ||||
local_use_curses = False | ||||
if local_use_curses: | ||||
import termios | ||||
import curses | ||||
# 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'] = '' | ||||
Michael Droettboom
|
r8966 | |||
Thomas Kluyver
|
r4897 | # Proceed with curses initialization | ||
scr = curses.initscr() | ||||
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 | ||||
Michael Droettboom
|
r8966 | |||
Thomas Kluyver
|
r4897 | # 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 | ||||
else: | ||||
return screen_lines_def | ||||
Brian Granger
|
r2498 | |||
def page(strng, start=0, screen_lines=0, pager_cmd=None): | ||||
Brian Granger
|
r2205 | """Print a string, piping through a pager after a certain length. | ||
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. | ||||
""" | ||||
# Some routines may auto-compute start offsets incorrectly and pass a | ||||
# negative value. Offset to 0 for robustness. | ||||
Brian Granger
|
r2498 | start = max(0, start) | ||
Brian Granger
|
r2205 | |||
# first, try the hook | ||||
ip = ipapi.get() | ||||
if ip: | ||||
try: | ||||
ip.hooks.show_in_pager(strng) | ||||
return | ||||
except TryNext: | ||||
pass | ||||
# 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: | ||
screen_lines += _detect_screen_size(use_curses, 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: | ||||
tmpname = tempfile.mktemp('.txt') | ||||
Brandon Parsons
|
r6650 | tmpfile = open(tmpname,'wt') | ||
Brian Granger
|
r2205 | tmpfile.write(strng) | ||
tmpfile.close() | ||||
cmd = "%s < %s" % (pager_cmd,tmpname) | ||||
if os.system(cmd): | ||||
retval = 1 | ||||
else: | ||||
retval = None | ||||
os.remove(tmpname) | ||||
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 | |||
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--- ') | ||
Brian Granger
|
r2205 | ans = msvcrt.getch() | ||
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(): | ||||
ans = raw_input('---Return to continue, q to quit--- ') | ||||
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: | ||||
- 0: only snip long strings | ||||
- 1: send to page() directly. | ||||
- 2: snip long strings and ask for full length viewing with page() | ||||
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: | ||||
if raw_input(header+' Snipped. View (y/n)? [N]').lower() == 'y': | ||||
page(str) | ||||
Brian Granger
|
r2498 | return snip | ||