|
|
# -*- coding: iso-8859-1 -*-
|
|
|
|
|
|
import curses, fcntl, signal, struct, tty, textwrap, inspect
|
|
|
|
|
|
import astyle, ipipe
|
|
|
|
|
|
|
|
|
# Python 2.3 compatibility
|
|
|
try:
|
|
|
set
|
|
|
except NameError:
|
|
|
import sets
|
|
|
set = sets.Set
|
|
|
|
|
|
|
|
|
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 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 __xiter__(self, mode):
|
|
|
# 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, 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, 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.noitem)
|
|
|
|
|
|
# Maps attribute names 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
|
|
|
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()
|
|
|
self.displayattrs = []
|
|
|
if self.attrs:
|
|
|
# If the browser object specifies a fixed list of attributes,
|
|
|
# simply use it (removing hidden attributes).
|
|
|
for attrname in self.attrs:
|
|
|
if attrname not in attrnames and attrname not in self.hiddenattrs:
|
|
|
self.displayattrs.append(attrname)
|
|
|
attrnames.add(attrname)
|
|
|
else:
|
|
|
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 and attrname not in self.hiddenattrs:
|
|
|
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.noitem)
|
|
|
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[attrname] = 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 returns a dictionary mapping
|
|
|
# column names to widths.
|
|
|
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 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, 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.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)
|
|
|
|
|
|
|
|
|
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("enterdefault", "\r\n")
|
|
|
# FIXME: What's happening here?
|
|
|
keymap.register("leave", curses.KEY_BACKSPACE, "x\x08\x7f")
|
|
|
keymap.register("hideattr", "h")
|
|
|
keymap.register("unhideattrs", "H")
|
|
|
keymap.register("help", "?")
|
|
|
keymap.register("enter", "e")
|
|
|
keymap.register("enterattr", "E")
|
|
|
keymap.register("detail", "d")
|
|
|
keymap.register("detailattr", "D")
|
|
|
keymap.register("tooglemark", " ")
|
|
|
keymap.register("markrange", "r")
|
|
|
keymap.register("sortattrasc", "v")
|
|
|
keymap.register("sortattrdesc", "V")
|
|
|
keymap.register("goto", "g")
|
|
|
keymap.register("find", "f")
|
|
|
keymap.register("findbackwards", "b")
|
|
|
|
|
|
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"
|
|
|
|
|
|
# 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, 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.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]
|
|
|
attrname = level.displayattr[1]
|
|
|
if attrname is ipipe.noitem:
|
|
|
curses.beep()
|
|
|
self.report(AttributeError(ipipe._attrname(attrname)))
|
|
|
return
|
|
|
attr = ipipe._getattr(level.items[level.cury].item, attrname)
|
|
|
if attr is ipipe.noitem:
|
|
|
curses.beep()
|
|
|
self.report(AttributeError(ipipe._attrname(attrname)))
|
|
|
else:
|
|
|
self.returnvalue = attr
|
|
|
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]
|
|
|
attrname = level.displayattr[1]
|
|
|
if attrname is ipipe.noitem:
|
|
|
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.noitem:
|
|
|
result.append(attr)
|
|
|
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]
|
|
|
attrname = level.displayattr[1]
|
|
|
if attrname is ipipe.noitem:
|
|
|
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.noitem:
|
|
|
result.append(attr)
|
|
|
self.returnvalue = result
|
|
|
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_enterdefault(self):
|
|
|
"""
|
|
|
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'.
|
|
|
"""
|
|
|
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):
|
|
|
"""
|
|
|
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_enter(self):
|
|
|
"""
|
|
|
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).
|
|
|
"""
|
|
|
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):
|
|
|
"""
|
|
|
Enter the attribute under the cursor.
|
|
|
"""
|
|
|
level = self.levels[-1]
|
|
|
attrname = level.displayattr[1]
|
|
|
if attrname is ipipe.noitem:
|
|
|
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.noitem:
|
|
|
self.report(AttributeError(ipipe._attrname(attrname)))
|
|
|
else:
|
|
|
self.report("entering object attribute %s..." % ipipe._attrname(attrname))
|
|
|
self.enter(attr, None)
|
|
|
|
|
|
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...")
|
|
|
self.enter(item, "detail")
|
|
|
|
|
|
def cmd_detailattr(self):
|
|
|
"""
|
|
|
Show a detail view of the attribute under the cursor.
|
|
|
"""
|
|
|
level = self.levels[-1]
|
|
|
attrname = level.displayattr[1]
|
|
|
if attrname is ipipe.noitem:
|
|
|
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.noitem:
|
|
|
self.report(AttributeError(ipipe._attrname(attrname)))
|
|
|
else:
|
|
|
self.report("entering detail view for attribute...")
|
|
|
self.enter(attr, "detail")
|
|
|
|
|
|
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]
|
|
|
attrname = level.displayattr[1]
|
|
|
if attrname is ipipe.noitem:
|
|
|
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):
|
|
|
"""
|
|
|
Sort the objects (in descending order) using the attribute under
|
|
|
the cursor as the sort key.
|
|
|
"""
|
|
|
level = self.levels[-1]
|
|
|
attrname = level.displayattr[1]
|
|
|
if attrname is ipipe.noitem:
|
|
|
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_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_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), "default")
|
|
|
|
|
|
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 key in ("quit", "help"):
|
|
|
key = self.keymap.findkey(key, None)
|
|
|
if key is not None:
|
|
|
keys.append("%s=quit" % self.keylabel(key))
|
|
|
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, " <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 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.noitem 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.noitem:
|
|
|
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:
|
|
|
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"):
|
|
|
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)
|
|
|
|