# -*- coding: iso-8859-1 -*-

import curses, fcntl, signal, struct, tty, textwrap, inspect

from IPython import ipapi

import astyle, ipipe


# Python 2.3 compatibility
try:
    set
except NameError:
    import sets
    set = sets.Set

# Python 2.3 compatibility
try:
    sorted
except NameError:
    from ipipe import sorted


class UnassignedKeyError(Exception):
    """
    Exception that is used for reporting unassigned keys.
    """


class UnknownCommandError(Exception):
    """
    Exception that is used for reporting unknown commands (this should never
    happen).
    """


class CommandError(Exception):
    """
    Exception that is used for reporting that a command can't be executed.
    """


class Keymap(dict):
    """
    Stores mapping of keys to commands.
    """
    def __init__(self):
        self._keymap = {}

    def __setitem__(self, key, command):
        if isinstance(key, str):
            for c in key:
                dict.__setitem__(self, ord(c), command)
        else:
            dict.__setitem__(self, key, command)

    def __getitem__(self, key):
        if isinstance(key, str):
            key = ord(key)
        return dict.__getitem__(self, key)

    def __detitem__(self, key):
        if isinstance(key, str):
            key = ord(key)
        dict.__detitem__(self, key)

    def register(self, command, *keys):
        for key in keys:
            self[key] = command

    def get(self, key, default=None):
        if isinstance(key, str):
            key = ord(key)
        return dict.get(self, key, default)

    def findkey(self, command, default=ipipe.noitem):
        for (key, commandcandidate) in self.iteritems():
            if commandcandidate == command:
                return key
        if default is ipipe.noitem:
            raise KeyError(command)
        return default


class _BrowserCachedItem(object):
    # This is used internally by ``ibrowse`` to store a item together with its
    # marked status.
    __slots__ = ("item", "marked")

    def __init__(self, item):
        self.item = item
        self.marked = False


class _BrowserHelp(object):
    style_header = astyle.Style.fromstr("yellow:black:bold")
    # This is used internally by ``ibrowse`` for displaying the help screen.
    def __init__(self, browser):
        self.browser = browser

    def __xrepr__(self, mode):
        yield (-1, True)
        if mode == "header" or mode == "footer":
            yield (astyle.style_default, "ibrowse help screen")
        else:
            yield (astyle.style_default, repr(self))

    def __iter__(self):
        # Get reverse key mapping
        allkeys = {}
        for (key, cmd) in self.browser.keymap.iteritems():
            allkeys.setdefault(cmd, []).append(key)

        fields = ("key", "description")

        commands = []
        for name in dir(self.browser):
            if name.startswith("cmd_"):
                command = getattr(self.browser, name)
                commands.append((inspect.getsourcelines(command)[-1], name[4:], command))
        commands.sort()
        commands = [(c[1], c[2]) for c in commands]
        for (i, (name, command)) in enumerate(commands):
            if i:
                yield ipipe.Fields(fields, key="", description="")

            description = command.__doc__
            if description is None:
                lines = []
            else:
                lines = [l.strip() for l in description.splitlines() if l.strip()]
                description = "\n".join(lines)
                lines = textwrap.wrap(description, 60)
            keys = allkeys.get(name, [])

            yield ipipe.Fields(fields, key="", description=astyle.Text((self.style_header, name)))
            for i in xrange(max(len(keys), len(lines))):
                try:
                    key = self.browser.keylabel(keys[i])
                except IndexError:
                    key = ""
                try:
                    line = lines[i]
                except IndexError:
                    line = ""
                yield ipipe.Fields(fields, key=key, description=line)


class _BrowserLevel(object):
    # This is used internally to store the state (iterator, fetch items,
    # position of cursor and screen, etc.) of one browser level
    # An ``ibrowse`` object keeps multiple ``_BrowserLevel`` objects in
    # a stack.
    def __init__(self, browser, input, mainsizey, *attrs):
        self.browser = browser
        self.input = input
        self.header = [x for x in ipipe.xrepr(input, "header") if not isinstance(x[0], int)]
        # iterator for the input
        self.iterator = ipipe.xiter(input)

        # is the iterator exhausted?
        self.exhausted = False

        # attributes to be display (autodetected if empty)
        self.attrs = attrs

        # fetched items (+ marked flag)
        self.items = ipipe.deque()

        # Number of marked objects
        self.marked = 0

        # Vertical cursor position
        self.cury = 0

        # Horizontal cursor position
        self.curx = 0

        # Index of first data column
        self.datastartx = 0

        # Index of first data line
        self.datastarty = 0

        # height of the data display area
        self.mainsizey = mainsizey

        # width of the data display area (changes when scrolling)
        self.mainsizex = 0

        # Size of row number (changes when scrolling)
        self.numbersizex = 0

        # Attributes to display (in this order)
        self.displayattrs = []

        # index and attribute under the cursor
        self.displayattr = (None, ipipe.noitem)

        # Maps attributes to column widths
        self.colwidths = {}

        # Set of hidden attributes
        self.hiddenattrs = set()

        # This takes care of all the caches etc.
        self.moveto(0, 0, refresh=True)

    def fetch(self, count):
        # Try to fill ``self.items`` with at least ``count`` objects.
        have = len(self.items)
        while not self.exhausted and have < count:
            try:
                item = self.iterator.next()
            except StopIteration:
                self.exhausted = True
                break
            except (KeyboardInterrupt, SystemExit):
                raise
            except Exception, exc:
                have += 1
                self.items.append(_BrowserCachedItem(exc))
                self.exhausted = True
                break
            else:
                have += 1
                self.items.append(_BrowserCachedItem(item))

    def calcdisplayattrs(self):
        # Calculate which attributes are available from the objects that are
        # currently visible on screen (and store it in ``self.displayattrs``)

        attrs = set()
        self.displayattrs = []
        if self.attrs:
            # If the browser object specifies a fixed list of attributes,
            # simply use it (removing hidden attributes).
            for attr in self.attrs:
                attr = ipipe.upgradexattr(attr)
                if attr not in attrs and attr not in self.hiddenattrs:
                    self.displayattrs.append(attr)
                    attrs.add(attr)
        else:
            endy = min(self.datastarty+self.mainsizey, len(self.items))
            for i in xrange(self.datastarty, endy):
                for attr in ipipe.xattrs(self.items[i].item, "default"):
                    if attr not in attrs and attr not in self.hiddenattrs:
                        self.displayattrs.append(attr)
                        attrs.add(attr)

    def getrow(self, i):
        # Return a dictionary with the attributes for the object
        # ``self.items[i]``. Attribute names are taken from
        # ``self.displayattrs`` so ``calcdisplayattrs()`` must have been
        # called before.
        row = {}
        item = self.items[i].item
        for attr in self.displayattrs:
            try:
                value = attr.value(item)
            except (KeyboardInterrupt, SystemExit):
                raise
            except Exception, exc:
                value = exc
            # only store attribute if it exists (or we got an exception)
            if value is not ipipe.noitem:
                # remember alignment, length and colored text
                row[attr] = ipipe.xformat(value, "cell", self.browser.maxattrlength)
        return row

    def calcwidths(self):
        # Recalculate the displayed fields and their widths.
        # ``calcdisplayattrs()'' must have been called and the cache
        # for attributes of the objects on screen (``self.displayrows``)
        # must have been filled. This sets ``self.colwidths`` which maps
        # attribute descriptors to widths.
        self.colwidths = {}
        for row in self.displayrows:
            for attr in self.displayattrs:
                try:
                    length = row[attr][1]
                except KeyError:
                    length = 0
                # always add attribute to colwidths, even if it doesn't exist
                if attr not in self.colwidths:
                    self.colwidths[attr] = len(attr.name())
                newwidth = max(self.colwidths[attr], length)
                self.colwidths[attr] = newwidth

        # How many characters do we need to paint the largest item number?
        self.numbersizex = len(str(self.datastarty+self.mainsizey-1))
        # How must space have we got to display data?
        self.mainsizex = self.browser.scrsizex-self.numbersizex-3
        # width of all columns
        self.datasizex = sum(self.colwidths.itervalues()) + len(self.colwidths)

    def calcdisplayattr(self):
        # Find out which attribute the cursor is on and store this
        # information in ``self.displayattr``.
        pos = 0
        for (i, attr) in enumerate(self.displayattrs):
            if pos+self.colwidths[attr] >= self.curx:
                self.displayattr = (i, attr)
                break
            pos += self.colwidths[attr]+1
        else:
            self.displayattr = (None, ipipe.noitem)

    def moveto(self, x, y, refresh=False):
        # Move the cursor to the position ``(x,y)`` (in data coordinates,
        # not in screen coordinates). If ``refresh`` is true, all cached
        # values will be recalculated (e.g. because the list has been
        # resorted, so screen positions etc. are no longer valid).
        olddatastarty = self.datastarty
        oldx = self.curx
        oldy = self.cury
        x = int(x+0.5)
        y = int(y+0.5)
        newx = x # remember where we wanted to move
        newy = y # remember where we wanted to move

        scrollbordery = min(self.browser.scrollbordery, self.mainsizey//2)
        scrollborderx = min(self.browser.scrollborderx, self.mainsizex//2)

        # Make sure that the cursor didn't leave the main area vertically
        if y < 0:
            y = 0
        # try to get enough items to fill the screen
        self.fetch(max(y+scrollbordery+1, self.mainsizey))
        if y >= len(self.items):
            y = max(0, len(self.items)-1)

        # Make sure that the cursor stays on screen vertically
        if y < self.datastarty+scrollbordery:
            self.datastarty = max(0, y-scrollbordery)
        elif y >= self.datastarty+self.mainsizey-scrollbordery:
            self.datastarty = max(0, min(y-self.mainsizey+scrollbordery+1,
                                         len(self.items)-self.mainsizey))

        if refresh: # Do we need to refresh the complete display?
            self.calcdisplayattrs()
            endy = min(self.datastarty+self.mainsizey, len(self.items))
            self.displayrows = map(self.getrow, xrange(self.datastarty, endy))
            self.calcwidths()
        # Did we scroll vertically => update displayrows
        # and various other attributes
        elif self.datastarty != olddatastarty:
            # Recalculate which attributes we have to display
            olddisplayattrs = self.displayattrs
            self.calcdisplayattrs()
            # If there are new attributes, recreate the cache
            if self.displayattrs != olddisplayattrs:
                endy = min(self.datastarty+self.mainsizey, len(self.items))
                self.displayrows = map(self.getrow, xrange(self.datastarty, endy))
            elif self.datastarty<olddatastarty: # we did scroll up
                # drop rows from the end
                del self.displayrows[self.datastarty-olddatastarty:]
                # fetch new items
                for i in xrange(min(olddatastarty, self.datastarty+self.mainsizey)-1,
                                self.datastarty-1, -1):
                    try:
                        row = self.getrow(i)
                    except IndexError:
                        # we didn't have enough objects to fill the screen
                        break
                    self.displayrows.insert(0, row)
            else: # we did scroll down
                # drop rows from the start
                del self.displayrows[:self.datastarty-olddatastarty]
                # fetch new items
                for i in xrange(max(olddatastarty+self.mainsizey, self.datastarty),
                                self.datastarty+self.mainsizey):
                    try:
                        row = self.getrow(i)
                    except IndexError:
                        # we didn't have enough objects to fill the screen
                        break
                    self.displayrows.append(row)
            self.calcwidths()

        # Make sure that the cursor didn't leave the data area horizontally
        if x < 0:
            x = 0
        elif x >= self.datasizex:
            x = max(0, self.datasizex-1)

        # Make sure that the cursor stays on screen horizontally
        if x < self.datastartx+scrollborderx:
            self.datastartx = max(0, x-scrollborderx)
        elif x >= self.datastartx+self.mainsizex-scrollborderx:
            self.datastartx = max(0, min(x-self.mainsizex+scrollborderx+1,
                                         self.datasizex-self.mainsizex))

        if x == oldx and y == oldy and (x != newx or y != newy): # couldn't move
            self.browser.beep()
        else:
            self.curx = x
            self.cury = y
            self.calcdisplayattr()

    def sort(self, key, reverse=False):
        """
        Sort the currently list of items using the key function ``key``. If
        ``reverse`` is true the sort order is reversed.
        """
        curitem = self.items[self.cury] # Remember where the cursor is now

        # Sort items
        def realkey(item):
            return key(item.item)
        self.items = ipipe.deque(sorted(self.items, key=realkey, reverse=reverse))

        # Find out where the object under the cursor went
        cury = self.cury
        for (i, item) in enumerate(self.items):
            if item is curitem:
                cury = i
                break

        self.moveto(self.curx, cury, refresh=True)

    def refresh(self):
        """
        Restart iterating the input.
        """
        self.iterator = ipipe.xiter(self.input)
        self.items.clear()
        self.exhausted = False
        self.datastartx = self.datastarty = 0
        self.moveto(0, 0, refresh=True)

    def refreshfind(self):
        """
        Restart iterating the input and go back to the same object as before
        (if it can be found in the new iterator).
        """
        try:
            oldobject = self.items[self.cury].item
        except IndexError:
            oldobject = ipipe.noitem
        self.iterator = ipipe.xiter(self.input)
        self.items.clear()
        self.exhausted = False
        while True:
            self.fetch(len(self.items)+1)
            if self.exhausted:
                curses.beep()
                self.datastartx = self.datastarty = 0
                self.moveto(self.curx, 0, refresh=True)
                break
            if self.items[-1].item == oldobject:
                self.datastartx = self.datastarty = 0
                self.moveto(self.curx, len(self.items)-1, refresh=True)
                break


class _CommandInput(object):
    keymap = Keymap()
    keymap.register("left", curses.KEY_LEFT)
    keymap.register("right", curses.KEY_RIGHT)
    keymap.register("home", curses.KEY_HOME, "\x01") # Ctrl-A
    keymap.register("end", curses.KEY_END, "\x05") # Ctrl-E
    # FIXME: What's happening here?
    keymap.register("backspace", curses.KEY_BACKSPACE, "\x08\x7f")
    keymap.register("delete", curses.KEY_DC)
    keymap.register("delend", 0x0b) # Ctrl-K
    keymap.register("execute", "\r\n")
    keymap.register("up", curses.KEY_UP)
    keymap.register("down", curses.KEY_DOWN)
    keymap.register("incsearchup", curses.KEY_PPAGE)
    keymap.register("incsearchdown", curses.KEY_NPAGE)
    keymap.register("exit", "\x18"), # Ctrl-X

    def __init__(self, prompt):
        self.prompt = prompt
        self.history = []
        self.maxhistory = 100
        self.input = ""
        self.curx = 0
        self.cury = -1 # blank line

    def start(self):
        self.input = ""
        self.curx = 0
        self.cury = -1 # blank line

    def handlekey(self, browser, key):
        cmdname = self.keymap.get(key, None)
        if cmdname is not None:
            cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
            if cmdfunc is not None:
                return cmdfunc(browser)
            curses.beep()
        elif key != -1:
            try:
                char = chr(key)
            except ValueError:
                curses.beep()
            else:
                return self.handlechar(browser, char)

    def handlechar(self, browser, char):
        self.input = self.input[:self.curx] + char + self.input[self.curx:]
        self.curx += 1
        return True

    def dohistory(self):
        self.history.insert(0, self.input)
        del self.history[:-self.maxhistory]

    def cmd_backspace(self, browser):
        if self.curx:
            self.input = self.input[:self.curx-1] + self.input[self.curx:]
            self.curx -= 1
            return True
        else:
            curses.beep()

    def cmd_delete(self, browser):
        if self.curx<len(self.input):
            self.input = self.input[:self.curx] + self.input[self.curx+1:]
            return True
        else:
            curses.beep()

    def cmd_delend(self, browser):
        if self.curx<len(self.input):
            self.input = self.input[:self.curx]
            return True

    def cmd_left(self, browser):
        if self.curx:
            self.curx -= 1
            return True
        else:
            curses.beep()

    def cmd_right(self, browser):
        if self.curx < len(self.input):
            self.curx += 1
            return True
        else:
            curses.beep()

    def cmd_home(self, browser):
        if self.curx:
            self.curx = 0
            return True
        else:
            curses.beep()

    def cmd_end(self, browser):
        if self.curx < len(self.input):
            self.curx = len(self.input)
            return True
        else:
            curses.beep()

    def cmd_up(self, browser):
        if self.cury < len(self.history)-1:
            self.cury += 1
            self.input = self.history[self.cury]
            self.curx = len(self.input)
            return True
        else:
            curses.beep()

    def cmd_down(self, browser):
        if self.cury >= 0:
            self.cury -= 1
            if self.cury>=0:
                self.input = self.history[self.cury]
            else:
                self.input = ""
            self.curx = len(self.input)
            return True
        else:
            curses.beep()

    def cmd_incsearchup(self, browser):
        prefix = self.input[:self.curx]
        cury = self.cury
        while True:
            cury += 1
            if cury >= len(self.history):
                break
            if self.history[cury].startswith(prefix):
                self.input = self.history[cury]
                self.cury = cury
                return True
        curses.beep()

    def cmd_incsearchdown(self, browser):
        prefix = self.input[:self.curx]
        cury = self.cury
        while True:
            cury -= 1
            if cury <= 0:
                break
            if self.history[cury].startswith(prefix):
                self.input = self.history[self.cury]
                self.cury = cury
                return True
        curses.beep()

    def cmd_exit(self, browser):
        browser.mode = "default"
        return True

    def cmd_execute(self, browser):
        raise NotImplementedError


class _CommandGoto(_CommandInput):
    def __init__(self):
        _CommandInput.__init__(self, "goto object #")

    def handlechar(self, browser, char):
        # Only accept digits
        if not "0" <= char <= "9":
            curses.beep()
        else:
            return _CommandInput.handlechar(self, browser, char)

    def cmd_execute(self, browser):
        level = browser.levels[-1]
        if self.input:
            self.dohistory()
            level.moveto(level.curx, int(self.input))
        browser.mode = "default"
        return True


class _CommandFind(_CommandInput):
    def __init__(self):
        _CommandInput.__init__(self, "find expression")

    def cmd_execute(self, browser):
        level = browser.levels[-1]
        if self.input:
            self.dohistory()
            while True:
                cury = level.cury
                level.moveto(level.curx, cury+1)
                if cury == level.cury:
                    curses.beep()
                    break # hit end
                item = level.items[level.cury].item
                try:
                    globals = ipipe.getglobals(None)
                    if eval(self.input, globals, ipipe.AttrNamespace(item)):
                        break # found something
                except (KeyboardInterrupt, SystemExit):
                    raise
                except Exception, exc:
                    browser.report(exc)
                    curses.beep()
                    break  # break on error
        browser.mode = "default"
        return True


class _CommandFindBackwards(_CommandInput):
    def __init__(self):
        _CommandInput.__init__(self, "find backwards expression")

    def cmd_execute(self, browser):
        level = browser.levels[-1]
        if self.input:
            self.dohistory()
            while level.cury:
                level.moveto(level.curx, level.cury-1)
                item = level.items[level.cury].item
                try:
                    globals = ipipe.getglobals(None)
                    if eval(self.input, globals, ipipe.AttrNamespace(item)):
                        break # found something
                except (KeyboardInterrupt, SystemExit):
                    raise
                except Exception, exc:
                    browser.report(exc)
                    curses.beep()
                    break # break on error
            else:
                curses.beep()
        browser.mode = "default"
        return True


class ibrowse(ipipe.Display):
    # Show this many lines from the previous screen when paging horizontally
    pageoverlapx = 1

    # Show this many lines from the previous screen when paging vertically
    pageoverlapy = 1

    # Start scrolling when the cursor is less than this number of columns
    # away from the left or right screen edge
    scrollborderx = 10

    # Start scrolling when the cursor is less than this number of lines
    # away from the top or bottom screen edge
    scrollbordery = 5

    # Accelerate by this factor when scrolling horizontally
    acceleratex = 1.05

    # Accelerate by this factor when scrolling vertically
    acceleratey = 1.05

    # The maximum horizontal scroll speed
    # (as a factor of the screen width (i.e. 0.5 == half a screen width)
    maxspeedx = 0.5

    # The maximum vertical scroll speed
    # (as a factor of the screen height (i.e. 0.5 == half a screen height)
    maxspeedy = 0.5

    # The maximum number of header lines for browser level
    # if the nesting is deeper, only the innermost levels are displayed
    maxheaders = 5

    # The approximate maximum length of a column entry
    maxattrlength = 200

    # Styles for various parts of the GUI
    style_objheadertext = astyle.Style.fromstr("white:black:bold|reverse")
    style_objheadernumber = astyle.Style.fromstr("white:blue:bold|reverse")
    style_objheaderobject = astyle.Style.fromstr("white:black:reverse")
    style_colheader = astyle.Style.fromstr("blue:white:reverse")
    style_colheaderhere = astyle.Style.fromstr("green:black:bold|reverse")
    style_colheadersep = astyle.Style.fromstr("blue:black:reverse")
    style_number = astyle.Style.fromstr("blue:white:reverse")
    style_numberhere = astyle.Style.fromstr("green:black:bold|reverse")
    style_sep = astyle.Style.fromstr("blue:black")
    style_data = astyle.Style.fromstr("white:black")
    style_datapad = astyle.Style.fromstr("blue:black:bold")
    style_footer = astyle.Style.fromstr("black:white")
    style_report = astyle.Style.fromstr("white:black")

    # Column separator in header
    headersepchar = "|"

    # Character for padding data cell entries
    datapadchar = "."

    # Column separator in data area
    datasepchar = "|"

    # Character to use for "empty" cell (i.e. for non-existing attributes)
    nodatachar = "-"

    # Prompts for modes that require keyboard input
    prompts = {
        "goto": _CommandGoto(),
        "find": _CommandFind(),
        "findbackwards": _CommandFindBackwards()
    }

    # Maps curses key codes to "function" names
    keymap = Keymap()
    keymap.register("quit", "q")
    keymap.register("up", curses.KEY_UP)
    keymap.register("down", curses.KEY_DOWN)
    keymap.register("pageup", curses.KEY_PPAGE)
    keymap.register("pagedown", curses.KEY_NPAGE)
    keymap.register("left", curses.KEY_LEFT)
    keymap.register("right", curses.KEY_RIGHT)
    keymap.register("home", curses.KEY_HOME, "\x01")
    keymap.register("end", curses.KEY_END, "\x05")
    keymap.register("prevattr", "<\x1b")
    keymap.register("nextattr", ">\t")
    keymap.register("pick", "p")
    keymap.register("pickattr", "P")
    keymap.register("pickallattrs", "C")
    keymap.register("pickmarked", "m")
    keymap.register("pickmarkedattr", "M")
    keymap.register("pickinput", "i")
    keymap.register("pickinputattr", "I")
    keymap.register("hideattr", "h")
    keymap.register("unhideattrs", "H")
    keymap.register("help", "?")
    keymap.register("enter", "\r\n")
    keymap.register("enterattr", "E")
    # FIXME: What's happening here?
    keymap.register("leave", curses.KEY_BACKSPACE, "x\x08\x7f")
    keymap.register("detail", "d")
    keymap.register("detailattr", "D")
    keymap.register("tooglemark", " ")
    keymap.register("markrange", "%")
    keymap.register("sortattrasc", "v")
    keymap.register("sortattrdesc", "V")
    keymap.register("goto", "g")
    keymap.register("find", "f")
    keymap.register("findbackwards", "b")
    keymap.register("refresh", "r")
    keymap.register("refreshfind", "R")

    def __init__(self, input=None, *attrs):
        """
        Create a new browser. If ``attrs`` is not empty, it is the list
        of attributes that will be displayed in the browser, otherwise
        these will be determined by the objects on screen.
        """
        ipipe.Display.__init__(self, input)

        self.attrs = attrs

        # Stack of browser levels
        self.levels = []
        # how many colums to scroll (Changes when accelerating)
        self.stepx = 1.

        # how many rows to scroll (Changes when accelerating)
        self.stepy = 1.

        # Beep on the edges of the data area? (Will be set to ``False``
        # once the cursor hits the edge of the screen, so we don't get
        # multiple beeps).
        self._dobeep = True

        # Cache for registered ``curses`` colors and styles.
        self._styles = {}
        self._colors = {}
        self._maxcolor = 1

        # How many header lines do we want to paint (the numbers of levels
        # we have, but with an upper bound)
        self._headerlines = 1

        # Index of first header line
        self._firstheaderline = 0

        # curses window
        self.scr = None
        # report in the footer line (error, executed command etc.)
        self._report = None

        # value to be returned to the caller (set by commands)
        self.returnvalue = None

        # The mode the browser is in
        # e.g. normal browsing or entering an argument for a command
        self.mode = "default"

        # set by the SIGWINCH signal handler
        self.resized = False

    def nextstepx(self, step):
        """
        Accelerate horizontally.
        """
        return max(1., min(step*self.acceleratex,
                           self.maxspeedx*self.levels[-1].mainsizex))

    def nextstepy(self, step):
        """
        Accelerate vertically.
        """
        return max(1., min(step*self.acceleratey,
                           self.maxspeedy*self.levels[-1].mainsizey))

    def getstyle(self, style):
        """
        Register the ``style`` with ``curses`` or get it from the cache,
        if it has been registered before.
        """
        try:
            return self._styles[style.fg, style.bg, style.attrs]
        except KeyError:
            attrs = 0
            for b in astyle.A2CURSES:
                if style.attrs & b:
                    attrs |= astyle.A2CURSES[b]
            try:
                color = self._colors[style.fg, style.bg]
            except KeyError:
                curses.init_pair(
                    self._maxcolor,
                    astyle.COLOR2CURSES[style.fg],
                    astyle.COLOR2CURSES[style.bg]
                )
                color = curses.color_pair(self._maxcolor)
                self._colors[style.fg, style.bg] = color
                self._maxcolor += 1
            c = color | attrs
            self._styles[style.fg, style.bg, style.attrs] = c
            return c

    def addstr(self, y, x, begx, endx, text, style):
        """
        A version of ``curses.addstr()`` that can handle ``x`` coordinates
        that are outside the screen.
        """
        text2 = text[max(0, begx-x):max(0, endx-x)]
        if text2:
            self.scr.addstr(y, max(x, begx), text2, self.getstyle(style))
        return len(text)

    def addchr(self, y, x, begx, endx, c, l, style):
        x0 = max(x, begx)
        x1 = min(x+l, endx)
        if x1>x0:
            self.scr.addstr(y, x0, c*(x1-x0), self.getstyle(style))
        return l

    def _calcheaderlines(self, levels):
        # Calculate how many headerlines do we have to display, if we have
        # ``levels`` browser levels
        if levels is None:
            levels = len(self.levels)
        self._headerlines = min(self.maxheaders, levels)
        self._firstheaderline = levels-self._headerlines

    def getstylehere(self, style):
        """
        Return a style for displaying the original style ``style``
        in the row the cursor is on.
        """
        return astyle.Style(style.fg, astyle.COLOR_BLUE, style.attrs | astyle.A_BOLD)

    def report(self, msg):
        """
        Store the message ``msg`` for display below the footer line. This
        will be displayed as soon as the screen is redrawn.
        """
        self._report = msg

    def enter(self, item, *attrs):
        """
        Enter the object ``item``. If ``attrs`` is specified, it will be used
        as a fixed list of attributes to display.
        """
        if self.levels and item is self.levels[-1].input:
            curses.beep()
            self.report(CommandError("Recursion on input object"))
        else:
            oldlevels = len(self.levels)
            self._calcheaderlines(oldlevels+1)
            try:
                level = _BrowserLevel(
                    self,
                    item,
                    self.scrsizey-1-self._headerlines-2,
                    *attrs
                )
            except (KeyboardInterrupt, SystemExit):
                raise
            except Exception, exc:
                if not self.levels:
                    raise
                self._calcheaderlines(oldlevels)
                curses.beep()
                self.report(exc)
            else:
                self.levels.append(level)

    def startkeyboardinput(self, mode):
        """
        Enter mode ``mode``, which requires keyboard input.
        """
        self.mode = mode
        self.prompts[mode].start()

    def keylabel(self, keycode):
        """
        Return a pretty name for the ``curses`` key ``keycode`` (used in the
        help screen and in reports about unassigned keys).
        """
        if keycode <= 0xff:
            specialsnames = {
                ord("\n"): "RETURN",
                ord(" "): "SPACE",
                ord("\t"): "TAB",
                ord("\x7f"): "DELETE",
                ord("\x08"): "BACKSPACE",
            }
            if keycode in specialsnames:
                return specialsnames[keycode]
            elif 0x00 < keycode < 0x20:
                return "CTRL-%s" % chr(keycode + 64)
            return repr(chr(keycode))
        for name in dir(curses):
            if name.startswith("KEY_") and getattr(curses, name) == keycode:
                return name
        return str(keycode)

    def beep(self, force=False):
        if force or self._dobeep:
            curses.beep()
            # don't beep again (as long as the same key is pressed)
            self._dobeep = False

    def cmd_up(self):
        """
        Move the cursor to the previous row.
        """
        level = self.levels[-1]
        self.report("up")
        level.moveto(level.curx, level.cury-self.stepy)

    def cmd_down(self):
        """
        Move the cursor to the next row.
        """
        level = self.levels[-1]
        self.report("down")
        level.moveto(level.curx, level.cury+self.stepy)

    def cmd_pageup(self):
        """
        Move the cursor up one page.
        """
        level = self.levels[-1]
        self.report("page up")
        level.moveto(level.curx, level.cury-level.mainsizey+self.pageoverlapy)

    def cmd_pagedown(self):
        """
        Move the cursor down one page.
        """
        level = self.levels[-1]
        self.report("page down")
        level.moveto(level.curx, level.cury+level.mainsizey-self.pageoverlapy)

    def cmd_left(self):
        """
        Move the cursor left.
        """
        level = self.levels[-1]
        self.report("left")
        level.moveto(level.curx-self.stepx, level.cury)

    def cmd_right(self):
        """
        Move the cursor right.
        """
        level = self.levels[-1]
        self.report("right")
        level.moveto(level.curx+self.stepx, level.cury)

    def cmd_home(self):
        """
        Move the cursor to the first column.
        """
        level = self.levels[-1]
        self.report("home")
        level.moveto(0, level.cury)

    def cmd_end(self):
        """
        Move the cursor to the last column.
        """
        level = self.levels[-1]
        self.report("end")
        level.moveto(level.datasizex+level.mainsizey-self.pageoverlapx, level.cury)

    def cmd_prevattr(self):
        """
        Move the cursor one attribute column to the left.
        """
        level = self.levels[-1]
        if level.displayattr[0] is None or level.displayattr[0] == 0:
            self.beep()
        else:
            self.report("prevattr")
            pos = 0
            for (i, attrname) in enumerate(level.displayattrs):
                if i == level.displayattr[0]-1:
                    break
                pos += level.colwidths[attrname] + 1
            level.moveto(pos, level.cury)

    def cmd_nextattr(self):
        """
        Move the cursor one attribute column to the right.
        """
        level = self.levels[-1]
        if level.displayattr[0] is None or level.displayattr[0] == len(level.displayattrs)-1:
            self.beep()
        else:
            self.report("nextattr")
            pos = 0
            for (i, attrname) in enumerate(level.displayattrs):
                if i == level.displayattr[0]+1:
                    break
                pos += level.colwidths[attrname] + 1
            level.moveto(pos, level.cury)

    def cmd_pick(self):
        """
        'Pick' the object under the cursor (i.e. the row the cursor is on).
        This leaves the browser and returns the picked object to the caller.
        (In IPython this object will be available as the ``_`` variable.)
        """
        level = self.levels[-1]
        self.returnvalue = level.items[level.cury].item
        return True

    def cmd_pickattr(self):
        """
        'Pick' the attribute under the cursor (i.e. the row/column the
        cursor is on).
        """
        level = self.levels[-1]
        attr = level.displayattr[1]
        if attr is ipipe.noitem:
            curses.beep()
            self.report(CommandError("no column under cursor"))
            return
        value = attr.value(level.items[level.cury].item)
        if value is ipipe.noitem:
            curses.beep()
            self.report(AttributeError(attr.name()))
        else:
            self.returnvalue = value
            return True

    def cmd_pickallattrs(self):
        """
        Pick' the complete column under the cursor (i.e. the attribute under
        the cursor) from all currently fetched objects. These attributes
        will be returned as a list.
        """
        level = self.levels[-1]
        attr = level.displayattr[1]
        if attr is ipipe.noitem:
            curses.beep()
            self.report(CommandError("no column under cursor"))
            return
        result = []
        for cache in level.items:
            value = attr.value(cache.item)
            if value is not ipipe.noitem:
                result.append(value)
        self.returnvalue = result
        return True

    def cmd_pickmarked(self):
        """
        'Pick' marked objects. Marked objects will be returned as a list.
        """
        level = self.levels[-1]
        self.returnvalue = [cache.item for cache in level.items if cache.marked]
        return True

    def cmd_pickmarkedattr(self):
        """
        'Pick' the attribute under the cursor from all marked objects
        (This returns a list).
        """

        level = self.levels[-1]
        attr = level.displayattr[1]
        if attr is ipipe.noitem:
            curses.beep()
            self.report(CommandError("no column under cursor"))
            return
        result = []
        for cache in level.items:
            if cache.marked:
                value = attr.value(cache.item)
                if value is not ipipe.noitem:
                    result.append(value)
        self.returnvalue = result
        return True

    def cmd_pickinput(self):
        """
        Use the object under the cursor (i.e. the row the cursor is on) as
        the next input line. This leaves the browser and puts the picked object
        in the input.
        """
        level = self.levels[-1]
        value = level.items[level.cury].item
        self.returnvalue = None
        api = ipapi.get()
        api.set_next_input(str(value))
        return True

    def cmd_pickinputattr(self):
        """
        Use the attribute under the cursor i.e. the row/column the cursor is on)
        as the next input line. This leaves the browser and puts the picked
        object in the input.
        """
        level = self.levels[-1]
        attr = level.displayattr[1]
        if attr is ipipe.noitem:
            curses.beep()
            self.report(CommandError("no column under cursor"))
            return
        value = attr.value(level.items[level.cury].item)
        if value is ipipe.noitem:
            curses.beep()
            self.report(AttributeError(attr.name()))
        self.returnvalue = None
        api = ipapi.get()
        api.set_next_input(str(value))
        return True

    def cmd_markrange(self):
        """
        Mark all objects from the last marked object before the current cursor
        position to the cursor position.
        """
        level = self.levels[-1]
        self.report("markrange")
        start = None
        if level.items:
            for i in xrange(level.cury, -1, -1):
                if level.items[i].marked:
                    start = i
                    break
        if start is None:
            self.report(CommandError("no mark before cursor"))
            curses.beep()
        else:
            for i in xrange(start, level.cury+1):
                cache = level.items[i]
                if not cache.marked:
                    cache.marked = True
                    level.marked += 1

    def cmd_enter(self):
        """
        Enter the object under the cursor. (what this mean depends on the object
        itself (i.e. how it implements iteration). This opens a new browser 'level'.
        """
        level = self.levels[-1]
        try:
            item = level.items[level.cury].item
        except IndexError:
            self.report(CommandError("No object"))
            curses.beep()
        else:
            self.report("entering object...")
            self.enter(item)

    def cmd_leave(self):
        """
        Leave the current browser level and go back to the previous one.
        """
        self.report("leave")
        if len(self.levels) > 1:
            self._calcheaderlines(len(self.levels)-1)
            self.levels.pop(-1)
        else:
            self.report(CommandError("This is the last level"))
            curses.beep()

    def cmd_enterattr(self):
        """
        Enter the attribute under the cursor.
        """
        level = self.levels[-1]
        attr = level.displayattr[1]
        if attr is ipipe.noitem:
            curses.beep()
            self.report(CommandError("no column under cursor"))
            return
        try:
            item = level.items[level.cury].item
        except IndexError:
            self.report(CommandError("No object"))
            curses.beep()
        else:
            value = attr.value(item)
            name = attr.name()
            if value is ipipe.noitem:
                self.report(AttributeError(name))
            else:
                self.report("entering object attribute %s..." % name)
                self.enter(value)

    def cmd_detail(self):
        """
        Show a detail view of the object under the cursor. This shows the
        name, type, doc string and value of the object attributes (and it
        might show more attributes than in the list view, depending on
        the object).
        """
        level = self.levels[-1]
        try:
            item = level.items[level.cury].item
        except IndexError:
            self.report(CommandError("No object"))
            curses.beep()
        else:
            self.report("entering detail view for object...")
            attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
            self.enter(attrs)

    def cmd_detailattr(self):
        """
        Show a detail view of the attribute under the cursor.
        """
        level = self.levels[-1]
        attr = level.displayattr[1]
        if attr is ipipe.noitem:
            curses.beep()
            self.report(CommandError("no attribute"))
            return
        try:
            item = level.items[level.cury].item
        except IndexError:
            self.report(CommandError("No object"))
            curses.beep()
        else:
            try:
                item = attr.value(item)
            except (KeyboardInterrupt, SystemExit):
                raise
            except Exception, exc:
                self.report(exc)
            else:
                self.report("entering detail view for attribute %s..." % attr.name())
                attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
                self.enter(attrs)

    def cmd_tooglemark(self):
        """
        Mark/unmark the object under the cursor. Marked objects have a '!'
        after the row number).
        """
        level = self.levels[-1]
        self.report("toggle mark")
        try:
            item = level.items[level.cury]
        except IndexError: # no items?
            pass
        else:
            if item.marked:
                item.marked = False
                level.marked -= 1
            else:
                item.marked = True
                level.marked += 1

    def cmd_sortattrasc(self):
        """
        Sort the objects (in ascending order) using the attribute under
        the cursor as the sort key.
        """
        level = self.levels[-1]
        attr = level.displayattr[1]
        if attr is ipipe.noitem:
            curses.beep()
            self.report(CommandError("no column under cursor"))
            return
        self.report("sort by %s (ascending)" % attr.name())
        def key(item):
            try:
                return attr.value(item)
            except (KeyboardInterrupt, SystemExit):
                raise
            except Exception:
                return None
        level.sort(key)

    def cmd_sortattrdesc(self):
        """
        Sort the objects (in descending order) using the attribute under
        the cursor as the sort key.
        """
        level = self.levels[-1]
        attr = level.displayattr[1]
        if attr is ipipe.noitem:
            curses.beep()
            self.report(CommandError("no column under cursor"))
            return
        self.report("sort by %s (descending)" % attr.name())
        def key(item):
            try:
                return attr.value(item)
            except (KeyboardInterrupt, SystemExit):
                raise
            except Exception:
                return None
        level.sort(key, reverse=True)

    def cmd_hideattr(self):
        """
        Hide the attribute under the cursor.
        """
        level = self.levels[-1]
        if level.displayattr[0] is None:
            self.beep()
        else:
            self.report("hideattr")
            level.hiddenattrs.add(level.displayattr[1])
            level.moveto(level.curx, level.cury, refresh=True)

    def cmd_unhideattrs(self):
        """
        Make all attributes visible again.
        """
        level = self.levels[-1]
        self.report("unhideattrs")
        level.hiddenattrs.clear()
        level.moveto(level.curx, level.cury, refresh=True)

    def cmd_goto(self):
        """
        Jump to a row. The row number can be entered at the
        bottom of the screen.
        """
        self.startkeyboardinput("goto")

    def cmd_find(self):
        """
        Search forward for a row. The search condition can be entered at the
        bottom of the screen.
        """
        self.startkeyboardinput("find")

    def cmd_findbackwards(self):
        """
        Search backward for a row. The search condition can be entered at the
        bottom of the screen.
        """
        self.startkeyboardinput("findbackwards")

    def cmd_refresh(self):
        """
        Refreshes the display by restarting the iterator.
        """
        level = self.levels[-1]
        self.report("refresh")
        level.refresh()

    def cmd_refreshfind(self):
        """
        Refreshes the display by restarting the iterator and goes back to the
        same object the cursor was on before restarting (if this object can't be
        found the cursor jumps back to the first object).
        """
        level = self.levels[-1]
        self.report("refreshfind")
        level.refreshfind()

    def cmd_help(self):
        """
        Opens the help screen as a new browser level, describing keyboard
        shortcuts.
        """
        for level in self.levels:
            if isinstance(level.input, _BrowserHelp):
                curses.beep()
                self.report(CommandError("help already active"))
                return

        self.enter(_BrowserHelp(self))

    def cmd_quit(self):
        """
        Quit the browser and return to the IPython prompt.
        """
        self.returnvalue = None
        return True

    def sigwinchhandler(self, signal, frame):
        self.resized = True

    def _dodisplay(self, scr):
        """
        This method is the workhorse of the browser. It handles screen
        drawing and the keyboard.
        """
        self.scr = scr
        curses.halfdelay(1)
        footery = 2

        keys = []
        for cmd in ("quit", "help"):
            key = self.keymap.findkey(cmd, None)
            if key is not None:
                keys.append("%s=%s" % (self.keylabel(key), cmd))
        helpmsg = " | %s" % " ".join(keys)

        scr.clear()
        msg = "Fetching first batch of objects..."
        (self.scrsizey, self.scrsizex) = scr.getmaxyx()
        scr.addstr(self.scrsizey//2, (self.scrsizex-len(msg))//2, msg)
        scr.refresh()

        lastc = -1

        self.levels = []
        # enter the first level
        self.enter(self.input, *self.attrs)

        self._calcheaderlines(None)

        while True:
            level = self.levels[-1]
            (self.scrsizey, self.scrsizex) = scr.getmaxyx()
            level.mainsizey = self.scrsizey-1-self._headerlines-footery

            # Paint object header
            for i in xrange(self._firstheaderline, self._firstheaderline+self._headerlines):
                lv = self.levels[i]
                posx = 0
                posy = i-self._firstheaderline
                endx = self.scrsizex
                if i: # not the first level
                    msg = " (%d/%d" % (self.levels[i-1].cury, len(self.levels[i-1].items))
                    if not self.levels[i-1].exhausted:
                        msg += "+"
                    msg += ") "
                    endx -= len(msg)+1
                posx += self.addstr(posy, posx, 0, endx, " ibrowse #%d: " % i, self.style_objheadertext)
                for (style, text) in lv.header:
                    posx += self.addstr(posy, posx, 0, endx, text, self.style_objheaderobject)
                    if posx >= endx:
                        break
                if i:
                    posx += self.addstr(posy, posx, 0, self.scrsizex, msg, self.style_objheadernumber)
                posx += self.addchr(posy, posx, 0, self.scrsizex, " ", self.scrsizex-posx, self.style_objheadernumber)

            if not level.items:
                self.addchr(self._headerlines, 0, 0, self.scrsizex, " ", self.scrsizex, self.style_colheader)
                self.addstr(self._headerlines+1, 0, 0, self.scrsizex, " <empty>", astyle.style_error)
                scr.clrtobot()
            else:
                # Paint column headers
                scr.move(self._headerlines, 0)
                scr.addstr(" %*s " % (level.numbersizex, "#"), self.getstyle(self.style_colheader))
                scr.addstr(self.headersepchar, self.getstyle(self.style_colheadersep))
                begx = level.numbersizex+3
                posx = begx-level.datastartx
                for attr in level.displayattrs:
                    attrname = attr.name()
                    cwidth = level.colwidths[attr]
                    header = attrname.ljust(cwidth)
                    if attr is level.displayattr[1]:
                        style = self.style_colheaderhere
                    else:
                        style = self.style_colheader
                    posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, header, style)
                    posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, self.headersepchar, self.style_colheadersep)
                    if posx >= self.scrsizex:
                        break
                else:
                    scr.addstr(" "*(self.scrsizex-posx), self.getstyle(self.style_colheader))

                # Paint rows
                posy = self._headerlines+1+level.datastarty
                for i in xrange(level.datastarty, min(level.datastarty+level.mainsizey, len(level.items))):
                    cache = level.items[i]
                    if i == level.cury:
                        style = self.style_numberhere
                    else:
                        style = self.style_number

                    posy = self._headerlines+1+i-level.datastarty
                    posx = begx-level.datastartx

                    scr.move(posy, 0)
                    scr.addstr(" %*d%s" % (level.numbersizex, i, " !"[cache.marked]), self.getstyle(style))
                    scr.addstr(self.headersepchar, self.getstyle(self.style_sep))

                    for attrname in level.displayattrs:
                        cwidth = level.colwidths[attrname]
                        try:
                            (align, length, parts) = level.displayrows[i-level.datastarty][attrname]
                        except KeyError:
                            align = 2
                            style = astyle.style_nodata
                            if i == level.cury:
                                style = self.getstylehere(style)
                        padstyle = self.style_datapad
                        sepstyle = self.style_sep
                        if i == level.cury:
                            padstyle = self.getstylehere(padstyle)
                            sepstyle = self.getstylehere(sepstyle)
                        if align == 2:
                            posx += self.addchr(posy, posx, begx, self.scrsizex, self.nodatachar, cwidth, style)
                        else:
                            if align == 1:
                                posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
                            elif align == 0:
                                pad1 = (cwidth-length)//2
                                pad2 = cwidth-length-len(pad1)
                                posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad1, padstyle)
                            for (style, text) in parts:
                                if i == level.cury:
                                    style = self.getstylehere(style)
                                posx += self.addstr(posy, posx, begx, self.scrsizex, text, style)
                                if posx >= self.scrsizex:
                                    break
                            if align == -1:
                                posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
                            elif align == 0:
                                posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad2, padstyle)
                        posx += self.addstr(posy, posx, begx, self.scrsizex, self.datasepchar, sepstyle)
                    else:
                        scr.clrtoeol()

                # Add blank row headers for the rest of the screen
                for posy in xrange(posy+1, self.scrsizey-2):
                    scr.addstr(posy, 0, " " * (level.numbersizex+2), self.getstyle(self.style_colheader))
                    scr.clrtoeol()

            posy = self.scrsizey-footery
            # Display footer
            scr.addstr(posy, 0, " "*self.scrsizex, self.getstyle(self.style_footer))

            if level.exhausted:
                flag = ""
            else:
                flag = "+"

            endx = self.scrsizex-len(helpmsg)-1
            scr.addstr(posy, endx, helpmsg, self.getstyle(self.style_footer))

            posx = 0
            msg = " %d%s objects (%d marked): " % (len(level.items), flag, level.marked)
            posx += self.addstr(posy, posx, 0, endx, msg, self.style_footer)
            try:
                item = level.items[level.cury].item
            except IndexError: # empty
                pass
            else:
                for (nostyle, text) in ipipe.xrepr(item, "footer"):
                    if not isinstance(nostyle, int):
                        posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
                        if posx >= endx:
                            break

                attrstyle = [(astyle.style_default, "no attribute")]
                attr = level.displayattr[1]
                if attr is not ipipe.noitem and not isinstance(attr, ipipe.SelfDescriptor):
                    posx += self.addstr(posy, posx, 0, endx, " | ", self.style_footer)
                    posx += self.addstr(posy, posx, 0, endx, attr.name(), self.style_footer)
                    posx += self.addstr(posy, posx, 0, endx, ": ", self.style_footer)
                    try:
                        value = attr.value(item)
                    except (SystemExit, KeyboardInterrupt):
                        raise
                    except Exception, exc:
                        value = exc
                    if value is not ipipe.noitem:
                        attrstyle = ipipe.xrepr(value, "footer")
                    for (nostyle, text) in attrstyle:
                        if not isinstance(nostyle, int):
                            posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
                            if posx >= endx:
                                break

            try:
                # Display input prompt
                if self.mode in self.prompts:
                    history = self.prompts[self.mode]
                    posx = 0
                    posy = self.scrsizey-1
                    posx += self.addstr(posy, posx, 0, endx, history.prompt, astyle.style_default)
                    posx += self.addstr(posy, posx, 0, endx, " [", astyle.style_default)
                    if history.cury==-1:
                        text = "new"
                    else:
                        text = str(history.cury+1)
                    posx += self.addstr(posy, posx, 0, endx, text, astyle.style_type_number)
                    if history.history:
                        posx += self.addstr(posy, posx, 0, endx, "/", astyle.style_default)
                        posx += self.addstr(posy, posx, 0, endx, str(len(history.history)), astyle.style_type_number)
                    posx += self.addstr(posy, posx, 0, endx, "]: ", astyle.style_default)
                    inputstartx = posx
                    posx += self.addstr(posy, posx, 0, endx, history.input, astyle.style_default)
                # Display report
                else:
                    if self._report is not None:
                        if isinstance(self._report, Exception):
                            style = self.getstyle(astyle.style_error)
                            if self._report.__class__.__module__ == "exceptions":
                                msg = "%s: %s" % \
                                      (self._report.__class__.__name__, self._report)
                            else:
                                msg = "%s.%s: %s" % \
                                      (self._report.__class__.__module__,
                                       self._report.__class__.__name__, self._report)
                        else:
                            style = self.getstyle(self.style_report)
                            msg = self._report
                        scr.addstr(self.scrsizey-1, 0, msg[:self.scrsizex], style)
                        self._report = None
                    else:
                        scr.move(self.scrsizey-1, 0)
            except curses.error:
                # Protect against errors from writing to the last line
                pass
            scr.clrtoeol()

            # Position cursor
            if self.mode in self.prompts:
                history = self.prompts[self.mode]
                scr.move(self.scrsizey-1, inputstartx+history.curx)
            else:
                scr.move(
                    1+self._headerlines+level.cury-level.datastarty,
                    level.numbersizex+3+level.curx-level.datastartx
                )
            scr.refresh()

            # Check keyboard
            while True:
                c = scr.getch()
                if self.resized:
                    size = fcntl.ioctl(0, tty.TIOCGWINSZ, "12345678")
                    size = struct.unpack("4H", size)
                    oldsize = scr.getmaxyx()
                    scr.erase()
                    curses.resize_term(size[0], size[1])
                    newsize = scr.getmaxyx()
                    scr.erase()
                    for l in self.levels:
                        l.mainsizey += newsize[0]-oldsize[0]
                        l.moveto(l.curx, l.cury, refresh=True)
                    scr.refresh()
                    self.resized = False
                    break # Redisplay
                if self.mode in self.prompts:
                    if self.prompts[self.mode].handlekey(self, c):
                       break # Redisplay
                else:
                    # if no key is pressed slow down and beep again
                    if c == -1:
                        self.stepx = 1.
                        self.stepy = 1.
                        self._dobeep = True
                    else:
                        # if a different key was pressed slow down and beep too
                        if c != lastc:
                            lastc = c
                            self.stepx = 1.
                            self.stepy = 1.
                            self._dobeep = True
                        cmdname = self.keymap.get(c, None)
                        if cmdname is None:
                            self.report(
                                UnassignedKeyError("Unassigned key %s" %
                                                   self.keylabel(c)))
                        else:
                            cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
                            if cmdfunc is None:
                                self.report(
                                    UnknownCommandError("Unknown command %r" %
                                                        (cmdname,)))
                            elif cmdfunc():
                                returnvalue = self.returnvalue
                                self.returnvalue = None
                                return returnvalue
                        self.stepx = self.nextstepx(self.stepx)
                        self.stepy = self.nextstepy(self.stepy)
                        curses.flushinp() # get rid of type ahead
                        break # Redisplay
        self.scr = None

    def display(self):
        if hasattr(curses, "resize_term") and hasattr(signal, 'SIGWINCH'):
            oldhandler = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
            try:
                return curses.wrapper(self._dodisplay)
            finally:
                signal.signal(signal.SIGWINCH, oldhandler)
        else:
            return curses.wrapper(self._dodisplay)
