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