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