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