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