##// END OF EJS Templates
Correct styling of ANSI patterns.
Gael Varoquaux -
Show More
@@ -1,143 +1,139 b''
1 1 """
2 2 Frontend class that uses IPython0 to prefilter the inputs.
3 3
4 4 Using the IPython0 mechanism gives us access to the magics.
5 5 """
6 6 __docformat__ = "restructuredtext en"
7 7
8 8 #-------------------------------------------------------------------------------
9 9 # Copyright (C) 2008 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-------------------------------------------------------------------------------
14 14
15 15 #-------------------------------------------------------------------------------
16 16 # Imports
17 17 #-------------------------------------------------------------------------------
18 18 import sys
19 19
20 20 from linefrontendbase import LineFrontEndBase, common_prefix
21 21
22 22 from IPython.ipmaker import make_IPython
23 23 from IPython.ipapi import IPApi
24 24 from IPython.kernel.core.sync_output_trap import SyncOutputTrap
25 25
26 26 from IPython.genutils import Term
27 27
28 28 #-------------------------------------------------------------------------------
29 29 # Utility functions (temporary, should be moved out of here)
30 30 #-------------------------------------------------------------------------------
31 31 import os
32 32 def xterm_system(command):
33 33 """ Run a command in a separate console window.
34 34 """
35 35 os.system("""
36 36 xterm -title "%s" -e \'/bin/sh -c "%s ;
37 37 printf \\"\\\\n\\";
38 38 printf \\"press a key to close\\" ;
39 39 printf \\"\x1b]0;%s (finished -- press a key to close)\x07\\" ;
40 40 read foo;"\'
41 41 """ % (command, command, command) )
42 42
43 43 #-------------------------------------------------------------------------------
44 44 # Frontend class using ipython0 to do the prefiltering.
45 45 #-------------------------------------------------------------------------------
46 46 class PrefilterFrontEnd(LineFrontEndBase):
47 47
48 48 def __init__(self, *args, **kwargs):
49 49 LineFrontEndBase.__init__(self, *args, **kwargs)
50 50 # Instanciate an IPython0 interpreter to be able to use the
51 51 # prefiltering.
52 52 self.ipython0 = make_IPython()
53 53 # Set the pager:
54 54 self.ipython0.set_hook('show_in_pager',
55 55 lambda s, string: self.write("\n"+string))
56 56 self.ipython0.write = self.write
57 57 self._ip = _ip = IPApi(self.ipython0)
58 58 # XXX: Hack: mix the two namespaces
59 59 self.shell.user_ns = self.ipython0.user_ns
60 60 self.shell.user_global_ns = self.ipython0.user_global_ns
61 61 # Make sure the raw system call doesn't get called, as we don't
62 62 # have a stdin accessible.
63 63 self._ip.system = xterm_system
64 64 # Redefine a serie of magics to avoid os.system:
65 65 # FIXME: I am redefining way too much magics.
66 66 for alias_name, (_, alias_value) in \
67 67 _ip.IP.shell.alias_table.iteritems():
68 68 magic = lambda s : _ip.magic('sx %s %s' % (alias_value, s))
69 69 setattr(_ip.IP, 'magic_%s' % alias_name, magic)
70 70 # FIXME: I should create a real file-like object dedicated to this
71 71 # terminal
72 Term.cout.flush = lambda : None
73 Term.cout.getvalue = lambda : ''
74 Term.cerr.flush = lambda : None
75 Term.cerr.getvalue = lambda : ''
76 72 self.shell.output_trap = SyncOutputTrap(write_out=self.write,
77 73 write_err=self.write)
78 74
79 75
80 76
81 77 def prefilter_input(self, input_string):
82 78 """ Using IPython0 to prefilter the commands.
83 79 """
84 80 input_string = LineFrontEndBase.prefilter_input(self, input_string)
85 81 filtered_lines = []
86 82 # The IPython0 prefilters sometime produce output. We need to
87 83 # capture it.
88 84 self.capture_output()
89 85 self.last_result = dict(number=self.prompt_number)
90 86 try:
91 87 for line in input_string.split('\n'):
92 88 filtered_lines.append(self.ipython0.prefilter(line, False))
93 89 except:
94 90 # XXX: probably not the right thing to do.
95 91 self.ipython0.showsyntaxerror()
96 92 self.after_execute()
97 93 finally:
98 94 self.release_output()
99 95
100 96 filtered_string = '\n'.join(filtered_lines)
101 97 return filtered_string
102 98
103 99
104 100 def show_traceback(self):
105 101 self.capture_output()
106 102 self.ipython0.showtraceback()
107 103 self.release_output()
108 104
109 105
110 106 def capture_output(self):
111 107 """ Capture all the output mechanism we can think of.
112 108 """
113 109 self.__old_cout_write = Term.cout.write
114 110 self.__old_err_write = Term.cerr.write
115 111 Term.cout.write = self.write
116 112 Term.cerr.write = self.write
117 113 self.__old_stdout = sys.stdout
118 114 self.__old_stderr= sys.stderr
119 115 sys.stdout = Term.cout
120 116 sys.stderr = Term.cerr
121 117 # FIXME: I still need to provide the writelines method
122 118
123 119 def release_output(self):
124 120 """ Release all the different captures we have made,
125 121 and flush the buffers.
126 122 """
127 123 Term.cout.write = self.__old_cout_write
128 124 Term.cerr.write = self.__old_err_write
129 125 sys.stdout = self.__old_stdout
130 126 sys.stderr = self.__old_stderr
131 127
132 128
133 129 def complete(self, line):
134 130 word = line.split('\n')[-1].split(' ')[-1]
135 131 completions = self.ipython0.complete(word)
136 132 key = lambda x: x.replace('_', '')
137 133 completions.sort(key=key)
138 134 if completions:
139 135 prefix = common_prefix(completions)
140 136 line = line[:-len(word)] + prefix
141 137 return line, completions
142 138
143 139
@@ -1,420 +1,418 b''
1 1 # encoding: utf-8
2 2 """
3 3 A Wx widget to act as a console and input commands.
4 4
5 5 This widget deals with prompts and provides an edit buffer
6 6 restricted to after the last prompt.
7 7 """
8 8
9 9 __docformat__ = "restructuredtext en"
10 10
11 11 #-------------------------------------------------------------------------------
12 12 # Copyright (C) 2008 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is
15 15 # in the file COPYING, distributed as part of this software.
16 16 #-------------------------------------------------------------------------------
17 17
18 18 #-------------------------------------------------------------------------------
19 19 # Imports
20 20 #-------------------------------------------------------------------------------
21 21
22 22 import wx
23 23 import wx.stc as stc
24 24
25 25 from wx.py import editwindow
26 26
27 27 import re
28 28
29 29 # FIXME: Need to provide an API for non user-generated display on the
30 30 # screen: this should not be editable by the user.
31 31
32 32 if wx.Platform == '__WXMSW__':
33 33 _DEFAULT_SIZE = 80
34 34 else:
35 35 _DEFAULT_SIZE = 10
36 36
37 37 _DEFAULT_STYLE = {
38 38 'stdout' : 'fore:#0000FF',
39 39 'stderr' : 'fore:#007f00',
40 40 'trace' : 'fore:#FF0000',
41 41
42 42 'default' : 'size:%d' % _DEFAULT_SIZE,
43 43 'bracegood' : 'fore:#FFFFFF,back:#0000FF,bold',
44 44 'bracebad' : 'fore:#000000,back:#FF0000,bold',
45 45
46 46 # properties for the various Python lexer styles
47 47 'comment' : 'fore:#007F00',
48 48 'number' : 'fore:#007F7F',
49 49 'string' : 'fore:#7F007F,italic',
50 50 'char' : 'fore:#7F007F,italic',
51 51 'keyword' : 'fore:#00007F,bold',
52 52 'triple' : 'fore:#7F0000',
53 53 'tripledouble' : 'fore:#7F0000',
54 54 'class' : 'fore:#0000FF,bold,underline',
55 55 'def' : 'fore:#007F7F,bold',
56 56 'operator' : 'bold'
57 57 }
58 58
59 59 # new style numbers
60 60 _STDOUT_STYLE = 15
61 61 _STDERR_STYLE = 16
62 62 _TRACE_STYLE = 17
63 63
64 64
65 65 # system colors
66 66 #SYS_COLOUR_BACKGROUND = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)
67 67
68 68 #-------------------------------------------------------------------------------
69 69 # The console widget class
70 70 #-------------------------------------------------------------------------------
71 71 class ConsoleWidget(editwindow.EditWindow):
72 72 """ Specialized styled text control view for console-like workflow.
73 73
74 74 This widget is mainly interested in dealing with the prompt and
75 75 keeping the cursor inside the editing line.
76 76 """
77 77
78 78 title = 'Console'
79 79
80 80 style = _DEFAULT_STYLE.copy()
81 81
82 82 # Translation table from ANSI escape sequences to color. Override
83 83 # this to specify your colors.
84 84 ANSI_STYLES = {'0;30': [0, 'BLACK'], '0;31': [1, 'RED'],
85 85 '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'],
86 86 '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'],
87 87 '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'],
88 88 '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'],
89 89 '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'],
90 90 '1;34': [12, 'LIGHT BLUE'], '1;35':
91 91 [13, 'MEDIUM VIOLET RED'],
92 92 '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']}
93 93
94 94 # The color of the carret (call _apply_style() after setting)
95 95 carret_color = 'BLACK'
96 96
97 97
98 98 #--------------------------------------------------------------------------
99 99 # Public API
100 100 #--------------------------------------------------------------------------
101 101
102 102 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
103 103 size=wx.DefaultSize, style=0, ):
104 104 editwindow.EditWindow.__init__(self, parent, id, pos, size, style)
105 105 self.configure_scintilla()
106 106
107 107 # FIXME: we need to retrieve this from the interpreter.
108 108 self.prompt = \
109 109 '\n\x01\x1b[0;34m\x02In [\x01\x1b[1;34m\x02%i\x01\x1b[0;34m\x02]: \x01\x1b[0m\x02'
110 110 self.new_prompt(self.prompt % 1)
111 111
112 112 self.Bind(wx.EVT_KEY_DOWN, self._on_key_down)
113 113 self.Bind(wx.EVT_KEY_UP, self._on_key_up)
114 114
115 115
116 116 def configure_scintilla(self):
117 117 # Ctrl"+" or Ctrl "-" can be used to zoomin/zoomout the text inside
118 118 # the widget
119 119 self.CmdKeyAssign(ord('+'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
120 120 self.CmdKeyAssign(ord('-'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
121 121 # Also allow Ctrl Shift "=" for poor non US keyboard users.
122 122 self.CmdKeyAssign(ord('='), stc.STC_SCMOD_CTRL|stc.STC_SCMOD_SHIFT,
123 123 stc.STC_CMD_ZOOMIN)
124 124
125 125 #self.CmdKeyAssign(stc.STC_KEY_PRIOR, stc.STC_SCMOD_SHIFT,
126 126 # stc.STC_CMD_PAGEUP)
127 127
128 128 #self.CmdKeyAssign(stc.STC_KEY_NEXT, stc.STC_SCMOD_SHIFT,
129 129 # stc.STC_CMD_PAGEDOWN)
130 130
131 131 # Keys: we need to clear some of the keys the that don't play
132 132 # well with a console.
133 133 self.CmdKeyClear(ord('D'), stc.STC_SCMOD_CTRL)
134 134 self.CmdKeyClear(ord('L'), stc.STC_SCMOD_CTRL)
135 135 self.CmdKeyClear(ord('T'), stc.STC_SCMOD_CTRL)
136 136
137 137
138 138 self.SetEOLMode(stc.STC_EOL_CRLF)
139 139 self.SetWrapMode(stc.STC_WRAP_CHAR)
140 140 self.SetWrapMode(stc.STC_WRAP_WORD)
141 141 self.SetBufferedDraw(True)
142 142 self.SetUseAntiAliasing(True)
143 143 self.SetLayoutCache(stc.STC_CACHE_PAGE)
144 144 self.SetUndoCollection(False)
145 145 self.SetUseTabs(True)
146 146 self.SetIndent(4)
147 147 self.SetTabWidth(4)
148 148
149 149 self.EnsureCaretVisible()
150 150 # we don't want scintilla's autocompletion to choose
151 151 # automaticaly out of a single choice list, as we pop it up
152 152 # automaticaly
153 153 self.AutoCompSetChooseSingle(False)
154 154 self.AutoCompSetMaxHeight(10)
155 155
156 156 self.SetMargins(3, 3) #text is moved away from border with 3px
157 157 # Suppressing Scintilla margins
158 158 self.SetMarginWidth(0, 0)
159 159 self.SetMarginWidth(1, 0)
160 160 self.SetMarginWidth(2, 0)
161 161
162 162 self._apply_style()
163 163
164 164 # Xterm escape sequences
165 165 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
166 166 self.title_pat = re.compile('\x1b]0;(.*?)\x07')
167 167
168 168 #self.SetEdgeMode(stc.STC_EDGE_LINE)
169 169 #self.SetEdgeColumn(80)
170 170
171 171 # styles
172 172 p = self.style
173 173 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, p['default'])
174 174 self.StyleClearAll()
175 175 self.StyleSetSpec(_STDOUT_STYLE, p['stdout'])
176 176 self.StyleSetSpec(_STDERR_STYLE, p['stderr'])
177 177 self.StyleSetSpec(_TRACE_STYLE, p['trace'])
178 178
179 179 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, p['bracegood'])
180 180 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, p['bracebad'])
181 181 self.StyleSetSpec(stc.STC_P_COMMENTLINE, p['comment'])
182 182 self.StyleSetSpec(stc.STC_P_NUMBER, p['number'])
183 183 self.StyleSetSpec(stc.STC_P_STRING, p['string'])
184 184 self.StyleSetSpec(stc.STC_P_CHARACTER, p['char'])
185 185 self.StyleSetSpec(stc.STC_P_WORD, p['keyword'])
186 186 self.StyleSetSpec(stc.STC_P_WORD2, p['keyword'])
187 187 self.StyleSetSpec(stc.STC_P_TRIPLE, p['triple'])
188 188 self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, p['tripledouble'])
189 189 self.StyleSetSpec(stc.STC_P_CLASSNAME, p['class'])
190 190 self.StyleSetSpec(stc.STC_P_DEFNAME, p['def'])
191 191 self.StyleSetSpec(stc.STC_P_OPERATOR, p['operator'])
192 192 self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, p['comment'])
193 193
194 194
195 195 def write(self, text):
196 196 """ Write given text to buffer, while translating the ansi escape
197 197 sequences.
198 198 """
199 199 title = self.title_pat.split(text)
200 200 if len(title)>0:
201 201 self.title = title[-1]
202 202
203 203 text = self.title_pat.sub('', text)
204 204 segments = self.color_pat.split(text)
205 205 segment = segments.pop(0)
206 206 self.StartStyling(self.GetLength(), 0xFF)
207 207 self.AppendText(segment)
208 208
209 209 if segments:
210 ansi_tags = self.color_pat.findall(text)
211
212 for tag in ansi_tags:
213 i = segments.index(tag)
210 for ansi_tag, text in zip(segments[::2], segments[1::2]):
214 211 self.StartStyling(self.GetLength(), 0xFF)
215 self.AppendText(segments[i+1])
212 self.AppendText(text)
216 213
217 if tag != '0':
218 self.SetStyling(len(segments[i+1]),
219 self.ANSI_STYLES[tag][0])
214 if ansi_tag == '0':
215 style = 0
216 else:
217 style = self.ANSI_STYLES[ansi_tag][0]
220 218
221 segments.pop(i)
219 self.SetStyling(len(text), style)
222 220
223 221 self.GotoPos(self.GetLength())
224 222 wx.Yield()
225 223
226 224
227 225 def new_prompt(self, prompt):
228 226 """ Prints a prompt at start of line, and move the start of the
229 227 current block there.
230 228
231 229 The prompt can be give with ascii escape sequences.
232 230 """
233 231 self.write(prompt)
234 232 # now we update our cursor giving end of prompt
235 233 self.current_prompt_pos = self.GetLength()
236 234 self.current_prompt_line = self.GetCurrentLine()
237 235 wx.Yield()
238 236 self.EnsureCaretVisible()
239 237
240 238
241 239 def replace_current_edit_buffer(self, text):
242 240 """ Replace currently entered command line with given text.
243 241 """
244 242 self.SetSelection(self.current_prompt_pos, self.GetLength())
245 243 self.ReplaceSelection(text)
246 244 self.GotoPos(self.GetLength())
247 245
248 246
249 247 def get_current_edit_buffer(self):
250 248 """ Returns the text in current edit buffer.
251 249 """
252 250 return self.GetTextRange(self.current_prompt_pos,
253 251 self.GetLength())
254 252
255 253
256 254 #--------------------------------------------------------------------------
257 255 # Private API
258 256 #--------------------------------------------------------------------------
259 257
260 258 def _apply_style(self):
261 259 """ Applies the colors for the different text elements and the
262 260 carret.
263 261 """
264 262 self.SetCaretForeground(self.carret_color)
265 263
266 264 #self.StyleClearAll()
267 265 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,
268 266 "fore:#FF0000,back:#0000FF,bold")
269 267 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,
270 268 "fore:#000000,back:#FF0000,bold")
271 269
272 270 for style in self.ANSI_STYLES.values():
273 271 self.StyleSetSpec(style[0], "bold,fore:%s" % style[1])
274 272
275 273
276 274 def write_completion(self, possibilities):
277 275 # FIXME: This is non Wx specific and needs to be moved into
278 276 # the base class.
279 277 current_buffer = self.get_current_edit_buffer()
280 278
281 279 self.write('\n')
282 280 max_len = len(max(possibilities, key=len)) + 1
283 281
284 282 #now we check how much symbol we can put on a line...
285 283 chars_per_line = self.GetSize()[0]/self.GetCharWidth()
286 284 symbols_per_line = max(1, chars_per_line/max_len)
287 285
288 286 pos = 1
289 287 buf = []
290 288 for symbol in possibilities:
291 289 if pos < symbols_per_line:
292 290 buf.append(symbol.ljust(max_len))
293 291 pos += 1
294 292 else:
295 293 buf.append(symbol.rstrip() +'\n')
296 294 pos = 1
297 295 self.write(''.join(buf))
298 296 self.new_prompt(self.prompt % (self.last_result['number'] + 1))
299 297 self.replace_current_edit_buffer(current_buffer)
300 298
301 299
302 300 def pop_completion(self, possibilities, offset=0):
303 301 """ Pops up an autocompletion menu. Offset is the offset
304 302 in characters of the position at which the menu should
305 303 appear, relativ to the cursor.
306 304 """
307 305 self.AutoCompSetIgnoreCase(False)
308 306 self.AutoCompSetAutoHide(False)
309 307 self.AutoCompSetMaxHeight(len(possibilities))
310 308 self.AutoCompShow(offset, " ".join(possibilities))
311 309
312 310
313 311 def scroll_to_bottom(self):
314 312 maxrange = self.GetScrollRange(wx.VERTICAL)
315 313 self.ScrollLines(maxrange)
316 314
317 315
318 316 def _on_enter(self):
319 317 """ Called when the return key is hit.
320 318 """
321 319 pass
322 320
323 321
324 322 def _on_key_down(self, event, skip=True):
325 323 """ Key press callback used for correcting behavior for
326 324 console-like interfaces: the cursor is constraint to be after
327 325 the last prompt.
328 326
329 327 Return True if event as been catched.
330 328 """
331 329 catched = True
332 330 # Intercept some specific keys.
333 331 if event.KeyCode == ord('L') and event.ControlDown() :
334 332 self.scroll_to_bottom()
335 333 elif event.KeyCode == ord('K') and event.ControlDown() :
336 334 self.replace_current_edit_buffer('')
337 335 elif event.KeyCode == wx.WXK_PAGEUP and event.ShiftDown():
338 336 self.ScrollPages(-1)
339 337 elif event.KeyCode == wx.WXK_PAGEDOWN and event.ShiftDown():
340 338 self.ScrollPages(1)
341 339 elif event.KeyCode == wx.WXK_UP and event.ShiftDown():
342 340 self.ScrollLines(-1)
343 341 elif event.KeyCode == wx.WXK_DOWN and event.ShiftDown():
344 342 self.ScrollLinees(1)
345 343 else:
346 344 catched = False
347 345
348 346 if self.AutoCompActive():
349 347 event.Skip()
350 348 else:
351 349 if event.KeyCode in (13, wx.WXK_NUMPAD_ENTER) and \
352 350 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
353 351 catched = True
354 352 self.CallTipCancel()
355 353 self.write('\n')
356 354 self._on_enter()
357 355
358 356 elif event.KeyCode == wx.WXK_HOME:
359 357 if event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
360 358 self.GotoPos(self.current_prompt_pos)
361 359 catched = True
362 360
363 361 elif event.Modifiers in (wx.MOD_SHIFT, wx.MOD_WIN) :
364 362 # FIXME: This behavior is not ideal: if the selection
365 363 # is already started, it will jump.
366 364 self.SetSelectionStart(self.current_prompt_pos)
367 365 self.SetSelectionEnd(self.GetCurrentPos())
368 366 catched = True
369 367
370 368 elif event.KeyCode == wx.WXK_UP:
371 369 if self.GetCurrentLine() > self.current_prompt_line:
372 370 if self.GetCurrentLine() == self.current_prompt_line + 1 \
373 371 and self.GetColumn(self.GetCurrentPos()) < \
374 372 self.GetColumn(self.current_prompt_pos):
375 373 self.GotoPos(self.current_prompt_pos)
376 374 else:
377 375 event.Skip()
378 376 catched = True
379 377
380 378 elif event.KeyCode in (wx.WXK_LEFT, wx.WXK_BACK):
381 379 if self.GetCurrentPos() > self.current_prompt_pos:
382 380 event.Skip()
383 381 catched = True
384 382
385 383 if skip and not catched:
386 384 event.Skip()
387 385
388 386 return catched
389 387
390 388
391 389 def _on_key_up(self, event, skip=True):
392 390 """ If cursor is outside the editing region, put it back.
393 391 """
394 392 event.Skip()
395 393 if self.GetCurrentPos() < self.current_prompt_pos:
396 394 self.GotoPos(self.current_prompt_pos)
397 395
398 396
399 397
400 398
401 399 if __name__ == '__main__':
402 400 # Some simple code to test the console widget.
403 401 class MainWindow(wx.Frame):
404 402 def __init__(self, parent, id, title):
405 403 wx.Frame.__init__(self, parent, id, title, size=(300,250))
406 404 self._sizer = wx.BoxSizer(wx.VERTICAL)
407 405 self.console_widget = ConsoleWidget(self)
408 406 self._sizer.Add(self.console_widget, 1, wx.EXPAND)
409 407 self.SetSizer(self._sizer)
410 408 self.SetAutoLayout(1)
411 409 self.Show(True)
412 410
413 411 app = wx.PySimpleApp()
414 412 w = MainWindow(None, wx.ID_ANY, 'ConsoleWidget')
415 413 w.SetSize((780, 460))
416 414 w.Show()
417 415
418 416 app.MainLoop()
419 417
420 418
General Comments 0
You need to be logged in to leave comments. Login now