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