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