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