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