page.py 13 KB

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