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