##// END OF EJS Templates
Add two new commands to ibrowse:...
walter.doerwald -
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1667 +1,1717 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 def __init__(self, browser, input, iterator, mainsizey, *attrs):
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 self.iterator = iterator
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 def refresh(self):
421 """
422 Restart iterating the input.
423 """
424 self.iterator = ipipe.xiter(self.input)
425 self.items.clear()
426 self.exhausted = False
427 self.moveto(0, 0, refresh=True)
428
429 def refreshfind(self):
430 """
431 Restart iterating the input and go back to the same object as before
432 (if it can be found in the new iterator).
433 """
434 try:
435 oldobject = self.items[self.cury].item
436 except IndexError:
437 oldobject = ipipe.noitem
438 self.iterator = ipipe.xiter(self.input)
439 self.items.clear()
440 self.exhausted = False
441 while True:
442 self.fetch(len(self.items)+1)
443 if self.exhausted:
444 curses.beep()
445 self.moveto(0, 0, refresh=True)
446 break
447 if self.items[-1].item == oldobject:
448 self.moveto(self.curx, len(self.items)-1, refresh=True)
449 break
450
420 451
421 452 class _CommandInput(object):
422 453 keymap = Keymap()
423 454 keymap.register("left", curses.KEY_LEFT)
424 455 keymap.register("right", curses.KEY_RIGHT)
425 456 keymap.register("home", curses.KEY_HOME, "\x01") # Ctrl-A
426 457 keymap.register("end", curses.KEY_END, "\x05") # Ctrl-E
427 458 # FIXME: What's happening here?
428 459 keymap.register("backspace", curses.KEY_BACKSPACE, "\x08\x7f")
429 460 keymap.register("delete", curses.KEY_DC)
430 461 keymap.register("delend", 0x0b) # Ctrl-K
431 462 keymap.register("execute", "\r\n")
432 463 keymap.register("up", curses.KEY_UP)
433 464 keymap.register("down", curses.KEY_DOWN)
434 465 keymap.register("incsearchup", curses.KEY_PPAGE)
435 466 keymap.register("incsearchdown", curses.KEY_NPAGE)
436 467 keymap.register("exit", "\x18"), # Ctrl-X
437 468
438 469 def __init__(self, prompt):
439 470 self.prompt = prompt
440 471 self.history = []
441 472 self.maxhistory = 100
442 473 self.input = ""
443 474 self.curx = 0
444 475 self.cury = -1 # blank line
445 476
446 477 def start(self):
447 478 self.input = ""
448 479 self.curx = 0
449 480 self.cury = -1 # blank line
450 481
451 482 def handlekey(self, browser, key):
452 483 cmdname = self.keymap.get(key, None)
453 484 if cmdname is not None:
454 485 cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
455 486 if cmdfunc is not None:
456 487 return cmdfunc(browser)
457 488 curses.beep()
458 489 elif key != -1:
459 490 try:
460 491 char = chr(key)
461 492 except ValueError:
462 493 curses.beep()
463 494 else:
464 495 return self.handlechar(browser, char)
465 496
466 497 def handlechar(self, browser, char):
467 498 self.input = self.input[:self.curx] + char + self.input[self.curx:]
468 499 self.curx += 1
469 500 return True
470 501
471 502 def dohistory(self):
472 503 self.history.insert(0, self.input)
473 504 del self.history[:-self.maxhistory]
474 505
475 506 def cmd_backspace(self, browser):
476 507 if self.curx:
477 508 self.input = self.input[:self.curx-1] + self.input[self.curx:]
478 509 self.curx -= 1
479 510 return True
480 511 else:
481 512 curses.beep()
482 513
483 514 def cmd_delete(self, browser):
484 515 if self.curx<len(self.input):
485 516 self.input = self.input[:self.curx] + self.input[self.curx+1:]
486 517 return True
487 518 else:
488 519 curses.beep()
489 520
490 521 def cmd_delend(self, browser):
491 522 if self.curx<len(self.input):
492 523 self.input = self.input[:self.curx]
493 524 return True
494 525
495 526 def cmd_left(self, browser):
496 527 if self.curx:
497 528 self.curx -= 1
498 529 return True
499 530 else:
500 531 curses.beep()
501 532
502 533 def cmd_right(self, browser):
503 534 if self.curx < len(self.input):
504 535 self.curx += 1
505 536 return True
506 537 else:
507 538 curses.beep()
508 539
509 540 def cmd_home(self, browser):
510 541 if self.curx:
511 542 self.curx = 0
512 543 return True
513 544 else:
514 545 curses.beep()
515 546
516 547 def cmd_end(self, browser):
517 548 if self.curx < len(self.input):
518 549 self.curx = len(self.input)
519 550 return True
520 551 else:
521 552 curses.beep()
522 553
523 554 def cmd_up(self, browser):
524 555 if self.cury < len(self.history)-1:
525 556 self.cury += 1
526 557 self.input = self.history[self.cury]
527 558 self.curx = len(self.input)
528 559 return True
529 560 else:
530 561 curses.beep()
531 562
532 563 def cmd_down(self, browser):
533 564 if self.cury >= 0:
534 565 self.cury -= 1
535 566 if self.cury>=0:
536 567 self.input = self.history[self.cury]
537 568 else:
538 569 self.input = ""
539 570 self.curx = len(self.input)
540 571 return True
541 572 else:
542 573 curses.beep()
543 574
544 575 def cmd_incsearchup(self, browser):
545 576 prefix = self.input[:self.curx]
546 577 cury = self.cury
547 578 while True:
548 579 cury += 1
549 580 if cury >= len(self.history):
550 581 break
551 582 if self.history[cury].startswith(prefix):
552 583 self.input = self.history[cury]
553 584 self.cury = cury
554 585 return True
555 586 curses.beep()
556 587
557 588 def cmd_incsearchdown(self, browser):
558 589 prefix = self.input[:self.curx]
559 590 cury = self.cury
560 591 while True:
561 592 cury -= 1
562 593 if cury <= 0:
563 594 break
564 595 if self.history[cury].startswith(prefix):
565 596 self.input = self.history[self.cury]
566 597 self.cury = cury
567 598 return True
568 599 curses.beep()
569 600
570 601 def cmd_exit(self, browser):
571 602 browser.mode = "default"
572 603 return True
573 604
574 605 def cmd_execute(self, browser):
575 606 raise NotImplementedError
576 607
577 608
578 609 class _CommandGoto(_CommandInput):
579 610 def __init__(self):
580 611 _CommandInput.__init__(self, "goto object #")
581 612
582 613 def handlechar(self, browser, char):
583 614 # Only accept digits
584 615 if not "0" <= char <= "9":
585 616 curses.beep()
586 617 else:
587 618 return _CommandInput.handlechar(self, browser, char)
588 619
589 620 def cmd_execute(self, browser):
590 621 level = browser.levels[-1]
591 622 if self.input:
592 623 self.dohistory()
593 624 level.moveto(level.curx, int(self.input))
594 625 browser.mode = "default"
595 626 return True
596 627
597 628
598 629 class _CommandFind(_CommandInput):
599 630 def __init__(self):
600 631 _CommandInput.__init__(self, "find expression")
601 632
602 633 def cmd_execute(self, browser):
603 634 level = browser.levels[-1]
604 635 if self.input:
605 636 self.dohistory()
606 637 while True:
607 638 cury = level.cury
608 639 level.moveto(level.curx, cury+1)
609 640 if cury == level.cury:
610 641 curses.beep()
611 642 break # hit end
612 643 item = level.items[level.cury].item
613 644 try:
614 645 globals = ipipe.getglobals(None)
615 646 if eval(self.input, globals, ipipe.AttrNamespace(item)):
616 647 break # found something
617 648 except (KeyboardInterrupt, SystemExit):
618 649 raise
619 650 except Exception, exc:
620 651 browser.report(exc)
621 652 curses.beep()
622 653 break # break on error
623 654 browser.mode = "default"
624 655 return True
625 656
626 657
627 658 class _CommandFindBackwards(_CommandInput):
628 659 def __init__(self):
629 660 _CommandInput.__init__(self, "find backwards expression")
630 661
631 662 def cmd_execute(self, browser):
632 663 level = browser.levels[-1]
633 664 if self.input:
634 665 self.dohistory()
635 666 while level.cury:
636 667 level.moveto(level.curx, level.cury-1)
637 668 item = level.items[level.cury].item
638 669 try:
639 670 globals = ipipe.getglobals(None)
640 671 if eval(self.input, globals, ipipe.AttrNamespace(item)):
641 672 break # found something
642 673 except (KeyboardInterrupt, SystemExit):
643 674 raise
644 675 except Exception, exc:
645 676 browser.report(exc)
646 677 curses.beep()
647 678 break # break on error
648 679 else:
649 680 curses.beep()
650 681 browser.mode = "default"
651 682 return True
652 683
653 684
654 685 class ibrowse(ipipe.Display):
655 686 # Show this many lines from the previous screen when paging horizontally
656 687 pageoverlapx = 1
657 688
658 689 # Show this many lines from the previous screen when paging vertically
659 690 pageoverlapy = 1
660 691
661 692 # Start scrolling when the cursor is less than this number of columns
662 693 # away from the left or right screen edge
663 694 scrollborderx = 10
664 695
665 696 # Start scrolling when the cursor is less than this number of lines
666 697 # away from the top or bottom screen edge
667 698 scrollbordery = 5
668 699
669 700 # Accelerate by this factor when scrolling horizontally
670 701 acceleratex = 1.05
671 702
672 703 # Accelerate by this factor when scrolling vertically
673 704 acceleratey = 1.05
674 705
675 706 # The maximum horizontal scroll speed
676 707 # (as a factor of the screen width (i.e. 0.5 == half a screen width)
677 708 maxspeedx = 0.5
678 709
679 710 # The maximum vertical scroll speed
680 711 # (as a factor of the screen height (i.e. 0.5 == half a screen height)
681 712 maxspeedy = 0.5
682 713
683 714 # The maximum number of header lines for browser level
684 715 # if the nesting is deeper, only the innermost levels are displayed
685 716 maxheaders = 5
686 717
687 718 # The approximate maximum length of a column entry
688 719 maxattrlength = 200
689 720
690 721 # Styles for various parts of the GUI
691 722 style_objheadertext = astyle.Style.fromstr("white:black:bold|reverse")
692 723 style_objheadernumber = astyle.Style.fromstr("white:blue:bold|reverse")
693 724 style_objheaderobject = astyle.Style.fromstr("white:black:reverse")
694 725 style_colheader = astyle.Style.fromstr("blue:white:reverse")
695 726 style_colheaderhere = astyle.Style.fromstr("green:black:bold|reverse")
696 727 style_colheadersep = astyle.Style.fromstr("blue:black:reverse")
697 728 style_number = astyle.Style.fromstr("blue:white:reverse")
698 729 style_numberhere = astyle.Style.fromstr("green:black:bold|reverse")
699 730 style_sep = astyle.Style.fromstr("blue:black")
700 731 style_data = astyle.Style.fromstr("white:black")
701 732 style_datapad = astyle.Style.fromstr("blue:black:bold")
702 733 style_footer = astyle.Style.fromstr("black:white")
703 734 style_report = astyle.Style.fromstr("white:black")
704 735
705 736 # Column separator in header
706 737 headersepchar = "|"
707 738
708 739 # Character for padding data cell entries
709 740 datapadchar = "."
710 741
711 742 # Column separator in data area
712 743 datasepchar = "|"
713 744
714 745 # Character to use for "empty" cell (i.e. for non-existing attributes)
715 746 nodatachar = "-"
716 747
717 748 # Prompts for modes that require keyboard input
718 749 prompts = {
719 750 "goto": _CommandGoto(),
720 751 "find": _CommandFind(),
721 752 "findbackwards": _CommandFindBackwards()
722 753 }
723 754
724 755 # Maps curses key codes to "function" names
725 756 keymap = Keymap()
726 757 keymap.register("quit", "q")
727 758 keymap.register("up", curses.KEY_UP)
728 759 keymap.register("down", curses.KEY_DOWN)
729 760 keymap.register("pageup", curses.KEY_PPAGE)
730 761 keymap.register("pagedown", curses.KEY_NPAGE)
731 762 keymap.register("left", curses.KEY_LEFT)
732 763 keymap.register("right", curses.KEY_RIGHT)
733 764 keymap.register("home", curses.KEY_HOME, "\x01")
734 765 keymap.register("end", curses.KEY_END, "\x05")
735 766 keymap.register("prevattr", "<\x1b")
736 767 keymap.register("nextattr", ">\t")
737 768 keymap.register("pick", "p")
738 769 keymap.register("pickattr", "P")
739 770 keymap.register("pickallattrs", "C")
740 771 keymap.register("pickmarked", "m")
741 772 keymap.register("pickmarkedattr", "M")
742 773 keymap.register("hideattr", "h")
743 774 keymap.register("unhideattrs", "H")
744 775 keymap.register("help", "?")
745 776 keymap.register("enter", "\r\n")
746 777 keymap.register("enterattr", "E")
747 778 # FIXME: What's happening here?
748 779 keymap.register("leave", curses.KEY_BACKSPACE, "x\x08\x7f")
749 780 keymap.register("detail", "d")
750 781 keymap.register("detailattr", "D")
751 782 keymap.register("tooglemark", " ")
752 keymap.register("markrange", "r")
783 keymap.register("markrange", "%")
753 784 keymap.register("sortattrasc", "v")
754 785 keymap.register("sortattrdesc", "V")
755 786 keymap.register("goto", "g")
756 787 keymap.register("find", "f")
757 788 keymap.register("findbackwards", "b")
789 keymap.register("refresh", "r")
790 keymap.register("refreshfind", "R")
758 791
759 792 def __init__(self, *attrs):
760 793 """
761 794 Create a new browser. If ``attrs`` is not empty, it is the list
762 795 of attributes that will be displayed in the browser, otherwise
763 796 these will be determined by the objects on screen.
764 797 """
765 798 self.attrs = attrs
766 799
767 800 # Stack of browser levels
768 801 self.levels = []
769 802 # how many colums to scroll (Changes when accelerating)
770 803 self.stepx = 1.
771 804
772 805 # how many rows to scroll (Changes when accelerating)
773 806 self.stepy = 1.
774 807
775 808 # Beep on the edges of the data area? (Will be set to ``False``
776 809 # once the cursor hits the edge of the screen, so we don't get
777 810 # multiple beeps).
778 811 self._dobeep = True
779 812
780 813 # Cache for registered ``curses`` colors and styles.
781 814 self._styles = {}
782 815 self._colors = {}
783 816 self._maxcolor = 1
784 817
785 818 # How many header lines do we want to paint (the numbers of levels
786 819 # we have, but with an upper bound)
787 820 self._headerlines = 1
788 821
789 822 # Index of first header line
790 823 self._firstheaderline = 0
791 824
792 825 # curses window
793 826 self.scr = None
794 827 # report in the footer line (error, executed command etc.)
795 828 self._report = None
796 829
797 830 # value to be returned to the caller (set by commands)
798 831 self.returnvalue = None
799 832
800 833 # The mode the browser is in
801 834 # e.g. normal browsing or entering an argument for a command
802 835 self.mode = "default"
803 836
804 837 # set by the SIGWINCH signal handler
805 838 self.resized = False
806 839
807 840 def nextstepx(self, step):
808 841 """
809 842 Accelerate horizontally.
810 843 """
811 844 return max(1., min(step*self.acceleratex,
812 845 self.maxspeedx*self.levels[-1].mainsizex))
813 846
814 847 def nextstepy(self, step):
815 848 """
816 849 Accelerate vertically.
817 850 """
818 851 return max(1., min(step*self.acceleratey,
819 852 self.maxspeedy*self.levels[-1].mainsizey))
820 853
821 854 def getstyle(self, style):
822 855 """
823 856 Register the ``style`` with ``curses`` or get it from the cache,
824 857 if it has been registered before.
825 858 """
826 859 try:
827 860 return self._styles[style.fg, style.bg, style.attrs]
828 861 except KeyError:
829 862 attrs = 0
830 863 for b in astyle.A2CURSES:
831 864 if style.attrs & b:
832 865 attrs |= astyle.A2CURSES[b]
833 866 try:
834 867 color = self._colors[style.fg, style.bg]
835 868 except KeyError:
836 869 curses.init_pair(
837 870 self._maxcolor,
838 871 astyle.COLOR2CURSES[style.fg],
839 872 astyle.COLOR2CURSES[style.bg]
840 873 )
841 874 color = curses.color_pair(self._maxcolor)
842 875 self._colors[style.fg, style.bg] = color
843 876 self._maxcolor += 1
844 877 c = color | attrs
845 878 self._styles[style.fg, style.bg, style.attrs] = c
846 879 return c
847 880
848 881 def addstr(self, y, x, begx, endx, text, style):
849 882 """
850 883 A version of ``curses.addstr()`` that can handle ``x`` coordinates
851 884 that are outside the screen.
852 885 """
853 886 text2 = text[max(0, begx-x):max(0, endx-x)]
854 887 if text2:
855 888 self.scr.addstr(y, max(x, begx), text2, self.getstyle(style))
856 889 return len(text)
857 890
858 891 def addchr(self, y, x, begx, endx, c, l, style):
859 892 x0 = max(x, begx)
860 893 x1 = min(x+l, endx)
861 894 if x1>x0:
862 895 self.scr.addstr(y, x0, c*(x1-x0), self.getstyle(style))
863 896 return l
864 897
865 898 def _calcheaderlines(self, levels):
866 899 # Calculate how many headerlines do we have to display, if we have
867 900 # ``levels`` browser levels
868 901 if levels is None:
869 902 levels = len(self.levels)
870 903 self._headerlines = min(self.maxheaders, levels)
871 904 self._firstheaderline = levels-self._headerlines
872 905
873 906 def getstylehere(self, style):
874 907 """
875 908 Return a style for displaying the original style ``style``
876 909 in the row the cursor is on.
877 910 """
878 911 return astyle.Style(style.fg, astyle.COLOR_BLUE, style.attrs | astyle.A_BOLD)
879 912
880 913 def report(self, msg):
881 914 """
882 915 Store the message ``msg`` for display below the footer line. This
883 916 will be displayed as soon as the screen is redrawn.
884 917 """
885 918 self._report = msg
886 919
887 920 def enter(self, item, *attrs):
888 921 """
889 922 Enter the object ``item``. If ``attrs`` is specified, it will be used
890 923 as a fixed list of attributes to display.
891 924 """
925 oldlevels = len(self.levels)
926 self._calcheaderlines(oldlevels+1)
892 927 try:
893 iterator = ipipe.xiter(item)
894 except (KeyboardInterrupt, SystemExit):
895 raise
896 except Exception, exc:
897 curses.beep()
898 self.report(exc)
899 else:
900 self._calcheaderlines(len(self.levels)+1)
901 928 level = _BrowserLevel(
902 929 self,
903 930 item,
904 iterator,
905 931 self.scrsizey-1-self._headerlines-2,
906 932 *attrs
907 933 )
934 except (KeyboardInterrupt, SystemExit):
935 raise
936 except Exception, exc:
937 self._calcheaderlines(oldlevels)
938 curses.beep()
939 self.report(exc)
940 else:
908 941 self.levels.append(level)
909 942
910 943 def startkeyboardinput(self, mode):
911 944 """
912 945 Enter mode ``mode``, which requires keyboard input.
913 946 """
914 947 self.mode = mode
915 948 self.prompts[mode].start()
916 949
917 950 def keylabel(self, keycode):
918 951 """
919 952 Return a pretty name for the ``curses`` key ``keycode`` (used in the
920 953 help screen and in reports about unassigned keys).
921 954 """
922 955 if keycode <= 0xff:
923 956 specialsnames = {
924 957 ord("\n"): "RETURN",
925 958 ord(" "): "SPACE",
926 959 ord("\t"): "TAB",
927 960 ord("\x7f"): "DELETE",
928 961 ord("\x08"): "BACKSPACE",
929 962 }
930 963 if keycode in specialsnames:
931 964 return specialsnames[keycode]
932 965 elif 0x00 < keycode < 0x20:
933 966 return "CTRL-%s" % chr(keycode + 64)
934 967 return repr(chr(keycode))
935 968 for name in dir(curses):
936 969 if name.startswith("KEY_") and getattr(curses, name) == keycode:
937 970 return name
938 971 return str(keycode)
939 972
940 973 def beep(self, force=False):
941 974 if force or self._dobeep:
942 975 curses.beep()
943 976 # don't beep again (as long as the same key is pressed)
944 977 self._dobeep = False
945 978
946 979 def cmd_up(self):
947 980 """
948 981 Move the cursor to the previous row.
949 982 """
950 983 level = self.levels[-1]
951 984 self.report("up")
952 985 level.moveto(level.curx, level.cury-self.stepy)
953 986
954 987 def cmd_down(self):
955 988 """
956 989 Move the cursor to the next row.
957 990 """
958 991 level = self.levels[-1]
959 992 self.report("down")
960 993 level.moveto(level.curx, level.cury+self.stepy)
961 994
962 995 def cmd_pageup(self):
963 996 """
964 997 Move the cursor up one page.
965 998 """
966 999 level = self.levels[-1]
967 1000 self.report("page up")
968 1001 level.moveto(level.curx, level.cury-level.mainsizey+self.pageoverlapy)
969 1002
970 1003 def cmd_pagedown(self):
971 1004 """
972 1005 Move the cursor down one page.
973 1006 """
974 1007 level = self.levels[-1]
975 1008 self.report("page down")
976 1009 level.moveto(level.curx, level.cury+level.mainsizey-self.pageoverlapy)
977 1010
978 1011 def cmd_left(self):
979 1012 """
980 1013 Move the cursor left.
981 1014 """
982 1015 level = self.levels[-1]
983 1016 self.report("left")
984 1017 level.moveto(level.curx-self.stepx, level.cury)
985 1018
986 1019 def cmd_right(self):
987 1020 """
988 1021 Move the cursor right.
989 1022 """
990 1023 level = self.levels[-1]
991 1024 self.report("right")
992 1025 level.moveto(level.curx+self.stepx, level.cury)
993 1026
994 1027 def cmd_home(self):
995 1028 """
996 1029 Move the cursor to the first column.
997 1030 """
998 1031 level = self.levels[-1]
999 1032 self.report("home")
1000 1033 level.moveto(0, level.cury)
1001 1034
1002 1035 def cmd_end(self):
1003 1036 """
1004 1037 Move the cursor to the last column.
1005 1038 """
1006 1039 level = self.levels[-1]
1007 1040 self.report("end")
1008 1041 level.moveto(level.datasizex+level.mainsizey-self.pageoverlapx, level.cury)
1009 1042
1010 1043 def cmd_prevattr(self):
1011 1044 """
1012 1045 Move the cursor one attribute column to the left.
1013 1046 """
1014 1047 level = self.levels[-1]
1015 1048 if level.displayattr[0] is None or level.displayattr[0] == 0:
1016 1049 self.beep()
1017 1050 else:
1018 1051 self.report("prevattr")
1019 1052 pos = 0
1020 1053 for (i, attrname) in enumerate(level.displayattrs):
1021 1054 if i == level.displayattr[0]-1:
1022 1055 break
1023 1056 pos += level.colwidths[attrname] + 1
1024 1057 level.moveto(pos, level.cury)
1025 1058
1026 1059 def cmd_nextattr(self):
1027 1060 """
1028 1061 Move the cursor one attribute column to the right.
1029 1062 """
1030 1063 level = self.levels[-1]
1031 1064 if level.displayattr[0] is None or level.displayattr[0] == len(level.displayattrs)-1:
1032 1065 self.beep()
1033 1066 else:
1034 1067 self.report("nextattr")
1035 1068 pos = 0
1036 1069 for (i, attrname) in enumerate(level.displayattrs):
1037 1070 if i == level.displayattr[0]+1:
1038 1071 break
1039 1072 pos += level.colwidths[attrname] + 1
1040 1073 level.moveto(pos, level.cury)
1041 1074
1042 1075 def cmd_pick(self):
1043 1076 """
1044 1077 'Pick' the object under the cursor (i.e. the row the cursor is on).
1045 1078 This leaves the browser and returns the picked object to the caller.
1046 1079 (In IPython this object will be available as the ``_`` variable.)
1047 1080 """
1048 1081 level = self.levels[-1]
1049 1082 self.returnvalue = level.items[level.cury].item
1050 1083 return True
1051 1084
1052 1085 def cmd_pickattr(self):
1053 1086 """
1054 1087 'Pick' the attribute under the cursor (i.e. the row/column the
1055 1088 cursor is on).
1056 1089 """
1057 1090 level = self.levels[-1]
1058 1091 attr = level.displayattr[1]
1059 1092 if attr is ipipe.noitem:
1060 1093 curses.beep()
1061 1094 self.report(CommandError("no column under cursor"))
1062 1095 return
1063 1096 value = attr.value(level.items[level.cury].item)
1064 1097 if value is ipipe.noitem:
1065 1098 curses.beep()
1066 1099 self.report(AttributeError(attr.name()))
1067 1100 else:
1068 1101 self.returnvalue = value
1069 1102 return True
1070 1103
1071 1104 def cmd_pickallattrs(self):
1072 1105 """
1073 1106 Pick' the complete column under the cursor (i.e. the attribute under
1074 1107 the cursor) from all currently fetched objects. These attributes
1075 1108 will be returned as a list.
1076 1109 """
1077 1110 level = self.levels[-1]
1078 1111 attr = level.displayattr[1]
1079 1112 if attr is ipipe.noitem:
1080 1113 curses.beep()
1081 1114 self.report(CommandError("no column under cursor"))
1082 1115 return
1083 1116 result = []
1084 1117 for cache in level.items:
1085 1118 value = attr.value(cache.item)
1086 1119 if value is not ipipe.noitem:
1087 1120 result.append(value)
1088 1121 self.returnvalue = result
1089 1122 return True
1090 1123
1091 1124 def cmd_pickmarked(self):
1092 1125 """
1093 1126 'Pick' marked objects. Marked objects will be returned as a list.
1094 1127 """
1095 1128 level = self.levels[-1]
1096 1129 self.returnvalue = [cache.item for cache in level.items if cache.marked]
1097 1130 return True
1098 1131
1099 1132 def cmd_pickmarkedattr(self):
1100 1133 """
1101 1134 'Pick' the attribute under the cursor from all marked objects
1102 1135 (This returns a list).
1103 1136 """
1104 1137
1105 1138 level = self.levels[-1]
1106 1139 attr = level.displayattr[1]
1107 1140 if attr is ipipe.noitem:
1108 1141 curses.beep()
1109 1142 self.report(CommandError("no column under cursor"))
1110 1143 return
1111 1144 result = []
1112 1145 for cache in level.items:
1113 1146 if cache.marked:
1114 1147 value = attr.value(cache.item)
1115 1148 if value is not ipipe.noitem:
1116 1149 result.append(value)
1117 1150 self.returnvalue = result
1118 1151 return True
1119 1152
1120 1153 def cmd_markrange(self):
1121 1154 """
1122 1155 Mark all objects from the last marked object before the current cursor
1123 1156 position to the cursor position.
1124 1157 """
1125 1158 level = self.levels[-1]
1126 1159 self.report("markrange")
1127 1160 start = None
1128 1161 if level.items:
1129 1162 for i in xrange(level.cury, -1, -1):
1130 1163 if level.items[i].marked:
1131 1164 start = i
1132 1165 break
1133 1166 if start is None:
1134 1167 self.report(CommandError("no mark before cursor"))
1135 1168 curses.beep()
1136 1169 else:
1137 1170 for i in xrange(start, level.cury+1):
1138 1171 cache = level.items[i]
1139 1172 if not cache.marked:
1140 1173 cache.marked = True
1141 1174 level.marked += 1
1142 1175
1143 1176 def cmd_enter(self):
1144 1177 """
1145 1178 Enter the object under the cursor. (what this mean depends on the object
1146 1179 itself (i.e. how it implements iteration). This opens a new browser 'level'.
1147 1180 """
1148 1181 level = self.levels[-1]
1149 1182 try:
1150 1183 item = level.items[level.cury].item
1151 1184 except IndexError:
1152 1185 self.report(CommandError("No object"))
1153 1186 curses.beep()
1154 1187 else:
1155 1188 self.report("entering object...")
1156 1189 self.enter(item)
1157 1190
1158 1191 def cmd_leave(self):
1159 1192 """
1160 1193 Leave the current browser level and go back to the previous one.
1161 1194 """
1162 1195 self.report("leave")
1163 1196 if len(self.levels) > 1:
1164 1197 self._calcheaderlines(len(self.levels)-1)
1165 1198 self.levels.pop(-1)
1166 1199 else:
1167 1200 self.report(CommandError("This is the last level"))
1168 1201 curses.beep()
1169 1202
1170 1203 def cmd_enterattr(self):
1171 1204 """
1172 1205 Enter the attribute under the cursor.
1173 1206 """
1174 1207 level = self.levels[-1]
1175 1208 attr = level.displayattr[1]
1176 1209 if attr is ipipe.noitem:
1177 1210 curses.beep()
1178 1211 self.report(CommandError("no column under cursor"))
1179 1212 return
1180 1213 try:
1181 1214 item = level.items[level.cury].item
1182 1215 except IndexError:
1183 1216 self.report(CommandError("No object"))
1184 1217 curses.beep()
1185 1218 else:
1186 1219 value = attr.value(item)
1187 1220 name = attr.name()
1188 1221 if value is ipipe.noitem:
1189 1222 self.report(AttributeError(name))
1190 1223 else:
1191 1224 self.report("entering object attribute %s..." % name)
1192 1225 self.enter(value)
1193 1226
1194 1227 def cmd_detail(self):
1195 1228 """
1196 1229 Show a detail view of the object under the cursor. This shows the
1197 1230 name, type, doc string and value of the object attributes (and it
1198 1231 might show more attributes than in the list view, depending on
1199 1232 the object).
1200 1233 """
1201 1234 level = self.levels[-1]
1202 1235 try:
1203 1236 item = level.items[level.cury].item
1204 1237 except IndexError:
1205 1238 self.report(CommandError("No object"))
1206 1239 curses.beep()
1207 1240 else:
1208 1241 self.report("entering detail view for object...")
1209 1242 attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
1210 1243 self.enter(attrs)
1211 1244
1212 1245 def cmd_detailattr(self):
1213 1246 """
1214 1247 Show a detail view of the attribute under the cursor.
1215 1248 """
1216 1249 level = self.levels[-1]
1217 1250 attr = level.displayattr[1]
1218 1251 if attr is ipipe.noitem:
1219 1252 curses.beep()
1220 1253 self.report(CommandError("no attribute"))
1221 1254 return
1222 1255 try:
1223 1256 item = level.items[level.cury].item
1224 1257 except IndexError:
1225 1258 self.report(CommandError("No object"))
1226 1259 curses.beep()
1227 1260 else:
1228 1261 try:
1229 1262 item = attr.value(item)
1230 1263 except (KeyboardInterrupt, SystemExit):
1231 1264 raise
1232 1265 except Exception, exc:
1233 1266 self.report(exc)
1234 1267 else:
1235 1268 self.report("entering detail view for attribute %s..." % attr.name())
1236 1269 attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
1237 1270 self.enter(attrs)
1238 1271
1239 1272 def cmd_tooglemark(self):
1240 1273 """
1241 1274 Mark/unmark the object under the cursor. Marked objects have a '!'
1242 1275 after the row number).
1243 1276 """
1244 1277 level = self.levels[-1]
1245 1278 self.report("toggle mark")
1246 1279 try:
1247 1280 item = level.items[level.cury]
1248 1281 except IndexError: # no items?
1249 1282 pass
1250 1283 else:
1251 1284 if item.marked:
1252 1285 item.marked = False
1253 1286 level.marked -= 1
1254 1287 else:
1255 1288 item.marked = True
1256 1289 level.marked += 1
1257 1290
1258 1291 def cmd_sortattrasc(self):
1259 1292 """
1260 1293 Sort the objects (in ascending order) using the attribute under
1261 1294 the cursor as the sort key.
1262 1295 """
1263 1296 level = self.levels[-1]
1264 1297 attr = level.displayattr[1]
1265 1298 if attr is ipipe.noitem:
1266 1299 curses.beep()
1267 1300 self.report(CommandError("no column under cursor"))
1268 1301 return
1269 1302 self.report("sort by %s (ascending)" % attr.name())
1270 1303 def key(item):
1271 1304 try:
1272 1305 return attr.value(item)
1273 1306 except (KeyboardInterrupt, SystemExit):
1274 1307 raise
1275 1308 except Exception:
1276 1309 return None
1277 1310 level.sort(key)
1278 1311
1279 1312 def cmd_sortattrdesc(self):
1280 1313 """
1281 1314 Sort the objects (in descending order) using the attribute under
1282 1315 the cursor as the sort key.
1283 1316 """
1284 1317 level = self.levels[-1]
1285 1318 attr = level.displayattr[1]
1286 1319 if attr is ipipe.noitem:
1287 1320 curses.beep()
1288 1321 self.report(CommandError("no column under cursor"))
1289 1322 return
1290 1323 self.report("sort by %s (descending)" % attr.name())
1291 1324 def key(item):
1292 1325 try:
1293 1326 return attr.value(item)
1294 1327 except (KeyboardInterrupt, SystemExit):
1295 1328 raise
1296 1329 except Exception:
1297 1330 return None
1298 1331 level.sort(key, reverse=True)
1299 1332
1300 1333 def cmd_hideattr(self):
1301 1334 """
1302 1335 Hide the attribute under the cursor.
1303 1336 """
1304 1337 level = self.levels[-1]
1305 1338 if level.displayattr[0] is None:
1306 1339 self.beep()
1307 1340 else:
1308 1341 self.report("hideattr")
1309 1342 level.hiddenattrs.add(level.displayattr[1])
1310 1343 level.moveto(level.curx, level.cury, refresh=True)
1311 1344
1312 1345 def cmd_unhideattrs(self):
1313 1346 """
1314 1347 Make all attributes visible again.
1315 1348 """
1316 1349 level = self.levels[-1]
1317 1350 self.report("unhideattrs")
1318 1351 level.hiddenattrs.clear()
1319 1352 level.moveto(level.curx, level.cury, refresh=True)
1320 1353
1321 1354 def cmd_goto(self):
1322 1355 """
1323 1356 Jump to a row. The row number can be entered at the
1324 1357 bottom of the screen.
1325 1358 """
1326 1359 self.startkeyboardinput("goto")
1327 1360
1328 1361 def cmd_find(self):
1329 1362 """
1330 1363 Search forward for a row. The search condition can be entered at the
1331 1364 bottom of the screen.
1332 1365 """
1333 1366 self.startkeyboardinput("find")
1334 1367
1335 1368 def cmd_findbackwards(self):
1336 1369 """
1337 1370 Search backward for a row. The search condition can be entered at the
1338 1371 bottom of the screen.
1339 1372 """
1340 1373 self.startkeyboardinput("findbackwards")
1341 1374
1375 def cmd_refresh(self):
1376 """
1377 Refreshes the display by restarting the iterator.
1378 """
1379 level = self.levels[-1]
1380 self.report("refresh")
1381 level.refresh()
1382
1383 def cmd_refreshfind(self):
1384 """
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).
1387 """
1388 level = self.levels[-1]
1389 self.report("refreshfind")
1390 level.refreshfind()
1391
1342 1392 def cmd_help(self):
1343 1393 """
1344 1394 Opens the help screen as a new browser level, describing keyboard
1345 1395 shortcuts.
1346 1396 """
1347 1397 for level in self.levels:
1348 1398 if isinstance(level.input, _BrowserHelp):
1349 1399 curses.beep()
1350 1400 self.report(CommandError("help already active"))
1351 1401 return
1352 1402
1353 1403 self.enter(_BrowserHelp(self))
1354 1404
1355 1405 def cmd_quit(self):
1356 1406 """
1357 1407 Quit the browser and return to the IPython prompt.
1358 1408 """
1359 1409 self.returnvalue = None
1360 1410 return True
1361 1411
1362 1412 def sigwinchhandler(self, signal, frame):
1363 1413 self.resized = True
1364 1414
1365 1415 def _dodisplay(self, scr):
1366 1416 """
1367 1417 This method is the workhorse of the browser. It handles screen
1368 1418 drawing and the keyboard.
1369 1419 """
1370 1420 self.scr = scr
1371 1421 curses.halfdelay(1)
1372 1422 footery = 2
1373 1423
1374 1424 keys = []
1375 1425 for cmd in ("quit", "help"):
1376 1426 key = self.keymap.findkey(cmd, None)
1377 1427 if key is not None:
1378 1428 keys.append("%s=%s" % (self.keylabel(key), cmd))
1379 1429 helpmsg = " | %s" % " ".join(keys)
1380 1430
1381 1431 scr.clear()
1382 1432 msg = "Fetching first batch of objects..."
1383 1433 (self.scrsizey, self.scrsizex) = scr.getmaxyx()
1384 1434 scr.addstr(self.scrsizey//2, (self.scrsizex-len(msg))//2, msg)
1385 1435 scr.refresh()
1386 1436
1387 1437 lastc = -1
1388 1438
1389 1439 self.levels = []
1390 1440 # enter the first level
1391 1441 self.enter(self.input, *self.attrs)
1392 1442
1393 1443 self._calcheaderlines(None)
1394 1444
1395 1445 while True:
1396 1446 level = self.levels[-1]
1397 1447 (self.scrsizey, self.scrsizex) = scr.getmaxyx()
1398 1448 level.mainsizey = self.scrsizey-1-self._headerlines-footery
1399 1449
1400 1450 # Paint object header
1401 1451 for i in xrange(self._firstheaderline, self._firstheaderline+self._headerlines):
1402 1452 lv = self.levels[i]
1403 1453 posx = 0
1404 1454 posy = i-self._firstheaderline
1405 1455 endx = self.scrsizex
1406 1456 if i: # not the first level
1407 1457 msg = " (%d/%d" % (self.levels[i-1].cury, len(self.levels[i-1].items))
1408 1458 if not self.levels[i-1].exhausted:
1409 1459 msg += "+"
1410 1460 msg += ") "
1411 1461 endx -= len(msg)+1
1412 1462 posx += self.addstr(posy, posx, 0, endx, " ibrowse #%d: " % i, self.style_objheadertext)
1413 1463 for (style, text) in lv.header:
1414 1464 posx += self.addstr(posy, posx, 0, endx, text, self.style_objheaderobject)
1415 1465 if posx >= endx:
1416 1466 break
1417 1467 if i:
1418 1468 posx += self.addstr(posy, posx, 0, self.scrsizex, msg, self.style_objheadernumber)
1419 1469 posx += self.addchr(posy, posx, 0, self.scrsizex, " ", self.scrsizex-posx, self.style_objheadernumber)
1420 1470
1421 1471 if not level.items:
1422 1472 self.addchr(self._headerlines, 0, 0, self.scrsizex, " ", self.scrsizex, self.style_colheader)
1423 1473 self.addstr(self._headerlines+1, 0, 0, self.scrsizex, " <empty>", astyle.style_error)
1424 1474 scr.clrtobot()
1425 1475 else:
1426 1476 # Paint column headers
1427 1477 scr.move(self._headerlines, 0)
1428 1478 scr.addstr(" %*s " % (level.numbersizex, "#"), self.getstyle(self.style_colheader))
1429 1479 scr.addstr(self.headersepchar, self.getstyle(self.style_colheadersep))
1430 1480 begx = level.numbersizex+3
1431 1481 posx = begx-level.datastartx
1432 1482 for attr in level.displayattrs:
1433 1483 attrname = attr.name()
1434 1484 cwidth = level.colwidths[attr]
1435 1485 header = attrname.ljust(cwidth)
1436 1486 if attr is level.displayattr[1]:
1437 1487 style = self.style_colheaderhere
1438 1488 else:
1439 1489 style = self.style_colheader
1440 1490 posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, header, style)
1441 1491 posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, self.headersepchar, self.style_colheadersep)
1442 1492 if posx >= self.scrsizex:
1443 1493 break
1444 1494 else:
1445 1495 scr.addstr(" "*(self.scrsizex-posx), self.getstyle(self.style_colheader))
1446 1496
1447 1497 # Paint rows
1448 1498 posy = self._headerlines+1+level.datastarty
1449 1499 for i in xrange(level.datastarty, min(level.datastarty+level.mainsizey, len(level.items))):
1450 1500 cache = level.items[i]
1451 1501 if i == level.cury:
1452 1502 style = self.style_numberhere
1453 1503 else:
1454 1504 style = self.style_number
1455 1505
1456 1506 posy = self._headerlines+1+i-level.datastarty
1457 1507 posx = begx-level.datastartx
1458 1508
1459 1509 scr.move(posy, 0)
1460 1510 scr.addstr(" %*d%s" % (level.numbersizex, i, " !"[cache.marked]), self.getstyle(style))
1461 1511 scr.addstr(self.headersepchar, self.getstyle(self.style_sep))
1462 1512
1463 1513 for attrname in level.displayattrs:
1464 1514 cwidth = level.colwidths[attrname]
1465 1515 try:
1466 1516 (align, length, parts) = level.displayrows[i-level.datastarty][attrname]
1467 1517 except KeyError:
1468 1518 align = 2
1469 1519 style = astyle.style_nodata
1470 1520 if i == level.cury:
1471 1521 style = self.getstylehere(style)
1472 1522 padstyle = self.style_datapad
1473 1523 sepstyle = self.style_sep
1474 1524 if i == level.cury:
1475 1525 padstyle = self.getstylehere(padstyle)
1476 1526 sepstyle = self.getstylehere(sepstyle)
1477 1527 if align == 2:
1478 1528 posx += self.addchr(posy, posx, begx, self.scrsizex, self.nodatachar, cwidth, style)
1479 1529 else:
1480 1530 if align == 1:
1481 1531 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
1482 1532 elif align == 0:
1483 1533 pad1 = (cwidth-length)//2
1484 1534 pad2 = cwidth-length-len(pad1)
1485 1535 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad1, padstyle)
1486 1536 for (style, text) in parts:
1487 1537 if i == level.cury:
1488 1538 style = self.getstylehere(style)
1489 1539 posx += self.addstr(posy, posx, begx, self.scrsizex, text, style)
1490 1540 if posx >= self.scrsizex:
1491 1541 break
1492 1542 if align == -1:
1493 1543 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
1494 1544 elif align == 0:
1495 1545 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad2, padstyle)
1496 1546 posx += self.addstr(posy, posx, begx, self.scrsizex, self.datasepchar, sepstyle)
1497 1547 else:
1498 1548 scr.clrtoeol()
1499 1549
1500 1550 # Add blank row headers for the rest of the screen
1501 1551 for posy in xrange(posy+1, self.scrsizey-2):
1502 1552 scr.addstr(posy, 0, " " * (level.numbersizex+2), self.getstyle(self.style_colheader))
1503 1553 scr.clrtoeol()
1504 1554
1505 1555 posy = self.scrsizey-footery
1506 1556 # Display footer
1507 1557 scr.addstr(posy, 0, " "*self.scrsizex, self.getstyle(self.style_footer))
1508 1558
1509 1559 if level.exhausted:
1510 1560 flag = ""
1511 1561 else:
1512 1562 flag = "+"
1513 1563
1514 1564 endx = self.scrsizex-len(helpmsg)-1
1515 1565 scr.addstr(posy, endx, helpmsg, self.getstyle(self.style_footer))
1516 1566
1517 1567 posx = 0
1518 1568 msg = " %d%s objects (%d marked): " % (len(level.items), flag, level.marked)
1519 1569 posx += self.addstr(posy, posx, 0, endx, msg, self.style_footer)
1520 1570 try:
1521 1571 item = level.items[level.cury].item
1522 1572 except IndexError: # empty
1523 1573 pass
1524 1574 else:
1525 1575 for (nostyle, text) in ipipe.xrepr(item, "footer"):
1526 1576 if not isinstance(nostyle, int):
1527 1577 posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
1528 1578 if posx >= endx:
1529 1579 break
1530 1580
1531 1581 attrstyle = [(astyle.style_default, "no attribute")]
1532 1582 attr = level.displayattr[1]
1533 1583 if attr is not ipipe.noitem and not isinstance(attr, ipipe.SelfDescriptor):
1534 1584 posx += self.addstr(posy, posx, 0, endx, " | ", self.style_footer)
1535 1585 posx += self.addstr(posy, posx, 0, endx, attr.name(), self.style_footer)
1536 1586 posx += self.addstr(posy, posx, 0, endx, ": ", self.style_footer)
1537 1587 try:
1538 1588 value = attr.value(item)
1539 1589 except (SystemExit, KeyboardInterrupt):
1540 1590 raise
1541 1591 except Exception, exc:
1542 1592 value = exc
1543 1593 if value is not ipipe.noitem:
1544 1594 attrstyle = ipipe.xrepr(value, "footer")
1545 1595 for (nostyle, text) in attrstyle:
1546 1596 if not isinstance(nostyle, int):
1547 1597 posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
1548 1598 if posx >= endx:
1549 1599 break
1550 1600
1551 1601 try:
1552 1602 # Display input prompt
1553 1603 if self.mode in self.prompts:
1554 1604 history = self.prompts[self.mode]
1555 1605 posx = 0
1556 1606 posy = self.scrsizey-1
1557 1607 posx += self.addstr(posy, posx, 0, endx, history.prompt, astyle.style_default)
1558 1608 posx += self.addstr(posy, posx, 0, endx, " [", astyle.style_default)
1559 1609 if history.cury==-1:
1560 1610 text = "new"
1561 1611 else:
1562 1612 text = str(history.cury+1)
1563 1613 posx += self.addstr(posy, posx, 0, endx, text, astyle.style_type_number)
1564 1614 if history.history:
1565 1615 posx += self.addstr(posy, posx, 0, endx, "/", astyle.style_default)
1566 1616 posx += self.addstr(posy, posx, 0, endx, str(len(history.history)), astyle.style_type_number)
1567 1617 posx += self.addstr(posy, posx, 0, endx, "]: ", astyle.style_default)
1568 1618 inputstartx = posx
1569 1619 posx += self.addstr(posy, posx, 0, endx, history.input, astyle.style_default)
1570 1620 # Display report
1571 1621 else:
1572 1622 if self._report is not None:
1573 1623 if isinstance(self._report, Exception):
1574 1624 style = self.getstyle(astyle.style_error)
1575 1625 if self._report.__class__.__module__ == "exceptions":
1576 1626 msg = "%s: %s" % \
1577 1627 (self._report.__class__.__name__, self._report)
1578 1628 else:
1579 1629 msg = "%s.%s: %s" % \
1580 1630 (self._report.__class__.__module__,
1581 1631 self._report.__class__.__name__, self._report)
1582 1632 else:
1583 1633 style = self.getstyle(self.style_report)
1584 1634 msg = self._report
1585 1635 scr.addstr(self.scrsizey-1, 0, msg[:self.scrsizex], style)
1586 1636 self._report = None
1587 1637 else:
1588 1638 scr.move(self.scrsizey-1, 0)
1589 1639 except curses.error:
1590 1640 # Protect against errors from writing to the last line
1591 1641 pass
1592 1642 scr.clrtoeol()
1593 1643
1594 1644 # Position cursor
1595 1645 if self.mode in self.prompts:
1596 1646 history = self.prompts[self.mode]
1597 1647 scr.move(self.scrsizey-1, inputstartx+history.curx)
1598 1648 else:
1599 1649 scr.move(
1600 1650 1+self._headerlines+level.cury-level.datastarty,
1601 1651 level.numbersizex+3+level.curx-level.datastartx
1602 1652 )
1603 1653 scr.refresh()
1604 1654
1605 1655 # Check keyboard
1606 1656 while True:
1607 1657 c = scr.getch()
1608 1658 if self.resized:
1609 1659 size = fcntl.ioctl(0, tty.TIOCGWINSZ, "12345678")
1610 1660 size = struct.unpack("4H", size)
1611 1661 oldsize = scr.getmaxyx()
1612 1662 scr.erase()
1613 1663 curses.resize_term(size[0], size[1])
1614 1664 newsize = scr.getmaxyx()
1615 1665 scr.erase()
1616 1666 for l in self.levels:
1617 1667 l.mainsizey += newsize[0]-oldsize[0]
1618 1668 l.moveto(l.curx, l.cury, refresh=True)
1619 1669 scr.refresh()
1620 1670 self.resized = False
1621 1671 break # Redisplay
1622 1672 if self.mode in self.prompts:
1623 1673 if self.prompts[self.mode].handlekey(self, c):
1624 1674 break # Redisplay
1625 1675 else:
1626 1676 # if no key is pressed slow down and beep again
1627 1677 if c == -1:
1628 1678 self.stepx = 1.
1629 1679 self.stepy = 1.
1630 1680 self._dobeep = True
1631 1681 else:
1632 1682 # if a different key was pressed slow down and beep too
1633 1683 if c != lastc:
1634 1684 lastc = c
1635 1685 self.stepx = 1.
1636 1686 self.stepy = 1.
1637 1687 self._dobeep = True
1638 1688 cmdname = self.keymap.get(c, None)
1639 1689 if cmdname is None:
1640 1690 self.report(
1641 1691 UnassignedKeyError("Unassigned key %s" %
1642 1692 self.keylabel(c)))
1643 1693 else:
1644 1694 cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
1645 1695 if cmdfunc is None:
1646 1696 self.report(
1647 1697 UnknownCommandError("Unknown command %r" %
1648 1698 (cmdname,)))
1649 1699 elif cmdfunc():
1650 1700 returnvalue = self.returnvalue
1651 1701 self.returnvalue = None
1652 1702 return returnvalue
1653 1703 self.stepx = self.nextstepx(self.stepx)
1654 1704 self.stepy = self.nextstepy(self.stepy)
1655 1705 curses.flushinp() # get rid of type ahead
1656 1706 break # Redisplay
1657 1707 self.scr = None
1658 1708
1659 1709 def display(self):
1660 1710 if hasattr(curses, "resize_term"):
1661 1711 oldhandler = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1662 1712 try:
1663 1713 return curses.wrapper(self._dodisplay)
1664 1714 finally:
1665 1715 signal.signal(signal.SIGWINCH, oldhandler)
1666 1716 else:
1667 1717 return curses.wrapper(self._dodisplay)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now