##// END OF EJS Templates
* Added support for Ctrl-L per Fernando's request....
epatters -
Show More
@@ -1,1354 +1,1363 b''
1 1 # Standard library imports
2 2 import re
3 3 import sys
4 4 from textwrap import dedent
5 5
6 6 # System library imports
7 7 from PyQt4 import QtCore, QtGui
8 8
9 9 # Local imports
10 10 from ansi_code_processor import QtAnsiCodeProcessor
11 11 from completion_widget import CompletionWidget
12 12
13 13
14 14 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
15 15 """ A QPlainTextEdit suitable for use with ConsoleWidget.
16 16 """
17 17 # Prevents text from being moved by drag and drop. Note that is not, for
18 18 # some reason, sufficient to catch drag events in the ConsoleWidget's
19 19 # event filter.
20 20 def dragEnterEvent(self, event): pass
21 21 def dragLeaveEvent(self, event): pass
22 22 def dragMoveEvent(self, event): pass
23 23 def dropEvent(self, event): pass
24 24
25 25 class ConsoleTextEdit(QtGui.QTextEdit):
26 26 """ A QTextEdit suitable for use with ConsoleWidget.
27 27 """
28 28 # See above.
29 29 def dragEnterEvent(self, event): pass
30 30 def dragLeaveEvent(self, event): pass
31 31 def dragMoveEvent(self, event): pass
32 32 def dropEvent(self, event): pass
33 33
34 34
35 35 class ConsoleWidget(QtGui.QWidget):
36 36 """ An abstract base class for console-type widgets. This class has
37 37 functionality for:
38 38
39 39 * Maintaining a prompt and editing region
40 40 * Providing the traditional Unix-style console keyboard shortcuts
41 41 * Performing tab completion
42 42 * Paging text
43 43 * Handling ANSI escape codes
44 44
45 45 ConsoleWidget also provides a number of utility methods that will be
46 46 convenient to implementors of a console-style widget.
47 47 """
48 48
49 49 # Whether to process ANSI escape codes.
50 50 ansi_codes = True
51 51
52 52 # The maximum number of lines of text before truncation.
53 53 buffer_size = 500
54 54
55 55 # Whether to use a list widget or plain text output for tab completion.
56 56 gui_completion = True
57 57
58 58 # Whether to override ShortcutEvents for the keybindings defined by this
59 59 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
60 60 # priority (when it has focus) over, e.g., window-level menu shortcuts.
61 61 override_shortcuts = False
62 62
63 63 # Signals that indicate ConsoleWidget state.
64 64 copy_available = QtCore.pyqtSignal(bool)
65 65 redo_available = QtCore.pyqtSignal(bool)
66 66 undo_available = QtCore.pyqtSignal(bool)
67 67
68 68 # Signal emitted when paging is needed and the paging style has been
69 69 # specified as 'custom'.
70 70 custom_page_requested = QtCore.pyqtSignal(object)
71 71
72 72 # Protected class variables.
73 73 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
74 74 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
75 75 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
76 76 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
77 77 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
78 78 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
79 79 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
80 80 _shortcuts = set(_ctrl_down_remap.keys() +
81 81 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
82 82
83 83 #---------------------------------------------------------------------------
84 84 # 'QObject' interface
85 85 #---------------------------------------------------------------------------
86 86
87 87 def __init__(self, kind='plain', paging='inside', parent=None):
88 88 """ Create a ConsoleWidget.
89 89
90 90 Parameters
91 91 ----------
92 92 kind : str, optional [default 'plain']
93 93 The type of underlying text widget to use. Valid values are 'plain',
94 94 which specifies a QPlainTextEdit, and 'rich', which specifies a
95 95 QTextEdit.
96 96
97 97 paging : str, optional [default 'inside']
98 98 The type of paging to use. Valid values are:
99 99 'inside' : The widget pages like a traditional terminal pager.
100 100 'hsplit' : When paging is requested, the widget is split
101 101 horizontally. The top pane contains the console,
102 102 and the bottom pane contains the paged text.
103 103 'vsplit' : Similar to 'hsplit', except that a vertical splitter
104 104 used.
105 105 'custom' : No action is taken by the widget beyond emitting a
106 106 'custom_page_requested(str)' signal.
107 107 'none' : The text is written directly to the console.
108 108
109 109 parent : QWidget, optional [default None]
110 110 The parent for this widget.
111 111 """
112 112 super(ConsoleWidget, self).__init__(parent)
113 113
114 114 # Create the layout and underlying text widget.
115 115 layout = QtGui.QStackedLayout(self)
116 116 layout.setMargin(0)
117 117 self._control = self._create_control(kind)
118 118 self._page_control = None
119 119 self._splitter = None
120 120 if paging in ('hsplit', 'vsplit'):
121 121 self._splitter = QtGui.QSplitter()
122 122 if paging == 'hsplit':
123 123 self._splitter.setOrientation(QtCore.Qt.Horizontal)
124 124 else:
125 125 self._splitter.setOrientation(QtCore.Qt.Vertical)
126 126 self._splitter.addWidget(self._control)
127 127 layout.addWidget(self._splitter)
128 128 else:
129 129 layout.addWidget(self._control)
130 130
131 131 # Create the paging widget, if necessary.
132 132 self._page_style = paging
133 133 if paging in ('inside', 'hsplit', 'vsplit'):
134 134 self._page_control = self._create_page_control()
135 135 if self._splitter:
136 136 self._page_control.hide()
137 137 self._splitter.addWidget(self._page_control)
138 138 else:
139 139 layout.addWidget(self._page_control)
140 140 elif paging not in ('custom', 'none'):
141 141 raise ValueError('Paging style %s unknown.' % repr(paging))
142 142
143 143 # Initialize protected variables. Some variables contain useful state
144 144 # information for subclasses; they should be considered read-only.
145 145 self._ansi_processor = QtAnsiCodeProcessor()
146 146 self._completion_widget = CompletionWidget(self._control)
147 147 self._continuation_prompt = '> '
148 148 self._continuation_prompt_html = None
149 149 self._executing = False
150 150 self._prompt = ''
151 151 self._prompt_html = None
152 152 self._prompt_pos = 0
153 153 self._prompt_sep = ''
154 154 self._reading = False
155 155 self._reading_callback = None
156 156 self._tab_width = 8
157 157
158 158 # Set a monospaced font.
159 159 self.reset_font()
160 160
161 161 def eventFilter(self, obj, event):
162 162 """ Reimplemented to ensure a console-like behavior in the underlying
163 163 text widget.
164 164 """
165 165 # Re-map keys for all filtered widgets.
166 166 etype = event.type()
167 167 if etype == QtCore.QEvent.KeyPress and \
168 168 self._control_key_down(event.modifiers()) and \
169 169 event.key() in self._ctrl_down_remap:
170 170 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
171 171 self._ctrl_down_remap[event.key()],
172 172 QtCore.Qt.NoModifier)
173 173 QtGui.qApp.sendEvent(obj, new_event)
174 174 return True
175 175
176 176 # Override shortucts for all filtered widgets. Note that on Mac OS it is
177 177 # always unnecessary to override shortcuts, hence the check below (users
178 178 # should just use the Control key instead of the Command key).
179 179 elif etype == QtCore.QEvent.ShortcutOverride and \
180 180 sys.platform != 'darwin' and \
181 181 self._control_key_down(event.modifiers()) and \
182 182 event.key() in self._shortcuts:
183 183 event.accept()
184 184 return False
185 185
186 186 elif etype == QtCore.QEvent.KeyPress:
187 187 if obj == self._control:
188 188 return self._event_filter_console_keypress(event)
189 189 elif obj == self._page_control:
190 190 return self._event_filter_page_keypress(event)
191 191
192 192 return super(ConsoleWidget, self).eventFilter(obj, event)
193 193
194 194 #---------------------------------------------------------------------------
195 195 # 'QWidget' interface
196 196 #---------------------------------------------------------------------------
197 197
198 198 def sizeHint(self):
199 199 """ Reimplemented to suggest a size that is 80 characters wide and
200 200 25 lines high.
201 201 """
202 202 style = self.style()
203 203 opt = QtGui.QStyleOptionHeader()
204 204 font_metrics = QtGui.QFontMetrics(self.font)
205 205 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
206 206
207 207 width = font_metrics.width(' ') * 80
208 208 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
209 209 if self._page_style == 'hsplit':
210 210 width = width * 2 + splitwidth
211 211
212 212 height = font_metrics.height() * 25
213 213 if self._page_style == 'vsplit':
214 214 height = height * 2 + splitwidth
215 215
216 216 return QtCore.QSize(width, height)
217 217
218 218 #---------------------------------------------------------------------------
219 219 # 'ConsoleWidget' public interface
220 220 #---------------------------------------------------------------------------
221 221
222 222 def can_paste(self):
223 223 """ Returns whether text can be pasted from the clipboard.
224 224 """
225 225 # Accept only text that can be ASCII encoded.
226 226 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
227 227 text = QtGui.QApplication.clipboard().text()
228 228 if not text.isEmpty():
229 229 try:
230 230 str(text)
231 231 return True
232 232 except UnicodeEncodeError:
233 233 pass
234 234 return False
235 235
236 236 def clear(self, keep_input=True):
237 237 """ Clear the console, then write a new prompt. If 'keep_input' is set,
238 238 restores the old input buffer when the new prompt is written.
239 239 """
240 240 if keep_input:
241 241 input_buffer = self.input_buffer
242 242 self._control.clear()
243 243 self._show_prompt()
244 244 if keep_input:
245 245 self.input_buffer = input_buffer
246 246
247 247 def copy(self):
248 248 """ Copy the current selected text to the clipboard.
249 249 """
250 250 self._control.copy()
251 251
252 252 def execute(self, source=None, hidden=False, interactive=False):
253 253 """ Executes source or the input buffer, possibly prompting for more
254 254 input.
255 255
256 256 Parameters:
257 257 -----------
258 258 source : str, optional
259 259
260 260 The source to execute. If not specified, the input buffer will be
261 261 used. If specified and 'hidden' is False, the input buffer will be
262 262 replaced with the source before execution.
263 263
264 264 hidden : bool, optional (default False)
265 265
266 266 If set, no output will be shown and the prompt will not be modified.
267 267 In other words, it will be completely invisible to the user that
268 268 an execution has occurred.
269 269
270 270 interactive : bool, optional (default False)
271 271
272 272 Whether the console is to treat the source as having been manually
273 273 entered by the user. The effect of this parameter depends on the
274 274 subclass implementation.
275 275
276 276 Raises:
277 277 -------
278 278 RuntimeError
279 279 If incomplete input is given and 'hidden' is True. In this case,
280 280 it is not possible to prompt for more input.
281 281
282 282 Returns:
283 283 --------
284 284 A boolean indicating whether the source was executed.
285 285 """
286 286 # WARNING: The order in which things happen here is very particular, in
287 287 # large part because our syntax highlighting is fragile. If you change
288 288 # something, test carefully!
289 289
290 290 # Decide what to execute.
291 291 if source is None:
292 292 source = self.input_buffer
293 293 if not hidden:
294 294 # A newline is appended later, but it should be considered part
295 295 # of the input buffer.
296 296 source += '\n'
297 297 elif not hidden:
298 298 self.input_buffer = source
299 299
300 300 # Execute the source or show a continuation prompt if it is incomplete.
301 301 complete = self._is_complete(source, interactive)
302 302 if hidden:
303 303 if complete:
304 304 self._execute(source, hidden)
305 305 else:
306 306 error = 'Incomplete noninteractive input: "%s"'
307 307 raise RuntimeError(error % source)
308 308 else:
309 309 if complete:
310 310 self._append_plain_text('\n')
311 311 self._executing_input_buffer = self.input_buffer
312 312 self._executing = True
313 313 self._prompt_finished()
314 314
315 315 # The maximum block count is only in effect during execution.
316 316 # This ensures that _prompt_pos does not become invalid due to
317 317 # text truncation.
318 318 self._control.document().setMaximumBlockCount(self.buffer_size)
319 319
320 320 # Setting a positive maximum block count will automatically
321 321 # disable the undo/redo history, but just to be safe:
322 322 self._control.setUndoRedoEnabled(False)
323 323
324 324 self._execute(source, hidden)
325 325
326 326 else:
327 327 # Do this inside an edit block so continuation prompts are
328 328 # removed seamlessly via undo/redo.
329 329 cursor = self._control.textCursor()
330 330 cursor.beginEditBlock()
331 331
332 332 self._append_plain_text('\n')
333 333 self._show_continuation_prompt()
334 334
335 335 cursor.endEditBlock()
336 336
337 337 return complete
338 338
339 339 def _get_input_buffer(self):
340 340 """ The text that the user has entered entered at the current prompt.
341 341 """
342 342 # If we're executing, the input buffer may not even exist anymore due to
343 343 # the limit imposed by 'buffer_size'. Therefore, we store it.
344 344 if self._executing:
345 345 return self._executing_input_buffer
346 346
347 347 cursor = self._get_end_cursor()
348 348 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
349 349 input_buffer = str(cursor.selection().toPlainText())
350 350
351 351 # Strip out continuation prompts.
352 352 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
353 353
354 354 def _set_input_buffer(self, string):
355 355 """ Replaces the text in the input buffer with 'string'.
356 356 """
357 357 # For now, it is an error to modify the input buffer during execution.
358 358 if self._executing:
359 359 raise RuntimeError("Cannot change input buffer during execution.")
360 360
361 361 # Remove old text.
362 362 cursor = self._get_end_cursor()
363 363 cursor.beginEditBlock()
364 364 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
365 365 cursor.removeSelectedText()
366 366
367 367 # Insert new text with continuation prompts.
368 368 lines = string.splitlines(True)
369 369 if lines:
370 370 self._append_plain_text(lines[0])
371 371 for i in xrange(1, len(lines)):
372 372 if self._continuation_prompt_html is None:
373 373 self._append_plain_text(self._continuation_prompt)
374 374 else:
375 375 self._append_html(self._continuation_prompt_html)
376 376 self._append_plain_text(lines[i])
377 377 cursor.endEditBlock()
378 378 self._control.moveCursor(QtGui.QTextCursor.End)
379 379
380 380 input_buffer = property(_get_input_buffer, _set_input_buffer)
381 381
382 382 def _get_font(self):
383 383 """ The base font being used by the ConsoleWidget.
384 384 """
385 385 return self._control.document().defaultFont()
386 386
387 387 def _set_font(self, font):
388 388 """ Sets the base font for the ConsoleWidget to the specified QFont.
389 389 """
390 390 font_metrics = QtGui.QFontMetrics(font)
391 391 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
392 392
393 393 self._completion_widget.setFont(font)
394 394 self._control.document().setDefaultFont(font)
395 395 if self._page_control:
396 396 self._page_control.document().setDefaultFont(font)
397 397
398 398 font = property(_get_font, _set_font)
399 399
400 400 def paste(self):
401 401 """ Paste the contents of the clipboard into the input region.
402 402 """
403 403 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
404 404 try:
405 405 text = str(QtGui.QApplication.clipboard().text())
406 406 except UnicodeEncodeError:
407 407 pass
408 408 else:
409 409 self._insert_plain_text_into_buffer(dedent(text))
410 410
411 411 def print_(self, printer):
412 412 """ Print the contents of the ConsoleWidget to the specified QPrinter.
413 413 """
414 414 self._control.print_(printer)
415 415
416 416 def redo(self):
417 417 """ Redo the last operation. If there is no operation to redo, nothing
418 418 happens.
419 419 """
420 420 self._control.redo()
421 421
422 422 def reset_font(self):
423 423 """ Sets the font to the default fixed-width font for this platform.
424 424 """
425 425 # FIXME: font family and size should be configurable by the user.
426 426
427 427 if sys.platform == 'win32':
428 428 # Fixme: we should test whether Consolas is available and use it
429 429 # first if it is. Consolas ships by default from Vista onwards,
430 430 # it's *vastly* more readable and prettier than Courier, and is
431 431 # often installed even on XP systems. So we should first check for
432 432 # it, and only fallback to Courier if absolutely necessary.
433 433 name = 'Courier'
434 434 elif sys.platform == 'darwin':
435 435 name = 'Monaco'
436 436 else:
437 437 name = 'Monospace'
438 438 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
439 439 font.setStyleHint(QtGui.QFont.TypeWriter)
440 440 self._set_font(font)
441 441
442 442 def select_all(self):
443 443 """ Selects all the text in the buffer.
444 444 """
445 445 self._control.selectAll()
446 446
447 447 def _get_tab_width(self):
448 448 """ The width (in terms of space characters) for tab characters.
449 449 """
450 450 return self._tab_width
451 451
452 452 def _set_tab_width(self, tab_width):
453 453 """ Sets the width (in terms of space characters) for tab characters.
454 454 """
455 455 font_metrics = QtGui.QFontMetrics(self.font)
456 456 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
457 457
458 458 self._tab_width = tab_width
459 459
460 460 tab_width = property(_get_tab_width, _set_tab_width)
461 461
462 462 def undo(self):
463 463 """ Undo the last operation. If there is no operation to undo, nothing
464 464 happens.
465 465 """
466 466 self._control.undo()
467 467
468 468 #---------------------------------------------------------------------------
469 469 # 'ConsoleWidget' abstract interface
470 470 #---------------------------------------------------------------------------
471 471
472 472 def _is_complete(self, source, interactive):
473 473 """ Returns whether 'source' can be executed. When triggered by an
474 474 Enter/Return key press, 'interactive' is True; otherwise, it is
475 475 False.
476 476 """
477 477 raise NotImplementedError
478 478
479 479 def _execute(self, source, hidden):
480 480 """ Execute 'source'. If 'hidden', do not show any output.
481 481 """
482 482 raise NotImplementedError
483 483
484 484 def _prompt_started_hook(self):
485 485 """ Called immediately after a new prompt is displayed.
486 486 """
487 487 pass
488 488
489 489 def _prompt_finished_hook(self):
490 490 """ Called immediately after a prompt is finished, i.e. when some input
491 491 will be processed and a new prompt displayed.
492 492 """
493 493 pass
494 494
495 495 def _up_pressed(self):
496 496 """ Called when the up key is pressed. Returns whether to continue
497 497 processing the event.
498 498 """
499 499 return True
500 500
501 501 def _down_pressed(self):
502 502 """ Called when the down key is pressed. Returns whether to continue
503 503 processing the event.
504 504 """
505 505 return True
506 506
507 507 def _tab_pressed(self):
508 508 """ Called when the tab key is pressed. Returns whether to continue
509 509 processing the event.
510 510 """
511 511 return False
512 512
513 513 #--------------------------------------------------------------------------
514 514 # 'ConsoleWidget' protected interface
515 515 #--------------------------------------------------------------------------
516 516
517 517 def _append_html(self, html):
518 518 """ Appends html at the end of the console buffer.
519 519 """
520 520 cursor = self._get_end_cursor()
521 521 self._insert_html(cursor, html)
522 522
523 523 def _append_html_fetching_plain_text(self, html):
524 524 """ Appends 'html', then returns the plain text version of it.
525 525 """
526 526 cursor = self._get_end_cursor()
527 527 return self._insert_html_fetching_plain_text(cursor, html)
528 528
529 529 def _append_plain_text(self, text):
530 530 """ Appends plain text at the end of the console buffer, processing
531 531 ANSI codes if enabled.
532 532 """
533 533 cursor = self._get_end_cursor()
534 534 self._insert_plain_text(cursor, text)
535 535
536 536 def _append_plain_text_keeping_prompt(self, text):
537 537 """ Writes 'text' after the current prompt, then restores the old prompt
538 538 with its old input buffer.
539 539 """
540 540 input_buffer = self.input_buffer
541 541 self._append_plain_text('\n')
542 542 self._prompt_finished()
543 543
544 544 self._append_plain_text(text)
545 545 self._show_prompt()
546 546 self.input_buffer = input_buffer
547 547
548 548 def _complete_with_items(self, cursor, items):
549 549 """ Performs completion with 'items' at the specified cursor location.
550 550 """
551 551 if len(items) == 1:
552 552 cursor.setPosition(self._control.textCursor().position(),
553 553 QtGui.QTextCursor.KeepAnchor)
554 554 cursor.insertText(items[0])
555 555 elif len(items) > 1:
556 556 if self.gui_completion:
557 557 self._completion_widget.show_items(cursor, items)
558 558 else:
559 559 text = self._format_as_columns(items)
560 560 self._append_plain_text_keeping_prompt(text)
561 561
562 562 def _control_key_down(self, modifiers):
563 563 """ Given a KeyboardModifiers flags object, return whether the Control
564 564 key is down (on Mac OS, treat the Command key as a synonym for
565 565 Control).
566 566 """
567 567 down = bool(modifiers & QtCore.Qt.ControlModifier)
568 568
569 569 # Note: on Mac OS, ControlModifier corresponds to the Command key while
570 570 # MetaModifier corresponds to the Control key.
571 571 if sys.platform == 'darwin':
572 572 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
573 573
574 574 return down
575 575
576 576 def _create_control(self, kind):
577 577 """ Creates and connects the underlying text widget.
578 578 """
579 579 if kind == 'plain':
580 580 control = ConsolePlainTextEdit()
581 581 elif kind == 'rich':
582 582 control = ConsoleTextEdit()
583 583 control.setAcceptRichText(False)
584 584 else:
585 585 raise ValueError("Kind %s unknown." % repr(kind))
586 586 control.installEventFilter(self)
587 587 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
588 588 control.customContextMenuRequested.connect(self._show_context_menu)
589 589 control.copyAvailable.connect(self.copy_available)
590 590 control.redoAvailable.connect(self.redo_available)
591 591 control.undoAvailable.connect(self.undo_available)
592 592 control.setReadOnly(True)
593 593 control.setUndoRedoEnabled(False)
594 594 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
595 595 return control
596 596
597 597 def _create_page_control(self):
598 598 """ Creates and connects the underlying paging widget.
599 599 """
600 600 control = ConsolePlainTextEdit()
601 601 control.installEventFilter(self)
602 602 control.setReadOnly(True)
603 603 control.setUndoRedoEnabled(False)
604 604 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
605 605 return control
606 606
607 607 def _event_filter_console_keypress(self, event):
608 608 """ Filter key events for the underlying text widget to create a
609 609 console-like interface.
610 610 """
611 611 intercepted = False
612 612 cursor = self._control.textCursor()
613 613 position = cursor.position()
614 614 key = event.key()
615 615 ctrl_down = self._control_key_down(event.modifiers())
616 616 alt_down = event.modifiers() & QtCore.Qt.AltModifier
617 617 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
618 618
619 619 if event.matches(QtGui.QKeySequence.Paste):
620 620 # Call our paste instead of the underlying text widget's.
621 621 self.paste()
622 622 intercepted = True
623 623
624 624 elif ctrl_down:
625 625 if key == QtCore.Qt.Key_K:
626 626 if self._in_buffer(position):
627 627 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
628 628 QtGui.QTextCursor.KeepAnchor)
629 629 cursor.removeSelectedText()
630 630 intercepted = True
631 631
632 elif key == QtCore.Qt.Key_L:
633 self.clear()
634 intercepted = True
635
632 636 elif key == QtCore.Qt.Key_X:
637 # FIXME: Instead of disabling cut completely, only allow it
638 # when safe.
633 639 intercepted = True
634 640
635 641 elif key == QtCore.Qt.Key_Y:
636 642 self.paste()
637 643 intercepted = True
638 644
639 645 elif alt_down:
640 646 if key == QtCore.Qt.Key_B:
641 647 self._set_cursor(self._get_word_start_cursor(position))
642 648 intercepted = True
643 649
644 650 elif key == QtCore.Qt.Key_F:
645 651 self._set_cursor(self._get_word_end_cursor(position))
646 652 intercepted = True
647 653
648 654 elif key == QtCore.Qt.Key_Backspace:
649 655 cursor = self._get_word_start_cursor(position)
650 656 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
651 657 cursor.removeSelectedText()
652 658 intercepted = True
653 659
654 660 elif key == QtCore.Qt.Key_D:
655 661 cursor = self._get_word_end_cursor(position)
656 662 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
657 663 cursor.removeSelectedText()
658 664 intercepted = True
659 665
660 666 else:
661 667 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
662 668 if self._reading:
663 669 self._append_plain_text('\n')
664 670 self._reading = False
665 671 if self._reading_callback:
666 672 self._reading_callback()
667 673 elif not self._executing:
668 674 self.execute(interactive=True)
669 675 intercepted = True
670 676
671 677 elif key == QtCore.Qt.Key_Up:
672 678 if self._reading or not self._up_pressed():
673 679 intercepted = True
674 680 else:
675 681 prompt_line = self._get_prompt_cursor().blockNumber()
676 682 intercepted = cursor.blockNumber() <= prompt_line
677 683
678 684 elif key == QtCore.Qt.Key_Down:
679 685 if self._reading or not self._down_pressed():
680 686 intercepted = True
681 687 else:
682 688 end_line = self._get_end_cursor().blockNumber()
683 689 intercepted = cursor.blockNumber() == end_line
684 690
685 691 elif key == QtCore.Qt.Key_Tab:
686 692 if not self._reading:
687 693 intercepted = not self._tab_pressed()
688 694
689 695 elif key == QtCore.Qt.Key_Left:
690 696 intercepted = not self._in_buffer(position - 1)
691 697
692 698 elif key == QtCore.Qt.Key_Home:
693 699 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
694 700 start_line = cursor.blockNumber()
695 701 if start_line == self._get_prompt_cursor().blockNumber():
696 702 start_pos = self._prompt_pos
697 703 else:
698 704 start_pos = cursor.position()
699 705 start_pos += len(self._continuation_prompt)
700 706 if shift_down and self._in_buffer(position):
701 707 self._set_selection(position, start_pos)
702 708 else:
703 709 self._set_position(start_pos)
704 710 intercepted = True
705 711
706 712 elif key == QtCore.Qt.Key_Backspace:
707 713
708 714 # Line deletion (remove continuation prompt)
709 715 len_prompt = len(self._continuation_prompt)
710 716 if not self._reading and \
711 717 cursor.columnNumber() == len_prompt and \
712 718 position != self._prompt_pos:
713 719 cursor.beginEditBlock()
714 720 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
715 721 QtGui.QTextCursor.KeepAnchor)
716 722 cursor.removeSelectedText()
717 723 cursor.deletePreviousChar()
718 724 cursor.endEditBlock()
719 725 intercepted = True
720 726
721 727 # Regular backwards deletion
722 728 else:
723 729 anchor = cursor.anchor()
724 730 if anchor == position:
725 731 intercepted = not self._in_buffer(position - 1)
726 732 else:
727 733 intercepted = not self._in_buffer(min(anchor, position))
728 734
729 735 elif key == QtCore.Qt.Key_Delete:
730 736
731 737 # Line deletion (remove continuation prompt)
732 738 if not self._reading and cursor.atBlockEnd() and not \
733 739 cursor.hasSelection():
734 740 cursor.movePosition(QtGui.QTextCursor.NextBlock,
735 741 QtGui.QTextCursor.KeepAnchor)
736 742 cursor.movePosition(QtGui.QTextCursor.Right,
737 743 QtGui.QTextCursor.KeepAnchor,
738 744 len(self._continuation_prompt))
739 745 cursor.removeSelectedText()
740 746 intercepted = True
741 747
742 748 # Regular forwards deletion:
743 749 else:
744 750 anchor = cursor.anchor()
745 751 intercepted = (not self._in_buffer(anchor) or
746 752 not self._in_buffer(position))
747 753
748 754 # Don't move the cursor if control is down to allow copy-paste using
749 755 # the keyboard in any part of the buffer.
750 756 if not ctrl_down:
751 757 self._keep_cursor_in_buffer()
752 758
753 759 return intercepted
754 760
755 761 def _event_filter_page_keypress(self, event):
756 762 """ Filter key events for the paging widget to create console-like
757 763 interface.
758 764 """
759 765 key = event.key()
760 766
761 767 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
762 768 if self._splitter:
763 769 self._page_control.hide()
764 770 else:
765 771 self.layout().setCurrentWidget(self._control)
766 772 return True
767 773
768 774 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
769 775 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
770 776 QtCore.Qt.Key_Down,
771 777 QtCore.Qt.NoModifier)
772 778 QtGui.qApp.sendEvent(self._page_control, new_event)
773 779 return True
774 780
775 781 return False
776 782
777 783 def _format_as_columns(self, items, separator=' '):
778 784 """ Transform a list of strings into a single string with columns.
779 785
780 786 Parameters
781 787 ----------
782 788 items : sequence of strings
783 789 The strings to process.
784 790
785 791 separator : str, optional [default is two spaces]
786 792 The string that separates columns.
787 793
788 794 Returns
789 795 -------
790 796 The formatted string.
791 797 """
792 798 # Note: this code is adapted from columnize 0.3.2.
793 799 # See http://code.google.com/p/pycolumnize/
794 800
795 801 # Calculate the number of characters available.
796 802 width = self._control.viewport().width()
797 803 char_width = QtGui.QFontMetrics(self.font).width(' ')
798 804 displaywidth = max(10, (width / char_width) - 1)
799 805
800 806 # Some degenerate cases.
801 807 size = len(items)
802 808 if size == 0:
803 809 return '\n'
804 810 elif size == 1:
805 811 return '%s\n' % str(items[0])
806 812
807 813 # Try every row count from 1 upwards
808 814 array_index = lambda nrows, row, col: nrows*col + row
809 815 for nrows in range(1, size):
810 816 ncols = (size + nrows - 1) // nrows
811 817 colwidths = []
812 818 totwidth = -len(separator)
813 819 for col in range(ncols):
814 820 # Get max column width for this column
815 821 colwidth = 0
816 822 for row in range(nrows):
817 823 i = array_index(nrows, row, col)
818 824 if i >= size: break
819 825 x = items[i]
820 826 colwidth = max(colwidth, len(x))
821 827 colwidths.append(colwidth)
822 828 totwidth += colwidth + len(separator)
823 829 if totwidth > displaywidth:
824 830 break
825 831 if totwidth <= displaywidth:
826 832 break
827 833
828 834 # The smallest number of rows computed and the max widths for each
829 835 # column has been obtained. Now we just have to format each of the rows.
830 836 string = ''
831 837 for row in range(nrows):
832 838 texts = []
833 839 for col in range(ncols):
834 840 i = row + nrows*col
835 841 if i >= size:
836 842 texts.append('')
837 843 else:
838 844 texts.append(items[i])
839 845 while texts and not texts[-1]:
840 846 del texts[-1]
841 847 for col in range(len(texts)):
842 848 texts[col] = texts[col].ljust(colwidths[col])
843 849 string += '%s\n' % str(separator.join(texts))
844 850 return string
845 851
846 852 def _get_block_plain_text(self, block):
847 853 """ Given a QTextBlock, return its unformatted text.
848 854 """
849 855 cursor = QtGui.QTextCursor(block)
850 856 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
851 857 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
852 858 QtGui.QTextCursor.KeepAnchor)
853 859 return str(cursor.selection().toPlainText())
854 860
855 861 def _get_cursor(self):
856 862 """ Convenience method that returns a cursor for the current position.
857 863 """
858 864 return self._control.textCursor()
859 865
860 866 def _get_end_cursor(self):
861 867 """ Convenience method that returns a cursor for the last character.
862 868 """
863 869 cursor = self._control.textCursor()
864 870 cursor.movePosition(QtGui.QTextCursor.End)
865 871 return cursor
866 872
867 873 def _get_input_buffer_cursor_column(self):
868 874 """ Returns the column of the cursor in the input buffer, excluding the
869 875 contribution by the prompt, or -1 if there is no such column.
870 876 """
871 877 prompt = self._get_input_buffer_cursor_prompt()
872 878 if prompt is None:
873 879 return -1
874 880 else:
875 881 cursor = self._control.textCursor()
876 882 return cursor.columnNumber() - len(prompt)
877 883
878 884 def _get_input_buffer_cursor_line(self):
879 885 """ Returns line of the input buffer that contains the cursor, or None
880 886 if there is no such line.
881 887 """
882 888 prompt = self._get_input_buffer_cursor_prompt()
883 889 if prompt is None:
884 890 return None
885 891 else:
886 892 cursor = self._control.textCursor()
887 893 text = self._get_block_plain_text(cursor.block())
888 894 return text[len(prompt):]
889 895
890 896 def _get_input_buffer_cursor_prompt(self):
891 897 """ Returns the (plain text) prompt for line of the input buffer that
892 898 contains the cursor, or None if there is no such line.
893 899 """
894 900 if self._executing:
895 901 return None
896 902 cursor = self._control.textCursor()
897 903 if cursor.position() >= self._prompt_pos:
898 904 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
899 905 return self._prompt
900 906 else:
901 907 return self._continuation_prompt
902 908 else:
903 909 return None
904 910
905 911 def _get_prompt_cursor(self):
906 912 """ Convenience method that returns a cursor for the prompt position.
907 913 """
908 914 cursor = self._control.textCursor()
909 915 cursor.setPosition(self._prompt_pos)
910 916 return cursor
911 917
912 918 def _get_selection_cursor(self, start, end):
913 919 """ Convenience method that returns a cursor with text selected between
914 920 the positions 'start' and 'end'.
915 921 """
916 922 cursor = self._control.textCursor()
917 923 cursor.setPosition(start)
918 924 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
919 925 return cursor
920 926
921 927 def _get_word_start_cursor(self, position):
922 928 """ Find the start of the word to the left the given position. If a
923 929 sequence of non-word characters precedes the first word, skip over
924 930 them. (This emulates the behavior of bash, emacs, etc.)
925 931 """
926 932 document = self._control.document()
927 933 position -= 1
928 934 while position >= self._prompt_pos and \
929 935 not document.characterAt(position).isLetterOrNumber():
930 936 position -= 1
931 937 while position >= self._prompt_pos and \
932 938 document.characterAt(position).isLetterOrNumber():
933 939 position -= 1
934 940 cursor = self._control.textCursor()
935 941 cursor.setPosition(position + 1)
936 942 return cursor
937 943
938 944 def _get_word_end_cursor(self, position):
939 945 """ Find the end of the word to the right the given position. If a
940 946 sequence of non-word characters precedes the first word, skip over
941 947 them. (This emulates the behavior of bash, emacs, etc.)
942 948 """
943 949 document = self._control.document()
944 950 end = self._get_end_cursor().position()
945 951 while position < end and \
946 952 not document.characterAt(position).isLetterOrNumber():
947 953 position += 1
948 954 while position < end and \
949 955 document.characterAt(position).isLetterOrNumber():
950 956 position += 1
951 957 cursor = self._control.textCursor()
952 958 cursor.setPosition(position)
953 959 return cursor
954 960
955 961 def _insert_html(self, cursor, html):
956 962 """ Inserts HTML using the specified cursor in such a way that future
957 963 formatting is unaffected.
958 964 """
959 965 cursor.beginEditBlock()
960 966 cursor.insertHtml(html)
961 967
962 968 # After inserting HTML, the text document "remembers" it's in "html
963 969 # mode", which means that subsequent calls adding plain text will result
964 970 # in unwanted formatting, lost tab characters, etc. The following code
965 971 # hacks around this behavior, which I consider to be a bug in Qt, by
966 972 # (crudely) resetting the document's style state.
967 973 cursor.movePosition(QtGui.QTextCursor.Left,
968 974 QtGui.QTextCursor.KeepAnchor)
969 975 if cursor.selection().toPlainText() == ' ':
970 976 cursor.removeSelectedText()
971 977 else:
972 978 cursor.movePosition(QtGui.QTextCursor.Right)
973 979 cursor.insertText(' ', QtGui.QTextCharFormat())
974 980 cursor.endEditBlock()
975 981
976 982 def _insert_html_fetching_plain_text(self, cursor, html):
977 983 """ Inserts HTML using the specified cursor, then returns its plain text
978 984 version.
979 985 """
980 986 cursor.beginEditBlock()
981 987 cursor.removeSelectedText()
982 988
983 989 start = cursor.position()
984 990 self._insert_html(cursor, html)
985 991 end = cursor.position()
986 992 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
987 993 text = str(cursor.selection().toPlainText())
988 994
989 995 cursor.setPosition(end)
990 996 cursor.endEditBlock()
991 997 return text
992 998
993 999 def _insert_plain_text(self, cursor, text):
994 1000 """ Inserts plain text using the specified cursor, processing ANSI codes
995 1001 if enabled.
996 1002 """
997 1003 cursor.beginEditBlock()
998 1004 if self.ansi_codes:
999 1005 for substring in self._ansi_processor.split_string(text):
1000 1006 for action in self._ansi_processor.actions:
1001 1007 if action.kind == 'erase' and action.area == 'screen':
1002 1008 cursor.select(QtGui.QTextCursor.Document)
1003 1009 cursor.removeSelectedText()
1004 1010 format = self._ansi_processor.get_format()
1005 1011 cursor.insertText(substring, format)
1006 1012 else:
1007 1013 cursor.insertText(text)
1008 1014 cursor.endEditBlock()
1009 1015
1010 1016 def _insert_plain_text_into_buffer(self, text):
1011 1017 """ Inserts text into the input buffer at the current cursor position,
1012 1018 ensuring that continuation prompts are inserted as necessary.
1013 1019 """
1014 1020 lines = str(text).splitlines(True)
1015 1021 if lines:
1016 1022 self._keep_cursor_in_buffer()
1017 1023 cursor = self._control.textCursor()
1018 1024 cursor.beginEditBlock()
1019 1025 cursor.insertText(lines[0])
1020 1026 for line in lines[1:]:
1021 1027 if self._continuation_prompt_html is None:
1022 1028 cursor.insertText(self._continuation_prompt)
1023 1029 else:
1024 1030 self._continuation_prompt = \
1025 1031 self._insert_html_fetching_plain_text(
1026 1032 cursor, self._continuation_prompt_html)
1027 1033 cursor.insertText(line)
1028 1034 cursor.endEditBlock()
1029 1035 self._control.setTextCursor(cursor)
1030 1036
1031 1037 def _in_buffer(self, position=None):
1032 1038 """ Returns whether the current cursor (or, if specified, a position) is
1033 1039 inside the editing region.
1034 1040 """
1035 1041 cursor = self._control.textCursor()
1036 1042 if position is None:
1037 1043 position = cursor.position()
1038 1044 else:
1039 1045 cursor.setPosition(position)
1040 1046 line = cursor.blockNumber()
1041 1047 prompt_line = self._get_prompt_cursor().blockNumber()
1042 1048 if line == prompt_line:
1043 1049 return position >= self._prompt_pos
1044 1050 elif line > prompt_line:
1045 1051 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1046 1052 prompt_pos = cursor.position() + len(self._continuation_prompt)
1047 1053 return position >= prompt_pos
1048 1054 return False
1049 1055
1050 1056 def _keep_cursor_in_buffer(self):
1051 1057 """ Ensures that the cursor is inside the editing region. Returns
1052 1058 whether the cursor was moved.
1053 1059 """
1054 1060 moved = not self._in_buffer()
1055 1061 if moved:
1056 1062 cursor = self._control.textCursor()
1057 1063 cursor.movePosition(QtGui.QTextCursor.End)
1058 1064 self._control.setTextCursor(cursor)
1059 1065 return moved
1060 1066
1061 1067 def _page(self, text):
1062 1068 """ Displays text using the pager if it exceeds the height of the
1063 1069 visible area.
1064 1070 """
1065 1071 if self._page_style == 'none':
1066 1072 self._append_plain_text(text)
1067 1073 else:
1068 1074 line_height = QtGui.QFontMetrics(self.font).height()
1069 1075 minlines = self._control.viewport().height() / line_height
1070 1076 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1071 1077 if self._page_style == 'custom':
1072 1078 self.custom_page_requested.emit(text)
1073 1079 else:
1074 1080 self._page_control.clear()
1075 1081 cursor = self._page_control.textCursor()
1076 1082 self._insert_plain_text(cursor, text)
1077 1083 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1078 1084
1079 1085 self._page_control.viewport().resize(self._control.size())
1080 1086 if self._splitter:
1081 1087 self._page_control.show()
1082 1088 self._page_control.setFocus()
1083 1089 else:
1084 1090 self.layout().setCurrentWidget(self._page_control)
1085 1091 else:
1086 1092 self._append_plain_text(text)
1087 1093
1088 1094 def _prompt_started(self):
1089 1095 """ Called immediately after a new prompt is displayed.
1090 1096 """
1091 1097 # Temporarily disable the maximum block count to permit undo/redo and
1092 1098 # to ensure that the prompt position does not change due to truncation.
1093 1099 # Because setting this property clears the undo/redo history, we only
1094 1100 # set it if we have to.
1095 1101 if self._control.document().maximumBlockCount() > 0:
1096 1102 self._control.document().setMaximumBlockCount(0)
1097 1103 self._control.setUndoRedoEnabled(True)
1098 1104
1099 1105 self._control.setReadOnly(False)
1100 1106 self._control.moveCursor(QtGui.QTextCursor.End)
1101 1107
1102 1108 self._executing = False
1103 1109 self._prompt_started_hook()
1104 1110
1105 1111 def _prompt_finished(self):
1106 1112 """ Called immediately after a prompt is finished, i.e. when some input
1107 1113 will be processed and a new prompt displayed.
1108 1114 """
1109 1115 self._control.setReadOnly(True)
1110 1116 self._prompt_finished_hook()
1111 1117
1112 1118 def _readline(self, prompt='', callback=None):
1113 1119 """ Reads one line of input from the user.
1114 1120
1115 1121 Parameters
1116 1122 ----------
1117 1123 prompt : str, optional
1118 1124 The prompt to print before reading the line.
1119 1125
1120 1126 callback : callable, optional
1121 1127 A callback to execute with the read line. If not specified, input is
1122 1128 read *synchronously* and this method does not return until it has
1123 1129 been read.
1124 1130
1125 1131 Returns
1126 1132 -------
1127 1133 If a callback is specified, returns nothing. Otherwise, returns the
1128 1134 input string with the trailing newline stripped.
1129 1135 """
1130 1136 if self._reading:
1131 1137 raise RuntimeError('Cannot read a line. Widget is already reading.')
1132 1138
1133 1139 if not callback and not self.isVisible():
1134 1140 # If the user cannot see the widget, this function cannot return.
1135 1141 raise RuntimeError('Cannot synchronously read a line if the widget '
1136 1142 'is not visible!')
1137 1143
1138 1144 self._reading = True
1139 1145 self._show_prompt(prompt, newline=False)
1140 1146
1141 1147 if callback is None:
1142 1148 self._reading_callback = None
1143 1149 while self._reading:
1144 1150 QtCore.QCoreApplication.processEvents()
1145 1151 return self.input_buffer.rstrip('\n')
1146 1152
1147 1153 else:
1148 1154 self._reading_callback = lambda: \
1149 1155 callback(self.input_buffer.rstrip('\n'))
1150 1156
1151 1157 def _set_continuation_prompt(self, prompt, html=False):
1152 1158 """ Sets the continuation prompt.
1153 1159
1154 1160 Parameters
1155 1161 ----------
1156 1162 prompt : str
1157 1163 The prompt to show when more input is needed.
1158 1164
1159 1165 html : bool, optional (default False)
1160 1166 If set, the prompt will be inserted as formatted HTML. Otherwise,
1161 1167 the prompt will be treated as plain text, though ANSI color codes
1162 1168 will be handled.
1163 1169 """
1164 1170 if html:
1165 1171 self._continuation_prompt_html = prompt
1166 1172 else:
1167 1173 self._continuation_prompt = prompt
1168 1174 self._continuation_prompt_html = None
1169 1175
1170 1176 def _set_cursor(self, cursor):
1171 1177 """ Convenience method to set the current cursor.
1172 1178 """
1173 1179 self._control.setTextCursor(cursor)
1174 1180
1175 1181 def _set_position(self, position):
1176 1182 """ Convenience method to set the position of the cursor.
1177 1183 """
1178 1184 cursor = self._control.textCursor()
1179 1185 cursor.setPosition(position)
1180 1186 self._control.setTextCursor(cursor)
1181 1187
1182 1188 def _set_selection(self, start, end):
1183 1189 """ Convenience method to set the current selected text.
1184 1190 """
1185 1191 self._control.setTextCursor(self._get_selection_cursor(start, end))
1186 1192
1187 1193 def _show_context_menu(self, pos):
1188 1194 """ Shows a context menu at the given QPoint (in widget coordinates).
1189 1195 """
1190 1196 menu = QtGui.QMenu()
1191 1197
1192 1198 copy_action = menu.addAction('Copy', self.copy)
1193 1199 copy_action.setEnabled(self._get_cursor().hasSelection())
1194 1200 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1195 1201
1196 1202 paste_action = menu.addAction('Paste', self.paste)
1197 1203 paste_action.setEnabled(self.can_paste())
1198 1204 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1199 1205
1200 1206 menu.addSeparator()
1201 1207 menu.addAction('Select All', self.select_all)
1202 1208
1203 1209 menu.exec_(self._control.mapToGlobal(pos))
1204 1210
1205 1211 def _show_prompt(self, prompt=None, html=False, newline=True):
1206 1212 """ Writes a new prompt at the end of the buffer.
1207 1213
1208 1214 Parameters
1209 1215 ----------
1210 1216 prompt : str, optional
1211 1217 The prompt to show. If not specified, the previous prompt is used.
1212 1218
1213 1219 html : bool, optional (default False)
1214 1220 Only relevant when a prompt is specified. If set, the prompt will
1215 1221 be inserted as formatted HTML. Otherwise, the prompt will be treated
1216 1222 as plain text, though ANSI color codes will be handled.
1217 1223
1218 1224 newline : bool, optional (default True)
1219 1225 If set, a new line will be written before showing the prompt if
1220 1226 there is not already a newline at the end of the buffer.
1221 1227 """
1222 1228 # Insert a preliminary newline, if necessary.
1223 1229 if newline:
1224 1230 cursor = self._get_end_cursor()
1225 1231 if cursor.position() > 0:
1226 1232 cursor.movePosition(QtGui.QTextCursor.Left,
1227 1233 QtGui.QTextCursor.KeepAnchor)
1228 1234 if str(cursor.selection().toPlainText()) != '\n':
1229 1235 self._append_plain_text('\n')
1230 1236
1231 1237 # Write the prompt.
1232 1238 self._append_plain_text(self._prompt_sep)
1233 1239 if prompt is None:
1234 1240 if self._prompt_html is None:
1235 1241 self._append_plain_text(self._prompt)
1236 1242 else:
1237 1243 self._append_html(self._prompt_html)
1238 1244 else:
1239 1245 if html:
1240 1246 self._prompt = self._append_html_fetching_plain_text(prompt)
1241 1247 self._prompt_html = prompt
1242 1248 else:
1243 1249 self._append_plain_text(prompt)
1244 1250 self._prompt = prompt
1245 1251 self._prompt_html = None
1246 1252
1247 1253 self._prompt_pos = self._get_end_cursor().position()
1248 1254 self._prompt_started()
1249 1255
1250 1256 def _show_continuation_prompt(self):
1251 1257 """ Writes a new continuation prompt at the end of the buffer.
1252 1258 """
1253 1259 if self._continuation_prompt_html is None:
1254 1260 self._append_plain_text(self._continuation_prompt)
1255 1261 else:
1256 1262 self._continuation_prompt = self._append_html_fetching_plain_text(
1257 1263 self._continuation_prompt_html)
1258 1264
1259 1265
1260 1266 class HistoryConsoleWidget(ConsoleWidget):
1261 1267 """ A ConsoleWidget that keeps a history of the commands that have been
1262 1268 executed.
1263 1269 """
1264 1270
1265 1271 #---------------------------------------------------------------------------
1266 1272 # 'object' interface
1267 1273 #---------------------------------------------------------------------------
1268 1274
1269 1275 def __init__(self, *args, **kw):
1270 1276 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1271 1277 self._history = []
1272 1278 self._history_index = 0
1273 1279
1274 1280 #---------------------------------------------------------------------------
1275 1281 # 'ConsoleWidget' public interface
1276 1282 #---------------------------------------------------------------------------
1277 1283
1278 1284 def execute(self, source=None, hidden=False, interactive=False):
1279 1285 """ Reimplemented to the store history.
1280 1286 """
1281 1287 if not hidden:
1282 1288 history = self.input_buffer if source is None else source
1283 1289
1284 1290 executed = super(HistoryConsoleWidget, self).execute(
1285 1291 source, hidden, interactive)
1286 1292
1287 1293 if executed and not hidden:
1288 self._history.append(history.rstrip())
1289 self._history_index = len(self._history)
1294 # Save the command unless it was a blank line.
1295 history = history.rstrip()
1296 if history:
1297 self._history.append(history)
1298 self._history_index = len(self._history)
1290 1299
1291 1300 return executed
1292 1301
1293 1302 #---------------------------------------------------------------------------
1294 1303 # 'ConsoleWidget' abstract interface
1295 1304 #---------------------------------------------------------------------------
1296 1305
1297 1306 def _up_pressed(self):
1298 1307 """ Called when the up key is pressed. Returns whether to continue
1299 1308 processing the event.
1300 1309 """
1301 1310 prompt_cursor = self._get_prompt_cursor()
1302 1311 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1303 1312 self.history_previous()
1304 1313
1305 1314 # Go to the first line of prompt for seemless history scrolling.
1306 1315 cursor = self._get_prompt_cursor()
1307 1316 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1308 1317 self._set_cursor(cursor)
1309 1318
1310 1319 return False
1311 1320 return True
1312 1321
1313 1322 def _down_pressed(self):
1314 1323 """ Called when the down key is pressed. Returns whether to continue
1315 1324 processing the event.
1316 1325 """
1317 1326 end_cursor = self._get_end_cursor()
1318 1327 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1319 1328 self.history_next()
1320 1329 return False
1321 1330 return True
1322 1331
1323 1332 #---------------------------------------------------------------------------
1324 1333 # 'HistoryConsoleWidget' public interface
1325 1334 #---------------------------------------------------------------------------
1326 1335
1327 1336 def history_previous(self):
1328 1337 """ If possible, set the input buffer to the previous item in the
1329 1338 history.
1330 1339 """
1331 1340 if self._history_index > 0:
1332 1341 self._history_index -= 1
1333 1342 self.input_buffer = self._history[self._history_index]
1334 1343
1335 1344 def history_next(self):
1336 1345 """ Set the input buffer to the next item in the history, or a blank
1337 1346 line if there is no subsequent item.
1338 1347 """
1339 1348 if self._history_index < len(self._history):
1340 1349 self._history_index += 1
1341 1350 if self._history_index < len(self._history):
1342 1351 self.input_buffer = self._history[self._history_index]
1343 1352 else:
1344 1353 self.input_buffer = ''
1345 1354
1346 1355 #---------------------------------------------------------------------------
1347 1356 # 'HistoryConsoleWidget' protected interface
1348 1357 #---------------------------------------------------------------------------
1349 1358
1350 1359 def _set_history(self, history):
1351 1360 """ Replace the current history with a sequence of history items.
1352 1361 """
1353 1362 self._history = list(history)
1354 1363 self._history_index = len(self._history)
@@ -1,99 +1,99 b''
1 1 #!/usr/bin/env python
2 2
3 3 """ A minimal application using the Qt console-style IPython frontend.
4 4 """
5 5
6 6 # Systemm library imports
7 7 from PyQt4 import QtGui
8 8
9 9 # Local imports
10 10 from IPython.external.argparse import ArgumentParser
11 11 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
12 12 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
13 13 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
14 14 from IPython.frontend.qt.kernelmanager import QtKernelManager
15 15
16 16 # Constants
17 17 LOCALHOST = '127.0.0.1'
18 18
19 19
20 20 def main():
21 21 """ Entry point for application.
22 22 """
23 23 # Parse command line arguments.
24 24 parser = ArgumentParser()
25 25 kgroup = parser.add_argument_group('kernel options')
26 26 kgroup.add_argument('-e', '--existing', action='store_true',
27 27 help='connect to an existing kernel')
28 28 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
29 29 help='set the kernel\'s IP address [default localhost]')
30 30 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
31 31 help='set the XREQ channel port [default random]')
32 32 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
33 33 help='set the SUB channel port [default random]')
34 34 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
35 35 help='set the REP channel port [default random]')
36 36
37 37 egroup = kgroup.add_mutually_exclusive_group()
38 38 egroup.add_argument('--pure', action='store_true', help = \
39 39 'use a pure Python kernel instead of an IPython kernel')
40 40 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
41 41 const='auto', help = \
42 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
43 given, the GUI backend is matplotlib's, otherwise use one of: \
44 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
42 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
43 given, the GUI backend is matplotlib's, otherwise use one of: \
44 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
45 45
46 46 wgroup = parser.add_argument_group('widget options')
47 47 wgroup.add_argument('--paging', type=str, default='inside',
48 48 choices = ['inside', 'hsplit', 'vsplit', 'none'],
49 49 help='set the paging style [default inside]')
50 50 wgroup.add_argument('--rich', action='store_true',
51 51 help='enable rich text support')
52 52 wgroup.add_argument('--tab-simple', action='store_true',
53 53 help='do tab completion ala a Unix terminal')
54 54
55 55 args = parser.parse_args()
56 56
57 57 # Don't let Qt or ZMQ swallow KeyboardInterupts.
58 58 import signal
59 59 signal.signal(signal.SIGINT, signal.SIG_DFL)
60 60
61 61 # Create a KernelManager and start a kernel.
62 62 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
63 63 sub_address=(args.ip, args.sub),
64 64 rep_address=(args.ip, args.rep))
65 65 if args.ip == LOCALHOST and not args.existing:
66 66 if args.pure:
67 67 kernel_manager.start_kernel(ipython=False)
68 68 elif args.pylab:
69 69 if args.rich:
70 70 kernel_manager.start_kernel(pylab='payload-svg')
71 71 else:
72 72 if args.pylab == 'auto':
73 73 kernel_manager.start_kernel(pylab='qt4')
74 74 else:
75 75 kernel_manager.start_kernel(pylab=args.pylab)
76 76 else:
77 77 kernel_manager.start_kernel()
78 78 kernel_manager.start_channels()
79 79
80 80 # Create the widget.
81 81 app = QtGui.QApplication([])
82 82 if args.pure:
83 83 kind = 'rich' if args.rich else 'plain'
84 84 widget = FrontendWidget(kind=kind, paging=args.paging)
85 85 elif args.rich:
86 86 widget = RichIPythonWidget(paging=args.paging)
87 87 else:
88 88 widget = IPythonWidget(paging=args.paging)
89 89 widget.gui_completion = not args.tab_simple
90 90 widget.kernel_manager = kernel_manager
91 91 widget.setWindowTitle('Python' if args.pure else 'IPython')
92 92 widget.show()
93 93
94 94 # Start the application main loop.
95 95 app.exec_()
96 96
97 97
98 98 if __name__ == '__main__':
99 99 main()
General Comments 0
You need to be logged in to leave comments. Login now