diff --git a/IPython/Extensions/ibrowse.py b/IPython/Extensions/ibrowse.py new file mode 100644 index 0000000..03286f7 --- /dev/null +++ b/IPython/Extensions/ibrowse.py @@ -0,0 +1,1413 @@ +# -*- coding: iso-8859-1 -*- + +import curses + +import astyle, ipipe + + +_ibrowse_help = """ +down +Move the cursor to the next line. + +up +Move the cursor to the previous line. + +pagedown +Move the cursor down one page (minus overlap). + +pageup +Move the cursor up one page (minus overlap). + +left +Move the cursor left. + +right +Move the cursor right. + +home +Move the cursor to the first column. + +end +Move the cursor to the last column. + +prevattr +Move the cursor one attribute column to the left. + +nextattr +Move the cursor one attribute column to the right. + +pick +'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.) + +pickattr +'Pick' the attribute under the cursor (i.e. the row/column the cursor is on). + +pickallattrs +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. + +tooglemark +Mark/unmark the object under the cursor. Marked objects have a '!' after the +row number). + +pickmarked +'Pick' marked objects. Marked objects will be returned as a list. + +pickmarkedattr +'Pick' the attribute under the cursor from all marked objects (This returns a +list). + +enterdefault +Enter the object under the cursor. (what this mean depends on the object +itself (i.e. how it implements the '__xiter__' method). This opens a new +browser 'level'. + +enter +Enter the object under the cursor. If the object provides different enter +modes a menu of all modes will be presented; choose one and enter it (via the +'enter' or 'enterdefault' command). + +enterattr +Enter the attribute under the cursor. + +leave +Leave the current browser level and go back to the previous one. + +detail +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). + +detailattr +Show a detail view of the attribute under the cursor. + +markrange +Mark all objects from the last marked object before the current cursor +position to the cursor position. + +sortattrasc +Sort the objects (in ascending order) using the attribute under the cursor as +the sort key. + +sortattrdesc +Sort the objects (in descending order) using the attribute under the cursor as +the sort key. + +goto +Jump to a row. The row number can be entered at the bottom of the screen. + +find +Search forward for a row. At the bottom of the screen the condition can be +entered. + +findbackwards +Search backward for a row. At the bottom of the screen the condition can be +entered. + +help +This screen. +""" + + +class UnassignedKeyError(Exception): + """ + Exception that is used for reporting unassigned keys. + """ + + +class UnknownCommandError(Exception): + """ + Exception that is used for reporting unknown command (this should never + happen). + """ + + +class CommandError(Exception): + """ + Exception that is used for reporting that a command can't be executed. + """ + + +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("red:blacK") + # 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 __xiter__(self, mode): + # Get reverse key mapping + allkeys = {} + for (key, cmd) in self.browser.keymap.iteritems(): + allkeys.setdefault(cmd, []).append(key) + + fields = ("key", "description") + + for (i, command) in enumerate(_ibrowse_help.strip().split("\n\n")): + if i: + yield Fields(fields, key="", description="") + + (name, description) = command.split("\n", 1) + keys = allkeys.get(name, []) + lines = textwrap.wrap(description, 60) + + yield Fields(fields, 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 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, iterator, 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 = iterator + + # 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 + + # Attribute names to display (in this order) + self.displayattrs = [] + + # index and name of attribute under the cursor + self.displayattr = (None, ipipe._default) + + # Maps attribute names to column widths + self.colwidths = {} + + self.fetch(mainsizey) + self.calcdisplayattrs() + # formatted attributes for the items on screen + # (i.e. self.items[self.datastarty:self.datastarty+self.mainsizey]) + self.displayrows = [self.getrow(i) for i in xrange(len(self.items))] + self.calcwidths() + self.calcdisplayattr() + + 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 + 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``) + attrnames = set() + # If the browser object specifies a fixed list of attributes, + # simply use it. + if self.attrs: + self.displayattrs = self.attrs + else: + self.displayattrs = [] + endy = min(self.datastarty+self.mainsizey, len(self.items)) + for i in xrange(self.datastarty, endy): + for attrname in ipipe.xattrs(self.items[i].item, "default"): + if attrname not in attrnames: + self.displayattrs.append(attrname) + attrnames.add(attrname) + + def getrow(self, i): + # Return a dictinary 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 attrname in self.displayattrs: + try: + value = ipipe._getattr(item, attrname, ipipe._default) + 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._default: + parts = [] + totallength = 0 + align = None + full = False + # Collect parts until we have enough + for part in ipipe.xrepr(value, "cell"): + # part gives (alignment, stop) + # instead of (style, text) + if isinstance(part[0], int): + # only consider the first occurence + if align is None: + align = part[0] + full = part[1] + else: + parts.append(part) + totallength += len(part[1]) + if totallength >= self.browser.maxattrlength and not full: + parts.append((astyle.style_ellisis, "...")) + totallength += 3 + break + # remember alignment, length and colored parts + row[attrname] = (align, totallength, parts) + return row + + def calcwidths(self): + # Recalculate the displayed fields and their width. + # ``calcdisplayattrs()'' must have been called and the cache + # for attributes of the objects on screen (``self.displayrows``) + # must have been filled. This returns a dictionary mapping + # colmn names to width. + self.colwidths = {} + for row in self.displayrows: + for attrname in self.displayattrs: + try: + length = row[attrname][1] + except KeyError: + length = 0 + # always add attribute to colwidths, even if it doesn't exist + if attrname not in self.colwidths: + self.colwidths[attrname] = len(ipipe._attrname(attrname)) + newwidth = max(self.colwidths[attrname], length) + self.colwidths[attrname] = newwidth + + # How many characters do we need to paint the 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 on which attribute the cursor is on and store this + # information in ``self.displayattr``. + pos = 0 + for (i, attrname) in enumerate(self.displayattrs): + if pos+self.colwidths[attrname] >= self.curx: + self.displayattr = (i, attrname) + break + pos += self.colwidths[attrname]+1 + else: + self.displayattr = (None, ipipe._default) + + 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 + self.fetch(y+scrollbordery+1) # try to get more items + 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= 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) + + +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": "goto object #: ", + "find": "find expression: ", + "findbackwards": "find backwards expression: " + } + + # Maps curses key codes to "function" names + keymap = { + ord("q"): "quit", + curses.KEY_UP: "up", + curses.KEY_DOWN: "down", + curses.KEY_PPAGE: "pageup", + curses.KEY_NPAGE: "pagedown", + curses.KEY_LEFT: "left", + curses.KEY_RIGHT: "right", + curses.KEY_HOME: "home", + curses.KEY_END: "end", + ord("<"): "prevattr", + 0x1b: "prevattr", # SHIFT-TAB + ord(">"): "nextattr", + ord("\t"):"nextattr", # TAB + ord("p"): "pick", + ord("P"): "pickattr", + ord("C"): "pickallattrs", + ord("m"): "pickmarked", + ord("M"): "pickmarkedattr", + ord("\n"): "enterdefault", + # FIXME: What's happening here? + 8: "leave", + 127: "leave", + curses.KEY_BACKSPACE: "leave", + ord("x"): "leave", + ord("h"): "help", + ord("e"): "enter", + ord("E"): "enterattr", + ord("d"): "detail", + ord("D"): "detailattr", + ord(" "): "tooglemark", + ord("r"): "markrange", + ord("v"): "sortattrasc", + ord("V"): "sortattrdesc", + ord("g"): "goto", + ord("f"): "find", + ord("b"): "findbackwards", + } + + def __init__(self, *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. + """ + 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" + + # The partially entered row number for the goto command + self.goto = "" + + 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, style.bg, 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, mode, *attrs): + """ + Enter the object ``item`` in the mode ``mode``. If ``attrs`` is + specified, it will be used as a fixed list of attributes to display. + """ + try: + iterator = ipipe.xiter(item, mode) + except (KeyboardInterrupt, SystemExit): + raise + except Exception, exc: + curses.beep() + self.report(exc) + else: + self._calcheaderlines(len(self.levels)+1) + level = _BrowserLevel( + self, + item, + iterator, + self.scrsizey-1-self._headerlines-2, + *attrs + ) + self.levels.append(level) + + def startkeyboardinput(self, mode): + """ + Enter mode ``mode``, which requires keyboard input. + """ + self.mode = mode + self.keyboardinput = "" + self.cursorpos = 0 + + def executekeyboardinput(self, mode): + exe = getattr(self, "exe_%s" % mode, None) + if exe is not None: + exe() + self.mode = "default" + + 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] + 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_quit(self): + self.returnvalue = None + return True + + def cmd_up(self): + level = self.levels[-1] + self.report("up") + level.moveto(level.curx, level.cury-self.stepy) + + def cmd_down(self): + level = self.levels[-1] + self.report("down") + level.moveto(level.curx, level.cury+self.stepy) + + def cmd_pageup(self): + level = self.levels[-1] + self.report("page up") + level.moveto(level.curx, level.cury-level.mainsizey+self.pageoverlapy) + + def cmd_pagedown(self): + level = self.levels[-1] + self.report("page down") + level.moveto(level.curx, level.cury+level.mainsizey-self.pageoverlapy) + + def cmd_left(self): + level = self.levels[-1] + self.report("left") + level.moveto(level.curx-self.stepx, level.cury) + + def cmd_right(self): + level = self.levels[-1] + self.report("right") + level.moveto(level.curx+self.stepx, level.cury) + + def cmd_home(self): + level = self.levels[-1] + self.report("home") + level.moveto(0, level.cury) + + def cmd_end(self): + level = self.levels[-1] + self.report("end") + level.moveto(level.datasizex+level.mainsizey-self.pageoverlapx, level.cury) + + def cmd_prevattr(self): + 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): + 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): + level = self.levels[-1] + self.returnvalue = level.items[level.cury].item + return True + + def cmd_pickattr(self): + level = self.levels[-1] + attrname = level.displayattr[1] + if attrname is ipipe._default: + curses.beep() + self.report(AttributeError(ipipe._attrname(attrname))) + return + attr = ipipe._getattr(level.items[level.cury].item, attrname) + if attr is ipipe._default: + curses.beep() + self.report(AttributeError(ipipe._attrname(attrname))) + else: + self.returnvalue = attr + return True + + def cmd_pickallattrs(self): + level = self.levels[-1] + attrname = level.displayattr[1] + if attrname is ipipe._default: + curses.beep() + self.report(AttributeError(ipipe._attrname(attrname))) + return + result = [] + for cache in level.items: + attr = ipipe._getattr(cache.item, attrname) + if attr is not ipipe._default: + result.append(attr) + self.returnvalue = result + return True + + def cmd_pickmarked(self): + level = self.levels[-1] + self.returnvalue = [cache.item for cache in level.items if cache.marked] + return True + + def cmd_pickmarkedattr(self): + level = self.levels[-1] + attrname = level.displayattr[1] + if attrname is ipipe._default: + curses.beep() + self.report(AttributeError(ipipe._attrname(attrname))) + return + result = [] + for cache in level.items: + if cache.marked: + attr = ipipe._getattr(cache.item, attrname) + if attr is not ipipe._default: + result.append(attr) + self.returnvalue = result + return True + + def cmd_markrange(self): + 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_enterdefault(self): + 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 (default mode)...") + self.enter(item, "default") + + def cmd_leave(self): + 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_enter(self): + 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, None) + + def cmd_enterattr(self): + level = self.levels[-1] + attrname = level.displayattr[1] + if attrname is ipipe._default: + curses.beep() + self.report(AttributeError(ipipe._attrname(attrname))) + return + try: + item = level.items[level.cury].item + except IndexError: + self.report(CommandError("No object")) + curses.beep() + else: + attr = ipipe._getattr(item, attrname) + if attr is ipipe._default: + self.report(AttributeError(ipipe._attrname(attrname))) + else: + self.report("entering object attribute %s..." % ipipe._attrname(attrname)) + self.enter(attr, None) + + def cmd_detail(self): + 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...") + self.enter(item, "detail") + + def cmd_detailattr(self): + level = self.levels[-1] + attrname = level.displayattr[1] + if attrname is ipipe._default: + curses.beep() + self.report(AttributeError(ipipe._attrname(attrname))) + return + try: + item = level.items[level.cury].item + except IndexError: + self.report(CommandError("No object")) + curses.beep() + else: + attr = ipipe._getattr(item, attrname) + if attr is ipipe._default: + self.report(AttributeError(ipipe._attrname(attrname))) + else: + self.report("entering detail view for attribute...") + self.enter(attr, "detail") + + def cmd_tooglemark(self): + 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): + level = self.levels[-1] + attrname = level.displayattr[1] + if attrname is ipipe._default: + curses.beep() + self.report(AttributeError(ipipe._attrname(attrname))) + return + self.report("sort by %s (ascending)" % ipipe._attrname(attrname)) + def key(item): + try: + return ipipe._getattr(item, attrname, None) + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + return None + level.sort(key) + + def cmd_sortattrdesc(self): + level = self.levels[-1] + attrname = level.displayattr[1] + if attrname is ipipe._default: + curses.beep() + self.report(AttributeError(ipipe._attrname(attrname))) + return + self.report("sort by %s (descending)" % ipipe._attrname(attrname)) + def key(item): + try: + return ipipe._getattr(item, attrname, None) + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + return None + level.sort(key, reverse=True) + + def cmd_goto(self): + self.startkeyboardinput("goto") + + def exe_goto(self): + level = self.levels[-1] + if self.keyboardinput: + level.moveto(level.curx, int(self.keyboardinput)) + + def cmd_find(self): + self.startkeyboardinput("find") + + def exe_find(self): + level = self.levels[-1] + if self.keyboardinput: + while True: + cury = level.cury + level.moveto(level.curx, cury+1) + if cury == level.cury: + curses.beep() + break + item = level.items[level.cury].item + try: + if eval(self.keyboardinput, globals(), ipipe.AttrNamespace(item)): + break + except (KeyboardInterrupt, SystemExit): + raise + except Exception, exc: + self.report(exc) + curses.beep() + break # break on error + + def cmd_findbackwards(self): + self.startkeyboardinput("findbackwards") + + def exe_findbackwards(self): + level = self.levels[-1] + if self.keyboardinput: + while level.cury: + level.moveto(level.curx, level.cury-1) + item = level.items[level.cury].item + try: + if eval(self.keyboardinput, globals(), ipipe.AttrNamespace(item)): + break + except (KeyboardInterrupt, SystemExit): + raise + except Exception, exc: + self.report(exc) + curses.beep() + break # break on error + else: + curses.beep() + + def cmd_help(self): + """ + The help command + """ + for level in self.levels: + if isinstance(level.input, _BrowserHelp): + curses.beep() + self.report(CommandError("help already active")) + return + + self.enter(_BrowserHelp(self), "default") + + 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 (key, cmd) in self.keymap.iteritems(): + if cmd == "quit": + keys.append("%s=%s" % (self.keylabel(key), cmd)) + for (key, cmd) in self.keymap.iteritems(): + if cmd == "help": + 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, ipipe.xiter(self.input, "default"), *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, " ", 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 attrname in level.displayattrs: + strattrname = ipipe._attrname(attrname) + cwidth = level.colwidths[attrname] + header = strattrname.ljust(cwidth) + if attrname == 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 + 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")] + attrname = level.displayattr[1] + if attrname is not ipipe._default and attrname is not None: + posx += self.addstr(posy, posx, 0, endx, " | ", self.style_footer) + posx += self.addstr(posy, posx, 0, endx, ipipe._attrname(attrname), self.style_footer) + posx += self.addstr(posy, posx, 0, endx, ": ", self.style_footer) + try: + attr = ipipe._getattr(item, attrname) + except (SystemExit, KeyboardInterrupt): + raise + except Exception, exc: + attr = exc + if attr is not ipipe._default: + attrstyle = ipipe.xrepr(attr, "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: + scr.addstr(self.scrsizey-1, 0, + self.prompts[self.mode] + self.keyboardinput, + self.getstyle(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 error from writing to the last line + pass + scr.clrtoeol() + + # Position cursor + if self.mode in self.prompts: + scr.move(self.scrsizey-1, len(self.prompts[self.mode])+self.cursorpos) + 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.mode in self.prompts: + if c in (8, 127, curses.KEY_BACKSPACE): + if self.cursorpos: + self.keyboardinput = self.keyboardinput[:self.cursorpos-1] + self.keyboardinput[self.cursorpos:] + self.cursorpos -= 1 + break + else: + curses.beep() + elif c == curses.KEY_LEFT: + if self.cursorpos: + self.cursorpos -= 1 + break + else: + curses.beep() + elif c == curses.KEY_RIGHT: + if self.cursorpos < len(self.keyboardinput): + self.cursorpos += 1 + break + else: + curses.beep() + elif c in (curses.KEY_UP, curses.KEY_DOWN): # cancel + self.mode = "default" + break + elif c == ord("\n"): + self.executekeyboardinput(self.mode) + break + elif c != -1: + try: + c = chr(c) + except ValueError: + curses.beep() + else: + if (self.mode == "goto" and not "0" <= c <= "9"): + curses.beep() + else: + self.keyboardinput = self.keyboardinput[:self.cursorpos] + c + self.keyboardinput[self.cursorpos:] + self.cursorpos += 1 + 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): + return curses.wrapper(self._dodisplay) diff --git a/IPython/Extensions/ipipe.py b/IPython/Extensions/ipipe.py index 79d5ffb..5a14802 100644 --- a/IPython/Extensions/ipipe.py +++ b/IPython/Extensions/ipipe.py @@ -152,11 +152,6 @@ try: except ImportError: grp = None -try: - import curses -except ImportError: - curses = None - import path try: from IPython import genutils @@ -176,10 +171,10 @@ __all__ = [ os.stat_float_times(True) # enable microseconds -class _AttrNamespace(object): +class AttrNamespace(object): """ - Internal helper class that is used for providing a namespace for evaluating - expressions containg attribute names of an object. + Helper class that is used for providing a namespace for evaluating + expressions containing attribute names of an object. """ def __init__(self, wrapped): self.wrapped = wrapped @@ -198,7 +193,7 @@ class _AttrNamespace(object): # normal uses case, bizarre ones like accessing the locals() # will fail try: - eval("_", None, _AttrNamespace(None)) + eval("_", None, AttrNamespace(None)) except TypeError: real_eval = eval def eval(codestring, _globals, _locals): @@ -1355,7 +1350,7 @@ class ifilter(Pipe): return self.expr(item) else: def test(item): - return eval(self.expr, globals(), _AttrNamespace(item)) + return eval(self.expr, globals(), AttrNamespace(item)) ok = 0 exc_info = None @@ -1426,7 +1421,7 @@ class ieval(Pipe): return self.expr(item) else: def do(item): - return eval(self.expr, globals(), _AttrNamespace(item)) + return eval(self.expr, globals(), AttrNamespace(item)) ok = 0 exc_info = None @@ -1514,7 +1509,7 @@ class isort(Pipe): ) else: def key(item): - return eval(self.key, globals(), _AttrNamespace(item)) + return eval(self.key, globals(), AttrNamespace(item)) items = sorted( xiter(self.input, mode), key=key, @@ -1779,1419 +1774,14 @@ class XAttr(object): return ("name", "type", "doc", "value") -_ibrowse_help = """ -down -Move the cursor to the next line. - -up -Move the cursor to the previous line. - -pagedown -Move the cursor down one page (minus overlap). - -pageup -Move the cursor up one page (minus overlap). - -left -Move the cursor left. - -right -Move the cursor right. - -home -Move the cursor to the first column. - -end -Move the cursor to the last column. - -prevattr -Move the cursor one attribute column to the left. - -nextattr -Move the cursor one attribute column to the right. - -pick -'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.) - -pickattr -'Pick' the attribute under the cursor (i.e. the row/column the cursor is on). - -pickallattrs -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. - -tooglemark -Mark/unmark the object under the cursor. Marked objects have a '!' after the -row number). - -pickmarked -'Pick' marked objects. Marked objects will be returned as a list. - -pickmarkedattr -'Pick' the attribute under the cursor from all marked objects (This returns a -list). - -enterdefault -Enter the object under the cursor. (what this mean depends on the object -itself (i.e. how it implements the '__xiter__' method). This opens a new -browser 'level'. - -enter -Enter the object under the cursor. If the object provides different enter -modes a menu of all modes will be presented; choose one and enter it (via the -'enter' or 'enterdefault' command). - -enterattr -Enter the attribute under the cursor. - -leave -Leave the current browser level and go back to the previous one. - -detail -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). - -detailattr -Show a detail view of the attribute under the cursor. - -markrange -Mark all objects from the last marked object before the current cursor -position to the cursor position. - -sortattrasc -Sort the objects (in ascending order) using the attribute under the cursor as -the sort key. - -sortattrdesc -Sort the objects (in descending order) using the attribute under the cursor as -the sort key. - -goto -Jump to a row. The row number can be entered at the bottom of the screen. - -find -Search forward for a row. At the bottom of the screen the condition can be -entered. - -findbackwards -Search backward for a row. At the bottom of the screen the condition can be -entered. - -help -This screen. -""" - - -if curses is not None: - class UnassignedKeyError(Exception): - """ - Exception that is used for reporting unassigned keys. - """ - - - class UnknownCommandError(Exception): - """ - Exception that is used for reporting unknown command (this should never - happen). - """ - - - class CommandError(Exception): - """ - Exception that is used for reporting that a command can't be executed. - """ - - - 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("red:blacK") - # 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 __xiter__(self, mode): - # Get reverse key mapping - allkeys = {} - for (key, cmd) in self.browser.keymap.iteritems(): - allkeys.setdefault(cmd, []).append(key) - - fields = ("key", "description") - - for (i, command) in enumerate(_ibrowse_help.strip().split("\n\n")): - if i: - yield Fields(fields, key="", description="") - - (name, description) = command.split("\n", 1) - keys = allkeys.get(name, []) - lines = textwrap.wrap(description, 60) - - yield Fields(fields, 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 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, iterator, mainsizey, *attrs): - self.browser = browser - self.input = input - self.header = [x for x in xrepr(input, "header") if not isinstance(x[0], int)] - # iterator for the input - self.iterator = iterator - - # is the iterator exhausted? - self.exhausted = False - - # attributes to be display (autodetected if empty) - self.attrs = attrs - - # fetched items (+ marked flag) - self.items = 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 - - # Attribute names to display (in this order) - self.displayattrs = [] - - # index and name of attribute under the cursor - self.displayattr = (None, _default) - - # Maps attribute names to column widths - self.colwidths = {} - - self.fetch(mainsizey) - self.calcdisplayattrs() - # formatted attributes for the items on screen - # (i.e. self.items[self.datastarty:self.datastarty+self.mainsizey]) - self.displayrows = [self.getrow(i) for i in xrange(len(self.items))] - self.calcwidths() - self.calcdisplayattr() - - 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 - 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``) - attrnames = set() - # If the browser object specifies a fixed list of attributes, - # simply use it. - if self.attrs: - self.displayattrs = self.attrs - else: - self.displayattrs = [] - endy = min(self.datastarty+self.mainsizey, len(self.items)) - for i in xrange(self.datastarty, endy): - for attrname in xattrs(self.items[i].item, "default"): - if attrname not in attrnames: - self.displayattrs.append(attrname) - attrnames.add(attrname) - - def getrow(self, i): - # Return a dictinary 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 attrname in self.displayattrs: - try: - value = _getattr(item, attrname, _default) - except (KeyboardInterrupt, SystemExit): - raise - except Exception, exc: - value = exc - # only store attribute if it exists (or we got an exception) - if value is not _default: - parts = [] - totallength = 0 - align = None - full = False - # Collect parts until we have enough - for part in xrepr(value, "cell"): - # part gives (alignment, stop) - # instead of (style, text) - if isinstance(part[0], int): - # only consider the first occurence - if align is None: - align = part[0] - full = part[1] - else: - parts.append(part) - totallength += len(part[1]) - if totallength >= self.browser.maxattrlength and not full: - parts.append((astyle.style_ellisis, "...")) - totallength += 3 - break - # remember alignment, length and colored parts - row[attrname] = (align, totallength, parts) - return row - - def calcwidths(self): - # Recalculate the displayed fields and their width. - # ``calcdisplayattrs()'' must have been called and the cache - # for attributes of the objects on screen (``self.displayrows``) - # must have been filled. This returns a dictionary mapping - # colmn names to width. - self.colwidths = {} - for row in self.displayrows: - for attrname in self.displayattrs: - try: - length = row[attrname][1] - except KeyError: - length = 0 - # always add attribute to colwidths, even if it doesn't exist - if attrname not in self.colwidths: - self.colwidths[attrname] = len(_attrname(attrname)) - newwidth = max(self.colwidths[attrname], length) - self.colwidths[attrname] = newwidth - - # How many characters do we need to paint the 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 on which attribute the cursor is on and store this - # information in ``self.displayattr``. - pos = 0 - for (i, attrname) in enumerate(self.displayattrs): - if pos+self.colwidths[attrname] >= self.curx: - self.displayattr = (i, attrname) - break - pos += self.colwidths[attrname]+1 - else: - self.displayattr = (None, _default) - - 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 - self.fetch(y+scrollbordery+1) # try to get more items - 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= 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 = 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) - - - class ibrowse(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": "goto object #: ", - "find": "find expression: ", - "findbackwards": "find backwards expression: " - } - - # Maps curses key codes to "function" names - keymap = { - ord("q"): "quit", - curses.KEY_UP: "up", - curses.KEY_DOWN: "down", - curses.KEY_PPAGE: "pageup", - curses.KEY_NPAGE: "pagedown", - curses.KEY_LEFT: "left", - curses.KEY_RIGHT: "right", - curses.KEY_HOME: "home", - curses.KEY_END: "end", - ord("<"): "prevattr", - 0x1b: "prevattr", # SHIFT-TAB - ord(">"): "nextattr", - ord("\t"):"nextattr", # TAB - ord("p"): "pick", - ord("P"): "pickattr", - ord("C"): "pickallattrs", - ord("m"): "pickmarked", - ord("M"): "pickmarkedattr", - ord("\n"): "enterdefault", - # FIXME: What's happening here? - 8: "leave", - 127: "leave", - curses.KEY_BACKSPACE: "leave", - ord("x"): "leave", - ord("h"): "help", - ord("e"): "enter", - ord("E"): "enterattr", - ord("d"): "detail", - ord("D"): "detailattr", - ord(" "): "tooglemark", - ord("r"): "markrange", - ord("v"): "sortattrasc", - ord("V"): "sortattrdesc", - ord("g"): "goto", - ord("f"): "find", - ord("b"): "findbackwards", - } - - def __init__(self, *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. - """ - 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" - - # The partially entered row number for the goto command - self.goto = "" - - 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, style.bg, 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, mode, *attrs): - """ - Enter the object ``item`` in the mode ``mode``. If ``attrs`` is - specified, it will be used as a fixed list of attributes to display. - """ - try: - iterator = xiter(item, mode) - except (KeyboardInterrupt, SystemExit): - raise - except Exception, exc: - curses.beep() - self.report(exc) - else: - self._calcheaderlines(len(self.levels)+1) - level = _BrowserLevel( - self, - item, - iterator, - self.scrsizey-1-self._headerlines-2, - *attrs - ) - self.levels.append(level) - - def startkeyboardinput(self, mode): - """ - Enter mode ``mode``, which requires keyboard input. - """ - self.mode = mode - self.keyboardinput = "" - self.cursorpos = 0 - - def executekeyboardinput(self, mode): - exe = getattr(self, "exe_%s" % mode, None) - if exe is not None: - exe() - self.mode = "default" - - 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] - 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_quit(self): - self.returnvalue = None - return True - - def cmd_up(self): - level = self.levels[-1] - self.report("up") - level.moveto(level.curx, level.cury-self.stepy) - - def cmd_down(self): - level = self.levels[-1] - self.report("down") - level.moveto(level.curx, level.cury+self.stepy) - - def cmd_pageup(self): - level = self.levels[-1] - self.report("page up") - level.moveto(level.curx, level.cury-level.mainsizey+self.pageoverlapy) - - def cmd_pagedown(self): - level = self.levels[-1] - self.report("page down") - level.moveto(level.curx, level.cury+level.mainsizey-self.pageoverlapy) - - def cmd_left(self): - level = self.levels[-1] - self.report("left") - level.moveto(level.curx-self.stepx, level.cury) - - def cmd_right(self): - level = self.levels[-1] - self.report("right") - level.moveto(level.curx+self.stepx, level.cury) - - def cmd_home(self): - level = self.levels[-1] - self.report("home") - level.moveto(0, level.cury) - - def cmd_end(self): - level = self.levels[-1] - self.report("end") - level.moveto(level.datasizex+level.mainsizey-self.pageoverlapx, level.cury) - - def cmd_prevattr(self): - 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): - 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): - level = self.levels[-1] - self.returnvalue = level.items[level.cury].item - return True - - def cmd_pickattr(self): - level = self.levels[-1] - attrname = level.displayattr[1] - if attrname is _default: - curses.beep() - self.report(AttributeError(_attrname(attrname))) - return - attr = _getattr(level.items[level.cury].item, attrname) - if attr is _default: - curses.beep() - self.report(AttributeError(_attrname(attrname))) - else: - self.returnvalue = attr - return True - - def cmd_pickallattrs(self): - level = self.levels[-1] - attrname = level.displayattr[1] - if attrname is _default: - curses.beep() - self.report(AttributeError(_attrname(attrname))) - return - result = [] - for cache in level.items: - attr = _getattr(cache.item, attrname) - if attr is not _default: - result.append(attr) - self.returnvalue = result - return True - - def cmd_pickmarked(self): - level = self.levels[-1] - self.returnvalue = [cache.item for cache in level.items if cache.marked] - return True - - def cmd_pickmarkedattr(self): - level = self.levels[-1] - attrname = level.displayattr[1] - if attrname is _default: - curses.beep() - self.report(AttributeError(_attrname(attrname))) - return - result = [] - for cache in level.items: - if cache.marked: - attr = _getattr(cache.item, attrname) - if attr is not _default: - result.append(attr) - self.returnvalue = result - return True - - def cmd_markrange(self): - 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_enterdefault(self): - 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 (default mode)...") - self.enter(item, "default") - - def cmd_leave(self): - 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_enter(self): - 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, None) - - def cmd_enterattr(self): - level = self.levels[-1] - attrname = level.displayattr[1] - if attrname is _default: - curses.beep() - self.report(AttributeError(_attrname(attrname))) - return - try: - item = level.items[level.cury].item - except IndexError: - self.report(CommandError("No object")) - curses.beep() - else: - attr = _getattr(item, attrname) - if attr is _default: - self.report(AttributeError(_attrname(attrname))) - else: - self.report("entering object attribute %s..." % _attrname(attrname)) - self.enter(attr, None) - - def cmd_detail(self): - 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...") - self.enter(item, "detail") - - def cmd_detailattr(self): - level = self.levels[-1] - attrname = level.displayattr[1] - if attrname is _default: - curses.beep() - self.report(AttributeError(_attrname(attrname))) - return - try: - item = level.items[level.cury].item - except IndexError: - self.report(CommandError("No object")) - curses.beep() - else: - attr = _getattr(item, attrname) - if attr is _default: - self.report(AttributeError(_attrname(attrname))) - else: - self.report("entering detail view for attribute...") - self.enter(attr, "detail") - - def cmd_tooglemark(self): - 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): - level = self.levels[-1] - attrname = level.displayattr[1] - if attrname is _default: - curses.beep() - self.report(AttributeError(_attrname(attrname))) - return - self.report("sort by %s (ascending)" % _attrname(attrname)) - def key(item): - try: - return _getattr(item, attrname, None) - except (KeyboardInterrupt, SystemExit): - raise - except Exception: - return None - level.sort(key) - - def cmd_sortattrdesc(self): - level = self.levels[-1] - attrname = level.displayattr[1] - if attrname is _default: - curses.beep() - self.report(AttributeError(_attrname(attrname))) - return - self.report("sort by %s (descending)" % _attrname(attrname)) - def key(item): - try: - return _getattr(item, attrname, None) - except (KeyboardInterrupt, SystemExit): - raise - except Exception: - return None - level.sort(key, reverse=True) - - def cmd_goto(self): - self.startkeyboardinput("goto") - - def exe_goto(self): - level = self.levels[-1] - if self.keyboardinput: - level.moveto(level.curx, int(self.keyboardinput)) - - def cmd_find(self): - self.startkeyboardinput("find") - - def exe_find(self): - level = self.levels[-1] - if self.keyboardinput: - while True: - cury = level.cury - level.moveto(level.curx, cury+1) - if cury == level.cury: - curses.beep() - break - item = level.items[level.cury].item - try: - if eval(self.keyboardinput, globals(), _AttrNamespace(item)): - break - except (KeyboardInterrupt, SystemExit): - raise - except Exception, exc: - self.report(exc) - curses.beep() - break # break on error - - def cmd_findbackwards(self): - self.startkeyboardinput("findbackwards") - - def exe_findbackwards(self): - level = self.levels[-1] - if self.keyboardinput: - while level.cury: - level.moveto(level.curx, level.cury-1) - item = level.items[level.cury].item - try: - if eval(self.keyboardinput, globals(), _AttrNamespace(item)): - break - except (KeyboardInterrupt, SystemExit): - raise - except Exception, exc: - self.report(exc) - curses.beep() - break # break on error - else: - curses.beep() - - def cmd_help(self): - """ - The help command - """ - for level in self.levels: - if isinstance(level.input, _BrowserHelp): - curses.beep() - self.report(CommandError("help already active")) - return - - self.enter(_BrowserHelp(self), "default") - - 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 (key, cmd) in self.keymap.iteritems(): - if cmd == "quit": - keys.append("%s=%s" % (self.keylabel(key), cmd)) - for (key, cmd) in self.keymap.iteritems(): - if cmd == "help": - 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, xiter(self.input, "default"), *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, " ", 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 attrname in level.displayattrs: - strattrname = _attrname(attrname) - cwidth = level.colwidths[attrname] - header = strattrname.ljust(cwidth) - if attrname == 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 = style_nodata - 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 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")] - attrname = level.displayattr[1] - if attrname is not _default and attrname is not None: - posx += self.addstr(posy, posx, 0, endx, " | ", self.style_footer) - posx += self.addstr(posy, posx, 0, endx, _attrname(attrname), self.style_footer) - posx += self.addstr(posy, posx, 0, endx, ": ", self.style_footer) - try: - attr = _getattr(item, attrname) - except (SystemExit, KeyboardInterrupt): - raise - except Exception, exc: - attr = exc - if attr is not _default: - attrstyle = xrepr(attr, "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: - scr.addstr(self.scrsizey-1, 0, - self.prompts[self.mode] + self.keyboardinput, - self.getstyle(style_default)) - # Display report - else: - if self._report is not None: - if isinstance(self._report, Exception): - style = self.getstyle(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 error from writing to the last line - pass - scr.clrtoeol() - - # Position cursor - if self.mode in self.prompts: - scr.move(self.scrsizey-1, len(self.prompts[self.mode])+self.cursorpos) - 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.mode in self.prompts: - if c in (8, 127, curses.KEY_BACKSPACE): - if self.cursorpos: - self.keyboardinput = self.keyboardinput[:self.cursorpos-1] + self.keyboardinput[self.cursorpos:] - self.cursorpos -= 1 - break - else: - curses.beep() - elif c == curses.KEY_LEFT: - if self.cursorpos: - self.cursorpos -= 1 - break - else: - curses.beep() - elif c == curses.KEY_RIGHT: - if self.cursorpos < len(self.keyboardinput): - self.cursorpos += 1 - break - else: - curses.beep() - elif c in (curses.KEY_UP, curses.KEY_DOWN): # cancel - self.mode = "default" - break - elif c == ord("\n"): - self.executekeyboardinput(self.mode) - break - elif c != -1: - try: - c = chr(c) - except ValueError: - curses.beep() - else: - if (self.mode == "goto" and not "0" <= c <= "9"): - curses.beep() - else: - self.keyboardinput = self.keyboardinput[:self.cursorpos] + c + self.keyboardinput[self.cursorpos:] - self.cursorpos += 1 - 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): - return curses.wrapper(self._dodisplay) - - defaultdisplay = ibrowse - __all__.append("ibrowse") -else: +try: + from ibrowse import ibrowse +except ImportError: # No curses (probably Windows) => use ``idump`` as the default display. defaultdisplay = idump +else: + defaultdisplay = ibrowse + __all__.append("ibrowse") # If we're running under IPython, install an IPython displayhook that