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