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