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