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