~ipython-dev/ipython/0.10.1

« back to all changes in this revision

Viewing changes to IPython/Extensions/ibrowse.py

  • Committer: Fernando Perez
  • Date: 2008-06-02 01:26:30 UTC
  • mfrom: (0.1.130 ipython-local)
  • Revision ID: fernando.perez@berkeley.edu-20080602012630-m14vezrhydzvahf8
Merge in all development done in bzr since February 16 2008.

At that time, a clean bzr branch was started from the SVN tree, but
without SVN history.  That SVN history has now been used as the basis
of this branch, and the development done on the history-less BZR
branch has been added and is the content of this merge.  

This branch will be the new official main line of development in
Launchpad (equivalent to the old SVN trunk).

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: iso-8859-1 -*-
 
2
 
 
3
import curses, fcntl, signal, struct, tty, textwrap, inspect
 
4
 
 
5
from IPython import ipapi
 
6
 
 
7
import astyle, ipipe
 
8
 
 
9
 
 
10
# Python 2.3 compatibility
 
11
try:
 
12
    set
 
13
except NameError:
 
14
    import sets
 
15
    set = sets.Set
 
16
 
 
17
# Python 2.3 compatibility
 
18
try:
 
19
    sorted
 
20
except NameError:
 
21
    from ipipe import sorted
 
22
 
 
23
 
 
24
class UnassignedKeyError(Exception):
 
25
    """
 
26
    Exception that is used for reporting unassigned keys.
 
27
    """
 
28
 
 
29
 
 
30
class UnknownCommandError(Exception):
 
31
    """
 
32
    Exception that is used for reporting unknown commands (this should never
 
33
    happen).
 
34
    """
 
35
 
 
36
 
 
37
class CommandError(Exception):
 
38
    """
 
39
    Exception that is used for reporting that a command can't be executed.
 
40
    """
 
41
 
 
42
 
 
43
class Keymap(dict):
 
44
    """
 
45
    Stores mapping of keys to commands.
 
46
    """
 
47
    def __init__(self):
 
48
        self._keymap = {}
 
49
 
 
50
    def __setitem__(self, key, command):
 
51
        if isinstance(key, str):
 
52
            for c in key:
 
53
                dict.__setitem__(self, ord(c), command)
 
54
        else:
 
55
            dict.__setitem__(self, key, command)
 
56
 
 
57
    def __getitem__(self, key):
 
58
        if isinstance(key, str):
 
59
            key = ord(key)
 
60
        return dict.__getitem__(self, key)
 
61
 
 
62
    def __detitem__(self, key):
 
63
        if isinstance(key, str):
 
64
            key = ord(key)
 
65
        dict.__detitem__(self, key)
 
66
 
 
67
    def register(self, command, *keys):
 
68
        for key in keys:
 
69
            self[key] = command
 
70
 
 
71
    def get(self, key, default=None):
 
72
        if isinstance(key, str):
 
73
            key = ord(key)
 
74
        return dict.get(self, key, default)
 
75
 
 
76
    def findkey(self, command, default=ipipe.noitem):
 
77
        for (key, commandcandidate) in self.iteritems():
 
78
            if commandcandidate == command:
 
79
                return key
 
80
        if default is ipipe.noitem:
 
81
            raise KeyError(command)
 
82
        return default
 
83
 
 
84
 
 
85
class _BrowserCachedItem(object):
 
86
    # This is used internally by ``ibrowse`` to store a item together with its
 
87
    # marked status.
 
88
    __slots__ = ("item", "marked")
 
89
 
 
90
    def __init__(self, item):
 
91
        self.item = item
 
92
        self.marked = False
 
93
 
 
94
 
 
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
 
100
 
 
101
    def __xrepr__(self, mode):
 
102
        yield (-1, True)
 
103
        if mode == "header" or mode == "footer":
 
104
            yield (astyle.style_default, "ibrowse help screen")
 
105
        else:
 
106
            yield (astyle.style_default, repr(self))
 
107
 
 
108
    def __iter__(self):
 
109
        # Get reverse key mapping
 
110
        allkeys = {}
 
111
        for (key, cmd) in self.browser.keymap.iteritems():
 
112
            allkeys.setdefault(cmd, []).append(key)
 
113
 
 
114
        fields = ("key", "description")
 
115
 
 
116
        commands = []
 
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))
 
121
        commands.sort()
 
122
        commands = [(c[1], c[2]) for c in commands]
 
123
        for (i, (name, command)) in enumerate(commands):
 
124
            if i:
 
125
                yield ipipe.Fields(fields, key="", description="")
 
126
 
 
127
            description = command.__doc__
 
128
            if description is None:
 
129
                lines = []
 
130
            else:
 
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, [])
 
135
 
 
136
            yield ipipe.Fields(fields, key="", description=astyle.Text((self.style_header, name)))
 
137
            for i in xrange(max(len(keys), len(lines))):
 
138
                try:
 
139
                    key = self.browser.keylabel(keys[i])
 
140
                except IndexError:
 
141
                    key = ""
 
142
                try:
 
143
                    line = lines[i]
 
144
                except IndexError:
 
145
                    line = ""
 
146
                yield ipipe.Fields(fields, key=key, description=line)
 
147
 
 
148
 
 
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
 
153
    # a stack.
 
154
    def __init__(self, browser, input, mainsizey, *attrs):
 
155
        self.browser = browser
 
156
        self.input = input
 
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)
 
160
 
 
161
        # is the iterator exhausted?
 
162
        self.exhausted = False
 
163
 
 
164
        # attributes to be display (autodetected if empty)
 
165
        self.attrs = attrs
 
166
 
 
167
        # fetched items (+ marked flag)
 
168
        self.items = ipipe.deque()
 
169
 
 
170
        # Number of marked objects
 
171
        self.marked = 0
 
172
 
 
173
        # Vertical cursor position
 
174
        self.cury = 0
 
175
 
 
176
        # Horizontal cursor position
 
177
        self.curx = 0
 
178
 
 
179
        # Index of first data column
 
180
        self.datastartx = 0
 
181
 
 
182
        # Index of first data line
 
183
        self.datastarty = 0
 
184
 
 
185
        # height of the data display area
 
186
        self.mainsizey = mainsizey
 
187
 
 
188
        # width of the data display area (changes when scrolling)
 
189
        self.mainsizex = 0
 
190
 
 
191
        # Size of row number (changes when scrolling)
 
192
        self.numbersizex = 0
 
193
 
 
194
        # Attributes to display (in this order)
 
195
        self.displayattrs = []
 
196
 
 
197
        # index and attribute under the cursor
 
198
        self.displayattr = (None, ipipe.noitem)
 
199
 
 
200
        # Maps attributes to column widths
 
201
        self.colwidths = {}
 
202
 
 
203
        # Set of hidden attributes
 
204
        self.hiddenattrs = set()
 
205
 
 
206
        # This takes care of all the caches etc.
 
207
        self.moveto(0, 0, refresh=True)
 
208
 
 
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:
 
213
            try:
 
214
                item = self.iterator.next()
 
215
            except StopIteration:
 
216
                self.exhausted = True
 
217
                break
 
218
            except (KeyboardInterrupt, SystemExit):
 
219
                raise
 
220
            except Exception, exc:
 
221
                have += 1
 
222
                self.items.append(_BrowserCachedItem(exc))
 
223
                self.exhausted = True
 
224
                break
 
225
            else:
 
226
                have += 1
 
227
                self.items.append(_BrowserCachedItem(item))
 
228
 
 
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``)
 
232
 
 
233
        attrs = set()
 
234
        self.displayattrs = []
 
235
        if self.attrs:
 
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)
 
242
                    attrs.add(attr)
 
243
        else:
 
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)
 
249
                        attrs.add(attr)
 
250
 
 
251
    def getrow(self, i):
 
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
 
255
        # called before.
 
256
        row = {}
 
257
        item = self.items[i].item
 
258
        for attr in self.displayattrs:
 
259
            try:
 
260
                value = attr.value(item)
 
261
            except (KeyboardInterrupt, SystemExit):
 
262
                raise
 
263
            except Exception, exc:
 
264
                value = 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)
 
269
        return row
 
270
 
 
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.
 
277
        self.colwidths = {}
 
278
        for row in self.displayrows:
 
279
            for attr in self.displayattrs:
 
280
                try:
 
281
                    length = row[attr][1]
 
282
                except KeyError:
 
283
                    length = 0
 
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
 
289
 
 
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)
 
296
 
 
297
    def calcdisplayattr(self):
 
298
        # Find out which attribute the cursor is on and store this
 
299
        # information in ``self.displayattr``.
 
300
        pos = 0
 
301
        for (i, attr) in enumerate(self.displayattrs):
 
302
            if pos+self.colwidths[attr] >= self.curx:
 
303
                self.displayattr = (i, attr)
 
304
                break
 
305
            pos += self.colwidths[attr]+1
 
306
        else:
 
307
            self.displayattr = (None, ipipe.noitem)
 
308
 
 
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
 
315
        oldx = self.curx
 
316
        oldy = self.cury
 
317
        x = int(x+0.5)
 
318
        y = int(y+0.5)
 
319
        newx = x # remember where we wanted to move
 
320
        newy = y # remember where we wanted to move
 
321
 
 
322
        scrollbordery = min(self.browser.scrollbordery, self.mainsizey//2)
 
323
        scrollborderx = min(self.browser.scrollborderx, self.mainsizex//2)
 
324
 
 
325
        # Make sure that the cursor didn't leave the main area vertically
 
326
        if y < 0:
 
327
            y = 0
 
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)
 
332
 
 
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))
 
339
 
 
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))
 
344
            self.calcwidths()
 
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:]
 
358
                # fetch new items
 
359
                for i in xrange(min(olddatastarty, self.datastarty+self.mainsizey)-1,
 
360
                                self.datastarty-1, -1):
 
361
                    try:
 
362
                        row = self.getrow(i)
 
363
                    except IndexError:
 
364
                        # we didn't have enough objects to fill the screen
 
365
                        break
 
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]
 
370
                # fetch new items
 
371
                for i in xrange(max(olddatastarty+self.mainsizey, self.datastarty),
 
372
                                self.datastarty+self.mainsizey):
 
373
                    try:
 
374
                        row = self.getrow(i)
 
375
                    except IndexError:
 
376
                        # we didn't have enough objects to fill the screen
 
377
                        break
 
378
                    self.displayrows.append(row)
 
379
            self.calcwidths()
 
380
 
 
381
        # Make sure that the cursor didn't leave the data area horizontally
 
382
        if x < 0:
 
383
            x = 0
 
384
        elif x >= self.datasizex:
 
385
            x = max(0, self.datasizex-1)
 
386
 
 
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))
 
393
 
 
394
        if x == oldx and y == oldy and (x != newx or y != newy): # couldn't move
 
395
            self.browser.beep()
 
396
        else:
 
397
            self.curx = x
 
398
            self.cury = y
 
399
            self.calcdisplayattr()
 
400
 
 
401
    def sort(self, key, reverse=False):
 
402
        """
 
403
        Sort the currently list of items using the key function ``key``. If
 
404
        ``reverse`` is true the sort order is reversed.
 
405
        """
 
406
        curitem = self.items[self.cury] # Remember where the cursor is now
 
407
 
 
408
        # Sort items
 
409
        def realkey(item):
 
410
            return key(item.item)
 
411
        self.items = ipipe.deque(sorted(self.items, key=realkey, reverse=reverse))
 
412
 
 
413
        # Find out where the object under the cursor went
 
414
        cury = self.cury
 
415
        for (i, item) in enumerate(self.items):
 
416
            if item is curitem:
 
417
                cury = i
 
418
                break
 
419
 
 
420
        self.moveto(self.curx, cury, refresh=True)
 
421
 
 
422
    def refresh(self):
 
423
        """
 
424
        Restart iterating the input.
 
425
        """
 
426
        self.iterator = ipipe.xiter(self.input)
 
427
        self.items.clear()
 
428
        self.exhausted = False
 
429
        self.datastartx = self.datastarty = 0
 
430
        self.moveto(0, 0, refresh=True)
 
431
 
 
432
    def refreshfind(self):
 
433
        """
 
434
        Restart iterating the input and go back to the same object as before
 
435
        (if it can be found in the new iterator).
 
436
        """
 
437
        try:
 
438
            oldobject = self.items[self.cury].item
 
439
        except IndexError:
 
440
            oldobject = ipipe.noitem
 
441
        self.iterator = ipipe.xiter(self.input)
 
442
        self.items.clear()
 
443
        self.exhausted = False
 
444
        while True:
 
445
            self.fetch(len(self.items)+1)
 
446
            if self.exhausted:
 
447
                curses.beep()
 
448
                self.datastartx = self.datastarty = 0
 
449
                self.moveto(self.curx, 0, refresh=True)
 
450
                break
 
451
            if self.items[-1].item == oldobject:
 
452
                self.datastartx = self.datastarty = 0
 
453
                self.moveto(self.curx, len(self.items)-1, refresh=True)
 
454
                break
 
455
 
 
456
 
 
457
class _CommandInput(object):
 
458
    keymap = Keymap()
 
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
 
473
 
 
474
    def __init__(self, prompt):
 
475
        self.prompt = prompt
 
476
        self.history = []
 
477
        self.maxhistory = 100
 
478
        self.input = ""
 
479
        self.curx = 0
 
480
        self.cury = -1 # blank line
 
481
 
 
482
    def start(self):
 
483
        self.input = ""
 
484
        self.curx = 0
 
485
        self.cury = -1 # blank line
 
486
 
 
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)
 
493
            curses.beep()
 
494
        elif key != -1:
 
495
            try:
 
496
                char = chr(key)
 
497
            except ValueError:
 
498
                curses.beep()
 
499
            else:
 
500
                return self.handlechar(browser, char)
 
501
 
 
502
    def handlechar(self, browser, char):
 
503
        self.input = self.input[:self.curx] + char + self.input[self.curx:]
 
504
        self.curx += 1
 
505
        return True
 
506
 
 
507
    def dohistory(self):
 
508
        self.history.insert(0, self.input)
 
509
        del self.history[:-self.maxhistory]
 
510
 
 
511
    def cmd_backspace(self, browser):
 
512
        if self.curx:
 
513
            self.input = self.input[:self.curx-1] + self.input[self.curx:]
 
514
            self.curx -= 1
 
515
            return True
 
516
        else:
 
517
            curses.beep()
 
518
 
 
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:]
 
522
            return True
 
523
        else:
 
524
            curses.beep()
 
525
 
 
526
    def cmd_delend(self, browser):
 
527
        if self.curx<len(self.input):
 
528
            self.input = self.input[:self.curx]
 
529
            return True
 
530
 
 
531
    def cmd_left(self, browser):
 
532
        if self.curx:
 
533
            self.curx -= 1
 
534
            return True
 
535
        else:
 
536
            curses.beep()
 
537
 
 
538
    def cmd_right(self, browser):
 
539
        if self.curx < len(self.input):
 
540
            self.curx += 1
 
541
            return True
 
542
        else:
 
543
            curses.beep()
 
544
 
 
545
    def cmd_home(self, browser):
 
546
        if self.curx:
 
547
            self.curx = 0
 
548
            return True
 
549
        else:
 
550
            curses.beep()
 
551
 
 
552
    def cmd_end(self, browser):
 
553
        if self.curx < len(self.input):
 
554
            self.curx = len(self.input)
 
555
            return True
 
556
        else:
 
557
            curses.beep()
 
558
 
 
559
    def cmd_up(self, browser):
 
560
        if self.cury < len(self.history)-1:
 
561
            self.cury += 1
 
562
            self.input = self.history[self.cury]
 
563
            self.curx = len(self.input)
 
564
            return True
 
565
        else:
 
566
            curses.beep()
 
567
 
 
568
    def cmd_down(self, browser):
 
569
        if self.cury >= 0:
 
570
            self.cury -= 1
 
571
            if self.cury>=0:
 
572
                self.input = self.history[self.cury]
 
573
            else:
 
574
                self.input = ""
 
575
            self.curx = len(self.input)
 
576
            return True
 
577
        else:
 
578
            curses.beep()
 
579
 
 
580
    def cmd_incsearchup(self, browser):
 
581
        prefix = self.input[:self.curx]
 
582
        cury = self.cury
 
583
        while True:
 
584
            cury += 1
 
585
            if cury >= len(self.history):
 
586
                break
 
587
            if self.history[cury].startswith(prefix):
 
588
                self.input = self.history[cury]
 
589
                self.cury = cury
 
590
                return True
 
591
        curses.beep()
 
592
 
 
593
    def cmd_incsearchdown(self, browser):
 
594
        prefix = self.input[:self.curx]
 
595
        cury = self.cury
 
596
        while True:
 
597
            cury -= 1
 
598
            if cury <= 0:
 
599
                break
 
600
            if self.history[cury].startswith(prefix):
 
601
                self.input = self.history[self.cury]
 
602
                self.cury = cury
 
603
                return True
 
604
        curses.beep()
 
605
 
 
606
    def cmd_exit(self, browser):
 
607
        browser.mode = "default"
 
608
        return True
 
609
 
 
610
    def cmd_execute(self, browser):
 
611
        raise NotImplementedError
 
612
 
 
613
 
 
614
class _CommandGoto(_CommandInput):
 
615
    def __init__(self):
 
616
        _CommandInput.__init__(self, "goto object #")
 
617
 
 
618
    def handlechar(self, browser, char):
 
619
        # Only accept digits
 
620
        if not "0" <= char <= "9":
 
621
            curses.beep()
 
622
        else:
 
623
            return _CommandInput.handlechar(self, browser, char)
 
624
 
 
625
    def cmd_execute(self, browser):
 
626
        level = browser.levels[-1]
 
627
        if self.input:
 
628
            self.dohistory()
 
629
            level.moveto(level.curx, int(self.input))
 
630
        browser.mode = "default"
 
631
        return True
 
632
 
 
633
 
 
634
class _CommandFind(_CommandInput):
 
635
    def __init__(self):
 
636
        _CommandInput.__init__(self, "find expression")
 
637
 
 
638
    def cmd_execute(self, browser):
 
639
        level = browser.levels[-1]
 
640
        if self.input:
 
641
            self.dohistory()
 
642
            while True:
 
643
                cury = level.cury
 
644
                level.moveto(level.curx, cury+1)
 
645
                if cury == level.cury:
 
646
                    curses.beep()
 
647
                    break # hit end
 
648
                item = level.items[level.cury].item
 
649
                try:
 
650
                    globals = ipipe.getglobals(None)
 
651
                    if eval(self.input, globals, ipipe.AttrNamespace(item)):
 
652
                        break # found something
 
653
                except (KeyboardInterrupt, SystemExit):
 
654
                    raise
 
655
                except Exception, exc:
 
656
                    browser.report(exc)
 
657
                    curses.beep()
 
658
                    break  # break on error
 
659
        browser.mode = "default"
 
660
        return True
 
661
 
 
662
 
 
663
class _CommandFindBackwards(_CommandInput):
 
664
    def __init__(self):
 
665
        _CommandInput.__init__(self, "find backwards expression")
 
666
 
 
667
    def cmd_execute(self, browser):
 
668
        level = browser.levels[-1]
 
669
        if self.input:
 
670
            self.dohistory()
 
671
            while level.cury:
 
672
                level.moveto(level.curx, level.cury-1)
 
673
                item = level.items[level.cury].item
 
674
                try:
 
675
                    globals = ipipe.getglobals(None)
 
676
                    if eval(self.input, globals, ipipe.AttrNamespace(item)):
 
677
                        break # found something
 
678
                except (KeyboardInterrupt, SystemExit):
 
679
                    raise
 
680
                except Exception, exc:
 
681
                    browser.report(exc)
 
682
                    curses.beep()
 
683
                    break # break on error
 
684
            else:
 
685
                curses.beep()
 
686
        browser.mode = "default"
 
687
        return True
 
688
 
 
689
 
 
690
class ibrowse(ipipe.Display):
 
691
    # Show this many lines from the previous screen when paging horizontally
 
692
    pageoverlapx = 1
 
693
 
 
694
    # Show this many lines from the previous screen when paging vertically
 
695
    pageoverlapy = 1
 
696
 
 
697
    # Start scrolling when the cursor is less than this number of columns
 
698
    # away from the left or right screen edge
 
699
    scrollborderx = 10
 
700
 
 
701
    # Start scrolling when the cursor is less than this number of lines
 
702
    # away from the top or bottom screen edge
 
703
    scrollbordery = 5
 
704
 
 
705
    # Accelerate by this factor when scrolling horizontally
 
706
    acceleratex = 1.05
 
707
 
 
708
    # Accelerate by this factor when scrolling vertically
 
709
    acceleratey = 1.05
 
710
 
 
711
    # The maximum horizontal scroll speed
 
712
    # (as a factor of the screen width (i.e. 0.5 == half a screen width)
 
713
    maxspeedx = 0.5
 
714
 
 
715
    # The maximum vertical scroll speed
 
716
    # (as a factor of the screen height (i.e. 0.5 == half a screen height)
 
717
    maxspeedy = 0.5
 
718
 
 
719
    # The maximum number of header lines for browser level
 
720
    # if the nesting is deeper, only the innermost levels are displayed
 
721
    maxheaders = 5
 
722
 
 
723
    # The approximate maximum length of a column entry
 
724
    maxattrlength = 200
 
725
 
 
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")
 
740
 
 
741
    # Column separator in header
 
742
    headersepchar = "|"
 
743
 
 
744
    # Character for padding data cell entries
 
745
    datapadchar = "."
 
746
 
 
747
    # Column separator in data area
 
748
    datasepchar = "|"
 
749
 
 
750
    # Character to use for "empty" cell (i.e. for non-existing attributes)
 
751
    nodatachar = "-"
 
752
 
 
753
    # Prompts for modes that require keyboard input
 
754
    prompts = {
 
755
        "goto": _CommandGoto(),
 
756
        "find": _CommandFind(),
 
757
        "findbackwards": _CommandFindBackwards()
 
758
    }
 
759
 
 
760
    # Maps curses key codes to "function" names
 
761
    keymap = Keymap()
 
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")
 
798
 
 
799
    def __init__(self, input=None, *attrs):
 
800
        """
 
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.
 
804
        """
 
805
        ipipe.Display.__init__(self, input)
 
806
 
 
807
        self.attrs = attrs
 
808
 
 
809
        # Stack of browser levels
 
810
        self.levels = []
 
811
        # how many colums to scroll (Changes when accelerating)
 
812
        self.stepx = 1.
 
813
 
 
814
        # how many rows to scroll (Changes when accelerating)
 
815
        self.stepy = 1.
 
816
 
 
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
 
819
        # multiple beeps).
 
820
        self._dobeep = True
 
821
 
 
822
        # Cache for registered ``curses`` colors and styles.
 
823
        self._styles = {}
 
824
        self._colors = {}
 
825
        self._maxcolor = 1
 
826
 
 
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
 
830
 
 
831
        # Index of first header line
 
832
        self._firstheaderline = 0
 
833
 
 
834
        # curses window
 
835
        self.scr = None
 
836
        # report in the footer line (error, executed command etc.)
 
837
        self._report = None
 
838
 
 
839
        # value to be returned to the caller (set by commands)
 
840
        self.returnvalue = None
 
841
 
 
842
        # The mode the browser is in
 
843
        # e.g. normal browsing or entering an argument for a command
 
844
        self.mode = "default"
 
845
 
 
846
        # set by the SIGWINCH signal handler
 
847
        self.resized = False
 
848
 
 
849
    def nextstepx(self, step):
 
850
        """
 
851
        Accelerate horizontally.
 
852
        """
 
853
        return max(1., min(step*self.acceleratex,
 
854
                           self.maxspeedx*self.levels[-1].mainsizex))
 
855
 
 
856
    def nextstepy(self, step):
 
857
        """
 
858
        Accelerate vertically.
 
859
        """
 
860
        return max(1., min(step*self.acceleratey,
 
861
                           self.maxspeedy*self.levels[-1].mainsizey))
 
862
 
 
863
    def getstyle(self, style):
 
864
        """
 
865
        Register the ``style`` with ``curses`` or get it from the cache,
 
866
        if it has been registered before.
 
867
        """
 
868
        try:
 
869
            return self._styles[style.fg, style.bg, style.attrs]
 
870
        except KeyError:
 
871
            attrs = 0
 
872
            for b in astyle.A2CURSES:
 
873
                if style.attrs & b:
 
874
                    attrs |= astyle.A2CURSES[b]
 
875
            try:
 
876
                color = self._colors[style.fg, style.bg]
 
877
            except KeyError:
 
878
                curses.init_pair(
 
879
                    self._maxcolor,
 
880
                    astyle.COLOR2CURSES[style.fg],
 
881
                    astyle.COLOR2CURSES[style.bg]
 
882
                )
 
883
                color = curses.color_pair(self._maxcolor)
 
884
                self._colors[style.fg, style.bg] = color
 
885
                self._maxcolor += 1
 
886
            c = color | attrs
 
887
            self._styles[style.fg, style.bg, style.attrs] = c
 
888
            return c
 
889
 
 
890
    def addstr(self, y, x, begx, endx, text, style):
 
891
        """
 
892
        A version of ``curses.addstr()`` that can handle ``x`` coordinates
 
893
        that are outside the screen.
 
894
        """
 
895
        text2 = text[max(0, begx-x):max(0, endx-x)]
 
896
        if text2:
 
897
            self.scr.addstr(y, max(x, begx), text2, self.getstyle(style))
 
898
        return len(text)
 
899
 
 
900
    def addchr(self, y, x, begx, endx, c, l, style):
 
901
        x0 = max(x, begx)
 
902
        x1 = min(x+l, endx)
 
903
        if x1>x0:
 
904
            self.scr.addstr(y, x0, c*(x1-x0), self.getstyle(style))
 
905
        return l
 
906
 
 
907
    def _calcheaderlines(self, levels):
 
908
        # Calculate how many headerlines do we have to display, if we have
 
909
        # ``levels`` browser levels
 
910
        if levels is None:
 
911
            levels = len(self.levels)
 
912
        self._headerlines = min(self.maxheaders, levels)
 
913
        self._firstheaderline = levels-self._headerlines
 
914
 
 
915
    def getstylehere(self, style):
 
916
        """
 
917
        Return a style for displaying the original style ``style``
 
918
        in the row the cursor is on.
 
919
        """
 
920
        return astyle.Style(style.fg, astyle.COLOR_BLUE, style.attrs | astyle.A_BOLD)
 
921
 
 
922
    def report(self, msg):
 
923
        """
 
924
        Store the message ``msg`` for display below the footer line. This
 
925
        will be displayed as soon as the screen is redrawn.
 
926
        """
 
927
        self._report = msg
 
928
 
 
929
    def enter(self, item, *attrs):
 
930
        """
 
931
        Enter the object ``item``. If ``attrs`` is specified, it will be used
 
932
        as a fixed list of attributes to display.
 
933
        """
 
934
        if self.levels and item is self.levels[-1].input:
 
935
            curses.beep()
 
936
            self.report(CommandError("Recursion on input object"))
 
937
        else:
 
938
            oldlevels = len(self.levels)
 
939
            self._calcheaderlines(oldlevels+1)
 
940
            try:
 
941
                level = _BrowserLevel(
 
942
                    self,
 
943
                    item,
 
944
                    self.scrsizey-1-self._headerlines-2,
 
945
                    *attrs
 
946
                )
 
947
            except (KeyboardInterrupt, SystemExit):
 
948
                raise
 
949
            except Exception, exc:
 
950
                if not self.levels:
 
951
                    raise
 
952
                self._calcheaderlines(oldlevels)
 
953
                curses.beep()
 
954
                self.report(exc)
 
955
            else:
 
956
                self.levels.append(level)
 
957
 
 
958
    def startkeyboardinput(self, mode):
 
959
        """
 
960
        Enter mode ``mode``, which requires keyboard input.
 
961
        """
 
962
        self.mode = mode
 
963
        self.prompts[mode].start()
 
964
 
 
965
    def keylabel(self, keycode):
 
966
        """
 
967
        Return a pretty name for the ``curses`` key ``keycode`` (used in the
 
968
        help screen and in reports about unassigned keys).
 
969
        """
 
970
        if keycode <= 0xff:
 
971
            specialsnames = {
 
972
                ord("\n"): "RETURN",
 
973
                ord(" "): "SPACE",
 
974
                ord("\t"): "TAB",
 
975
                ord("\x7f"): "DELETE",
 
976
                ord("\x08"): "BACKSPACE",
 
977
            }
 
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:
 
985
                return name
 
986
        return str(keycode)
 
987
 
 
988
    def beep(self, force=False):
 
989
        if force or self._dobeep:
 
990
            curses.beep()
 
991
            # don't beep again (as long as the same key is pressed)
 
992
            self._dobeep = False
 
993
 
 
994
    def cmd_up(self):
 
995
        """
 
996
        Move the cursor to the previous row.
 
997
        """
 
998
        level = self.levels[-1]
 
999
        self.report("up")
 
1000
        level.moveto(level.curx, level.cury-self.stepy)
 
1001
 
 
1002
    def cmd_down(self):
 
1003
        """
 
1004
        Move the cursor to the next row.
 
1005
        """
 
1006
        level = self.levels[-1]
 
1007
        self.report("down")
 
1008
        level.moveto(level.curx, level.cury+self.stepy)
 
1009
 
 
1010
    def cmd_pageup(self):
 
1011
        """
 
1012
        Move the cursor up one page.
 
1013
        """
 
1014
        level = self.levels[-1]
 
1015
        self.report("page up")
 
1016
        level.moveto(level.curx, level.cury-level.mainsizey+self.pageoverlapy)
 
1017
 
 
1018
    def cmd_pagedown(self):
 
1019
        """
 
1020
        Move the cursor down one page.
 
1021
        """
 
1022
        level = self.levels[-1]
 
1023
        self.report("page down")
 
1024
        level.moveto(level.curx, level.cury+level.mainsizey-self.pageoverlapy)
 
1025
 
 
1026
    def cmd_left(self):
 
1027
        """
 
1028
        Move the cursor left.
 
1029
        """
 
1030
        level = self.levels[-1]
 
1031
        self.report("left")
 
1032
        level.moveto(level.curx-self.stepx, level.cury)
 
1033
 
 
1034
    def cmd_right(self):
 
1035
        """
 
1036
        Move the cursor right.
 
1037
        """
 
1038
        level = self.levels[-1]
 
1039
        self.report("right")
 
1040
        level.moveto(level.curx+self.stepx, level.cury)
 
1041
 
 
1042
    def cmd_home(self):
 
1043
        """
 
1044
        Move the cursor to the first column.
 
1045
        """
 
1046
        level = self.levels[-1]
 
1047
        self.report("home")
 
1048
        level.moveto(0, level.cury)
 
1049
 
 
1050
    def cmd_end(self):
 
1051
        """
 
1052
        Move the cursor to the last column.
 
1053
        """
 
1054
        level = self.levels[-1]
 
1055
        self.report("end")
 
1056
        level.moveto(level.datasizex+level.mainsizey-self.pageoverlapx, level.cury)
 
1057
 
 
1058
    def cmd_prevattr(self):
 
1059
        """
 
1060
        Move the cursor one attribute column to the left.
 
1061
        """
 
1062
        level = self.levels[-1]
 
1063
        if level.displayattr[0] is None or level.displayattr[0] == 0:
 
1064
            self.beep()
 
1065
        else:
 
1066
            self.report("prevattr")
 
1067
            pos = 0
 
1068
            for (i, attrname) in enumerate(level.displayattrs):
 
1069
                if i == level.displayattr[0]-1:
 
1070
                    break
 
1071
                pos += level.colwidths[attrname] + 1
 
1072
            level.moveto(pos, level.cury)
 
1073
 
 
1074
    def cmd_nextattr(self):
 
1075
        """
 
1076
        Move the cursor one attribute column to the right.
 
1077
        """
 
1078
        level = self.levels[-1]
 
1079
        if level.displayattr[0] is None or level.displayattr[0] == len(level.displayattrs)-1:
 
1080
            self.beep()
 
1081
        else:
 
1082
            self.report("nextattr")
 
1083
            pos = 0
 
1084
            for (i, attrname) in enumerate(level.displayattrs):
 
1085
                if i == level.displayattr[0]+1:
 
1086
                    break
 
1087
                pos += level.colwidths[attrname] + 1
 
1088
            level.moveto(pos, level.cury)
 
1089
 
 
1090
    def cmd_pick(self):
 
1091
        """
 
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.)
 
1095
        """
 
1096
        level = self.levels[-1]
 
1097
        self.returnvalue = level.items[level.cury].item
 
1098
        return True
 
1099
 
 
1100
    def cmd_pickattr(self):
 
1101
        """
 
1102
        'Pick' the attribute under the cursor (i.e. the row/column the
 
1103
        cursor is on).
 
1104
        """
 
1105
        level = self.levels[-1]
 
1106
        attr = level.displayattr[1]
 
1107
        if attr is ipipe.noitem:
 
1108
            curses.beep()
 
1109
            self.report(CommandError("no column under cursor"))
 
1110
            return
 
1111
        value = attr.value(level.items[level.cury].item)
 
1112
        if value is ipipe.noitem:
 
1113
            curses.beep()
 
1114
            self.report(AttributeError(attr.name()))
 
1115
        else:
 
1116
            self.returnvalue = value
 
1117
            return True
 
1118
 
 
1119
    def cmd_pickallattrs(self):
 
1120
        """
 
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.
 
1124
        """
 
1125
        level = self.levels[-1]
 
1126
        attr = level.displayattr[1]
 
1127
        if attr is ipipe.noitem:
 
1128
            curses.beep()
 
1129
            self.report(CommandError("no column under cursor"))
 
1130
            return
 
1131
        result = []
 
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
 
1137
        return True
 
1138
 
 
1139
    def cmd_pickmarked(self):
 
1140
        """
 
1141
        'Pick' marked objects. Marked objects will be returned as a list.
 
1142
        """
 
1143
        level = self.levels[-1]
 
1144
        self.returnvalue = [cache.item for cache in level.items if cache.marked]
 
1145
        return True
 
1146
 
 
1147
    def cmd_pickmarkedattr(self):
 
1148
        """
 
1149
        'Pick' the attribute under the cursor from all marked objects
 
1150
        (This returns a list).
 
1151
        """
 
1152
 
 
1153
        level = self.levels[-1]
 
1154
        attr = level.displayattr[1]
 
1155
        if attr is ipipe.noitem:
 
1156
            curses.beep()
 
1157
            self.report(CommandError("no column under cursor"))
 
1158
            return
 
1159
        result = []
 
1160
        for cache in level.items:
 
1161
            if cache.marked:
 
1162
                value = attr.value(cache.item)
 
1163
                if value is not ipipe.noitem:
 
1164
                    result.append(value)
 
1165
        self.returnvalue = result
 
1166
        return True
 
1167
 
 
1168
    def cmd_pickinput(self):
 
1169
        """
 
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
 
1172
        in the input.
 
1173
        """
 
1174
        level = self.levels[-1]
 
1175
        value = level.items[level.cury].item
 
1176
        self.returnvalue = None
 
1177
        api = ipapi.get()
 
1178
        api.set_next_input(str(value))
 
1179
        return True
 
1180
 
 
1181
    def cmd_pickinputattr(self):
 
1182
        """
 
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.
 
1186
        """
 
1187
        level = self.levels[-1]
 
1188
        attr = level.displayattr[1]
 
1189
        if attr is ipipe.noitem:
 
1190
            curses.beep()
 
1191
            self.report(CommandError("no column under cursor"))
 
1192
            return
 
1193
        value = attr.value(level.items[level.cury].item)
 
1194
        if value is ipipe.noitem:
 
1195
            curses.beep()
 
1196
            self.report(AttributeError(attr.name()))
 
1197
        self.returnvalue = None
 
1198
        api = ipapi.get()
 
1199
        api.set_next_input(str(value))
 
1200
        return True
 
1201
 
 
1202
    def cmd_markrange(self):
 
1203
        """
 
1204
        Mark all objects from the last marked object before the current cursor
 
1205
        position to the cursor position.
 
1206
        """
 
1207
        level = self.levels[-1]
 
1208
        self.report("markrange")
 
1209
        start = None
 
1210
        if level.items:
 
1211
            for i in xrange(level.cury, -1, -1):
 
1212
                if level.items[i].marked:
 
1213
                    start = i
 
1214
                    break
 
1215
        if start is None:
 
1216
            self.report(CommandError("no mark before cursor"))
 
1217
            curses.beep()
 
1218
        else:
 
1219
            for i in xrange(start, level.cury+1):
 
1220
                cache = level.items[i]
 
1221
                if not cache.marked:
 
1222
                    cache.marked = True
 
1223
                    level.marked += 1
 
1224
 
 
1225
    def cmd_enter(self):
 
1226
        """
 
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'.
 
1229
        """
 
1230
        level = self.levels[-1]
 
1231
        try:
 
1232
            item = level.items[level.cury].item
 
1233
        except IndexError:
 
1234
            self.report(CommandError("No object"))
 
1235
            curses.beep()
 
1236
        else:
 
1237
            self.report("entering object...")
 
1238
            self.enter(item)
 
1239
 
 
1240
    def cmd_leave(self):
 
1241
        """
 
1242
        Leave the current browser level and go back to the previous one.
 
1243
        """
 
1244
        self.report("leave")
 
1245
        if len(self.levels) > 1:
 
1246
            self._calcheaderlines(len(self.levels)-1)
 
1247
            self.levels.pop(-1)
 
1248
        else:
 
1249
            self.report(CommandError("This is the last level"))
 
1250
            curses.beep()
 
1251
 
 
1252
    def cmd_enterattr(self):
 
1253
        """
 
1254
        Enter the attribute under the cursor.
 
1255
        """
 
1256
        level = self.levels[-1]
 
1257
        attr = level.displayattr[1]
 
1258
        if attr is ipipe.noitem:
 
1259
            curses.beep()
 
1260
            self.report(CommandError("no column under cursor"))
 
1261
            return
 
1262
        try:
 
1263
            item = level.items[level.cury].item
 
1264
        except IndexError:
 
1265
            self.report(CommandError("No object"))
 
1266
            curses.beep()
 
1267
        else:
 
1268
            value = attr.value(item)
 
1269
            name = attr.name()
 
1270
            if value is ipipe.noitem:
 
1271
                self.report(AttributeError(name))
 
1272
            else:
 
1273
                self.report("entering object attribute %s..." % name)
 
1274
                self.enter(value)
 
1275
 
 
1276
    def cmd_detail(self):
 
1277
        """
 
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
 
1281
        the object).
 
1282
        """
 
1283
        level = self.levels[-1]
 
1284
        try:
 
1285
            item = level.items[level.cury].item
 
1286
        except IndexError:
 
1287
            self.report(CommandError("No object"))
 
1288
            curses.beep()
 
1289
        else:
 
1290
            self.report("entering detail view for object...")
 
1291
            attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
 
1292
            self.enter(attrs)
 
1293
 
 
1294
    def cmd_detailattr(self):
 
1295
        """
 
1296
        Show a detail view of the attribute under the cursor.
 
1297
        """
 
1298
        level = self.levels[-1]
 
1299
        attr = level.displayattr[1]
 
1300
        if attr is ipipe.noitem:
 
1301
            curses.beep()
 
1302
            self.report(CommandError("no attribute"))
 
1303
            return
 
1304
        try:
 
1305
            item = level.items[level.cury].item
 
1306
        except IndexError:
 
1307
            self.report(CommandError("No object"))
 
1308
            curses.beep()
 
1309
        else:
 
1310
            try:
 
1311
                item = attr.value(item)
 
1312
            except (KeyboardInterrupt, SystemExit):
 
1313
                raise
 
1314
            except Exception, exc:
 
1315
                self.report(exc)
 
1316
            else:
 
1317
                self.report("entering detail view for attribute %s..." % attr.name())
 
1318
                attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
 
1319
                self.enter(attrs)
 
1320
 
 
1321
    def cmd_tooglemark(self):
 
1322
        """
 
1323
        Mark/unmark the object under the cursor. Marked objects have a '!'
 
1324
        after the row number).
 
1325
        """
 
1326
        level = self.levels[-1]
 
1327
        self.report("toggle mark")
 
1328
        try:
 
1329
            item = level.items[level.cury]
 
1330
        except IndexError: # no items?
 
1331
            pass
 
1332
        else:
 
1333
            if item.marked:
 
1334
                item.marked = False
 
1335
                level.marked -= 1
 
1336
            else:
 
1337
                item.marked = True
 
1338
                level.marked += 1
 
1339
 
 
1340
    def cmd_sortattrasc(self):
 
1341
        """
 
1342
        Sort the objects (in ascending order) using the attribute under
 
1343
        the cursor as the sort key.
 
1344
        """
 
1345
        level = self.levels[-1]
 
1346
        attr = level.displayattr[1]
 
1347
        if attr is ipipe.noitem:
 
1348
            curses.beep()
 
1349
            self.report(CommandError("no column under cursor"))
 
1350
            return
 
1351
        self.report("sort by %s (ascending)" % attr.name())
 
1352
        def key(item):
 
1353
            try:
 
1354
                return attr.value(item)
 
1355
            except (KeyboardInterrupt, SystemExit):
 
1356
                raise
 
1357
            except Exception:
 
1358
                return None
 
1359
        level.sort(key)
 
1360
 
 
1361
    def cmd_sortattrdesc(self):
 
1362
        """
 
1363
        Sort the objects (in descending order) using the attribute under
 
1364
        the cursor as the sort key.
 
1365
        """
 
1366
        level = self.levels[-1]
 
1367
        attr = level.displayattr[1]
 
1368
        if attr is ipipe.noitem:
 
1369
            curses.beep()
 
1370
            self.report(CommandError("no column under cursor"))
 
1371
            return
 
1372
        self.report("sort by %s (descending)" % attr.name())
 
1373
        def key(item):
 
1374
            try:
 
1375
                return attr.value(item)
 
1376
            except (KeyboardInterrupt, SystemExit):
 
1377
                raise
 
1378
            except Exception:
 
1379
                return None
 
1380
        level.sort(key, reverse=True)
 
1381
 
 
1382
    def cmd_hideattr(self):
 
1383
        """
 
1384
        Hide the attribute under the cursor.
 
1385
        """
 
1386
        level = self.levels[-1]
 
1387
        if level.displayattr[0] is None:
 
1388
            self.beep()
 
1389
        else:
 
1390
            self.report("hideattr")
 
1391
            level.hiddenattrs.add(level.displayattr[1])
 
1392
            level.moveto(level.curx, level.cury, refresh=True)
 
1393
 
 
1394
    def cmd_unhideattrs(self):
 
1395
        """
 
1396
        Make all attributes visible again.
 
1397
        """
 
1398
        level = self.levels[-1]
 
1399
        self.report("unhideattrs")
 
1400
        level.hiddenattrs.clear()
 
1401
        level.moveto(level.curx, level.cury, refresh=True)
 
1402
 
 
1403
    def cmd_goto(self):
 
1404
        """
 
1405
        Jump to a row. The row number can be entered at the
 
1406
        bottom of the screen.
 
1407
        """
 
1408
        self.startkeyboardinput("goto")
 
1409
 
 
1410
    def cmd_find(self):
 
1411
        """
 
1412
        Search forward for a row. The search condition can be entered at the
 
1413
        bottom of the screen.
 
1414
        """
 
1415
        self.startkeyboardinput("find")
 
1416
 
 
1417
    def cmd_findbackwards(self):
 
1418
        """
 
1419
        Search backward for a row. The search condition can be entered at the
 
1420
        bottom of the screen.
 
1421
        """
 
1422
        self.startkeyboardinput("findbackwards")
 
1423
 
 
1424
    def cmd_refresh(self):
 
1425
        """
 
1426
        Refreshes the display by restarting the iterator.
 
1427
        """
 
1428
        level = self.levels[-1]
 
1429
        self.report("refresh")
 
1430
        level.refresh()
 
1431
 
 
1432
    def cmd_refreshfind(self):
 
1433
        """
 
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).
 
1437
        """
 
1438
        level = self.levels[-1]
 
1439
        self.report("refreshfind")
 
1440
        level.refreshfind()
 
1441
 
 
1442
    def cmd_help(self):
 
1443
        """
 
1444
        Opens the help screen as a new browser level, describing keyboard
 
1445
        shortcuts.
 
1446
        """
 
1447
        for level in self.levels:
 
1448
            if isinstance(level.input, _BrowserHelp):
 
1449
                curses.beep()
 
1450
                self.report(CommandError("help already active"))
 
1451
                return
 
1452
 
 
1453
        self.enter(_BrowserHelp(self))
 
1454
 
 
1455
    def cmd_quit(self):
 
1456
        """
 
1457
        Quit the browser and return to the IPython prompt.
 
1458
        """
 
1459
        self.returnvalue = None
 
1460
        return True
 
1461
 
 
1462
    def sigwinchhandler(self, signal, frame):
 
1463
        self.resized = True
 
1464
 
 
1465
    def _dodisplay(self, scr):
 
1466
        """
 
1467
        This method is the workhorse of the browser. It handles screen
 
1468
        drawing and the keyboard.
 
1469
        """
 
1470
        self.scr = scr
 
1471
        curses.halfdelay(1)
 
1472
        footery = 2
 
1473
 
 
1474
        keys = []
 
1475
        for cmd in ("quit", "help"):
 
1476
            key = self.keymap.findkey(cmd, None)
 
1477
            if key is not None:
 
1478
                keys.append("%s=%s" % (self.keylabel(key), cmd))
 
1479
        helpmsg = " | %s" % " ".join(keys)
 
1480
 
 
1481
        scr.clear()
 
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)
 
1485
        scr.refresh()
 
1486
 
 
1487
        lastc = -1
 
1488
 
 
1489
        self.levels = []
 
1490
        # enter the first level
 
1491
        self.enter(self.input, *self.attrs)
 
1492
 
 
1493
        self._calcheaderlines(None)
 
1494
 
 
1495
        while True:
 
1496
            level = self.levels[-1]
 
1497
            (self.scrsizey, self.scrsizex) = scr.getmaxyx()
 
1498
            level.mainsizey = self.scrsizey-1-self._headerlines-footery
 
1499
 
 
1500
            # Paint object header
 
1501
            for i in xrange(self._firstheaderline, self._firstheaderline+self._headerlines):
 
1502
                lv = self.levels[i]
 
1503
                posx = 0
 
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:
 
1509
                        msg += "+"
 
1510
                    msg += ") "
 
1511
                    endx -= len(msg)+1
 
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)
 
1515
                    if posx >= endx:
 
1516
                        break
 
1517
                if i:
 
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)
 
1520
 
 
1521
            if not level.items:
 
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)
 
1524
                scr.clrtobot()
 
1525
            else:
 
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
 
1538
                    else:
 
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:
 
1543
                        break
 
1544
                else:
 
1545
                    scr.addstr(" "*(self.scrsizex-posx), self.getstyle(self.style_colheader))
 
1546
 
 
1547
                # Paint rows
 
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]
 
1551
                    if i == level.cury:
 
1552
                        style = self.style_numberhere
 
1553
                    else:
 
1554
                        style = self.style_number
 
1555
 
 
1556
                    posy = self._headerlines+1+i-level.datastarty
 
1557
                    posx = begx-level.datastartx
 
1558
 
 
1559
                    scr.move(posy, 0)
 
1560
                    scr.addstr(" %*d%s" % (level.numbersizex, i, " !"[cache.marked]), self.getstyle(style))
 
1561
                    scr.addstr(self.headersepchar, self.getstyle(self.style_sep))
 
1562
 
 
1563
                    for attrname in level.displayattrs:
 
1564
                        cwidth = level.colwidths[attrname]
 
1565
                        try:
 
1566
                            (align, length, parts) = level.displayrows[i-level.datastarty][attrname]
 
1567
                        except KeyError:
 
1568
                            align = 2
 
1569
                            style = astyle.style_nodata
 
1570
                            if i == level.cury:
 
1571
                                style = self.getstylehere(style)
 
1572
                        padstyle = self.style_datapad
 
1573
                        sepstyle = self.style_sep
 
1574
                        if i == level.cury:
 
1575
                            padstyle = self.getstylehere(padstyle)
 
1576
                            sepstyle = self.getstylehere(sepstyle)
 
1577
                        if align == 2:
 
1578
                            posx += self.addchr(posy, posx, begx, self.scrsizex, self.nodatachar, cwidth, style)
 
1579
                        else:
 
1580
                            if align == 1:
 
1581
                                posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
 
1582
                            elif align == 0:
 
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:
 
1587
                                if i == level.cury:
 
1588
                                    style = self.getstylehere(style)
 
1589
                                posx += self.addstr(posy, posx, begx, self.scrsizex, text, style)
 
1590
                                if posx >= self.scrsizex:
 
1591
                                    break
 
1592
                            if align == -1:
 
1593
                                posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
 
1594
                            elif align == 0:
 
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)
 
1597
                    else:
 
1598
                        scr.clrtoeol()
 
1599
 
 
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))
 
1603
                    scr.clrtoeol()
 
1604
 
 
1605
            posy = self.scrsizey-footery
 
1606
            # Display footer
 
1607
            scr.addstr(posy, 0, " "*self.scrsizex, self.getstyle(self.style_footer))
 
1608
 
 
1609
            if level.exhausted:
 
1610
                flag = ""
 
1611
            else:
 
1612
                flag = "+"
 
1613
 
 
1614
            endx = self.scrsizex-len(helpmsg)-1
 
1615
            scr.addstr(posy, endx, helpmsg, self.getstyle(self.style_footer))
 
1616
 
 
1617
            posx = 0
 
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)
 
1620
            try:
 
1621
                item = level.items[level.cury].item
 
1622
            except IndexError: # empty
 
1623
                pass
 
1624
            else:
 
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)
 
1628
                        if posx >= endx:
 
1629
                            break
 
1630
 
 
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)
 
1637
                    try:
 
1638
                        value = attr.value(item)
 
1639
                    except (SystemExit, KeyboardInterrupt):
 
1640
                        raise
 
1641
                    except Exception, exc:
 
1642
                        value = 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)
 
1648
                            if posx >= endx:
 
1649
                                break
 
1650
 
 
1651
            try:
 
1652
                # Display input prompt
 
1653
                if self.mode in self.prompts:
 
1654
                    history = self.prompts[self.mode]
 
1655
                    posx = 0
 
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:
 
1660
                        text = "new"
 
1661
                    else:
 
1662
                        text = str(history.cury+1)
 
1663
                    posx += self.addstr(posy, posx, 0, endx, text, astyle.style_type_number)
 
1664
                    if history.history:
 
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)
 
1668
                    inputstartx = posx
 
1669
                    posx += self.addstr(posy, posx, 0, endx, history.input, astyle.style_default)
 
1670
                # Display report
 
1671
                else:
 
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":
 
1676
                                msg = "%s: %s" % \
 
1677
                                      (self._report.__class__.__name__, self._report)
 
1678
                            else:
 
1679
                                msg = "%s.%s: %s" % \
 
1680
                                      (self._report.__class__.__module__,
 
1681
                                       self._report.__class__.__name__, self._report)
 
1682
                        else:
 
1683
                            style = self.getstyle(self.style_report)
 
1684
                            msg = self._report
 
1685
                        scr.addstr(self.scrsizey-1, 0, msg[:self.scrsizex], style)
 
1686
                        self._report = None
 
1687
                    else:
 
1688
                        scr.move(self.scrsizey-1, 0)
 
1689
            except curses.error:
 
1690
                # Protect against errors from writing to the last line
 
1691
                pass
 
1692
            scr.clrtoeol()
 
1693
 
 
1694
            # Position cursor
 
1695
            if self.mode in self.prompts:
 
1696
                history = self.prompts[self.mode]
 
1697
                scr.move(self.scrsizey-1, inputstartx+history.curx)
 
1698
            else:
 
1699
                scr.move(
 
1700
                    1+self._headerlines+level.cury-level.datastarty,
 
1701
                    level.numbersizex+3+level.curx-level.datastartx
 
1702
                )
 
1703
            scr.refresh()
 
1704
 
 
1705
            # Check keyboard
 
1706
            while True:
 
1707
                c = scr.getch()
 
1708
                if self.resized:
 
1709
                    size = fcntl.ioctl(0, tty.TIOCGWINSZ, "12345678")
 
1710
                    size = struct.unpack("4H", size)
 
1711
                    oldsize = scr.getmaxyx()
 
1712
                    scr.erase()
 
1713
                    curses.resize_term(size[0], size[1])
 
1714
                    newsize = scr.getmaxyx()
 
1715
                    scr.erase()
 
1716
                    for l in self.levels:
 
1717
                        l.mainsizey += newsize[0]-oldsize[0]
 
1718
                        l.moveto(l.curx, l.cury, refresh=True)
 
1719
                    scr.refresh()
 
1720
                    self.resized = False
 
1721
                    break # Redisplay
 
1722
                if self.mode in self.prompts:
 
1723
                    if self.prompts[self.mode].handlekey(self, c):
 
1724
                       break # Redisplay
 
1725
                else:
 
1726
                    # if no key is pressed slow down and beep again
 
1727
                    if c == -1:
 
1728
                        self.stepx = 1.
 
1729
                        self.stepy = 1.
 
1730
                        self._dobeep = True
 
1731
                    else:
 
1732
                        # if a different key was pressed slow down and beep too
 
1733
                        if c != lastc:
 
1734
                            lastc = c
 
1735
                            self.stepx = 1.
 
1736
                            self.stepy = 1.
 
1737
                            self._dobeep = True
 
1738
                        cmdname = self.keymap.get(c, None)
 
1739
                        if cmdname is None:
 
1740
                            self.report(
 
1741
                                UnassignedKeyError("Unassigned key %s" %
 
1742
                                                   self.keylabel(c)))
 
1743
                        else:
 
1744
                            cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
 
1745
                            if cmdfunc is None:
 
1746
                                self.report(
 
1747
                                    UnknownCommandError("Unknown command %r" %
 
1748
                                                        (cmdname,)))
 
1749
                            elif cmdfunc():
 
1750
                                returnvalue = self.returnvalue
 
1751
                                self.returnvalue = None
 
1752
                                return returnvalue
 
1753
                        self.stepx = self.nextstepx(self.stepx)
 
1754
                        self.stepy = self.nextstepy(self.stepy)
 
1755
                        curses.flushinp() # get rid of type ahead
 
1756
                        break # Redisplay
 
1757
        self.scr = None
 
1758
 
 
1759
    def display(self):
 
1760
        if hasattr(curses, "resize_term"):
 
1761
            oldhandler = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
 
1762
            try:
 
1763
                return curses.wrapper(self._dodisplay)
 
1764
            finally:
 
1765
                signal.signal(signal.SIGWINCH, oldhandler)
 
1766
        else:
 
1767
            return curses.wrapper(self._dodisplay)