##// END OF EJS Templates
Capture Xterm title sequence.
Gael Varoquaux -
Show More
@@ -1,398 +1,407 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
60 60 # new style numbers
61 61 _STDOUT_STYLE = 15
62 62 _STDERR_STYLE = 16
63 63 _TRACE_STYLE = 17
64 64
65 65
66 66 #-------------------------------------------------------------------------------
67 67 # The console widget class
68 68 #-------------------------------------------------------------------------------
69 69 class ConsoleWidget(editwindow.EditWindow):
70 70 """ Specialized styled text control view for console-like workflow.
71 71
72 72 This widget is mainly interested in dealing with the prompt and
73 73 keeping the cursor inside the editing line.
74 74 """
75 75
76 title = 'Console'
77
76 78 style = _DEFAULT_STYLE.copy()
77 79
78 80 # Translation table from ANSI escape sequences to color. Override
79 81 # this to specify your colors.
80 82 ANSI_STYLES = {'0;30': [0, 'BLACK'], '0;31': [1, 'RED'],
81 83 '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'],
82 84 '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'],
83 85 '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'],
84 86 '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'],
85 87 '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'],
86 88 '1;34': [12, 'LIGHT BLUE'], '1;35':
87 89 [13, 'MEDIUM VIOLET RED'],
88 90 '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']}
89 91
90 92 # The color of the carret (call _apply_style() after setting)
91 93 carret_color = 'BLACK'
92 94
93 95
94 96 #--------------------------------------------------------------------------
95 97 # Public API
96 98 #--------------------------------------------------------------------------
97 99
98 100 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
99 101 size=wx.DefaultSize, style=0,
100 102 autocomplete_mode='IPYTHON'):
101 103 """ Autocomplete_mode: Can be 'IPYTHON' or 'STC'
102 104 'IPYTHON' show autocompletion the ipython way
103 105 'STC" show it scintilla text control way
104 106 """
105 107 editwindow.EditWindow.__init__(self, parent, id, pos, size, style)
106 108 self.configure_scintilla()
107 109
108 110 # FIXME: we need to retrieve this from the interpreter.
109 111 self.prompt = \
110 112 '\n\x01\x1b[0;34m\x02In [\x01\x1b[1;34m\x02%i\x01\x1b[0;34m\x02]: \x01\x1b[0m\x02'
111 113 self.new_prompt(self.prompt % 1)
112 114
113 115 self.autocomplete_mode = autocomplete_mode
114 116
115 117 self.Bind(wx.EVT_KEY_DOWN, self._on_key_down)
116 118 self.Bind(wx.EVT_KEY_UP, self._on_key_up)
117 119
118 120
119 121 def configure_scintilla(self):
120 122 # Ctrl"+" or Ctrl "-" can be used to zoomin/zoomout the text inside
121 123 # the widget
122 124 self.CmdKeyAssign(ord('+'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
123 125 self.CmdKeyAssign(ord('-'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
124 126 # Also allow Ctrl Shift "=" for poor non US keyboard users.
125 127 self.CmdKeyAssign(ord('='), stc.STC_SCMOD_CTRL|stc.STC_SCMOD_SHIFT,
126 128 stc.STC_CMD_ZOOMIN)
127 129
128 130 #self.CmdKeyAssign(stc.STC_KEY_PRIOR, stc.STC_SCMOD_SHIFT,
129 131 # stc.STC_CMD_PAGEUP)
130 132
131 133 #self.CmdKeyAssign(stc.STC_KEY_NEXT, stc.STC_SCMOD_SHIFT,
132 134 # stc.STC_CMD_PAGEDOWN)
133 135
134 136 # Keys: we need to clear some of the keys the that don't play
135 137 # well with a console.
136 138 self.CmdKeyClear(ord('D'), stc.STC_SCMOD_CTRL)
137 139 self.CmdKeyClear(ord('L'), stc.STC_SCMOD_CTRL)
138 140 self.CmdKeyClear(ord('T'), stc.STC_SCMOD_CTRL)
139 141
140 142
141 143 self.SetEOLMode(stc.STC_EOL_CRLF)
142 144 self.SetWrapMode(stc.STC_WRAP_CHAR)
143 145 self.SetWrapMode(stc.STC_WRAP_WORD)
144 146 self.SetBufferedDraw(True)
145 147 self.SetUseAntiAliasing(True)
146 148 self.SetLayoutCache(stc.STC_CACHE_PAGE)
147 149 self.SetUndoCollection(False)
148 150 self.SetUseTabs(True)
149 151 self.SetIndent(4)
150 152 self.SetTabWidth(4)
151 153
152 154 self.EnsureCaretVisible()
153 155
154 156 self.SetMargins(3, 3) #text is moved away from border with 3px
155 157 # Suppressing Scintilla margins
156 158 self.SetMarginWidth(0, 0)
157 159 self.SetMarginWidth(1, 0)
158 160 self.SetMarginWidth(2, 0)
159 161
160 162 self._apply_style()
161
163
164 # Xterm escape sequences
162 165 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
166 self.title_pat = re.compile('\x1b]0;(.*?)\x07')
163 167
164 168 #self.SetEdgeMode(stc.STC_EDGE_LINE)
165 169 #self.SetEdgeColumn(80)
166 170
167 171 # styles
168 172 p = self.style
169 173 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, p['default'])
170 174 self.StyleClearAll()
171 175 self.StyleSetSpec(_STDOUT_STYLE, p['stdout'])
172 176 self.StyleSetSpec(_STDERR_STYLE, p['stderr'])
173 177 self.StyleSetSpec(_TRACE_STYLE, p['trace'])
174 178
175 179 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, p['bracegood'])
176 180 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, p['bracebad'])
177 181 self.StyleSetSpec(stc.STC_P_COMMENTLINE, p['comment'])
178 182 self.StyleSetSpec(stc.STC_P_NUMBER, p['number'])
179 183 self.StyleSetSpec(stc.STC_P_STRING, p['string'])
180 184 self.StyleSetSpec(stc.STC_P_CHARACTER, p['char'])
181 185 self.StyleSetSpec(stc.STC_P_WORD, p['keyword'])
182 186 self.StyleSetSpec(stc.STC_P_TRIPLE, p['triple'])
183 187 self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, p['tripledouble'])
184 188 self.StyleSetSpec(stc.STC_P_CLASSNAME, p['class'])
185 189 self.StyleSetSpec(stc.STC_P_DEFNAME, p['def'])
186 190 self.StyleSetSpec(stc.STC_P_OPERATOR, p['operator'])
187 191 self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, p['comment'])
188 192
189 193
190 194 def write(self, text):
191 195 """ Write given text to buffer, while translating the ansi escape
192 196 sequences.
193 197 """
198 title = self.title_pat.split(text)
199 if len(title)>0:
200 self.title = title[-1]
201
202 text = self.title_pat.sub('', text)
194 203 segments = self.color_pat.split(text)
195 204 segment = segments.pop(0)
196 205 self.StartStyling(self.GetLength(), 0xFF)
197 206 self.AppendText(segment)
198
207
199 208 if segments:
200 209 ansi_tags = self.color_pat.findall(text)
201 210
202 211 for tag in ansi_tags:
203 212 i = segments.index(tag)
204 213 self.StartStyling(self.GetLength(), 0xFF)
205 214 self.AppendText(segments[i+1])
206 215
207 216 if tag != '0':
208 217 self.SetStyling(len(segments[i+1]),
209 218 self.ANSI_STYLES[tag][0])
210 219
211 220 segments.pop(i)
212 221
213 222 self.GotoPos(self.GetLength())
214 223
215 224
216 225 def new_prompt(self, prompt):
217 226 """ Prints a prompt at start of line, and move the start of the
218 227 current block there.
219 228
220 229 The prompt can be give with ascii escape sequences.
221 230 """
222 231 self.write(prompt)
223 232 # now we update our cursor giving end of prompt
224 233 self.current_prompt_pos = self.GetLength()
225 234 self.current_prompt_line = self.GetCurrentLine()
226 235
227 236
228 237 def replace_current_edit_buffer(self, text):
229 238 """ Replace currently entered command line with given text.
230 239 """
231 240 self.SetSelection(self.current_prompt_pos, self.GetLength())
232 241 self.ReplaceSelection(text)
233 242 self.GotoPos(self.GetLength())
234 243
235 244
236 245 def get_current_edit_buffer(self):
237 246 """ Returns the text in current edit buffer.
238 247 """
239 248 return self.GetTextRange(self.current_prompt_pos,
240 249 self.GetLength())
241 250
242 251
243 252 #--------------------------------------------------------------------------
244 253 # Private API
245 254 #--------------------------------------------------------------------------
246 255
247 256 def _apply_style(self):
248 257 """ Applies the colors for the different text elements and the
249 258 carret.
250 259 """
251 260 self.SetCaretForeground(self.carret_color)
252 261
253 262 self.StyleClearAll()
254 263 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,
255 264 "fore:#FF0000,back:#0000FF,bold")
256 265 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,
257 266 "fore:#000000,back:#FF0000,bold")
258 267
259 268 for style in self.ANSI_STYLES.values():
260 269 self.StyleSetSpec(style[0], "bold,fore:%s" % style[1])
261 270
262 271
263 272 def writeCompletion(self, possibilities):
264 273 if self.autocomplete_mode == 'IPYTHON':
265 274 max_len = len(max(possibilities, key=len))
266 275 max_symbol = ' '*max_len
267 276
268 277 #now we check how much symbol we can put on a line...
269 278 test_buffer = max_symbol + ' '*4
270 279
271 280 allowed_symbols = 80/len(test_buffer)
272 281 if allowed_symbols == 0:
273 282 allowed_symbols = 1
274 283
275 284 pos = 1
276 285 buf = ''
277 286 for symbol in possibilities:
278 287 #buf += symbol+'\n'#*spaces)
279 288 if pos < allowed_symbols:
280 289 spaces = max_len - len(symbol) + 4
281 290 buf += symbol+' '*spaces
282 291 pos += 1
283 292 else:
284 293 buf += symbol+'\n'
285 294 pos = 1
286 295 self.write(buf)
287 296 else:
288 297 possibilities.sort() # Python sorts are case sensitive
289 298 self.AutoCompSetIgnoreCase(False)
290 299 self.AutoCompSetAutoHide(False)
291 300 #let compute the length ot last word
292 301 splitter = [' ', '(', '[', '{']
293 302 last_word = self.get_current_edit_buffer()
294 303 for breaker in splitter:
295 304 last_word = last_word.split(breaker)[-1]
296 305 self.AutoCompShow(len(last_word), " ".join(possibilities))
297 306
298 307
299 308 def scroll_to_bottom(self):
300 309 maxrange = self.GetScrollRange(wx.VERTICAL)
301 310 self.ScrollLines(maxrange)
302 311
303 312 def on_enter(self):
304 313 """ Called when the return key is hit.
305 314 """
306 315 pass
307 316
308 317
309 318 def _on_key_down(self, event, skip=True):
310 319 """ Key press callback used for correcting behavior for
311 320 console-like interfaces: the cursor is constraint to be after
312 321 the last prompt.
313 322
314 323 Return True if event as been catched.
315 324 """
316 325 catched = False
317 326 # Intercept some specific keys.
318 327 if event.KeyCode == ord('L') and event.ControlDown() :
319 328 catched = True
320 329 self.scroll_to_bottom()
321 330 elif event.KeyCode == wx.WXK_PAGEUP and event.ShiftDown():
322 331 catched = True
323 332 self.ScrollPages(-1)
324 333 elif event.KeyCode == wx.WXK_PAGEDOWN and event.ShiftDown():
325 334 catched = True
326 335 self.ScrollPages(1)
327 336
328 337 if self.AutoCompActive():
329 338 event.Skip()
330 339 else:
331 340 if event.KeyCode in (13, wx.WXK_NUMPAD_ENTER) and \
332 341 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
333 342 catched = True
334 343 self._on_enter()
335 344
336 345 elif event.KeyCode == wx.WXK_HOME:
337 346 if event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
338 347 self.GotoPos(self.current_prompt_pos)
339 348 catched = True
340 349
341 350 elif event.Modifiers in (wx.MOD_SHIFT, wx.MOD_WIN) :
342 351 # FIXME: This behavior is not ideal: if the selection
343 352 # is already started, it will jump.
344 353 self.SetSelectionStart(self.current_prompt_pos)
345 354 self.SetSelectionEnd(self.GetCurrentPos())
346 355 catched = True
347 356
348 357 elif event.KeyCode == wx.WXK_UP:
349 358 if self.GetCurrentLine() > self.current_prompt_line:
350 359 if self.GetCurrentLine() == self.current_prompt_line + 1 \
351 360 and self.GetColumn(self.GetCurrentPos()) < \
352 361 self.GetColumn(self.current_prompt_pos):
353 362 self.GotoPos(self.current_prompt_pos)
354 363 else:
355 364 event.Skip()
356 365 catched = True
357 366
358 367 elif event.KeyCode in (wx.WXK_LEFT, wx.WXK_BACK):
359 368 if self.GetCurrentPos() > self.current_prompt_pos:
360 369 event.Skip()
361 370 catched = True
362 371
363 372 if skip and not catched:
364 373 event.Skip()
365 374
366 375 return catched
367 376
368 377
369 378 def _on_key_up(self, event, skip=True):
370 379 """ If cursor is outside the editing region, put it back.
371 380 """
372 381 event.Skip()
373 382 if self.GetCurrentPos() < self.current_prompt_pos:
374 383 self.GotoPos(self.current_prompt_pos)
375 384
376 385
377 386
378 387
379 388 if __name__ == '__main__':
380 389 # Some simple code to test the console widget.
381 390 class MainWindow(wx.Frame):
382 391 def __init__(self, parent, id, title):
383 392 wx.Frame.__init__(self, parent, id, title, size=(300,250))
384 393 self._sizer = wx.BoxSizer(wx.VERTICAL)
385 394 self.console_widget = ConsoleWidget(self)
386 395 self._sizer.Add(self.console_widget, 1, wx.EXPAND)
387 396 self.SetSizer(self._sizer)
388 397 self.SetAutoLayout(1)
389 398 self.Show(True)
390 399
391 400 app = wx.PySimpleApp()
392 401 w = MainWindow(None, wx.ID_ANY, 'ConsoleWidget')
393 402 w.SetSize((780, 460))
394 403 w.Show()
395 404
396 405 app.MainLoop()
397 406
398 407
General Comments 0
You need to be logged in to leave comments. Login now