1
# -*- coding: iso-8859-1 -*-
3
import curses, fcntl, signal, struct, tty, textwrap, inspect
5
from IPython import ipapi
10
# Python 2.3 compatibility
17
# Python 2.3 compatibility
21
from ipipe import sorted
24
class UnassignedKeyError(Exception):
26
Exception that is used for reporting unassigned keys.
30
class UnknownCommandError(Exception):
32
Exception that is used for reporting unknown commands (this should never
37
class CommandError(Exception):
39
Exception that is used for reporting that a command can't be executed.
45
Stores mapping of keys to commands.
50
def __setitem__(self, key, command):
51
if isinstance(key, str):
53
dict.__setitem__(self, ord(c), command)
55
dict.__setitem__(self, key, command)
57
def __getitem__(self, key):
58
if isinstance(key, str):
60
return dict.__getitem__(self, key)
62
def __detitem__(self, key):
63
if isinstance(key, str):
65
dict.__detitem__(self, key)
67
def register(self, command, *keys):
71
def get(self, key, default=None):
72
if isinstance(key, str):
74
return dict.get(self, key, default)
76
def findkey(self, command, default=ipipe.noitem):
77
for (key, commandcandidate) in self.iteritems():
78
if commandcandidate == command:
80
if default is ipipe.noitem:
81
raise KeyError(command)
85
class _BrowserCachedItem(object):
86
# This is used internally by ``ibrowse`` to store a item together with its
88
__slots__ = ("item", "marked")
90
def __init__(self, item):
95
class _BrowserHelp(object):
96
style_header = astyle.Style.fromstr("yellow:black:bold")
97
# This is used internally by ``ibrowse`` for displaying the help screen.
98
def __init__(self, browser):
99
self.browser = browser
101
def __xrepr__(self, mode):
103
if mode == "header" or mode == "footer":
104
yield (astyle.style_default, "ibrowse help screen")
106
yield (astyle.style_default, repr(self))
109
# Get reverse key mapping
111
for (key, cmd) in self.browser.keymap.iteritems():
112
allkeys.setdefault(cmd, []).append(key)
114
fields = ("key", "description")
117
for name in dir(self.browser):
118
if name.startswith("cmd_"):
119
command = getattr(self.browser, name)
120
commands.append((inspect.getsourcelines(command)[-1], name[4:], command))
122
commands = [(c[1], c[2]) for c in commands]
123
for (i, (name, command)) in enumerate(commands):
125
yield ipipe.Fields(fields, key="", description="")
127
description = command.__doc__
128
if description is None:
131
lines = [l.strip() for l in description.splitlines() if l.strip()]
132
description = "\n".join(lines)
133
lines = textwrap.wrap(description, 60)
134
keys = allkeys.get(name, [])
136
yield ipipe.Fields(fields, key="", description=astyle.Text((self.style_header, name)))
137
for i in xrange(max(len(keys), len(lines))):
139
key = self.browser.keylabel(keys[i])
146
yield ipipe.Fields(fields, key=key, description=line)
149
class _BrowserLevel(object):
150
# This is used internally to store the state (iterator, fetch items,
151
# position of cursor and screen, etc.) of one browser level
152
# An ``ibrowse`` object keeps multiple ``_BrowserLevel`` objects in
154
def __init__(self, browser, input, mainsizey, *attrs):
155
self.browser = browser
157
self.header = [x for x in ipipe.xrepr(input, "header") if not isinstance(x[0], int)]
158
# iterator for the input
159
self.iterator = ipipe.xiter(input)
161
# is the iterator exhausted?
162
self.exhausted = False
164
# attributes to be display (autodetected if empty)
167
# fetched items (+ marked flag)
168
self.items = ipipe.deque()
170
# Number of marked objects
173
# Vertical cursor position
176
# Horizontal cursor position
179
# Index of first data column
182
# Index of first data line
185
# height of the data display area
186
self.mainsizey = mainsizey
188
# width of the data display area (changes when scrolling)
191
# Size of row number (changes when scrolling)
194
# Attributes to display (in this order)
195
self.displayattrs = []
197
# index and attribute under the cursor
198
self.displayattr = (None, ipipe.noitem)
200
# Maps attributes to column widths
203
# Set of hidden attributes
204
self.hiddenattrs = set()
206
# This takes care of all the caches etc.
207
self.moveto(0, 0, refresh=True)
209
def fetch(self, count):
210
# Try to fill ``self.items`` with at least ``count`` objects.
211
have = len(self.items)
212
while not self.exhausted and have < count:
214
item = self.iterator.next()
215
except StopIteration:
216
self.exhausted = True
218
except (KeyboardInterrupt, SystemExit):
220
except Exception, exc:
222
self.items.append(_BrowserCachedItem(exc))
223
self.exhausted = True
227
self.items.append(_BrowserCachedItem(item))
229
def calcdisplayattrs(self):
230
# Calculate which attributes are available from the objects that are
231
# currently visible on screen (and store it in ``self.displayattrs``)
234
self.displayattrs = []
236
# If the browser object specifies a fixed list of attributes,
237
# simply use it (removing hidden attributes).
238
for attr in self.attrs:
239
attr = ipipe.upgradexattr(attr)
240
if attr not in attrs and attr not in self.hiddenattrs:
241
self.displayattrs.append(attr)
244
endy = min(self.datastarty+self.mainsizey, len(self.items))
245
for i in xrange(self.datastarty, endy):
246
for attr in ipipe.xattrs(self.items[i].item, "default"):
247
if attr not in attrs and attr not in self.hiddenattrs:
248
self.displayattrs.append(attr)
252
# Return a dictionary with the attributes for the object
253
# ``self.items[i]``. Attribute names are taken from
254
# ``self.displayattrs`` so ``calcdisplayattrs()`` must have been
257
item = self.items[i].item
258
for attr in self.displayattrs:
260
value = attr.value(item)
261
except (KeyboardInterrupt, SystemExit):
263
except Exception, exc:
265
# only store attribute if it exists (or we got an exception)
266
if value is not ipipe.noitem:
267
# remember alignment, length and colored text
268
row[attr] = ipipe.xformat(value, "cell", self.browser.maxattrlength)
271
def calcwidths(self):
272
# Recalculate the displayed fields and their widths.
273
# ``calcdisplayattrs()'' must have been called and the cache
274
# for attributes of the objects on screen (``self.displayrows``)
275
# must have been filled. This sets ``self.colwidths`` which maps
276
# attribute descriptors to widths.
278
for row in self.displayrows:
279
for attr in self.displayattrs:
281
length = row[attr][1]
284
# always add attribute to colwidths, even if it doesn't exist
285
if attr not in self.colwidths:
286
self.colwidths[attr] = len(attr.name())
287
newwidth = max(self.colwidths[attr], length)
288
self.colwidths[attr] = newwidth
290
# How many characters do we need to paint the largest item number?
291
self.numbersizex = len(str(self.datastarty+self.mainsizey-1))
292
# How must space have we got to display data?
293
self.mainsizex = self.browser.scrsizex-self.numbersizex-3
294
# width of all columns
295
self.datasizex = sum(self.colwidths.itervalues()) + len(self.colwidths)
297
def calcdisplayattr(self):
298
# Find out which attribute the cursor is on and store this
299
# information in ``self.displayattr``.
301
for (i, attr) in enumerate(self.displayattrs):
302
if pos+self.colwidths[attr] >= self.curx:
303
self.displayattr = (i, attr)
305
pos += self.colwidths[attr]+1
307
self.displayattr = (None, ipipe.noitem)
309
def moveto(self, x, y, refresh=False):
310
# Move the cursor to the position ``(x,y)`` (in data coordinates,
311
# not in screen coordinates). If ``refresh`` is true, all cached
312
# values will be recalculated (e.g. because the list has been
313
# resorted, so screen positions etc. are no longer valid).
314
olddatastarty = self.datastarty
319
newx = x # remember where we wanted to move
320
newy = y # remember where we wanted to move
322
scrollbordery = min(self.browser.scrollbordery, self.mainsizey//2)
323
scrollborderx = min(self.browser.scrollborderx, self.mainsizex//2)
325
# Make sure that the cursor didn't leave the main area vertically
328
# try to get enough items to fill the screen
329
self.fetch(max(y+scrollbordery+1, self.mainsizey))
330
if y >= len(self.items):
331
y = max(0, len(self.items)-1)
333
# Make sure that the cursor stays on screen vertically
334
if y < self.datastarty+scrollbordery:
335
self.datastarty = max(0, y-scrollbordery)
336
elif y >= self.datastarty+self.mainsizey-scrollbordery:
337
self.datastarty = max(0, min(y-self.mainsizey+scrollbordery+1,
338
len(self.items)-self.mainsizey))
340
if refresh: # Do we need to refresh the complete display?
341
self.calcdisplayattrs()
342
endy = min(self.datastarty+self.mainsizey, len(self.items))
343
self.displayrows = map(self.getrow, xrange(self.datastarty, endy))
345
# Did we scroll vertically => update displayrows
346
# and various other attributes
347
elif self.datastarty != olddatastarty:
348
# Recalculate which attributes we have to display
349
olddisplayattrs = self.displayattrs
350
self.calcdisplayattrs()
351
# If there are new attributes, recreate the cache
352
if self.displayattrs != olddisplayattrs:
353
endy = min(self.datastarty+self.mainsizey, len(self.items))
354
self.displayrows = map(self.getrow, xrange(self.datastarty, endy))
355
elif self.datastarty<olddatastarty: # we did scroll up
356
# drop rows from the end
357
del self.displayrows[self.datastarty-olddatastarty:]
359
for i in xrange(min(olddatastarty, self.datastarty+self.mainsizey)-1,
360
self.datastarty-1, -1):
364
# we didn't have enough objects to fill the screen
366
self.displayrows.insert(0, row)
367
else: # we did scroll down
368
# drop rows from the start
369
del self.displayrows[:self.datastarty-olddatastarty]
371
for i in xrange(max(olddatastarty+self.mainsizey, self.datastarty),
372
self.datastarty+self.mainsizey):
376
# we didn't have enough objects to fill the screen
378
self.displayrows.append(row)
381
# Make sure that the cursor didn't leave the data area horizontally
384
elif x >= self.datasizex:
385
x = max(0, self.datasizex-1)
387
# Make sure that the cursor stays on screen horizontally
388
if x < self.datastartx+scrollborderx:
389
self.datastartx = max(0, x-scrollborderx)
390
elif x >= self.datastartx+self.mainsizex-scrollborderx:
391
self.datastartx = max(0, min(x-self.mainsizex+scrollborderx+1,
392
self.datasizex-self.mainsizex))
394
if x == oldx and y == oldy and (x != newx or y != newy): # couldn't move
399
self.calcdisplayattr()
401
def sort(self, key, reverse=False):
403
Sort the currently list of items using the key function ``key``. If
404
``reverse`` is true the sort order is reversed.
406
curitem = self.items[self.cury] # Remember where the cursor is now
410
return key(item.item)
411
self.items = ipipe.deque(sorted(self.items, key=realkey, reverse=reverse))
413
# Find out where the object under the cursor went
415
for (i, item) in enumerate(self.items):
420
self.moveto(self.curx, cury, refresh=True)
424
Restart iterating the input.
426
self.iterator = ipipe.xiter(self.input)
428
self.exhausted = False
429
self.datastartx = self.datastarty = 0
430
self.moveto(0, 0, refresh=True)
432
def refreshfind(self):
434
Restart iterating the input and go back to the same object as before
435
(if it can be found in the new iterator).
438
oldobject = self.items[self.cury].item
440
oldobject = ipipe.noitem
441
self.iterator = ipipe.xiter(self.input)
443
self.exhausted = False
445
self.fetch(len(self.items)+1)
448
self.datastartx = self.datastarty = 0
449
self.moveto(self.curx, 0, refresh=True)
451
if self.items[-1].item == oldobject:
452
self.datastartx = self.datastarty = 0
453
self.moveto(self.curx, len(self.items)-1, refresh=True)
457
class _CommandInput(object):
459
keymap.register("left", curses.KEY_LEFT)
460
keymap.register("right", curses.KEY_RIGHT)
461
keymap.register("home", curses.KEY_HOME, "\x01") # Ctrl-A
462
keymap.register("end", curses.KEY_END, "\x05") # Ctrl-E
463
# FIXME: What's happening here?
464
keymap.register("backspace", curses.KEY_BACKSPACE, "\x08\x7f")
465
keymap.register("delete", curses.KEY_DC)
466
keymap.register("delend", 0x0b) # Ctrl-K
467
keymap.register("execute", "\r\n")
468
keymap.register("up", curses.KEY_UP)
469
keymap.register("down", curses.KEY_DOWN)
470
keymap.register("incsearchup", curses.KEY_PPAGE)
471
keymap.register("incsearchdown", curses.KEY_NPAGE)
472
keymap.register("exit", "\x18"), # Ctrl-X
474
def __init__(self, prompt):
477
self.maxhistory = 100
480
self.cury = -1 # blank line
485
self.cury = -1 # blank line
487
def handlekey(self, browser, key):
488
cmdname = self.keymap.get(key, None)
489
if cmdname is not None:
490
cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
491
if cmdfunc is not None:
492
return cmdfunc(browser)
500
return self.handlechar(browser, char)
502
def handlechar(self, browser, char):
503
self.input = self.input[:self.curx] + char + self.input[self.curx:]
508
self.history.insert(0, self.input)
509
del self.history[:-self.maxhistory]
511
def cmd_backspace(self, browser):
513
self.input = self.input[:self.curx-1] + self.input[self.curx:]
519
def cmd_delete(self, browser):
520
if self.curx<len(self.input):
521
self.input = self.input[:self.curx] + self.input[self.curx+1:]
526
def cmd_delend(self, browser):
527
if self.curx<len(self.input):
528
self.input = self.input[:self.curx]
531
def cmd_left(self, browser):
538
def cmd_right(self, browser):
539
if self.curx < len(self.input):
545
def cmd_home(self, browser):
552
def cmd_end(self, browser):
553
if self.curx < len(self.input):
554
self.curx = len(self.input)
559
def cmd_up(self, browser):
560
if self.cury < len(self.history)-1:
562
self.input = self.history[self.cury]
563
self.curx = len(self.input)
568
def cmd_down(self, browser):
572
self.input = self.history[self.cury]
575
self.curx = len(self.input)
580
def cmd_incsearchup(self, browser):
581
prefix = self.input[:self.curx]
585
if cury >= len(self.history):
587
if self.history[cury].startswith(prefix):
588
self.input = self.history[cury]
593
def cmd_incsearchdown(self, browser):
594
prefix = self.input[:self.curx]
600
if self.history[cury].startswith(prefix):
601
self.input = self.history[self.cury]
606
def cmd_exit(self, browser):
607
browser.mode = "default"
610
def cmd_execute(self, browser):
611
raise NotImplementedError
614
class _CommandGoto(_CommandInput):
616
_CommandInput.__init__(self, "goto object #")
618
def handlechar(self, browser, char):
620
if not "0" <= char <= "9":
623
return _CommandInput.handlechar(self, browser, char)
625
def cmd_execute(self, browser):
626
level = browser.levels[-1]
629
level.moveto(level.curx, int(self.input))
630
browser.mode = "default"
634
class _CommandFind(_CommandInput):
636
_CommandInput.__init__(self, "find expression")
638
def cmd_execute(self, browser):
639
level = browser.levels[-1]
644
level.moveto(level.curx, cury+1)
645
if cury == level.cury:
648
item = level.items[level.cury].item
650
globals = ipipe.getglobals(None)
651
if eval(self.input, globals, ipipe.AttrNamespace(item)):
652
break # found something
653
except (KeyboardInterrupt, SystemExit):
655
except Exception, exc:
658
break # break on error
659
browser.mode = "default"
663
class _CommandFindBackwards(_CommandInput):
665
_CommandInput.__init__(self, "find backwards expression")
667
def cmd_execute(self, browser):
668
level = browser.levels[-1]
672
level.moveto(level.curx, level.cury-1)
673
item = level.items[level.cury].item
675
globals = ipipe.getglobals(None)
676
if eval(self.input, globals, ipipe.AttrNamespace(item)):
677
break # found something
678
except (KeyboardInterrupt, SystemExit):
680
except Exception, exc:
683
break # break on error
686
browser.mode = "default"
690
class ibrowse(ipipe.Display):
691
# Show this many lines from the previous screen when paging horizontally
694
# Show this many lines from the previous screen when paging vertically
697
# Start scrolling when the cursor is less than this number of columns
698
# away from the left or right screen edge
701
# Start scrolling when the cursor is less than this number of lines
702
# away from the top or bottom screen edge
705
# Accelerate by this factor when scrolling horizontally
708
# Accelerate by this factor when scrolling vertically
711
# The maximum horizontal scroll speed
712
# (as a factor of the screen width (i.e. 0.5 == half a screen width)
715
# The maximum vertical scroll speed
716
# (as a factor of the screen height (i.e. 0.5 == half a screen height)
719
# The maximum number of header lines for browser level
720
# if the nesting is deeper, only the innermost levels are displayed
723
# The approximate maximum length of a column entry
726
# Styles for various parts of the GUI
727
style_objheadertext = astyle.Style.fromstr("white:black:bold|reverse")
728
style_objheadernumber = astyle.Style.fromstr("white:blue:bold|reverse")
729
style_objheaderobject = astyle.Style.fromstr("white:black:reverse")
730
style_colheader = astyle.Style.fromstr("blue:white:reverse")
731
style_colheaderhere = astyle.Style.fromstr("green:black:bold|reverse")
732
style_colheadersep = astyle.Style.fromstr("blue:black:reverse")
733
style_number = astyle.Style.fromstr("blue:white:reverse")
734
style_numberhere = astyle.Style.fromstr("green:black:bold|reverse")
735
style_sep = astyle.Style.fromstr("blue:black")
736
style_data = astyle.Style.fromstr("white:black")
737
style_datapad = astyle.Style.fromstr("blue:black:bold")
738
style_footer = astyle.Style.fromstr("black:white")
739
style_report = astyle.Style.fromstr("white:black")
741
# Column separator in header
744
# Character for padding data cell entries
747
# Column separator in data area
750
# Character to use for "empty" cell (i.e. for non-existing attributes)
753
# Prompts for modes that require keyboard input
755
"goto": _CommandGoto(),
756
"find": _CommandFind(),
757
"findbackwards": _CommandFindBackwards()
760
# Maps curses key codes to "function" names
762
keymap.register("quit", "q")
763
keymap.register("up", curses.KEY_UP)
764
keymap.register("down", curses.KEY_DOWN)
765
keymap.register("pageup", curses.KEY_PPAGE)
766
keymap.register("pagedown", curses.KEY_NPAGE)
767
keymap.register("left", curses.KEY_LEFT)
768
keymap.register("right", curses.KEY_RIGHT)
769
keymap.register("home", curses.KEY_HOME, "\x01")
770
keymap.register("end", curses.KEY_END, "\x05")
771
keymap.register("prevattr", "<\x1b")
772
keymap.register("nextattr", ">\t")
773
keymap.register("pick", "p")
774
keymap.register("pickattr", "P")
775
keymap.register("pickallattrs", "C")
776
keymap.register("pickmarked", "m")
777
keymap.register("pickmarkedattr", "M")
778
keymap.register("pickinput", "i")
779
keymap.register("pickinputattr", "I")
780
keymap.register("hideattr", "h")
781
keymap.register("unhideattrs", "H")
782
keymap.register("help", "?")
783
keymap.register("enter", "\r\n")
784
keymap.register("enterattr", "E")
785
# FIXME: What's happening here?
786
keymap.register("leave", curses.KEY_BACKSPACE, "x\x08\x7f")
787
keymap.register("detail", "d")
788
keymap.register("detailattr", "D")
789
keymap.register("tooglemark", " ")
790
keymap.register("markrange", "%")
791
keymap.register("sortattrasc", "v")
792
keymap.register("sortattrdesc", "V")
793
keymap.register("goto", "g")
794
keymap.register("find", "f")
795
keymap.register("findbackwards", "b")
796
keymap.register("refresh", "r")
797
keymap.register("refreshfind", "R")
799
def __init__(self, input=None, *attrs):
801
Create a new browser. If ``attrs`` is not empty, it is the list
802
of attributes that will be displayed in the browser, otherwise
803
these will be determined by the objects on screen.
805
ipipe.Display.__init__(self, input)
809
# Stack of browser levels
811
# how many colums to scroll (Changes when accelerating)
814
# how many rows to scroll (Changes when accelerating)
817
# Beep on the edges of the data area? (Will be set to ``False``
818
# once the cursor hits the edge of the screen, so we don't get
822
# Cache for registered ``curses`` colors and styles.
827
# How many header lines do we want to paint (the numbers of levels
828
# we have, but with an upper bound)
829
self._headerlines = 1
831
# Index of first header line
832
self._firstheaderline = 0
836
# report in the footer line (error, executed command etc.)
839
# value to be returned to the caller (set by commands)
840
self.returnvalue = None
842
# The mode the browser is in
843
# e.g. normal browsing or entering an argument for a command
844
self.mode = "default"
846
# set by the SIGWINCH signal handler
849
def nextstepx(self, step):
851
Accelerate horizontally.
853
return max(1., min(step*self.acceleratex,
854
self.maxspeedx*self.levels[-1].mainsizex))
856
def nextstepy(self, step):
858
Accelerate vertically.
860
return max(1., min(step*self.acceleratey,
861
self.maxspeedy*self.levels[-1].mainsizey))
863
def getstyle(self, style):
865
Register the ``style`` with ``curses`` or get it from the cache,
866
if it has been registered before.
869
return self._styles[style.fg, style.bg, style.attrs]
872
for b in astyle.A2CURSES:
874
attrs |= astyle.A2CURSES[b]
876
color = self._colors[style.fg, style.bg]
880
astyle.COLOR2CURSES[style.fg],
881
astyle.COLOR2CURSES[style.bg]
883
color = curses.color_pair(self._maxcolor)
884
self._colors[style.fg, style.bg] = color
887
self._styles[style.fg, style.bg, style.attrs] = c
890
def addstr(self, y, x, begx, endx, text, style):
892
A version of ``curses.addstr()`` that can handle ``x`` coordinates
893
that are outside the screen.
895
text2 = text[max(0, begx-x):max(0, endx-x)]
897
self.scr.addstr(y, max(x, begx), text2, self.getstyle(style))
900
def addchr(self, y, x, begx, endx, c, l, style):
904
self.scr.addstr(y, x0, c*(x1-x0), self.getstyle(style))
907
def _calcheaderlines(self, levels):
908
# Calculate how many headerlines do we have to display, if we have
909
# ``levels`` browser levels
911
levels = len(self.levels)
912
self._headerlines = min(self.maxheaders, levels)
913
self._firstheaderline = levels-self._headerlines
915
def getstylehere(self, style):
917
Return a style for displaying the original style ``style``
918
in the row the cursor is on.
920
return astyle.Style(style.fg, astyle.COLOR_BLUE, style.attrs | astyle.A_BOLD)
922
def report(self, msg):
924
Store the message ``msg`` for display below the footer line. This
925
will be displayed as soon as the screen is redrawn.
929
def enter(self, item, *attrs):
931
Enter the object ``item``. If ``attrs`` is specified, it will be used
932
as a fixed list of attributes to display.
934
if self.levels and item is self.levels[-1].input:
936
self.report(CommandError("Recursion on input object"))
938
oldlevels = len(self.levels)
939
self._calcheaderlines(oldlevels+1)
941
level = _BrowserLevel(
944
self.scrsizey-1-self._headerlines-2,
947
except (KeyboardInterrupt, SystemExit):
949
except Exception, exc:
952
self._calcheaderlines(oldlevels)
956
self.levels.append(level)
958
def startkeyboardinput(self, mode):
960
Enter mode ``mode``, which requires keyboard input.
963
self.prompts[mode].start()
965
def keylabel(self, keycode):
967
Return a pretty name for the ``curses`` key ``keycode`` (used in the
968
help screen and in reports about unassigned keys).
975
ord("\x7f"): "DELETE",
976
ord("\x08"): "BACKSPACE",
978
if keycode in specialsnames:
979
return specialsnames[keycode]
980
elif 0x00 < keycode < 0x20:
981
return "CTRL-%s" % chr(keycode + 64)
982
return repr(chr(keycode))
983
for name in dir(curses):
984
if name.startswith("KEY_") and getattr(curses, name) == keycode:
988
def beep(self, force=False):
989
if force or self._dobeep:
991
# don't beep again (as long as the same key is pressed)
996
Move the cursor to the previous row.
998
level = self.levels[-1]
1000
level.moveto(level.curx, level.cury-self.stepy)
1004
Move the cursor to the next row.
1006
level = self.levels[-1]
1008
level.moveto(level.curx, level.cury+self.stepy)
1010
def cmd_pageup(self):
1012
Move the cursor up one page.
1014
level = self.levels[-1]
1015
self.report("page up")
1016
level.moveto(level.curx, level.cury-level.mainsizey+self.pageoverlapy)
1018
def cmd_pagedown(self):
1020
Move the cursor down one page.
1022
level = self.levels[-1]
1023
self.report("page down")
1024
level.moveto(level.curx, level.cury+level.mainsizey-self.pageoverlapy)
1028
Move the cursor left.
1030
level = self.levels[-1]
1032
level.moveto(level.curx-self.stepx, level.cury)
1034
def cmd_right(self):
1036
Move the cursor right.
1038
level = self.levels[-1]
1039
self.report("right")
1040
level.moveto(level.curx+self.stepx, level.cury)
1044
Move the cursor to the first column.
1046
level = self.levels[-1]
1048
level.moveto(0, level.cury)
1052
Move the cursor to the last column.
1054
level = self.levels[-1]
1056
level.moveto(level.datasizex+level.mainsizey-self.pageoverlapx, level.cury)
1058
def cmd_prevattr(self):
1060
Move the cursor one attribute column to the left.
1062
level = self.levels[-1]
1063
if level.displayattr[0] is None or level.displayattr[0] == 0:
1066
self.report("prevattr")
1068
for (i, attrname) in enumerate(level.displayattrs):
1069
if i == level.displayattr[0]-1:
1071
pos += level.colwidths[attrname] + 1
1072
level.moveto(pos, level.cury)
1074
def cmd_nextattr(self):
1076
Move the cursor one attribute column to the right.
1078
level = self.levels[-1]
1079
if level.displayattr[0] is None or level.displayattr[0] == len(level.displayattrs)-1:
1082
self.report("nextattr")
1084
for (i, attrname) in enumerate(level.displayattrs):
1085
if i == level.displayattr[0]+1:
1087
pos += level.colwidths[attrname] + 1
1088
level.moveto(pos, level.cury)
1092
'Pick' the object under the cursor (i.e. the row the cursor is on).
1093
This leaves the browser and returns the picked object to the caller.
1094
(In IPython this object will be available as the ``_`` variable.)
1096
level = self.levels[-1]
1097
self.returnvalue = level.items[level.cury].item
1100
def cmd_pickattr(self):
1102
'Pick' the attribute under the cursor (i.e. the row/column the
1105
level = self.levels[-1]
1106
attr = level.displayattr[1]
1107
if attr is ipipe.noitem:
1109
self.report(CommandError("no column under cursor"))
1111
value = attr.value(level.items[level.cury].item)
1112
if value is ipipe.noitem:
1114
self.report(AttributeError(attr.name()))
1116
self.returnvalue = value
1119
def cmd_pickallattrs(self):
1121
Pick' the complete column under the cursor (i.e. the attribute under
1122
the cursor) from all currently fetched objects. These attributes
1123
will be returned as a list.
1125
level = self.levels[-1]
1126
attr = level.displayattr[1]
1127
if attr is ipipe.noitem:
1129
self.report(CommandError("no column under cursor"))
1132
for cache in level.items:
1133
value = attr.value(cache.item)
1134
if value is not ipipe.noitem:
1135
result.append(value)
1136
self.returnvalue = result
1139
def cmd_pickmarked(self):
1141
'Pick' marked objects. Marked objects will be returned as a list.
1143
level = self.levels[-1]
1144
self.returnvalue = [cache.item for cache in level.items if cache.marked]
1147
def cmd_pickmarkedattr(self):
1149
'Pick' the attribute under the cursor from all marked objects
1150
(This returns a list).
1153
level = self.levels[-1]
1154
attr = level.displayattr[1]
1155
if attr is ipipe.noitem:
1157
self.report(CommandError("no column under cursor"))
1160
for cache in level.items:
1162
value = attr.value(cache.item)
1163
if value is not ipipe.noitem:
1164
result.append(value)
1165
self.returnvalue = result
1168
def cmd_pickinput(self):
1170
Use the object under the cursor (i.e. the row the cursor is on) as
1171
the next input line. This leaves the browser and puts the picked object
1174
level = self.levels[-1]
1175
value = level.items[level.cury].item
1176
self.returnvalue = None
1178
api.set_next_input(str(value))
1181
def cmd_pickinputattr(self):
1183
Use the attribute under the cursor i.e. the row/column the cursor is on)
1184
as the next input line. This leaves the browser and puts the picked
1185
object in the input.
1187
level = self.levels[-1]
1188
attr = level.displayattr[1]
1189
if attr is ipipe.noitem:
1191
self.report(CommandError("no column under cursor"))
1193
value = attr.value(level.items[level.cury].item)
1194
if value is ipipe.noitem:
1196
self.report(AttributeError(attr.name()))
1197
self.returnvalue = None
1199
api.set_next_input(str(value))
1202
def cmd_markrange(self):
1204
Mark all objects from the last marked object before the current cursor
1205
position to the cursor position.
1207
level = self.levels[-1]
1208
self.report("markrange")
1211
for i in xrange(level.cury, -1, -1):
1212
if level.items[i].marked:
1216
self.report(CommandError("no mark before cursor"))
1219
for i in xrange(start, level.cury+1):
1220
cache = level.items[i]
1221
if not cache.marked:
1225
def cmd_enter(self):
1227
Enter the object under the cursor. (what this mean depends on the object
1228
itself (i.e. how it implements iteration). This opens a new browser 'level'.
1230
level = self.levels[-1]
1232
item = level.items[level.cury].item
1234
self.report(CommandError("No object"))
1237
self.report("entering object...")
1240
def cmd_leave(self):
1242
Leave the current browser level and go back to the previous one.
1244
self.report("leave")
1245
if len(self.levels) > 1:
1246
self._calcheaderlines(len(self.levels)-1)
1249
self.report(CommandError("This is the last level"))
1252
def cmd_enterattr(self):
1254
Enter the attribute under the cursor.
1256
level = self.levels[-1]
1257
attr = level.displayattr[1]
1258
if attr is ipipe.noitem:
1260
self.report(CommandError("no column under cursor"))
1263
item = level.items[level.cury].item
1265
self.report(CommandError("No object"))
1268
value = attr.value(item)
1270
if value is ipipe.noitem:
1271
self.report(AttributeError(name))
1273
self.report("entering object attribute %s..." % name)
1276
def cmd_detail(self):
1278
Show a detail view of the object under the cursor. This shows the
1279
name, type, doc string and value of the object attributes (and it
1280
might show more attributes than in the list view, depending on
1283
level = self.levels[-1]
1285
item = level.items[level.cury].item
1287
self.report(CommandError("No object"))
1290
self.report("entering detail view for object...")
1291
attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
1294
def cmd_detailattr(self):
1296
Show a detail view of the attribute under the cursor.
1298
level = self.levels[-1]
1299
attr = level.displayattr[1]
1300
if attr is ipipe.noitem:
1302
self.report(CommandError("no attribute"))
1305
item = level.items[level.cury].item
1307
self.report(CommandError("No object"))
1311
item = attr.value(item)
1312
except (KeyboardInterrupt, SystemExit):
1314
except Exception, exc:
1317
self.report("entering detail view for attribute %s..." % attr.name())
1318
attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
1321
def cmd_tooglemark(self):
1323
Mark/unmark the object under the cursor. Marked objects have a '!'
1324
after the row number).
1326
level = self.levels[-1]
1327
self.report("toggle mark")
1329
item = level.items[level.cury]
1330
except IndexError: # no items?
1340
def cmd_sortattrasc(self):
1342
Sort the objects (in ascending order) using the attribute under
1343
the cursor as the sort key.
1345
level = self.levels[-1]
1346
attr = level.displayattr[1]
1347
if attr is ipipe.noitem:
1349
self.report(CommandError("no column under cursor"))
1351
self.report("sort by %s (ascending)" % attr.name())
1354
return attr.value(item)
1355
except (KeyboardInterrupt, SystemExit):
1361
def cmd_sortattrdesc(self):
1363
Sort the objects (in descending order) using the attribute under
1364
the cursor as the sort key.
1366
level = self.levels[-1]
1367
attr = level.displayattr[1]
1368
if attr is ipipe.noitem:
1370
self.report(CommandError("no column under cursor"))
1372
self.report("sort by %s (descending)" % attr.name())
1375
return attr.value(item)
1376
except (KeyboardInterrupt, SystemExit):
1380
level.sort(key, reverse=True)
1382
def cmd_hideattr(self):
1384
Hide the attribute under the cursor.
1386
level = self.levels[-1]
1387
if level.displayattr[0] is None:
1390
self.report("hideattr")
1391
level.hiddenattrs.add(level.displayattr[1])
1392
level.moveto(level.curx, level.cury, refresh=True)
1394
def cmd_unhideattrs(self):
1396
Make all attributes visible again.
1398
level = self.levels[-1]
1399
self.report("unhideattrs")
1400
level.hiddenattrs.clear()
1401
level.moveto(level.curx, level.cury, refresh=True)
1405
Jump to a row. The row number can be entered at the
1406
bottom of the screen.
1408
self.startkeyboardinput("goto")
1412
Search forward for a row. The search condition can be entered at the
1413
bottom of the screen.
1415
self.startkeyboardinput("find")
1417
def cmd_findbackwards(self):
1419
Search backward for a row. The search condition can be entered at the
1420
bottom of the screen.
1422
self.startkeyboardinput("findbackwards")
1424
def cmd_refresh(self):
1426
Refreshes the display by restarting the iterator.
1428
level = self.levels[-1]
1429
self.report("refresh")
1432
def cmd_refreshfind(self):
1434
Refreshes the display by restarting the iterator and goes back to the
1435
same object the cursor was on before restarting (if this object can't be
1436
found the cursor jumps back to the first object).
1438
level = self.levels[-1]
1439
self.report("refreshfind")
1444
Opens the help screen as a new browser level, describing keyboard
1447
for level in self.levels:
1448
if isinstance(level.input, _BrowserHelp):
1450
self.report(CommandError("help already active"))
1453
self.enter(_BrowserHelp(self))
1457
Quit the browser and return to the IPython prompt.
1459
self.returnvalue = None
1462
def sigwinchhandler(self, signal, frame):
1465
def _dodisplay(self, scr):
1467
This method is the workhorse of the browser. It handles screen
1468
drawing and the keyboard.
1475
for cmd in ("quit", "help"):
1476
key = self.keymap.findkey(cmd, None)
1478
keys.append("%s=%s" % (self.keylabel(key), cmd))
1479
helpmsg = " | %s" % " ".join(keys)
1482
msg = "Fetching first batch of objects..."
1483
(self.scrsizey, self.scrsizex) = scr.getmaxyx()
1484
scr.addstr(self.scrsizey//2, (self.scrsizex-len(msg))//2, msg)
1490
# enter the first level
1491
self.enter(self.input, *self.attrs)
1493
self._calcheaderlines(None)
1496
level = self.levels[-1]
1497
(self.scrsizey, self.scrsizex) = scr.getmaxyx()
1498
level.mainsizey = self.scrsizey-1-self._headerlines-footery
1500
# Paint object header
1501
for i in xrange(self._firstheaderline, self._firstheaderline+self._headerlines):
1504
posy = i-self._firstheaderline
1505
endx = self.scrsizex
1506
if i: # not the first level
1507
msg = " (%d/%d" % (self.levels[i-1].cury, len(self.levels[i-1].items))
1508
if not self.levels[i-1].exhausted:
1512
posx += self.addstr(posy, posx, 0, endx, " ibrowse #%d: " % i, self.style_objheadertext)
1513
for (style, text) in lv.header:
1514
posx += self.addstr(posy, posx, 0, endx, text, self.style_objheaderobject)
1518
posx += self.addstr(posy, posx, 0, self.scrsizex, msg, self.style_objheadernumber)
1519
posx += self.addchr(posy, posx, 0, self.scrsizex, " ", self.scrsizex-posx, self.style_objheadernumber)
1522
self.addchr(self._headerlines, 0, 0, self.scrsizex, " ", self.scrsizex, self.style_colheader)
1523
self.addstr(self._headerlines+1, 0, 0, self.scrsizex, " <empty>", astyle.style_error)
1526
# Paint column headers
1527
scr.move(self._headerlines, 0)
1528
scr.addstr(" %*s " % (level.numbersizex, "#"), self.getstyle(self.style_colheader))
1529
scr.addstr(self.headersepchar, self.getstyle(self.style_colheadersep))
1530
begx = level.numbersizex+3
1531
posx = begx-level.datastartx
1532
for attr in level.displayattrs:
1533
attrname = attr.name()
1534
cwidth = level.colwidths[attr]
1535
header = attrname.ljust(cwidth)
1536
if attr is level.displayattr[1]:
1537
style = self.style_colheaderhere
1539
style = self.style_colheader
1540
posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, header, style)
1541
posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, self.headersepchar, self.style_colheadersep)
1542
if posx >= self.scrsizex:
1545
scr.addstr(" "*(self.scrsizex-posx), self.getstyle(self.style_colheader))
1548
posy = self._headerlines+1+level.datastarty
1549
for i in xrange(level.datastarty, min(level.datastarty+level.mainsizey, len(level.items))):
1550
cache = level.items[i]
1552
style = self.style_numberhere
1554
style = self.style_number
1556
posy = self._headerlines+1+i-level.datastarty
1557
posx = begx-level.datastartx
1560
scr.addstr(" %*d%s" % (level.numbersizex, i, " !"[cache.marked]), self.getstyle(style))
1561
scr.addstr(self.headersepchar, self.getstyle(self.style_sep))
1563
for attrname in level.displayattrs:
1564
cwidth = level.colwidths[attrname]
1566
(align, length, parts) = level.displayrows[i-level.datastarty][attrname]
1569
style = astyle.style_nodata
1571
style = self.getstylehere(style)
1572
padstyle = self.style_datapad
1573
sepstyle = self.style_sep
1575
padstyle = self.getstylehere(padstyle)
1576
sepstyle = self.getstylehere(sepstyle)
1578
posx += self.addchr(posy, posx, begx, self.scrsizex, self.nodatachar, cwidth, style)
1581
posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
1583
pad1 = (cwidth-length)//2
1584
pad2 = cwidth-length-len(pad1)
1585
posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad1, padstyle)
1586
for (style, text) in parts:
1588
style = self.getstylehere(style)
1589
posx += self.addstr(posy, posx, begx, self.scrsizex, text, style)
1590
if posx >= self.scrsizex:
1593
posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
1595
posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad2, padstyle)
1596
posx += self.addstr(posy, posx, begx, self.scrsizex, self.datasepchar, sepstyle)
1600
# Add blank row headers for the rest of the screen
1601
for posy in xrange(posy+1, self.scrsizey-2):
1602
scr.addstr(posy, 0, " " * (level.numbersizex+2), self.getstyle(self.style_colheader))
1605
posy = self.scrsizey-footery
1607
scr.addstr(posy, 0, " "*self.scrsizex, self.getstyle(self.style_footer))
1614
endx = self.scrsizex-len(helpmsg)-1
1615
scr.addstr(posy, endx, helpmsg, self.getstyle(self.style_footer))
1618
msg = " %d%s objects (%d marked): " % (len(level.items), flag, level.marked)
1619
posx += self.addstr(posy, posx, 0, endx, msg, self.style_footer)
1621
item = level.items[level.cury].item
1622
except IndexError: # empty
1625
for (nostyle, text) in ipipe.xrepr(item, "footer"):
1626
if not isinstance(nostyle, int):
1627
posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
1631
attrstyle = [(astyle.style_default, "no attribute")]
1632
attr = level.displayattr[1]
1633
if attr is not ipipe.noitem and not isinstance(attr, ipipe.SelfDescriptor):
1634
posx += self.addstr(posy, posx, 0, endx, " | ", self.style_footer)
1635
posx += self.addstr(posy, posx, 0, endx, attr.name(), self.style_footer)
1636
posx += self.addstr(posy, posx, 0, endx, ": ", self.style_footer)
1638
value = attr.value(item)
1639
except (SystemExit, KeyboardInterrupt):
1641
except Exception, exc:
1643
if value is not ipipe.noitem:
1644
attrstyle = ipipe.xrepr(value, "footer")
1645
for (nostyle, text) in attrstyle:
1646
if not isinstance(nostyle, int):
1647
posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
1652
# Display input prompt
1653
if self.mode in self.prompts:
1654
history = self.prompts[self.mode]
1656
posy = self.scrsizey-1
1657
posx += self.addstr(posy, posx, 0, endx, history.prompt, astyle.style_default)
1658
posx += self.addstr(posy, posx, 0, endx, " [", astyle.style_default)
1659
if history.cury==-1:
1662
text = str(history.cury+1)
1663
posx += self.addstr(posy, posx, 0, endx, text, astyle.style_type_number)
1665
posx += self.addstr(posy, posx, 0, endx, "/", astyle.style_default)
1666
posx += self.addstr(posy, posx, 0, endx, str(len(history.history)), astyle.style_type_number)
1667
posx += self.addstr(posy, posx, 0, endx, "]: ", astyle.style_default)
1669
posx += self.addstr(posy, posx, 0, endx, history.input, astyle.style_default)
1672
if self._report is not None:
1673
if isinstance(self._report, Exception):
1674
style = self.getstyle(astyle.style_error)
1675
if self._report.__class__.__module__ == "exceptions":
1677
(self._report.__class__.__name__, self._report)
1679
msg = "%s.%s: %s" % \
1680
(self._report.__class__.__module__,
1681
self._report.__class__.__name__, self._report)
1683
style = self.getstyle(self.style_report)
1685
scr.addstr(self.scrsizey-1, 0, msg[:self.scrsizex], style)
1688
scr.move(self.scrsizey-1, 0)
1689
except curses.error:
1690
# Protect against errors from writing to the last line
1695
if self.mode in self.prompts:
1696
history = self.prompts[self.mode]
1697
scr.move(self.scrsizey-1, inputstartx+history.curx)
1700
1+self._headerlines+level.cury-level.datastarty,
1701
level.numbersizex+3+level.curx-level.datastartx
1709
size = fcntl.ioctl(0, tty.TIOCGWINSZ, "12345678")
1710
size = struct.unpack("4H", size)
1711
oldsize = scr.getmaxyx()
1713
curses.resize_term(size[0], size[1])
1714
newsize = scr.getmaxyx()
1716
for l in self.levels:
1717
l.mainsizey += newsize[0]-oldsize[0]
1718
l.moveto(l.curx, l.cury, refresh=True)
1720
self.resized = False
1722
if self.mode in self.prompts:
1723
if self.prompts[self.mode].handlekey(self, c):
1726
# if no key is pressed slow down and beep again
1732
# if a different key was pressed slow down and beep too
1738
cmdname = self.keymap.get(c, None)
1741
UnassignedKeyError("Unassigned key %s" %
1744
cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
1747
UnknownCommandError("Unknown command %r" %
1750
returnvalue = self.returnvalue
1751
self.returnvalue = None
1753
self.stepx = self.nextstepx(self.stepx)
1754
self.stepy = self.nextstepy(self.stepy)
1755
curses.flushinp() # get rid of type ahead
1760
if hasattr(curses, "resize_term"):
1761
oldhandler = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1763
return curses.wrapper(self._dodisplay)
1765
signal.signal(signal.SIGWINCH, oldhandler)
1767
return curses.wrapper(self._dodisplay)