1# encoding: utf-8 2""" 3Paging capabilities for IPython.core 4 5Notes 6----- 7 8For now this uses IPython hooks, so it can't be in IPython.utils. If we can get 9rid of that dependency, we could move it there. 10----- 11""" 12 13# Copyright (c) IPython Development Team. 14# Distributed under the terms of the Modified BSD License. 15 16 17import os 18import io 19import re 20import sys 21import tempfile 22import subprocess 23 24from io import UnsupportedOperation 25 26from IPython import get_ipython 27from IPython.core.display import display 28from IPython.core.error import TryNext 29from IPython.utils.data import chop 30from IPython.utils.process import system 31from IPython.utils.terminal import get_terminal_size 32from IPython.utils import py3compat 33 34 35def display_page(strng, start=0, screen_lines=25): 36 """Just display, no paging. screen_lines is ignored.""" 37 if isinstance(strng, dict): 38 data = strng 39 else: 40 if start: 41 strng = u'\n'.join(strng.splitlines()[start:]) 42 data = { 'text/plain': strng } 43 display(data, raw=True) 44 45 46def as_hook(page_func): 47 """Wrap a pager func to strip the `self` arg 48 49 so it can be called as a hook. 50 """ 51 return lambda self, *args, **kwargs: page_func(*args, **kwargs) 52 53 54esc_re = re.compile(r"(\x1b[^m]+m)") 55 56def page_dumb(strng, start=0, screen_lines=25): 57 """Very dumb 'pager' in Python, for when nothing else works. 58 59 Only moves forward, same interface as page(), except for pager_cmd and 60 mode. 61 """ 62 if isinstance(strng, dict): 63 strng = strng.get('text/plain', '') 64 out_ln = strng.splitlines()[start:] 65 screens = chop(out_ln,screen_lines-1) 66 if len(screens) == 1: 67 print(os.linesep.join(screens[0])) 68 else: 69 last_escape = "" 70 for scr in screens[0:-1]: 71 hunk = os.linesep.join(scr) 72 print(last_escape + hunk) 73 if not page_more(): 74 return 75 esc_list = esc_re.findall(hunk) 76 if len(esc_list) > 0: 77 last_escape = esc_list[-1] 78 print(last_escape + os.linesep.join(screens[-1])) 79 80def _detect_screen_size(screen_lines_def): 81 """Attempt to work out the number of lines on the screen. 82 83 This is called by page(). It can raise an error (e.g. when run in the 84 test suite), so it's separated out so it can easily be called in a try block. 85 """ 86 TERM = os.environ.get('TERM',None) 87 if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'): 88 # curses causes problems on many terminals other than xterm, and 89 # some termios calls lock up on Sun OS5. 90 return screen_lines_def 91 92 try: 93 import termios 94 import curses 95 except ImportError: 96 return screen_lines_def 97 98 # There is a bug in curses, where *sometimes* it fails to properly 99 # initialize, and then after the endwin() call is made, the 100 # terminal is left in an unusable state. Rather than trying to 101 # check every time for this (by requesting and comparing termios 102 # flags each time), we just save the initial terminal state and 103 # unconditionally reset it every time. It's cheaper than making 104 # the checks. 105 try: 106 term_flags = termios.tcgetattr(sys.stdout) 107 except termios.error as err: 108 # can fail on Linux 2.6, pager_page will catch the TypeError 109 raise TypeError('termios error: {0}'.format(err)) 110 111 try: 112 scr = curses.initscr() 113 except AttributeError: 114 # Curses on Solaris may not be complete, so we can't use it there 115 return screen_lines_def 116 117 screen_lines_real,screen_cols = scr.getmaxyx() 118 curses.endwin() 119 120 # Restore terminal state in case endwin() didn't. 121 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags) 122 # Now we have what we needed: the screen size in rows/columns 123 return screen_lines_real 124 #print '***Screen size:',screen_lines_real,'lines x',\ 125 #screen_cols,'columns.' # dbg 126 127def pager_page(strng, start=0, screen_lines=0, pager_cmd=None): 128 """Display a string, piping through a pager after a certain length. 129 130 strng can be a mime-bundle dict, supplying multiple representations, 131 keyed by mime-type. 132 133 The screen_lines parameter specifies the number of *usable* lines of your 134 terminal screen (total lines minus lines you need to reserve to show other 135 information). 136 137 If you set screen_lines to a number <=0, page() will try to auto-determine 138 your screen size and will only use up to (screen_size+screen_lines) for 139 printing, paging after that. That is, if you want auto-detection but need 140 to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for 141 auto-detection without any lines reserved simply use screen_lines = 0. 142 143 If a string won't fit in the allowed lines, it is sent through the 144 specified pager command. If none given, look for PAGER in the environment, 145 and ultimately default to less. 146 147 If no system pager works, the string is sent through a 'dumb pager' 148 written in python, very simplistic. 149 """ 150 151 # for compatibility with mime-bundle form: 152 if isinstance(strng, dict): 153 strng = strng['text/plain'] 154 155 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs 156 TERM = os.environ.get('TERM','dumb') 157 if TERM in ['dumb','emacs'] and os.name != 'nt': 158 print(strng) 159 return 160 # chop off the topmost part of the string we don't want to see 161 str_lines = strng.splitlines()[start:] 162 str_toprint = os.linesep.join(str_lines) 163 num_newlines = len(str_lines) 164 len_str = len(str_toprint) 165 166 # Dumb heuristics to guesstimate number of on-screen lines the string 167 # takes. Very basic, but good enough for docstrings in reasonable 168 # terminals. If someone later feels like refining it, it's not hard. 169 numlines = max(num_newlines,int(len_str/80)+1) 170 171 screen_lines_def = get_terminal_size()[1] 172 173 # auto-determine screen size 174 if screen_lines <= 0: 175 try: 176 screen_lines += _detect_screen_size(screen_lines_def) 177 except (TypeError, UnsupportedOperation): 178 print(str_toprint) 179 return 180 181 #print 'numlines',numlines,'screenlines',screen_lines # dbg 182 if numlines <= screen_lines : 183 #print '*** normal print' # dbg 184 print(str_toprint) 185 else: 186 # Try to open pager and default to internal one if that fails. 187 # All failure modes are tagged as 'retval=1', to match the return 188 # value of a failed system command. If any intermediate attempt 189 # sets retval to 1, at the end we resort to our own page_dumb() pager. 190 pager_cmd = get_pager_cmd(pager_cmd) 191 pager_cmd += ' ' + get_pager_start(pager_cmd,start) 192 if os.name == 'nt': 193 if pager_cmd.startswith('type'): 194 # The default WinXP 'type' command is failing on complex strings. 195 retval = 1 196 else: 197 fd, tmpname = tempfile.mkstemp('.txt') 198 try: 199 os.close(fd) 200 with open(tmpname, 'wt') as tmpfile: 201 tmpfile.write(strng) 202 cmd = "%s < %s" % (pager_cmd, tmpname) 203 # tmpfile needs to be closed for windows 204 if os.system(cmd): 205 retval = 1 206 else: 207 retval = None 208 finally: 209 os.remove(tmpname) 210 else: 211 try: 212 retval = None 213 # Emulate os.popen, but redirect stderr 214 proc = subprocess.Popen(pager_cmd, 215 shell=True, 216 stdin=subprocess.PIPE, 217 stderr=subprocess.DEVNULL 218 ) 219 pager = os._wrap_close(io.TextIOWrapper(proc.stdin), proc) 220 try: 221 pager_encoding = pager.encoding or sys.stdout.encoding 222 pager.write(strng) 223 finally: 224 retval = pager.close() 225 except IOError as msg: # broken pipe when user quits 226 if msg.args == (32, 'Broken pipe'): 227 retval = None 228 else: 229 retval = 1 230 except OSError: 231 # Other strange problems, sometimes seen in Win2k/cygwin 232 retval = 1 233 if retval is not None: 234 page_dumb(strng,screen_lines=screen_lines) 235 236 237def page(data, start=0, screen_lines=0, pager_cmd=None): 238 """Display content in a pager, piping through a pager after a certain length. 239 240 data can be a mime-bundle dict, supplying multiple representations, 241 keyed by mime-type, or text. 242 243 Pager is dispatched via the `show_in_pager` IPython hook. 244 If no hook is registered, `pager_page` will be used. 245 """ 246 # Some routines may auto-compute start offsets incorrectly and pass a 247 # negative value. Offset to 0 for robustness. 248 start = max(0, start) 249 250 # first, try the hook 251 ip = get_ipython() 252 if ip: 253 try: 254 ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines) 255 return 256 except TryNext: 257 pass 258 259 # fallback on default pager 260 return pager_page(data, start, screen_lines, pager_cmd) 261 262 263def page_file(fname, start=0, pager_cmd=None): 264 """Page a file, using an optional pager command and starting line. 265 """ 266 267 pager_cmd = get_pager_cmd(pager_cmd) 268 pager_cmd += ' ' + get_pager_start(pager_cmd,start) 269 270 try: 271 if os.environ['TERM'] in ['emacs','dumb']: 272 raise EnvironmentError 273 system(pager_cmd + ' ' + fname) 274 except: 275 try: 276 if start > 0: 277 start -= 1 278 page(open(fname).read(),start) 279 except: 280 print('Unable to show file',repr(fname)) 281 282 283def get_pager_cmd(pager_cmd=None): 284 """Return a pager command. 285 286 Makes some attempts at finding an OS-correct one. 287 """ 288 if os.name == 'posix': 289 default_pager_cmd = 'less -R' # -R for color control sequences 290 elif os.name in ['nt','dos']: 291 default_pager_cmd = 'type' 292 293 if pager_cmd is None: 294 try: 295 pager_cmd = os.environ['PAGER'] 296 except: 297 pager_cmd = default_pager_cmd 298 299 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower(): 300 pager_cmd += ' -R' 301 302 return pager_cmd 303 304 305def get_pager_start(pager, start): 306 """Return the string for paging files with an offset. 307 308 This is the '+N' argument which less and more (under Unix) accept. 309 """ 310 311 if pager in ['less','more']: 312 if start: 313 start_string = '+' + str(start) 314 else: 315 start_string = '' 316 else: 317 start_string = '' 318 return start_string 319 320 321# (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch() 322if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs': 323 import msvcrt 324 def page_more(): 325 """ Smart pausing between pages 326 327 @return: True if need print more lines, False if quit 328 """ 329 sys.stdout.write('---Return to continue, q to quit--- ') 330 ans = msvcrt.getwch() 331 if ans in ("q", "Q"): 332 result = False 333 else: 334 result = True 335 sys.stdout.write("\b"*37 + " "*37 + "\b"*37) 336 return result 337else: 338 def page_more(): 339 ans = py3compat.input('---Return to continue, q to quit--- ') 340 if ans.lower().startswith('q'): 341 return False 342 else: 343 return True 344