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