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