##// END OF EJS Templates
Add a __delitem__() method to Keymap.
walter.doerwald -
Show More
@@ -1,1660 +1,1665 b''
1 1 # -*- coding: iso-8859-1 -*-
2 2
3 3 import curses, fcntl, signal, struct, tty, textwrap, inspect
4 4
5 5 import astyle, ipipe
6 6
7 7
8 8 # Python 2.3 compatibility
9 9 try:
10 10 set
11 11 except NameError:
12 12 import sets
13 13 set = sets.Set
14 14
15 15
16 16 class UnassignedKeyError(Exception):
17 17 """
18 18 Exception that is used for reporting unassigned keys.
19 19 """
20 20
21 21
22 22 class UnknownCommandError(Exception):
23 23 """
24 24 Exception that is used for reporting unknown command (this should never
25 25 happen).
26 26 """
27 27
28 28
29 29 class CommandError(Exception):
30 30 """
31 31 Exception that is used for reporting that a command can't be executed.
32 32 """
33 33
34 34
35 35 class Keymap(dict):
36 36 """
37 37 Stores mapping of keys to commands.
38 38 """
39 39 def __init__(self):
40 40 self._keymap = {}
41 41
42 42 def __setitem__(self, key, command):
43 43 if isinstance(key, str):
44 44 for c in key:
45 45 dict.__setitem__(self, ord(c), command)
46 46 else:
47 47 dict.__setitem__(self, key, command)
48 48
49 def register(self, command, *keys):
50 for key in keys:
51 self[key] = command
52
53 49 def __getitem__(self, key):
54 50 if isinstance(key, str):
55 51 key = ord(key)
56 52 return dict.__getitem__(self, key)
57 53
54 def __detitem__(self, key):
55 if isinstance(key, str):
56 key = ord(key)
57 dict.__detitem__(self, key)
58
59 def register(self, command, *keys):
60 for key in keys:
61 self[key] = command
62
58 63 def get(self, key, default=None):
59 64 if isinstance(key, str):
60 65 key = ord(key)
61 66 return dict.get(self, key, default)
62 67
63 68 def findkey(self, command, default=ipipe.noitem):
64 69 for (key, commandcandidate) in self.iteritems():
65 70 if commandcandidate == command:
66 71 return key
67 72 if default is ipipe.noitem:
68 73 raise KeyError(command)
69 74 return default
70 75
71 76
72 77 class _BrowserCachedItem(object):
73 78 # This is used internally by ``ibrowse`` to store a item together with its
74 79 # marked status.
75 80 __slots__ = ("item", "marked")
76 81
77 82 def __init__(self, item):
78 83 self.item = item
79 84 self.marked = False
80 85
81 86
82 87 class _BrowserHelp(object):
83 88 style_header = astyle.Style.fromstr("red:blacK")
84 89 # This is used internally by ``ibrowse`` for displaying the help screen.
85 90 def __init__(self, browser):
86 91 self.browser = browser
87 92
88 93 def __xrepr__(self, mode):
89 94 yield (-1, True)
90 95 if mode == "header" or mode == "footer":
91 96 yield (astyle.style_default, "ibrowse help screen")
92 97 else:
93 98 yield (astyle.style_default, repr(self))
94 99
95 100 def __xiter__(self, mode):
96 101 # Get reverse key mapping
97 102 allkeys = {}
98 103 for (key, cmd) in self.browser.keymap.iteritems():
99 104 allkeys.setdefault(cmd, []).append(key)
100 105
101 106 fields = ("key", "description")
102 107
103 108 commands = []
104 109 for name in dir(self.browser):
105 110 if name.startswith("cmd_"):
106 111 command = getattr(self.browser, name)
107 112 commands.append((inspect.getsourcelines(command)[-1], name[4:], command))
108 113 commands.sort()
109 114 commands = [(c[1], c[2]) for c in commands]
110 115 for (i, (name, command)) in enumerate(commands):
111 116 if i:
112 117 yield ipipe.Fields(fields, key="", description="")
113 118
114 119 description = command.__doc__
115 120 if description is None:
116 121 lines = []
117 122 else:
118 123 lines = [l.strip() for l in description.splitlines() if l.strip()]
119 124 description = "\n".join(lines)
120 125 lines = textwrap.wrap(description, 60)
121 126 keys = allkeys.get(name, [])
122 127
123 128 yield ipipe.Fields(fields, description=astyle.Text((self.style_header, name)))
124 129 for i in xrange(max(len(keys), len(lines))):
125 130 try:
126 131 key = self.browser.keylabel(keys[i])
127 132 except IndexError:
128 133 key = ""
129 134 try:
130 135 line = lines[i]
131 136 except IndexError:
132 137 line = ""
133 138 yield ipipe.Fields(fields, key=key, description=line)
134 139
135 140
136 141 class _BrowserLevel(object):
137 142 # This is used internally to store the state (iterator, fetch items,
138 143 # position of cursor and screen, etc.) of one browser level
139 144 # An ``ibrowse`` object keeps multiple ``_BrowserLevel`` objects in
140 145 # a stack.
141 146 def __init__(self, browser, input, iterator, mainsizey, *attrs):
142 147 self.browser = browser
143 148 self.input = input
144 149 self.header = [x for x in ipipe.xrepr(input, "header") if not isinstance(x[0], int)]
145 150 # iterator for the input
146 151 self.iterator = iterator
147 152
148 153 # is the iterator exhausted?
149 154 self.exhausted = False
150 155
151 156 # attributes to be display (autodetected if empty)
152 157 self.attrs = attrs
153 158
154 159 # fetched items (+ marked flag)
155 160 self.items = ipipe.deque()
156 161
157 162 # Number of marked objects
158 163 self.marked = 0
159 164
160 165 # Vertical cursor position
161 166 self.cury = 0
162 167
163 168 # Horizontal cursor position
164 169 self.curx = 0
165 170
166 171 # Index of first data column
167 172 self.datastartx = 0
168 173
169 174 # Index of first data line
170 175 self.datastarty = 0
171 176
172 177 # height of the data display area
173 178 self.mainsizey = mainsizey
174 179
175 180 # width of the data display area (changes when scrolling)
176 181 self.mainsizex = 0
177 182
178 183 # Size of row number (changes when scrolling)
179 184 self.numbersizex = 0
180 185
181 186 # Attribute names to display (in this order)
182 187 self.displayattrs = []
183 188
184 189 # index and name of attribute under the cursor
185 190 self.displayattr = (None, ipipe.noitem)
186 191
187 192 # Maps attribute names to column widths
188 193 self.colwidths = {}
189 194
190 195 # Set of hidden attributes
191 196 self.hiddenattrs = set()
192 197
193 198 # This takes care of all the caches etc.
194 199 self.moveto(0, 0, refresh=True)
195 200
196 201 def fetch(self, count):
197 202 # Try to fill ``self.items`` with at least ``count`` objects.
198 203 have = len(self.items)
199 204 while not self.exhausted and have < count:
200 205 try:
201 206 item = self.iterator.next()
202 207 except StopIteration:
203 208 self.exhausted = True
204 209 break
205 210 else:
206 211 have += 1
207 212 self.items.append(_BrowserCachedItem(item))
208 213
209 214 def calcdisplayattrs(self):
210 215 # Calculate which attributes are available from the objects that are
211 216 # currently visible on screen (and store it in ``self.displayattrs``)
212 217
213 218 attrnames = set()
214 219 self.displayattrs = []
215 220 if self.attrs:
216 221 # If the browser object specifies a fixed list of attributes,
217 222 # simply use it (removing hidden attributes).
218 223 for attrname in self.attrs:
219 224 if attrname not in attrnames and attrname not in self.hiddenattrs:
220 225 self.displayattrs.append(attrname)
221 226 attrnames.add(attrname)
222 227 else:
223 228 endy = min(self.datastarty+self.mainsizey, len(self.items))
224 229 for i in xrange(self.datastarty, endy):
225 230 for attrname in ipipe.xattrs(self.items[i].item, "default"):
226 231 if attrname not in attrnames and attrname not in self.hiddenattrs:
227 232 self.displayattrs.append(attrname)
228 233 attrnames.add(attrname)
229 234
230 235 def getrow(self, i):
231 236 # Return a dictinary with the attributes for the object
232 237 # ``self.items[i]``. Attribute names are taken from
233 238 # ``self.displayattrs`` so ``calcdisplayattrs()`` must have been
234 239 # called before.
235 240 row = {}
236 241 item = self.items[i].item
237 242 for attrname in self.displayattrs:
238 243 try:
239 244 value = ipipe._getattr(item, attrname, ipipe.noitem)
240 245 except (KeyboardInterrupt, SystemExit):
241 246 raise
242 247 except Exception, exc:
243 248 value = exc
244 249 # only store attribute if it exists (or we got an exception)
245 250 if value is not ipipe.noitem:
246 251 # remember alignment, length and colored text
247 252 row[attrname] = ipipe.xformat(value, "cell", self.browser.maxattrlength)
248 253 return row
249 254
250 255 def calcwidths(self):
251 256 # Recalculate the displayed fields and their widths.
252 257 # ``calcdisplayattrs()'' must have been called and the cache
253 258 # for attributes of the objects on screen (``self.displayrows``)
254 259 # must have been filled. This returns a dictionary mapping
255 260 # column names to widths.
256 261 self.colwidths = {}
257 262 for row in self.displayrows:
258 263 for attrname in self.displayattrs:
259 264 try:
260 265 length = row[attrname][1]
261 266 except KeyError:
262 267 length = 0
263 268 # always add attribute to colwidths, even if it doesn't exist
264 269 if attrname not in self.colwidths:
265 270 self.colwidths[attrname] = len(ipipe._attrname(attrname))
266 271 newwidth = max(self.colwidths[attrname], length)
267 272 self.colwidths[attrname] = newwidth
268 273
269 274 # How many characters do we need to paint the largest item number?
270 275 self.numbersizex = len(str(self.datastarty+self.mainsizey-1))
271 276 # How must space have we got to display data?
272 277 self.mainsizex = self.browser.scrsizex-self.numbersizex-3
273 278 # width of all columns
274 279 self.datasizex = sum(self.colwidths.itervalues()) + len(self.colwidths)
275 280
276 281 def calcdisplayattr(self):
277 282 # Find out which attribute the cursor is on and store this
278 283 # information in ``self.displayattr``.
279 284 pos = 0
280 285 for (i, attrname) in enumerate(self.displayattrs):
281 286 if pos+self.colwidths[attrname] >= self.curx:
282 287 self.displayattr = (i, attrname)
283 288 break
284 289 pos += self.colwidths[attrname]+1
285 290 else:
286 291 self.displayattr = (None, ipipe.noitem)
287 292
288 293 def moveto(self, x, y, refresh=False):
289 294 # Move the cursor to the position ``(x,y)`` (in data coordinates,
290 295 # not in screen coordinates). If ``refresh`` is true, all cached
291 296 # values will be recalculated (e.g. because the list has been
292 297 # resorted, so screen positions etc. are no longer valid).
293 298 olddatastarty = self.datastarty
294 299 oldx = self.curx
295 300 oldy = self.cury
296 301 x = int(x+0.5)
297 302 y = int(y+0.5)
298 303 newx = x # remember where we wanted to move
299 304 newy = y # remember where we wanted to move
300 305
301 306 scrollbordery = min(self.browser.scrollbordery, self.mainsizey//2)
302 307 scrollborderx = min(self.browser.scrollborderx, self.mainsizex//2)
303 308
304 309 # Make sure that the cursor didn't leave the main area vertically
305 310 if y < 0:
306 311 y = 0
307 312 # try to get enough items to fill the screen
308 313 self.fetch(max(y+scrollbordery+1, self.mainsizey))
309 314 if y >= len(self.items):
310 315 y = max(0, len(self.items)-1)
311 316
312 317 # Make sure that the cursor stays on screen vertically
313 318 if y < self.datastarty+scrollbordery:
314 319 self.datastarty = max(0, y-scrollbordery)
315 320 elif y >= self.datastarty+self.mainsizey-scrollbordery:
316 321 self.datastarty = max(0, min(y-self.mainsizey+scrollbordery+1,
317 322 len(self.items)-self.mainsizey))
318 323
319 324 if refresh: # Do we need to refresh the complete display?
320 325 self.calcdisplayattrs()
321 326 endy = min(self.datastarty+self.mainsizey, len(self.items))
322 327 self.displayrows = map(self.getrow, xrange(self.datastarty, endy))
323 328 self.calcwidths()
324 329 # Did we scroll vertically => update displayrows
325 330 # and various other attributes
326 331 elif self.datastarty != olddatastarty:
327 332 # Recalculate which attributes we have to display
328 333 olddisplayattrs = self.displayattrs
329 334 self.calcdisplayattrs()
330 335 # If there are new attributes, recreate the cache
331 336 if self.displayattrs != olddisplayattrs:
332 337 endy = min(self.datastarty+self.mainsizey, len(self.items))
333 338 self.displayrows = map(self.getrow, xrange(self.datastarty, endy))
334 339 elif self.datastarty<olddatastarty: # we did scroll up
335 340 # drop rows from the end
336 341 del self.displayrows[self.datastarty-olddatastarty:]
337 342 # fetch new items
338 343 for i in xrange(olddatastarty-1,
339 344 self.datastarty-1, -1):
340 345 try:
341 346 row = self.getrow(i)
342 347 except IndexError:
343 348 # we didn't have enough objects to fill the screen
344 349 break
345 350 self.displayrows.insert(0, row)
346 351 else: # we did scroll down
347 352 # drop rows from the start
348 353 del self.displayrows[:self.datastarty-olddatastarty]
349 354 # fetch new items
350 355 for i in xrange(olddatastarty+self.mainsizey,
351 356 self.datastarty+self.mainsizey):
352 357 try:
353 358 row = self.getrow(i)
354 359 except IndexError:
355 360 # we didn't have enough objects to fill the screen
356 361 break
357 362 self.displayrows.append(row)
358 363 self.calcwidths()
359 364
360 365 # Make sure that the cursor didn't leave the data area horizontally
361 366 if x < 0:
362 367 x = 0
363 368 elif x >= self.datasizex:
364 369 x = max(0, self.datasizex-1)
365 370
366 371 # Make sure that the cursor stays on screen horizontally
367 372 if x < self.datastartx+scrollborderx:
368 373 self.datastartx = max(0, x-scrollborderx)
369 374 elif x >= self.datastartx+self.mainsizex-scrollborderx:
370 375 self.datastartx = max(0, min(x-self.mainsizex+scrollborderx+1,
371 376 self.datasizex-self.mainsizex))
372 377
373 378 if x == oldx and y == oldy and (x != newx or y != newy): # couldn't move
374 379 self.browser.beep()
375 380 else:
376 381 self.curx = x
377 382 self.cury = y
378 383 self.calcdisplayattr()
379 384
380 385 def sort(self, key, reverse=False):
381 386 """
382 387 Sort the currently list of items using the key function ``key``. If
383 388 ``reverse`` is true the sort order is reversed.
384 389 """
385 390 curitem = self.items[self.cury] # Remember where the cursor is now
386 391
387 392 # Sort items
388 393 def realkey(item):
389 394 return key(item.item)
390 395 self.items = ipipe.deque(sorted(self.items, key=realkey, reverse=reverse))
391 396
392 397 # Find out where the object under the cursor went
393 398 cury = self.cury
394 399 for (i, item) in enumerate(self.items):
395 400 if item is curitem:
396 401 cury = i
397 402 break
398 403
399 404 self.moveto(self.curx, cury, refresh=True)
400 405
401 406
402 407 class _CommandInput(object):
403 408 keymap = Keymap()
404 409 keymap.register("left", curses.KEY_LEFT)
405 410 keymap.register("right", curses.KEY_RIGHT)
406 411 keymap.register("home", curses.KEY_HOME, "\x01") # Ctrl-A
407 412 keymap.register("end", curses.KEY_END, "\x05") # Ctrl-E
408 413 # FIXME: What's happening here?
409 414 keymap.register("backspace", curses.KEY_BACKSPACE, "x\x08\x7f")
410 415 keymap.register("delete", curses.KEY_DC)
411 416 keymap.register("delend", 0x0b) # Ctrl-K
412 417 keymap.register("execute", "\r\n")
413 418 keymap.register("up", curses.KEY_UP)
414 419 keymap.register("down", curses.KEY_DOWN)
415 420 keymap.register("incsearchup", curses.KEY_PPAGE)
416 421 keymap.register("incsearchdown", curses.KEY_NPAGE)
417 422 keymap.register("exit", "\x18"), # Ctrl-X
418 423
419 424 def __init__(self, prompt):
420 425 self.prompt = prompt
421 426 self.history = []
422 427 self.maxhistory = 100
423 428 self.input = ""
424 429 self.curx = 0
425 430 self.cury = -1 # blank line
426 431
427 432 def start(self):
428 433 self.input = ""
429 434 self.curx = 0
430 435 self.cury = -1 # blank line
431 436
432 437 def handlekey(self, browser, key):
433 438 cmdname = self.keymap.get(key, None)
434 439 if cmdname is not None:
435 440 cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
436 441 if cmdfunc is not None:
437 442 return cmdfunc(browser)
438 443 curses.beep()
439 444 elif key != -1:
440 445 try:
441 446 char = chr(key)
442 447 except ValueError:
443 448 curses.beep()
444 449 else:
445 450 return self.handlechar(browser, char)
446 451
447 452 def handlechar(self, browser, char):
448 453 self.input = self.input[:self.curx] + char + self.input[self.curx:]
449 454 self.curx += 1
450 455 return True
451 456
452 457 def dohistory(self):
453 458 self.history.insert(0, self.input)
454 459 del self.history[:-self.maxhistory]
455 460
456 461 def cmd_backspace(self, browser):
457 462 if self.curx:
458 463 self.input = self.input[:self.curx-1] + self.input[self.curx:]
459 464 self.curx -= 1
460 465 return True
461 466 else:
462 467 curses.beep()
463 468
464 469 def cmd_delete(self, browser):
465 470 if self.curx<len(self.input):
466 471 self.input = self.input[:self.curx] + self.input[self.curx+1:]
467 472 return True
468 473 else:
469 474 curses.beep()
470 475
471 476 def cmd_delend(self, browser):
472 477 if self.curx<len(self.input):
473 478 self.input = self.input[:self.curx]
474 479 return True
475 480
476 481 def cmd_left(self, browser):
477 482 if self.curx:
478 483 self.curx -= 1
479 484 return True
480 485 else:
481 486 curses.beep()
482 487
483 488 def cmd_right(self, browser):
484 489 if self.curx < len(self.input):
485 490 self.curx += 1
486 491 return True
487 492 else:
488 493 curses.beep()
489 494
490 495 def cmd_home(self, browser):
491 496 if self.curx:
492 497 self.curx = 0
493 498 return True
494 499 else:
495 500 curses.beep()
496 501
497 502 def cmd_end(self, browser):
498 503 if self.curx < len(self.input):
499 504 self.curx = len(self.input)
500 505 return True
501 506 else:
502 507 curses.beep()
503 508
504 509 def cmd_up(self, browser):
505 510 if self.cury < len(self.history)-1:
506 511 self.cury += 1
507 512 self.input = self.history[self.cury]
508 513 self.curx = len(self.input)
509 514 return True
510 515 else:
511 516 curses.beep()
512 517
513 518 def cmd_down(self, browser):
514 519 if self.cury >= 0:
515 520 self.cury -= 1
516 521 if self.cury>=0:
517 522 self.input = self.history[self.cury]
518 523 else:
519 524 self.input = ""
520 525 self.curx = len(self.input)
521 526 return True
522 527 else:
523 528 curses.beep()
524 529
525 530 def cmd_incsearchup(self, browser):
526 531 prefix = self.input[:self.curx]
527 532 cury = self.cury
528 533 while True:
529 534 cury += 1
530 535 if cury >= len(self.history):
531 536 break
532 537 if self.history[cury].startswith(prefix):
533 538 self.input = self.history[cury]
534 539 self.cury = cury
535 540 return True
536 541 curses.beep()
537 542
538 543 def cmd_incsearchdown(self, browser):
539 544 prefix = self.input[:self.curx]
540 545 cury = self.cury
541 546 while True:
542 547 cury -= 1
543 548 if cury <= 0:
544 549 break
545 550 if self.history[cury].startswith(prefix):
546 551 self.input = self.history[self.cury]
547 552 self.cury = cury
548 553 return True
549 554 curses.beep()
550 555
551 556 def cmd_exit(self, browser):
552 557 browser.mode = "default"
553 558 return True
554 559
555 560 def cmd_execute(self, browser):
556 561 raise NotImplementedError
557 562
558 563
559 564 class _CommandGoto(_CommandInput):
560 565 def __init__(self):
561 566 _CommandInput.__init__(self, "goto object #")
562 567
563 568 def handlechar(self, browser, char):
564 569 # Only accept digits
565 570 if not "0" <= char <= "9":
566 571 curses.beep()
567 572 else:
568 573 return _CommandInput.handlechar(self, browser, char)
569 574
570 575 def cmd_execute(self, browser):
571 576 level = browser.levels[-1]
572 577 if self.input:
573 578 self.dohistory()
574 579 level.moveto(level.curx, int(self.input))
575 580 browser.mode = "default"
576 581 return True
577 582
578 583
579 584 class _CommandFind(_CommandInput):
580 585 def __init__(self):
581 586 _CommandInput.__init__(self, "find expression")
582 587
583 588 def cmd_execute(self, browser):
584 589 level = browser.levels[-1]
585 590 if self.input:
586 591 self.dohistory()
587 592 while True:
588 593 cury = level.cury
589 594 level.moveto(level.curx, cury+1)
590 595 if cury == level.cury:
591 596 curses.beep()
592 597 break # hit end
593 598 item = level.items[level.cury].item
594 599 try:
595 600 globals = ipipe.getglobals(None)
596 601 if eval(self.input, globals, ipipe.AttrNamespace(item)):
597 602 break # found something
598 603 except (KeyboardInterrupt, SystemExit):
599 604 raise
600 605 except Exception, exc:
601 606 browser.report(exc)
602 607 curses.beep()
603 608 break # break on error
604 609 browser.mode = "default"
605 610 return True
606 611
607 612
608 613 class _CommandFindBackwards(_CommandInput):
609 614 def __init__(self):
610 615 _CommandInput.__init__(self, "find backwards expression")
611 616
612 617 def cmd_execute(self, browser):
613 618 level = browser.levels[-1]
614 619 if self.input:
615 620 self.dohistory()
616 621 while level.cury:
617 622 level.moveto(level.curx, level.cury-1)
618 623 item = level.items[level.cury].item
619 624 try:
620 625 globals = ipipe.getglobals(None)
621 626 if eval(self.input, globals, ipipe.AttrNamespace(item)):
622 627 break # found something
623 628 except (KeyboardInterrupt, SystemExit):
624 629 raise
625 630 except Exception, exc:
626 631 browser.report(exc)
627 632 curses.beep()
628 633 break # break on error
629 634 else:
630 635 curses.beep()
631 636 browser.mode = "default"
632 637 return True
633 638
634 639
635 640 class ibrowse(ipipe.Display):
636 641 # Show this many lines from the previous screen when paging horizontally
637 642 pageoverlapx = 1
638 643
639 644 # Show this many lines from the previous screen when paging vertically
640 645 pageoverlapy = 1
641 646
642 647 # Start scrolling when the cursor is less than this number of columns
643 648 # away from the left or right screen edge
644 649 scrollborderx = 10
645 650
646 651 # Start scrolling when the cursor is less than this number of lines
647 652 # away from the top or bottom screen edge
648 653 scrollbordery = 5
649 654
650 655 # Accelerate by this factor when scrolling horizontally
651 656 acceleratex = 1.05
652 657
653 658 # Accelerate by this factor when scrolling vertically
654 659 acceleratey = 1.05
655 660
656 661 # The maximum horizontal scroll speed
657 662 # (as a factor of the screen width (i.e. 0.5 == half a screen width)
658 663 maxspeedx = 0.5
659 664
660 665 # The maximum vertical scroll speed
661 666 # (as a factor of the screen height (i.e. 0.5 == half a screen height)
662 667 maxspeedy = 0.5
663 668
664 669 # The maximum number of header lines for browser level
665 670 # if the nesting is deeper, only the innermost levels are displayed
666 671 maxheaders = 5
667 672
668 673 # The approximate maximum length of a column entry
669 674 maxattrlength = 200
670 675
671 676 # Styles for various parts of the GUI
672 677 style_objheadertext = astyle.Style.fromstr("white:black:bold|reverse")
673 678 style_objheadernumber = astyle.Style.fromstr("white:blue:bold|reverse")
674 679 style_objheaderobject = astyle.Style.fromstr("white:black:reverse")
675 680 style_colheader = astyle.Style.fromstr("blue:white:reverse")
676 681 style_colheaderhere = astyle.Style.fromstr("green:black:bold|reverse")
677 682 style_colheadersep = astyle.Style.fromstr("blue:black:reverse")
678 683 style_number = astyle.Style.fromstr("blue:white:reverse")
679 684 style_numberhere = astyle.Style.fromstr("green:black:bold|reverse")
680 685 style_sep = astyle.Style.fromstr("blue:black")
681 686 style_data = astyle.Style.fromstr("white:black")
682 687 style_datapad = astyle.Style.fromstr("blue:black:bold")
683 688 style_footer = astyle.Style.fromstr("black:white")
684 689 style_report = astyle.Style.fromstr("white:black")
685 690
686 691 # Column separator in header
687 692 headersepchar = "|"
688 693
689 694 # Character for padding data cell entries
690 695 datapadchar = "."
691 696
692 697 # Column separator in data area
693 698 datasepchar = "|"
694 699
695 700 # Character to use for "empty" cell (i.e. for non-existing attributes)
696 701 nodatachar = "-"
697 702
698 703 # Prompts for modes that require keyboard input
699 704 prompts = {
700 705 "goto": _CommandGoto(),
701 706 "find": _CommandFind(),
702 707 "findbackwards": _CommandFindBackwards()
703 708 }
704 709
705 710 # Maps curses key codes to "function" names
706 711 keymap = Keymap()
707 712 keymap.register("quit", "q")
708 713 keymap.register("up", curses.KEY_UP)
709 714 keymap.register("down", curses.KEY_DOWN)
710 715 keymap.register("pageup", curses.KEY_PPAGE)
711 716 keymap.register("pagedown", curses.KEY_NPAGE)
712 717 keymap.register("left", curses.KEY_LEFT)
713 718 keymap.register("right", curses.KEY_RIGHT)
714 719 keymap.register("home", curses.KEY_HOME, "\x01")
715 720 keymap.register("end", curses.KEY_END, "\x05")
716 721 keymap.register("prevattr", "<\x1b")
717 722 keymap.register("nextattr", ">\t")
718 723 keymap.register("pick", "p")
719 724 keymap.register("pickattr", "P")
720 725 keymap.register("pickallattrs", "C")
721 726 keymap.register("pickmarked", "m")
722 727 keymap.register("pickmarkedattr", "M")
723 728 keymap.register("enterdefault", "\r\n")
724 729 # FIXME: What's happening here?
725 730 keymap.register("leave", curses.KEY_BACKSPACE, "x\x08\x7f")
726 731 keymap.register("hideattr", "h")
727 732 keymap.register("unhideattrs", "H")
728 733 keymap.register("help", "?")
729 734 keymap.register("enter", "e")
730 735 keymap.register("enterattr", "E")
731 736 keymap.register("detail", "d")
732 737 keymap.register("detailattr", "D")
733 738 keymap.register("tooglemark", " ")
734 739 keymap.register("markrange", "r")
735 740 keymap.register("sortattrasc", "v")
736 741 keymap.register("sortattrdesc", "V")
737 742 keymap.register("goto", "g")
738 743 keymap.register("find", "f")
739 744 keymap.register("findbackwards", "b")
740 745
741 746 def __init__(self, *attrs):
742 747 """
743 748 Create a new browser. If ``attrs`` is not empty, it is the list
744 749 of attributes that will be displayed in the browser, otherwise
745 750 these will be determined by the objects on screen.
746 751 """
747 752 self.attrs = attrs
748 753
749 754 # Stack of browser levels
750 755 self.levels = []
751 756 # how many colums to scroll (Changes when accelerating)
752 757 self.stepx = 1.
753 758
754 759 # how many rows to scroll (Changes when accelerating)
755 760 self.stepy = 1.
756 761
757 762 # Beep on the edges of the data area? (Will be set to ``False``
758 763 # once the cursor hits the edge of the screen, so we don't get
759 764 # multiple beeps).
760 765 self._dobeep = True
761 766
762 767 # Cache for registered ``curses`` colors and styles.
763 768 self._styles = {}
764 769 self._colors = {}
765 770 self._maxcolor = 1
766 771
767 772 # How many header lines do we want to paint (the numbers of levels
768 773 # we have, but with an upper bound)
769 774 self._headerlines = 1
770 775
771 776 # Index of first header line
772 777 self._firstheaderline = 0
773 778
774 779 # curses window
775 780 self.scr = None
776 781 # report in the footer line (error, executed command etc.)
777 782 self._report = None
778 783
779 784 # value to be returned to the caller (set by commands)
780 785 self.returnvalue = None
781 786
782 787 # The mode the browser is in
783 788 # e.g. normal browsing or entering an argument for a command
784 789 self.mode = "default"
785 790
786 791 # set by the SIGWINCH signal handler
787 792 self.resized = False
788 793
789 794 def nextstepx(self, step):
790 795 """
791 796 Accelerate horizontally.
792 797 """
793 798 return max(1., min(step*self.acceleratex,
794 799 self.maxspeedx*self.levels[-1].mainsizex))
795 800
796 801 def nextstepy(self, step):
797 802 """
798 803 Accelerate vertically.
799 804 """
800 805 return max(1., min(step*self.acceleratey,
801 806 self.maxspeedy*self.levels[-1].mainsizey))
802 807
803 808 def getstyle(self, style):
804 809 """
805 810 Register the ``style`` with ``curses`` or get it from the cache,
806 811 if it has been registered before.
807 812 """
808 813 try:
809 814 return self._styles[style.fg, style.bg, style.attrs]
810 815 except KeyError:
811 816 attrs = 0
812 817 for b in astyle.A2CURSES:
813 818 if style.attrs & b:
814 819 attrs |= astyle.A2CURSES[b]
815 820 try:
816 821 color = self._colors[style.fg, style.bg]
817 822 except KeyError:
818 823 curses.init_pair(
819 824 self._maxcolor,
820 825 astyle.COLOR2CURSES[style.fg],
821 826 astyle.COLOR2CURSES[style.bg]
822 827 )
823 828 color = curses.color_pair(self._maxcolor)
824 829 self._colors[style.fg, style.bg] = color
825 830 self._maxcolor += 1
826 831 c = color | attrs
827 832 self._styles[style.fg, style.bg, style.attrs] = c
828 833 return c
829 834
830 835 def addstr(self, y, x, begx, endx, text, style):
831 836 """
832 837 A version of ``curses.addstr()`` that can handle ``x`` coordinates
833 838 that are outside the screen.
834 839 """
835 840 text2 = text[max(0, begx-x):max(0, endx-x)]
836 841 if text2:
837 842 self.scr.addstr(y, max(x, begx), text2, self.getstyle(style))
838 843 return len(text)
839 844
840 845 def addchr(self, y, x, begx, endx, c, l, style):
841 846 x0 = max(x, begx)
842 847 x1 = min(x+l, endx)
843 848 if x1>x0:
844 849 self.scr.addstr(y, x0, c*(x1-x0), self.getstyle(style))
845 850 return l
846 851
847 852 def _calcheaderlines(self, levels):
848 853 # Calculate how many headerlines do we have to display, if we have
849 854 # ``levels`` browser levels
850 855 if levels is None:
851 856 levels = len(self.levels)
852 857 self._headerlines = min(self.maxheaders, levels)
853 858 self._firstheaderline = levels-self._headerlines
854 859
855 860 def getstylehere(self, style):
856 861 """
857 862 Return a style for displaying the original style ``style``
858 863 in the row the cursor is on.
859 864 """
860 865 return astyle.Style(style.fg, style.bg, style.attrs | astyle.A_BOLD)
861 866
862 867 def report(self, msg):
863 868 """
864 869 Store the message ``msg`` for display below the footer line. This
865 870 will be displayed as soon as the screen is redrawn.
866 871 """
867 872 self._report = msg
868 873
869 874 def enter(self, item, mode, *attrs):
870 875 """
871 876 Enter the object ``item`` in the mode ``mode``. If ``attrs`` is
872 877 specified, it will be used as a fixed list of attributes to display.
873 878 """
874 879 try:
875 880 iterator = ipipe.xiter(item, mode)
876 881 except (KeyboardInterrupt, SystemExit):
877 882 raise
878 883 except Exception, exc:
879 884 curses.beep()
880 885 self.report(exc)
881 886 else:
882 887 self._calcheaderlines(len(self.levels)+1)
883 888 level = _BrowserLevel(
884 889 self,
885 890 item,
886 891 iterator,
887 892 self.scrsizey-1-self._headerlines-2,
888 893 *attrs
889 894 )
890 895 self.levels.append(level)
891 896
892 897 def startkeyboardinput(self, mode):
893 898 """
894 899 Enter mode ``mode``, which requires keyboard input.
895 900 """
896 901 self.mode = mode
897 902 self.prompts[mode].start()
898 903
899 904 def keylabel(self, keycode):
900 905 """
901 906 Return a pretty name for the ``curses`` key ``keycode`` (used in the
902 907 help screen and in reports about unassigned keys).
903 908 """
904 909 if keycode <= 0xff:
905 910 specialsnames = {
906 911 ord("\n"): "RETURN",
907 912 ord(" "): "SPACE",
908 913 ord("\t"): "TAB",
909 914 ord("\x7f"): "DELETE",
910 915 ord("\x08"): "BACKSPACE",
911 916 }
912 917 if keycode in specialsnames:
913 918 return specialsnames[keycode]
914 919 elif 0x00 < keycode < 0x20:
915 920 return "CTRL-%s" % chr(keycode + 64)
916 921 return repr(chr(keycode))
917 922 for name in dir(curses):
918 923 if name.startswith("KEY_") and getattr(curses, name) == keycode:
919 924 return name
920 925 return str(keycode)
921 926
922 927 def beep(self, force=False):
923 928 if force or self._dobeep:
924 929 curses.beep()
925 930 # don't beep again (as long as the same key is pressed)
926 931 self._dobeep = False
927 932
928 933 def cmd_up(self):
929 934 """
930 935 Move the cursor to the previous row.
931 936 """
932 937 level = self.levels[-1]
933 938 self.report("up")
934 939 level.moveto(level.curx, level.cury-self.stepy)
935 940
936 941 def cmd_down(self):
937 942 """
938 943 Move the cursor to the next row.
939 944 """
940 945 level = self.levels[-1]
941 946 self.report("down")
942 947 level.moveto(level.curx, level.cury+self.stepy)
943 948
944 949 def cmd_pageup(self):
945 950 """
946 951 Move the cursor up one page.
947 952 """
948 953 level = self.levels[-1]
949 954 self.report("page up")
950 955 level.moveto(level.curx, level.cury-level.mainsizey+self.pageoverlapy)
951 956
952 957 def cmd_pagedown(self):
953 958 """
954 959 Move the cursor down one page.
955 960 """
956 961 level = self.levels[-1]
957 962 self.report("page down")
958 963 level.moveto(level.curx, level.cury+level.mainsizey-self.pageoverlapy)
959 964
960 965 def cmd_left(self):
961 966 """
962 967 Move the cursor left.
963 968 """
964 969 level = self.levels[-1]
965 970 self.report("left")
966 971 level.moveto(level.curx-self.stepx, level.cury)
967 972
968 973 def cmd_right(self):
969 974 """
970 975 Move the cursor right.
971 976 """
972 977 level = self.levels[-1]
973 978 self.report("right")
974 979 level.moveto(level.curx+self.stepx, level.cury)
975 980
976 981 def cmd_home(self):
977 982 """
978 983 Move the cursor to the first column.
979 984 """
980 985 level = self.levels[-1]
981 986 self.report("home")
982 987 level.moveto(0, level.cury)
983 988
984 989 def cmd_end(self):
985 990 """
986 991 Move the cursor to the last column.
987 992 """
988 993 level = self.levels[-1]
989 994 self.report("end")
990 995 level.moveto(level.datasizex+level.mainsizey-self.pageoverlapx, level.cury)
991 996
992 997 def cmd_prevattr(self):
993 998 """
994 999 Move the cursor one attribute column to the left.
995 1000 """
996 1001 level = self.levels[-1]
997 1002 if level.displayattr[0] is None or level.displayattr[0] == 0:
998 1003 self.beep()
999 1004 else:
1000 1005 self.report("prevattr")
1001 1006 pos = 0
1002 1007 for (i, attrname) in enumerate(level.displayattrs):
1003 1008 if i == level.displayattr[0]-1:
1004 1009 break
1005 1010 pos += level.colwidths[attrname] + 1
1006 1011 level.moveto(pos, level.cury)
1007 1012
1008 1013 def cmd_nextattr(self):
1009 1014 """
1010 1015 Move the cursor one attribute column to the right.
1011 1016 """
1012 1017 level = self.levels[-1]
1013 1018 if level.displayattr[0] is None or level.displayattr[0] == len(level.displayattrs)-1:
1014 1019 self.beep()
1015 1020 else:
1016 1021 self.report("nextattr")
1017 1022 pos = 0
1018 1023 for (i, attrname) in enumerate(level.displayattrs):
1019 1024 if i == level.displayattr[0]+1:
1020 1025 break
1021 1026 pos += level.colwidths[attrname] + 1
1022 1027 level.moveto(pos, level.cury)
1023 1028
1024 1029 def cmd_pick(self):
1025 1030 """
1026 1031 'Pick' the object under the cursor (i.e. the row the cursor is on).
1027 1032 This leaves the browser and returns the picked object to the caller.
1028 1033 (In IPython this object will be available as the '_' variable.)
1029 1034 """
1030 1035 level = self.levels[-1]
1031 1036 self.returnvalue = level.items[level.cury].item
1032 1037 return True
1033 1038
1034 1039 def cmd_pickattr(self):
1035 1040 """
1036 1041 'Pick' the attribute under the cursor (i.e. the row/column the
1037 1042 cursor is on).
1038 1043 """
1039 1044 level = self.levels[-1]
1040 1045 attrname = level.displayattr[1]
1041 1046 if attrname is ipipe.noitem:
1042 1047 curses.beep()
1043 1048 self.report(AttributeError(ipipe._attrname(attrname)))
1044 1049 return
1045 1050 attr = ipipe._getattr(level.items[level.cury].item, attrname)
1046 1051 if attr is ipipe.noitem:
1047 1052 curses.beep()
1048 1053 self.report(AttributeError(ipipe._attrname(attrname)))
1049 1054 else:
1050 1055 self.returnvalue = attr
1051 1056 return True
1052 1057
1053 1058 def cmd_pickallattrs(self):
1054 1059 """
1055 1060 Pick' the complete column under the cursor (i.e. the attribute under
1056 1061 the cursor) from all currently fetched objects. These attributes
1057 1062 will be returned as a list.
1058 1063 """
1059 1064 level = self.levels[-1]
1060 1065 attrname = level.displayattr[1]
1061 1066 if attrname is ipipe.noitem:
1062 1067 curses.beep()
1063 1068 self.report(AttributeError(ipipe._attrname(attrname)))
1064 1069 return
1065 1070 result = []
1066 1071 for cache in level.items:
1067 1072 attr = ipipe._getattr(cache.item, attrname)
1068 1073 if attr is not ipipe.noitem:
1069 1074 result.append(attr)
1070 1075 self.returnvalue = result
1071 1076 return True
1072 1077
1073 1078 def cmd_pickmarked(self):
1074 1079 """
1075 1080 'Pick' marked objects. Marked objects will be returned as a list.
1076 1081 """
1077 1082 level = self.levels[-1]
1078 1083 self.returnvalue = [cache.item for cache in level.items if cache.marked]
1079 1084 return True
1080 1085
1081 1086 def cmd_pickmarkedattr(self):
1082 1087 """
1083 1088 'Pick' the attribute under the cursor from all marked objects
1084 1089 (This returns a list).
1085 1090 """
1086 1091
1087 1092 level = self.levels[-1]
1088 1093 attrname = level.displayattr[1]
1089 1094 if attrname is ipipe.noitem:
1090 1095 curses.beep()
1091 1096 self.report(AttributeError(ipipe._attrname(attrname)))
1092 1097 return
1093 1098 result = []
1094 1099 for cache in level.items:
1095 1100 if cache.marked:
1096 1101 attr = ipipe._getattr(cache.item, attrname)
1097 1102 if attr is not ipipe.noitem:
1098 1103 result.append(attr)
1099 1104 self.returnvalue = result
1100 1105 return True
1101 1106
1102 1107 def cmd_markrange(self):
1103 1108 """
1104 1109 Mark all objects from the last marked object before the current cursor
1105 1110 position to the cursor position.
1106 1111 """
1107 1112 level = self.levels[-1]
1108 1113 self.report("markrange")
1109 1114 start = None
1110 1115 if level.items:
1111 1116 for i in xrange(level.cury, -1, -1):
1112 1117 if level.items[i].marked:
1113 1118 start = i
1114 1119 break
1115 1120 if start is None:
1116 1121 self.report(CommandError("no mark before cursor"))
1117 1122 curses.beep()
1118 1123 else:
1119 1124 for i in xrange(start, level.cury+1):
1120 1125 cache = level.items[i]
1121 1126 if not cache.marked:
1122 1127 cache.marked = True
1123 1128 level.marked += 1
1124 1129
1125 1130 def cmd_enterdefault(self):
1126 1131 """
1127 1132 Enter the object under the cursor. (what this mean depends on the object
1128 1133 itself (i.e. how it implements the '__xiter__' method). This opens a new
1129 1134 browser 'level'.
1130 1135 """
1131 1136 level = self.levels[-1]
1132 1137 try:
1133 1138 item = level.items[level.cury].item
1134 1139 except IndexError:
1135 1140 self.report(CommandError("No object"))
1136 1141 curses.beep()
1137 1142 else:
1138 1143 self.report("entering object (default mode)...")
1139 1144 self.enter(item, "default")
1140 1145
1141 1146 def cmd_leave(self):
1142 1147 """
1143 1148 Leave the current browser level and go back to the previous one.
1144 1149 """
1145 1150 self.report("leave")
1146 1151 if len(self.levels) > 1:
1147 1152 self._calcheaderlines(len(self.levels)-1)
1148 1153 self.levels.pop(-1)
1149 1154 else:
1150 1155 self.report(CommandError("This is the last level"))
1151 1156 curses.beep()
1152 1157
1153 1158 def cmd_enter(self):
1154 1159 """
1155 1160 Enter the object under the cursor. If the object provides different
1156 1161 enter modes a menu of all modes will be presented; choose one and enter
1157 1162 it (via the 'enter' or 'enterdefault' command).
1158 1163 """
1159 1164 level = self.levels[-1]
1160 1165 try:
1161 1166 item = level.items[level.cury].item
1162 1167 except IndexError:
1163 1168 self.report(CommandError("No object"))
1164 1169 curses.beep()
1165 1170 else:
1166 1171 self.report("entering object...")
1167 1172 self.enter(item, None)
1168 1173
1169 1174 def cmd_enterattr(self):
1170 1175 """
1171 1176 Enter the attribute under the cursor.
1172 1177 """
1173 1178 level = self.levels[-1]
1174 1179 attrname = level.displayattr[1]
1175 1180 if attrname is ipipe.noitem:
1176 1181 curses.beep()
1177 1182 self.report(AttributeError(ipipe._attrname(attrname)))
1178 1183 return
1179 1184 try:
1180 1185 item = level.items[level.cury].item
1181 1186 except IndexError:
1182 1187 self.report(CommandError("No object"))
1183 1188 curses.beep()
1184 1189 else:
1185 1190 attr = ipipe._getattr(item, attrname)
1186 1191 if attr is ipipe.noitem:
1187 1192 self.report(AttributeError(ipipe._attrname(attrname)))
1188 1193 else:
1189 1194 self.report("entering object attribute %s..." % ipipe._attrname(attrname))
1190 1195 self.enter(attr, None)
1191 1196
1192 1197 def cmd_detail(self):
1193 1198 """
1194 1199 Show a detail view of the object under the cursor. This shows the
1195 1200 name, type, doc string and value of the object attributes (and it
1196 1201 might show more attributes than in the list view, depending on
1197 1202 the object).
1198 1203 """
1199 1204 level = self.levels[-1]
1200 1205 try:
1201 1206 item = level.items[level.cury].item
1202 1207 except IndexError:
1203 1208 self.report(CommandError("No object"))
1204 1209 curses.beep()
1205 1210 else:
1206 1211 self.report("entering detail view for object...")
1207 1212 self.enter(item, "detail")
1208 1213
1209 1214 def cmd_detailattr(self):
1210 1215 """
1211 1216 Show a detail view of the attribute under the cursor.
1212 1217 """
1213 1218 level = self.levels[-1]
1214 1219 attrname = level.displayattr[1]
1215 1220 if attrname is ipipe.noitem:
1216 1221 curses.beep()
1217 1222 self.report(AttributeError(ipipe._attrname(attrname)))
1218 1223 return
1219 1224 try:
1220 1225 item = level.items[level.cury].item
1221 1226 except IndexError:
1222 1227 self.report(CommandError("No object"))
1223 1228 curses.beep()
1224 1229 else:
1225 1230 attr = ipipe._getattr(item, attrname)
1226 1231 if attr is ipipe.noitem:
1227 1232 self.report(AttributeError(ipipe._attrname(attrname)))
1228 1233 else:
1229 1234 self.report("entering detail view for attribute...")
1230 1235 self.enter(attr, "detail")
1231 1236
1232 1237 def cmd_tooglemark(self):
1233 1238 """
1234 1239 Mark/unmark the object under the cursor. Marked objects have a '!'
1235 1240 after the row number).
1236 1241 """
1237 1242 level = self.levels[-1]
1238 1243 self.report("toggle mark")
1239 1244 try:
1240 1245 item = level.items[level.cury]
1241 1246 except IndexError: # no items?
1242 1247 pass
1243 1248 else:
1244 1249 if item.marked:
1245 1250 item.marked = False
1246 1251 level.marked -= 1
1247 1252 else:
1248 1253 item.marked = True
1249 1254 level.marked += 1
1250 1255
1251 1256 def cmd_sortattrasc(self):
1252 1257 """
1253 1258 Sort the objects (in ascending order) using the attribute under
1254 1259 the cursor as the sort key.
1255 1260 """
1256 1261 level = self.levels[-1]
1257 1262 attrname = level.displayattr[1]
1258 1263 if attrname is ipipe.noitem:
1259 1264 curses.beep()
1260 1265 self.report(AttributeError(ipipe._attrname(attrname)))
1261 1266 return
1262 1267 self.report("sort by %s (ascending)" % ipipe._attrname(attrname))
1263 1268 def key(item):
1264 1269 try:
1265 1270 return ipipe._getattr(item, attrname, None)
1266 1271 except (KeyboardInterrupt, SystemExit):
1267 1272 raise
1268 1273 except Exception:
1269 1274 return None
1270 1275 level.sort(key)
1271 1276
1272 1277 def cmd_sortattrdesc(self):
1273 1278 """
1274 1279 Sort the objects (in descending order) using the attribute under
1275 1280 the cursor as the sort key.
1276 1281 """
1277 1282 level = self.levels[-1]
1278 1283 attrname = level.displayattr[1]
1279 1284 if attrname is ipipe.noitem:
1280 1285 curses.beep()
1281 1286 self.report(AttributeError(ipipe._attrname(attrname)))
1282 1287 return
1283 1288 self.report("sort by %s (descending)" % ipipe._attrname(attrname))
1284 1289 def key(item):
1285 1290 try:
1286 1291 return ipipe._getattr(item, attrname, None)
1287 1292 except (KeyboardInterrupt, SystemExit):
1288 1293 raise
1289 1294 except Exception:
1290 1295 return None
1291 1296 level.sort(key, reverse=True)
1292 1297
1293 1298 def cmd_hideattr(self):
1294 1299 """
1295 1300 Hide the attribute under the cursor.
1296 1301 """
1297 1302 level = self.levels[-1]
1298 1303 if level.displayattr[0] is None:
1299 1304 self.beep()
1300 1305 else:
1301 1306 self.report("hideattr")
1302 1307 level.hiddenattrs.add(level.displayattr[1])
1303 1308 level.moveto(level.curx, level.cury, refresh=True)
1304 1309
1305 1310 def cmd_unhideattrs(self):
1306 1311 """
1307 1312 Make all attributes visible again.
1308 1313 """
1309 1314 level = self.levels[-1]
1310 1315 self.report("unhideattrs")
1311 1316 level.hiddenattrs.clear()
1312 1317 level.moveto(level.curx, level.cury, refresh=True)
1313 1318
1314 1319 def cmd_goto(self):
1315 1320 """
1316 1321 Jump to a row. The row number can be entered at the
1317 1322 bottom of the screen.
1318 1323 """
1319 1324 self.startkeyboardinput("goto")
1320 1325
1321 1326 def cmd_find(self):
1322 1327 """
1323 1328 Search forward for a row. The search condition can be entered at the
1324 1329 bottom of the screen.
1325 1330 """
1326 1331 self.startkeyboardinput("find")
1327 1332
1328 1333 def cmd_findbackwards(self):
1329 1334 """
1330 1335 Search backward for a row. The search condition can be entered at the
1331 1336 bottom of the screen.
1332 1337 """
1333 1338 self.startkeyboardinput("findbackwards")
1334 1339
1335 1340 def cmd_help(self):
1336 1341 """
1337 1342 Opens the help screen as a new browser level, describing keyboard
1338 1343 shortcuts.
1339 1344 """
1340 1345 for level in self.levels:
1341 1346 if isinstance(level.input, _BrowserHelp):
1342 1347 curses.beep()
1343 1348 self.report(CommandError("help already active"))
1344 1349 return
1345 1350
1346 1351 self.enter(_BrowserHelp(self), "default")
1347 1352
1348 1353 def cmd_quit(self):
1349 1354 """
1350 1355 Quit the browser and return to the IPython prompt.
1351 1356 """
1352 1357 self.returnvalue = None
1353 1358 return True
1354 1359
1355 1360 def sigwinchhandler(self, signal, frame):
1356 1361 self.resized = True
1357 1362
1358 1363 def _dodisplay(self, scr):
1359 1364 """
1360 1365 This method is the workhorse of the browser. It handles screen
1361 1366 drawing and the keyboard.
1362 1367 """
1363 1368 self.scr = scr
1364 1369 curses.halfdelay(1)
1365 1370 footery = 2
1366 1371
1367 1372 keys = []
1368 1373 quitkey = self.keymap.findkey("quit", None)
1369 1374 if quitkey is not None:
1370 1375 keys.append("%s=quit" % self.keylabel(quitkey))
1371 1376 helpkey = self.keymap.findkey("help", None)
1372 1377 if helpkey is not None:
1373 1378 keys.append("%s=help" % self.keylabel(helpkey))
1374 1379 helpmsg = " | %s" % " ".join(keys)
1375 1380
1376 1381 scr.clear()
1377 1382 msg = "Fetching first batch of objects..."
1378 1383 (self.scrsizey, self.scrsizex) = scr.getmaxyx()
1379 1384 scr.addstr(self.scrsizey//2, (self.scrsizex-len(msg))//2, msg)
1380 1385 scr.refresh()
1381 1386
1382 1387 lastc = -1
1383 1388
1384 1389 self.levels = []
1385 1390 # enter the first level
1386 1391 self.enter(self.input, ipipe.xiter(self.input, "default"), *self.attrs)
1387 1392
1388 1393 self._calcheaderlines(None)
1389 1394
1390 1395 while True:
1391 1396 level = self.levels[-1]
1392 1397 (self.scrsizey, self.scrsizex) = scr.getmaxyx()
1393 1398 level.mainsizey = self.scrsizey-1-self._headerlines-footery
1394 1399
1395 1400 # Paint object header
1396 1401 for i in xrange(self._firstheaderline, self._firstheaderline+self._headerlines):
1397 1402 lv = self.levels[i]
1398 1403 posx = 0
1399 1404 posy = i-self._firstheaderline
1400 1405 endx = self.scrsizex
1401 1406 if i: # not the first level
1402 1407 msg = " (%d/%d" % (self.levels[i-1].cury, len(self.levels[i-1].items))
1403 1408 if not self.levels[i-1].exhausted:
1404 1409 msg += "+"
1405 1410 msg += ") "
1406 1411 endx -= len(msg)+1
1407 1412 posx += self.addstr(posy, posx, 0, endx, " ibrowse #%d: " % i, self.style_objheadertext)
1408 1413 for (style, text) in lv.header:
1409 1414 posx += self.addstr(posy, posx, 0, endx, text, self.style_objheaderobject)
1410 1415 if posx >= endx:
1411 1416 break
1412 1417 if i:
1413 1418 posx += self.addstr(posy, posx, 0, self.scrsizex, msg, self.style_objheadernumber)
1414 1419 posx += self.addchr(posy, posx, 0, self.scrsizex, " ", self.scrsizex-posx, self.style_objheadernumber)
1415 1420
1416 1421 if not level.items:
1417 1422 self.addchr(self._headerlines, 0, 0, self.scrsizex, " ", self.scrsizex, self.style_colheader)
1418 1423 self.addstr(self._headerlines+1, 0, 0, self.scrsizex, " <empty>", astyle.style_error)
1419 1424 scr.clrtobot()
1420 1425 else:
1421 1426 # Paint column headers
1422 1427 scr.move(self._headerlines, 0)
1423 1428 scr.addstr(" %*s " % (level.numbersizex, "#"), self.getstyle(self.style_colheader))
1424 1429 scr.addstr(self.headersepchar, self.getstyle(self.style_colheadersep))
1425 1430 begx = level.numbersizex+3
1426 1431 posx = begx-level.datastartx
1427 1432 for attrname in level.displayattrs:
1428 1433 strattrname = ipipe._attrname(attrname)
1429 1434 cwidth = level.colwidths[attrname]
1430 1435 header = strattrname.ljust(cwidth)
1431 1436 if attrname == level.displayattr[1]:
1432 1437 style = self.style_colheaderhere
1433 1438 else:
1434 1439 style = self.style_colheader
1435 1440 posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, header, style)
1436 1441 posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, self.headersepchar, self.style_colheadersep)
1437 1442 if posx >= self.scrsizex:
1438 1443 break
1439 1444 else:
1440 1445 scr.addstr(" "*(self.scrsizex-posx), self.getstyle(self.style_colheader))
1441 1446
1442 1447 # Paint rows
1443 1448 posy = self._headerlines+1+level.datastarty
1444 1449 for i in xrange(level.datastarty, min(level.datastarty+level.mainsizey, len(level.items))):
1445 1450 cache = level.items[i]
1446 1451 if i == level.cury:
1447 1452 style = self.style_numberhere
1448 1453 else:
1449 1454 style = self.style_number
1450 1455
1451 1456 posy = self._headerlines+1+i-level.datastarty
1452 1457 posx = begx-level.datastartx
1453 1458
1454 1459 scr.move(posy, 0)
1455 1460 scr.addstr(" %*d%s" % (level.numbersizex, i, " !"[cache.marked]), self.getstyle(style))
1456 1461 scr.addstr(self.headersepchar, self.getstyle(self.style_sep))
1457 1462
1458 1463 for attrname in level.displayattrs:
1459 1464 cwidth = level.colwidths[attrname]
1460 1465 try:
1461 1466 (align, length, parts) = level.displayrows[i-level.datastarty][attrname]
1462 1467 except KeyError:
1463 1468 align = 2
1464 1469 style = astyle.style_nodata
1465 1470 padstyle = self.style_datapad
1466 1471 sepstyle = self.style_sep
1467 1472 if i == level.cury:
1468 1473 padstyle = self.getstylehere(padstyle)
1469 1474 sepstyle = self.getstylehere(sepstyle)
1470 1475 if align == 2:
1471 1476 posx += self.addchr(posy, posx, begx, self.scrsizex, self.nodatachar, cwidth, style)
1472 1477 else:
1473 1478 if align == 1:
1474 1479 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
1475 1480 elif align == 0:
1476 1481 pad1 = (cwidth-length)//2
1477 1482 pad2 = cwidth-length-len(pad1)
1478 1483 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad1, padstyle)
1479 1484 for (style, text) in parts:
1480 1485 if i == level.cury:
1481 1486 style = self.getstylehere(style)
1482 1487 posx += self.addstr(posy, posx, begx, self.scrsizex, text, style)
1483 1488 if posx >= self.scrsizex:
1484 1489 break
1485 1490 if align == -1:
1486 1491 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
1487 1492 elif align == 0:
1488 1493 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad2, padstyle)
1489 1494 posx += self.addstr(posy, posx, begx, self.scrsizex, self.datasepchar, sepstyle)
1490 1495 else:
1491 1496 scr.clrtoeol()
1492 1497
1493 1498 # Add blank row headers for the rest of the screen
1494 1499 for posy in xrange(posy+1, self.scrsizey-2):
1495 1500 scr.addstr(posy, 0, " " * (level.numbersizex+2), self.getstyle(self.style_colheader))
1496 1501 scr.clrtoeol()
1497 1502
1498 1503 posy = self.scrsizey-footery
1499 1504 # Display footer
1500 1505 scr.addstr(posy, 0, " "*self.scrsizex, self.getstyle(self.style_footer))
1501 1506
1502 1507 if level.exhausted:
1503 1508 flag = ""
1504 1509 else:
1505 1510 flag = "+"
1506 1511
1507 1512 endx = self.scrsizex-len(helpmsg)-1
1508 1513 scr.addstr(posy, endx, helpmsg, self.getstyle(self.style_footer))
1509 1514
1510 1515 posx = 0
1511 1516 msg = " %d%s objects (%d marked): " % (len(level.items), flag, level.marked)
1512 1517 posx += self.addstr(posy, posx, 0, endx, msg, self.style_footer)
1513 1518 try:
1514 1519 item = level.items[level.cury].item
1515 1520 except IndexError: # empty
1516 1521 pass
1517 1522 else:
1518 1523 for (nostyle, text) in ipipe.xrepr(item, "footer"):
1519 1524 if not isinstance(nostyle, int):
1520 1525 posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
1521 1526 if posx >= endx:
1522 1527 break
1523 1528
1524 1529 attrstyle = [(astyle.style_default, "no attribute")]
1525 1530 attrname = level.displayattr[1]
1526 1531 if attrname is not ipipe.noitem and attrname is not None:
1527 1532 posx += self.addstr(posy, posx, 0, endx, " | ", self.style_footer)
1528 1533 posx += self.addstr(posy, posx, 0, endx, ipipe._attrname(attrname), self.style_footer)
1529 1534 posx += self.addstr(posy, posx, 0, endx, ": ", self.style_footer)
1530 1535 try:
1531 1536 attr = ipipe._getattr(item, attrname)
1532 1537 except (SystemExit, KeyboardInterrupt):
1533 1538 raise
1534 1539 except Exception, exc:
1535 1540 attr = exc
1536 1541 if attr is not ipipe.noitem:
1537 1542 attrstyle = ipipe.xrepr(attr, "footer")
1538 1543 for (nostyle, text) in attrstyle:
1539 1544 if not isinstance(nostyle, int):
1540 1545 posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
1541 1546 if posx >= endx:
1542 1547 break
1543 1548
1544 1549 try:
1545 1550 # Display input prompt
1546 1551 if self.mode in self.prompts:
1547 1552 history = self.prompts[self.mode]
1548 1553 posx = 0
1549 1554 posy = self.scrsizey-1
1550 1555 posx += self.addstr(posy, posx, 0, endx, history.prompt, astyle.style_default)
1551 1556 posx += self.addstr(posy, posx, 0, endx, " [", astyle.style_default)
1552 1557 if history.cury==-1:
1553 1558 text = "new"
1554 1559 else:
1555 1560 text = str(history.cury+1)
1556 1561 posx += self.addstr(posy, posx, 0, endx, text, astyle.style_type_number)
1557 1562 if history.history:
1558 1563 posx += self.addstr(posy, posx, 0, endx, "/", astyle.style_default)
1559 1564 posx += self.addstr(posy, posx, 0, endx, str(len(history.history)), astyle.style_type_number)
1560 1565 posx += self.addstr(posy, posx, 0, endx, "]: ", astyle.style_default)
1561 1566 inputstartx = posx
1562 1567 posx += self.addstr(posy, posx, 0, endx, history.input, astyle.style_default)
1563 1568 # Display report
1564 1569 else:
1565 1570 if self._report is not None:
1566 1571 if isinstance(self._report, Exception):
1567 1572 style = self.getstyle(astyle.style_error)
1568 1573 if self._report.__class__.__module__ == "exceptions":
1569 1574 msg = "%s: %s" % \
1570 1575 (self._report.__class__.__name__, self._report)
1571 1576 else:
1572 1577 msg = "%s.%s: %s" % \
1573 1578 (self._report.__class__.__module__,
1574 1579 self._report.__class__.__name__, self._report)
1575 1580 else:
1576 1581 style = self.getstyle(self.style_report)
1577 1582 msg = self._report
1578 1583 scr.addstr(self.scrsizey-1, 0, msg[:self.scrsizex], style)
1579 1584 self._report = None
1580 1585 else:
1581 1586 scr.move(self.scrsizey-1, 0)
1582 1587 except curses.error:
1583 1588 # Protect against errors from writing to the last line
1584 1589 pass
1585 1590 scr.clrtoeol()
1586 1591
1587 1592 # Position cursor
1588 1593 if self.mode in self.prompts:
1589 1594 history = self.prompts[self.mode]
1590 1595 scr.move(self.scrsizey-1, inputstartx+history.curx)
1591 1596 else:
1592 1597 scr.move(
1593 1598 1+self._headerlines+level.cury-level.datastarty,
1594 1599 level.numbersizex+3+level.curx-level.datastartx
1595 1600 )
1596 1601 scr.refresh()
1597 1602
1598 1603 # Check keyboard
1599 1604 while True:
1600 1605 c = scr.getch()
1601 1606 if self.resized:
1602 1607 size = fcntl.ioctl(0, tty.TIOCGWINSZ, "12345678")
1603 1608 size = struct.unpack("4H", size)
1604 1609 oldsize = scr.getmaxyx()
1605 1610 scr.erase()
1606 1611 curses.resize_term(size[0], size[1])
1607 1612 newsize = scr.getmaxyx()
1608 1613 scr.erase()
1609 1614 for l in self.levels:
1610 1615 l.mainsizey += newsize[0]-oldsize[0]
1611 1616 l.moveto(l.curx, l.cury, refresh=True)
1612 1617 scr.refresh()
1613 1618 self.resized = False
1614 1619 break # Redisplay
1615 1620 if self.mode in self.prompts:
1616 1621 if self.prompts[self.mode].handlekey(self, c):
1617 1622 break # Redisplay
1618 1623 else:
1619 1624 # if no key is pressed slow down and beep again
1620 1625 if c == -1:
1621 1626 self.stepx = 1.
1622 1627 self.stepy = 1.
1623 1628 self._dobeep = True
1624 1629 else:
1625 1630 # if a different key was pressed slow down and beep too
1626 1631 if c != lastc:
1627 1632 lastc = c
1628 1633 self.stepx = 1.
1629 1634 self.stepy = 1.
1630 1635 self._dobeep = True
1631 1636 cmdname = self.keymap.get(c, None)
1632 1637 if cmdname is None:
1633 1638 self.report(
1634 1639 UnassignedKeyError("Unassigned key %s" %
1635 1640 self.keylabel(c)))
1636 1641 else:
1637 1642 cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
1638 1643 if cmdfunc is None:
1639 1644 self.report(
1640 1645 UnknownCommandError("Unknown command %r" %
1641 1646 (cmdname,)))
1642 1647 elif cmdfunc():
1643 1648 returnvalue = self.returnvalue
1644 1649 self.returnvalue = None
1645 1650 return returnvalue
1646 1651 self.stepx = self.nextstepx(self.stepx)
1647 1652 self.stepy = self.nextstepy(self.stepy)
1648 1653 curses.flushinp() # get rid of type ahead
1649 1654 break # Redisplay
1650 1655 self.scr = None
1651 1656
1652 1657 def display(self):
1653 1658 if hasattr(curses, "resize_term"):
1654 1659 oldhandler = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1655 1660 try:
1656 1661 return curses.wrapper(self._dodisplay)
1657 1662 finally:
1658 1663 signal.signal(signal.SIGWINCH, oldhandler)
1659 1664 else:
1660 1665 return curses.wrapper(self._dodisplay)
General Comments 0
You need to be logged in to leave comments. Login now