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