##// END OF EJS Templates
Cleaning up lignes longer than 80 characters.
Gael Varoquaux -
Show More
@@ -1,767 +1,706 b''
1 1 #!/usr/bin/python
2 2 # -*- coding: iso-8859-15 -*-
3 3 '''
4 4 Provides IPython WX console widgets.
5 5
6 6 @author: Laurent Dufrechou
7 7 laurent.dufrechou _at_ gmail.com
8 8 This WX widget is based on the original work of Eitan Isaacson
9 9 that provided the console for the GTK toolkit.
10 10
11 11 Original work from:
12 12 @author: Eitan Isaacson
13 13 @organization: IBM Corporation
14 14 @copyright: Copyright (c) 2007 IBM Corporation
15 15 @license: BSD
16 16
17 17 All rights reserved. This program and the accompanying materials are made
18 18 available under the terms of the BSD which accompanies this distribution, and
19 19 is available at U{http://www.opensource.org/licenses/bsd-license.php}
20 20 '''
21 21
22 22 __version__ = 0.8
23 23 __author__ = "Laurent Dufrechou"
24 24 __email__ = "laurent.dufrechou _at_ gmail.com"
25 25 __license__ = "BSD"
26 26
27 27 import wx
28 28 import wx.stc as stc
29 29 import wx.lib.newevent
30 30
31 31 import re
32 32 import sys
33 33 import os
34 34 import locale
35 35 import time
36 36 from StringIO import StringIO
37 37 try:
38 38 import IPython
39 39 except Exception,e:
40 40 raise "Error importing IPython (%s)" % str(e)
41 41
42 42
43 43 from non_blocking_ip_shell import NonBlockingIPShell
44 44
45 45 class WxNonBlockingIPShell(NonBlockingIPShell):
46 46 '''
47 47 An NonBlockingIPShell Thread that is WX dependent.
48 48 '''
49 49 def __init__(self, parent,
50 50 argv=[],user_ns={},user_global_ns=None,
51 51 cin=None, cout=None, cerr=None,
52 52 ask_exit_handler=None):
53 53
54 54 NonBlockingIPShell.__init__(self,argv,user_ns,user_global_ns,
55 55 cin, cout, cerr,
56 56 ask_exit_handler)
57 57
58 58 self.parent = parent
59 59
60 60 self.ask_exit_callback = ask_exit_handler
61 61 self._IP.ask_exit = self._askExit
62 62
63 63
64 64 def addGUIShortcut(self,text,func):
65 65 wx.CallAfter(self.parent.add_button_handler,
66 66 button_info={ 'text':text,
67 67 'func':self.parent.doExecuteLine(func)})
68 68
69 69 def _askExit(self):
70 70 wx.CallAfter(self.ask_exit_callback, ())
71 71
72 72 def _afterExecute(self):
73 73 wx.CallAfter(self.parent.evtStateExecuteDone, ())
74 74
75 75
76 76 class WxConsoleView(stc.StyledTextCtrl):
77 77 '''
78 78 Specialized styled text control view for console-like workflow.
79 79 We use here a scintilla frontend thus it can be reused in any GUI taht supports
80 80 scintilla with less work.
81 81
82 82 @cvar ANSI_COLORS_BLACK: Mapping of terminal colors to X11 names.(with Black background)
83 83 @type ANSI_COLORS_BLACK: dictionary
84 84
85 85 @cvar ANSI_COLORS_WHITE: Mapping of terminal colors to X11 names.(with White background)
86 86 @type ANSI_COLORS_WHITE: dictionary
87 87
88 88 @ivar color_pat: Regex of terminal color pattern
89 89 @type color_pat: _sre.SRE_Pattern
90 90 '''
91 91 ANSI_STYLES_BLACK ={'0;30': [0,'WHITE'], '0;31': [1,'RED'],
92 92 '0;32': [2,'GREEN'], '0;33': [3,'BROWN'],
93 93 '0;34': [4,'BLUE'], '0;35': [5,'PURPLE'],
94 94 '0;36': [6,'CYAN'], '0;37': [7,'LIGHT GREY'],
95 95 '1;30': [8,'DARK GREY'], '1;31': [9,'RED'],
96 96 '1;32': [10,'SEA GREEN'], '1;33': [11,'YELLOW'],
97 97 '1;34': [12,'LIGHT BLUE'], '1;35': [13,'MEDIUM VIOLET RED'],
98 98 '1;36': [14,'LIGHT STEEL BLUE'], '1;37': [15,'YELLOW']}
99 99
100 100 ANSI_STYLES_WHITE ={'0;30': [0,'BLACK'], '0;31': [1,'RED'],
101 101 '0;32': [2,'GREEN'], '0;33': [3,'BROWN'],
102 102 '0;34': [4,'BLUE'], '0;35': [5,'PURPLE'],
103 103 '0;36': [6,'CYAN'], '0;37': [7,'LIGHT GREY'],
104 104 '1;30': [8,'DARK GREY'], '1;31': [9,'RED'],
105 105 '1;32': [10,'SEA GREEN'], '1;33': [11,'YELLOW'],
106 106 '1;34': [12,'LIGHT BLUE'], '1;35': [13,'MEDIUM VIOLET RED'],
107 107 '1;36': [14,'LIGHT STEEL BLUE'], '1;37': [15,'YELLOW']}
108 108
109 def __init__(self,parent,prompt,intro="",background_color="BLACK",pos=wx.DefaultPosition, ID = -1, size=wx.DefaultSize,
109 def __init__(self,parent,prompt,intro="",background_color="BLACK",
110 pos=wx.DefaultPosition, ID = -1, size=wx.DefaultSize,
110 111 style=0):
111 112 '''
112 113 Initialize console view.
113 114
114 115 @param parent: Parent widget
115 116 @param prompt: User specified prompt
116 117 @type intro: string
117 118 @param intro: User specified startup introduction string
118 119 @type intro: string
119 120 @param background_color: Can be BLACK or WHITE
120 121 @type background_color: string
121 122 @param other: init param of styledTextControl (can be used as-is)
122 123 '''
123 124 stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style)
124 125
125 ####### Scintilla configuration ##################################################
126 ####### Scintilla configuration ###################################
126 127
127 # Ctrl + B or Ctrl + N can be used to zoomin/zoomout the text inside the widget
128 # Ctrl + B or Ctrl + N can be used to zoomin/zoomout the text inside
129 # the widget
128 130 self.CmdKeyAssign(ord('B'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
129 131 self.CmdKeyAssign(ord('N'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
130 132
131 133 #we define platform specific fonts
132 134 if wx.Platform == '__WXMSW__':
133 135 faces = { 'times': 'Times New Roman',
134 136 'mono' : 'Courier New',
135 137 'helv' : 'Arial',
136 138 'other': 'Comic Sans MS',
137 139 'size' : 10,
138 140 'size2': 8,
139 141 }
140 142 elif wx.Platform == '__WXMAC__':
141 143 faces = { 'times': 'Times New Roman',
142 144 'mono' : 'Monaco',
143 145 'helv' : 'Arial',
144 146 'other': 'Comic Sans MS',
145 147 'size' : 10,
146 148 'size2': 8,
147 149 }
148 150 else:
149 151 faces = { 'times': 'Times',
150 152 'mono' : 'Courier',
151 153 'helv' : 'Helvetica',
152 154 'other': 'new century schoolbook',
153 155 'size' : 10,
154 156 'size2': 8,
155 157 }
156 158
157 159 #We draw a line at position 80
158 160 self.SetEdgeMode(stc.STC_EDGE_LINE)
159 161 self.SetEdgeColumn(80)
160 162 self.SetEdgeColour(wx.LIGHT_GREY)
161 163
162 164 #self.SetViewWhiteSpace(True)
163 165 #self.SetViewEOL(True)
164 166 self.SetEOLMode(stc.STC_EOL_CRLF)
165 167 #self.SetWrapMode(stc.STC_WRAP_CHAR)
166 168 #self.SetWrapMode(stc.STC_WRAP_WORD)
167 169 self.SetBufferedDraw(True)
168 170 #self.SetUseAntiAliasing(True)
169 171 self.SetLayoutCache(stc.STC_CACHE_PAGE)
170 172
171 173 self.EnsureCaretVisible()
172 174
173 175 self.SetMargins(3,3) #text is moved away from border with 3px
174 176 # Suppressing Scintilla margins
175 177 self.SetMarginWidth(0,0)
176 178 self.SetMarginWidth(1,0)
177 179 self.SetMarginWidth(2,0)
178 180
179 181 # make some styles
180 182 if background_color != "BLACK":
181 183 self.background_color = "WHITE"
182 184 self.SetCaretForeground("BLACK")
183 185 self.ANSI_STYLES = self.ANSI_STYLES_WHITE
184 186 else:
185 187 self.background_color = background_color
186 188 self.SetCaretForeground("WHITE")
187 189 self.ANSI_STYLES = self.ANSI_STYLES_BLACK
188 190
189 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "fore:%s,back:%s,size:%d,face:%s" % (self.ANSI_STYLES['0;30'][1],
190 self.background_color,
191 faces['size'], faces['mono']))
191 self.StyleSetSpec(stc.STC_STYLE_DEFAULT,
192 "fore:%s,back:%s,size:%d,face:%s"
193 % (self.ANSI_STYLES['0;30'][1],
194 self.background_color,
195 faces['size'], faces['mono']))
192 196 self.StyleClearAll()
193 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, "fore:#FF0000,back:#0000FF,bold")
194 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, "fore:#000000,back:#FF0000,bold")
197 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,
198 "fore:#FF0000,back:#0000FF,bold")
199 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,
200 "fore:#000000,back:#FF0000,bold")
195 201
196 202 for style in self.ANSI_STYLES.values():
197 203 self.StyleSetSpec(style[0], "bold,fore:%s" % style[1])
198 204
199 205 #######################################################################
200 206
201 207 self.indent = 0
202 208 self.prompt_count = 0
203 209 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
204 210
205 211 self.write(intro)
206 212 self.setPrompt(prompt)
207 213 self.showPrompt()
208 214
209 215 self.Bind(wx.EVT_KEY_DOWN, self._onKeypress, self)
210 #self.Bind(stc.EVT_STC_UPDATEUI, self.OnUpdateUI)
211 216
212 217 def write(self, text):
213 218 '''
214 219 Write given text to buffer.
215 220
216 221 @param text: Text to append.
217 222 @type text: string
218 223 '''
219 224 segments = self.color_pat.split(text)
220 225 segment = segments.pop(0)
221 226 self.StartStyling(self.getCurrentLineEnd(),0xFF)
222 227 self.AppendText(segment)
223 228
224 229 if segments:
225 230 ansi_tags = self.color_pat.findall(text)
226 231
227 232 for tag in ansi_tags:
228 233 i = segments.index(tag)
229 234 self.StartStyling(self.getCurrentLineEnd(),0xFF)
230 235 self.AppendText(segments[i+1])
231 236
232 237 if tag != '0':
233 238 self.SetStyling(len(segments[i+1]),self.ANSI_STYLES[tag][0])
234 239
235 240 segments.pop(i)
236 241
237 242 self.moveCursor(self.getCurrentLineEnd())
238 243
239 244 def getPromptLen(self):
240 245 '''
241 246 Return the length of current prompt
242 247 '''
243 248 return len(str(self.prompt_count)) + 7
244 249
245 250 def setPrompt(self,prompt):
246 251 self.prompt = prompt
247 252
248 253 def setIndentation(self,indentation):
249 254 self.indent = indentation
250 255
251 256 def setPromptCount(self,count):
252 257 self.prompt_count = count
253 258
254 259 def showPrompt(self):
255 260 '''
256 261 Prints prompt at start of line.
257 262
258 263 @param prompt: Prompt to print.
259 264 @type prompt: string
260 265 '''
261 266 self.write(self.prompt)
262 267 #now we update the position of end of prompt
263 268 self.current_start = self.getCurrentLineEnd()
264 269
265 270 autoindent = self.indent*' '
266 271 autoindent = autoindent.replace(' ','\t')
267 272 self.write(autoindent)
268 273
269 274 def changeLine(self, text):
270 275 '''
271 276 Replace currently entered command line with given text.
272 277
273 278 @param text: Text to use as replacement.
274 279 @type text: string
275 280 '''
276 281 self.SetSelection(self.getCurrentPromptStart(),self.getCurrentLineEnd())
277 282 self.ReplaceSelection(text)
278 283 self.moveCursor(self.getCurrentLineEnd())
279 284
280 285 def getCurrentPromptStart(self):
281 286 return self.current_start
282 287
283 288 def getCurrentLineStart(self):
284 289 return self.GotoLine(self.LineFromPosition(self.GetCurrentPos()))
285 290
286 291 def getCurrentLineEnd(self):
287 292 return self.GetLength()
288 293
289 294 def getCurrentLine(self):
290 295 '''
291 296 Get text in current command line.
292 297
293 298 @return: Text of current command line.
294 299 @rtype: string
295 300 '''
296 301 return self.GetTextRange(self.getCurrentPromptStart(),
297 302 self.getCurrentLineEnd())
298 303
299 304 def showReturned(self, text):
300 305 '''
301 306 Show returned text from last command and print new prompt.
302 307
303 308 @param text: Text to show.
304 309 @type text: string
305 310 '''
306 311 self.write('\n'+text)
307 312 if text:
308 313 self.write('\n')
309 314 self.showPrompt()
310 315
311 316 def moveCursorOnNewValidKey(self):
312 317 #If cursor is at wrong position put it at last line...
313 318 if self.GetCurrentPos() < self.getCurrentPromptStart():
314 319 self.GotoPos(self.getCurrentPromptStart())
315 320
316 321 def removeFromTo(self,from_pos,to_pos):
317 322 if from_pos < to_pos:
318 323 self.SetSelection(from_pos,to_pos)
319 324 self.DeleteBack()
320 325
321 326 def removeCurrentLine(self):
322 327 self.LineDelete()
323 328
324 329 def moveCursor(self,position):
325 330 self.GotoPos(position)
326 331
327 332 def getCursorPos(self):
328 333 return self.GetCurrentPos()
329 334
330 335 def selectFromTo(self,from_pos,to_pos):
331 336 self.SetSelectionStart(from_pos)
332 337 self.SetSelectionEnd(to_pos)
333 338
334 339 def writeHistory(self,history):
335 340 self.removeFromTo(self.getCurrentPromptStart(),self.getCurrentLineEnd())
336 341 self.changeLine(history)
337 342
338 343 def writeCompletion(self, possibilities):
339 344 max_len = len(max(possibilities,key=len))
340 345 max_symbol =' '*max_len
341 346
342 347 #now we check how much symbol we can put on a line...
343 348 cursor_pos = self.getCursorPos()
344 349 test_buffer = max_symbol + ' '*4
345 350 current_lines = self.GetLineCount()
346 351
347 352 allowed_symbols = 80/len(test_buffer)
348 353 if allowed_symbols == 0:
349 354 allowed_symbols = 1
350 355
351 356 pos = 1
352 357 buf = ''
353 358 for symbol in possibilities:
354 359 #buf += symbol+'\n'#*spaces)
355 360 if pos<allowed_symbols:
356 361 spaces = max_len - len(symbol) + 4
357 362 buf += symbol+' '*spaces
358 363 pos += 1
359 364 else:
360 365 buf+=symbol+'\n'
361 366 pos = 1
362 367 self.write(buf)
363 368
364 369 def _onKeypress(self, event, skip=True):
365 370 '''
366 371 Key press callback used for correcting behavior for console-like
367 372 interfaces. For example 'home' should go to prompt, not to begining of
368 373 line.
369 374
370 375 @param widget: Widget that key press accored in.
371 376 @type widget: gtk.Widget
372 377 @param event: Event object
373 378 @type event: gtk.gdk.Event
374 379
375 380 @return: Return True if event as been catched.
376 381 @rtype: boolean
377 382 '''
378 383
379 384 if event.GetKeyCode() == wx.WXK_HOME:
380 385 if event.Modifiers == wx.MOD_NONE:
381 386 self.moveCursorOnNewValidKey()
382 387 self.moveCursor(self.getCurrentPromptStart())
383 388 return True
384 389 elif event.Modifiers == wx.MOD_SHIFT:
385 390 self.moveCursorOnNewValidKey()
386 391 self.selectFromTo(self.getCurrentPromptStart(),self.getCursorPos())
387 392 return True
388 393 else:
389 394 return False
390 395
391 396 elif event.GetKeyCode() == wx.WXK_LEFT:
392 397 if event.Modifiers == wx.MOD_NONE:
393 398 self.moveCursorOnNewValidKey()
394 399
395 400 self.moveCursor(self.getCursorPos()-1)
396 401 if self.getCursorPos() < self.getCurrentPromptStart():
397 402 self.moveCursor(self.getCurrentPromptStart())
398 403 return True
399 404
400 405 elif event.GetKeyCode() == wx.WXK_BACK:
401 406 self.moveCursorOnNewValidKey()
402 407 if self.getCursorPos() > self.getCurrentPromptStart():
403 408 self.removeFromTo(self.getCursorPos()-1,self.getCursorPos())
404 409 return True
405 410
406 411 if skip:
407 412 if event.GetKeyCode() not in [wx.WXK_PAGEUP,wx.WXK_PAGEDOWN] and event.Modifiers == wx.MOD_NONE:
408 413 self.moveCursorOnNewValidKey()
409 414
410 415 event.Skip()
411 416 return True
412 417 return False
413 418
414 419 def OnUpdateUI(self, evt):
415 420 # check for matching braces
416 421 braceAtCaret = -1
417 422 braceOpposite = -1
418 423 charBefore = None
419 424 caretPos = self.GetCurrentPos()
420 425
421 426 if caretPos > 0:
422 427 charBefore = self.GetCharAt(caretPos - 1)
423 428 styleBefore = self.GetStyleAt(caretPos - 1)
424 429
425 430 # check before
426 431 if charBefore and chr(charBefore) in "[]{}()" and styleBefore == stc.STC_P_OPERATOR:
427 432 braceAtCaret = caretPos - 1
428 433
429 434 # check after
430 435 if braceAtCaret < 0:
431 436 charAfter = self.GetCharAt(caretPos)
432 437 styleAfter = self.GetStyleAt(caretPos)
433 438
434 439 if charAfter and chr(charAfter) in "[]{}()" and styleAfter == stc.STC_P_OPERATOR:
435 440 braceAtCaret = caretPos
436 441
437 442 if braceAtCaret >= 0:
438 443 braceOpposite = self.BraceMatch(braceAtCaret)
439 444
440 445 if braceAtCaret != -1 and braceOpposite == -1:
441 446 self.BraceBadLight(braceAtCaret)
442 447 else:
443 448 self.BraceHighlight(braceAtCaret, braceOpposite)
444 449 #pt = self.PointFromPosition(braceOpposite)
445 450 #self.Refresh(True, wxRect(pt.x, pt.y, 5,5))
446 451 #print pt
447 452 #self.Refresh(False)
448 453
449 454 class WxIPythonViewPanel(wx.Panel):
450 455 '''
451 456 This is wx.Panel that embbed the IPython Thread and the wx.StyledTextControl
452 457 If you want to port this to any other GUI toolkit, just replace the WxConsoleView
453 458 by YOURGUIConsoleView and make YOURGUIIPythonView derivate from whatever container you want.
454 459 I've choosed to derivate from a wx.Panel because it seems to be ore usefull
455 460 Any idea to make it more 'genric' welcomed.
456 461 '''
457 462
458 463 def __init__(self, parent, ask_exit_handler=None, intro=None,
459 464 background_color="BLACK", add_button_handler=None,
460 465 wx_ip_shell=None,
461 466 ):
462 467 '''
463 468 Initialize.
464 469 Instanciate an IPython thread.
465 470 Instanciate a WxConsoleView.
466 471 Redirect I/O to console.
467 472 '''
468 473 wx.Panel.__init__(self,parent,-1)
469 474
470 475 ### IPython thread instanciation ###
471 476 self.cout = StringIO()
472 477
473 478 self.add_button_handler = add_button_handler
474 479 self.ask_exit_handler = ask_exit_handler
475 480
476 481 if wx_ip_shell is not None:
477 482 self.IP = wx_ip_shell
478 483 else:
479 484 self.IP = WxNonBlockingIPShell(self,
480 485 cout=self.cout,cerr=self.cout,
481 486 ask_exit_handler = ask_exit_handler)
482 487 ### IPython wx console view instanciation ###
483 488 #If user didn't defined an intro text, we create one for him
484 #If you really wnat an empty intrp just call wxIPythonViewPanel with intro=''
489 #If you really wnat an empty intrp just call wxIPythonViewPanel
490 #with intro=''
485 491 if intro == None:
486 492 welcome_text = "Welcome to WxIPython Shell.\n\n"
487 493 welcome_text+= self.IP.getBanner()
488 494 welcome_text+= "!command -> Execute command in shell\n"
489 495 welcome_text+= "TAB -> Autocompletion\n"
490 496
491 497 self.text_ctrl = WxConsoleView(self,
492 498 self.IP.getPrompt(),
493 499 intro=welcome_text,
494 500 background_color=background_color)
495 501
496 502 self.text_ctrl.Bind(wx.EVT_KEY_DOWN, self.keyPress, self.text_ctrl)
497 503
498 504 ### making the layout of the panel ###
499 505 sizer = wx.BoxSizer(wx.VERTICAL)
500 506 sizer.Add(self.text_ctrl, 1, wx.EXPAND)
501 507 self.SetAutoLayout(True)
502 508 sizer.Fit(self)
503 509 sizer.SetSizeHints(self)
504 510 self.SetSizer(sizer)
505 511 #and we focus on the widget :)
506 512 self.SetFocus()
507 513
508 ### below are the thread communication variable ###
509 # the IPython thread is managed via unidirectional communication.
510 # It's a thread slave that can't interact by itself with the GUI.
511 # When the GUI event loop is done runStateMachine() is called and the thread sate is then
512 # managed.
513
514 #Initialize the state machine #kept for information
515 #self.states = ['IDLE',
516 # 'DO_EXECUTE_LINE',
517 # 'WAIT_END_OF_EXECUTION',
518 # 'SHOW_DOC',
519 # 'SHOW_PROMPT']
520
521 514 self.cur_state = 'IDLE'
522 515 self.pager_state = 'DONE'
523 #wx.CallAfter(self.runStateMachine)
524 516
525 # This creates a new Event class and a EVT binder function
526 #(self.AskExitEvent, EVT_ASK_EXIT) = wx.lib.newevent.NewEvent()
527 #(self.AddButtonEvent, EVT_ADDBUTTON_EXIT) = wx.lib.newevent.NewEvent()
528
529
530 #self.Bind(wx.EVT_IDLE, self.runStateMachine)
531
532 517 def __del__(self):
533 518 WxConsoleView.__del__()
534 519
535 #---------------------------- IPython Thread Management ---------------------------------------
520 #---------------------- IPython Thread Management ------------------------
536 521 def stateDoExecuteLine(self):
537 522 #print >>sys.__stdout__,"command:",self.getCurrentLine()
538 523 self.doExecuteLine(self.text_ctrl.getCurrentLine())
539 524
540 525 def doExecuteLine(self,line):
541 526 #print >>sys.__stdout__,"command:",line
542 527 self.IP.doExecute(line.replace('\t',' '*4))
543 528 self.updateHistoryTracker(self.text_ctrl.getCurrentLine())
544 529 self.cur_state = 'WAIT_END_OF_EXECUTION'
545 530
546 531
547 532 def evtStateExecuteDone(self,evt):
548 533 self.doc = self.IP.getDocText()
549 534 self.help = self.IP.getHelpText()
550 535 if self.doc:
551 536 self.pager_state = 'INIT'
552 537 self.cur_state = 'SHOW_DOC'
553 538 self.pager(self.doc)
554 539 #if self.pager_state == 'DONE':
555 540 if self.help:
556 541 self.pager_state = 'INIT_HELP'
557 542 self.cur_state = 'SHOW_DOC'
558 543 self.pager(self.help)
559 544
560 545 else:
561 546 self.stateShowPrompt()
562 547
563 548 def stateShowPrompt(self):
564 549 self.cur_state = 'SHOW_PROMPT'
565 550 self.text_ctrl.setPrompt(self.IP.getPrompt())
566 551 self.text_ctrl.setIndentation(self.IP.getIndentation())
567 552 self.text_ctrl.setPromptCount(self.IP.getPromptCount())
568 553 rv = self.cout.getvalue()
569 554 if rv: rv = rv.strip('\n')
570 555 self.text_ctrl.showReturned(rv)
571 556 self.cout.truncate(0)
572 557 self.IP.initHistoryIndex()
573 558 self.cur_state = 'IDLE'
574
575 ## def runStateMachine(self,event):
576 ## #print >>sys.__stdout__,"state:",self.cur_state
577 ## self.updateStatusTracker(self.cur_state)
578 ##
579 ## #if self.cur_state == 'DO_EXECUTE_LINE':
580 ## # self.doExecuteLine()
581 ##
582 ## if self.cur_state == 'WAIT_END_OF_EXECUTION':
583 ## if self.IP.isExecuteDone():
584 ## #self.button = self.IP.getAddButton()
585 ## #if self.IP.getAskExit():
586 ## # evt = self.AskExitEvent()
587 ## # wx.PostEvent(self, evt)
588 ## # self.IP.clearAskExit()
589 ## self.doc = self.IP.getDocText()
590 ## if self.doc:
591 ## self.pager_state = 'INIT'
592 ## self.cur_state = 'SHOW_DOC'
593 ## #if self.button:
594 ## #self.IP.doExecute('print "cool"')#self.button['func'])
595 ## #self.updateHistoryTracker(self.text_ctrl.getCurrentLine())
596 ##
597 ## # self.button['func']='print "cool!"'
598 ## # self.add_button_handler(self.button)
599 ## # self.IP.shortcutProcessed()
600 ##
601 ## else:
602 ## self.cur_state = 'SHOW_PROMPT'
603 ##
604 ## if self.cur_state == 'SHOW_PROMPT':
605 ## self.text_ctrl.setPrompt(self.IP.getPrompt())
606 ## self.text_ctrl.setIndentation(self.IP.getIndentation())
607 ## self.text_ctrl.setPromptCount(self.IP.getPromptCount())
608 ## rv = self.cout.getvalue()
609 ## if rv: rv = rv.strip('\n')
610 ## self.text_ctrl.showReturned(rv)
611 ## self.cout.truncate(0)
612 ## self.IP.initHistoryIndex()
613 ## self.cur_state = 'IDLE'
614 ##
615 ## if self.cur_state == 'SHOW_DOC':
616 ## self.pager(self.doc)
617 ## if self.pager_state == 'DONE':
618 ## self.cur_state = 'SHOW_PROMPT'
619 ##
620 ## event.Skip()
621
622 #---------------------------- IPython pager ---------------------------------------
559
560 #------------------------ IPython pager ----------------------------------
623 561 def pager(self,text):#,start=0,screen_lines=0,pager_cmd = None):
624 562 if self.pager_state == 'WAITING':
625 563 #print >>sys.__stdout__,"PAGER waiting"
626 564 return
627 565
628 566 if self.pager_state == 'INIT':
629 567 #print >>sys.__stdout__,"PAGER state:",self.pager_state
630 568 self.pager_lines = text[7:].split('\n')
631 569 self.pager_nb_lines = len(self.pager_lines)
632 570 self.pager_index = 0
633 571 self.pager_do_remove = False
634 572 self.text_ctrl.write('\n')
635 573 self.pager_state = 'PROCESS_LINES'
636 574
637 575 if self.pager_state == 'INIT_HELP':
638 576 #print >>sys.__stdout__,"HELP PAGER state:",self.pager_state
639 577 self.pager_lines = text[:].split('\n')
640 578 self.pager_nb_lines = len(self.pager_lines)
641 579 self.pager_index = 0
642 580 self.pager_do_remove = False
643 581 self.text_ctrl.write('\n')
644 582 self.pager_state = 'PROCESS_LINES'
645 583
646 584 if self.pager_state == 'PROCESS_LINES':
647 585 #print >>sys.__stdout__,"PAGER state:",self.pager_state
648 586 if self.pager_do_remove == True:
649 587 self.text_ctrl.removeCurrentLine()
650 588 self.pager_do_remove = False
651 589
652 590 if self.pager_nb_lines > 10:
653 591 #print >>sys.__stdout__,"PAGER processing 10 lines"
654 592 if self.pager_index > 0:
655 593 self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n')
656 594 else:
657 595 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n')
658 596
659 597 for line in self.pager_lines[self.pager_index+1:self.pager_index+9]:
660 598 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n')
661 599 self.pager_index += 10
662 600 self.pager_nb_lines -= 10
663 601 self.text_ctrl.write("--- Push Enter to continue or 'Q' to quit---")
664 602 self.pager_do_remove = True
665 603 self.pager_state = 'WAITING'
666 604 return
667 605 else:
668 606 #print >>sys.__stdout__,"PAGER processing last lines"
669 607 if self.pager_nb_lines > 0:
670 608 if self.pager_index > 0:
671 609 self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n')
672 610 else:
673 611 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n')
674 612
675 613 self.pager_index += 1
676 614 self.pager_nb_lines -= 1
677 615 if self.pager_nb_lines > 0:
678 616 for line in self.pager_lines[self.pager_index:]:
679 617 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n')
680 618 self.pager_nb_lines = 0
681 619 self.pager_state = 'DONE'
682 620 self.stateShowPrompt()
683 621
684 #---------------------------- Key Handler --------------------------------------------
622 #------------------------ Key Handler ------------------------------------
685 623 def keyPress(self, event):
686 624 '''
687 625 Key press callback with plenty of shell goodness, like history,
688 626 autocompletions, etc.
689 627 '''
690 628
691 629 if event.GetKeyCode() == ord('C'):
692 630 if event.Modifiers == wx.MOD_CONTROL:
693 631 if self.cur_state == 'WAIT_END_OF_EXECUTION':
694 632 #we raise an exception inside the IPython thread container
695 633 self.IP.ce.raise_exc(KeyboardInterrupt)
696 634 return
697 635
698 636 if event.KeyCode == wx.WXK_RETURN:
699 637 if self.cur_state == 'IDLE':
700 638 #we change the state ot the state machine
701 639 self.cur_state = 'DO_EXECUTE_LINE'
702 640 self.stateDoExecuteLine()
703 641 return
704 642 if self.pager_state == 'WAITING':
705 643 self.pager_state = 'PROCESS_LINES'
706 644 self.pager(self.doc)
707 645 return
708 646
709 647 if event.GetKeyCode() in [ord('q'),ord('Q')]:
710 648 if self.pager_state == 'WAITING':
711 649 self.pager_state = 'DONE'
712 650 self.stateShowPrompt()
713 651 return
714 652
715 653 #scroll_position = self.text_ctrl.GetScrollPos(wx.VERTICAL)
716 654 if self.cur_state == 'IDLE':
717 655 if event.KeyCode == wx.WXK_UP:
718 656 history = self.IP.historyBack()
719 657 self.text_ctrl.writeHistory(history)
720 658 return
721 659 if event.KeyCode == wx.WXK_DOWN:
722 660 history = self.IP.historyForward()
723 661 self.text_ctrl.writeHistory(history)
724 662 return
725 663 if event.KeyCode == wx.WXK_TAB:
726 664 #if line empty we disable tab completion
727 665 if not self.text_ctrl.getCurrentLine().strip():
728 666 self.text_ctrl.write('\t')
729 667 return
730 668 completed, possibilities = self.IP.complete(self.text_ctrl.getCurrentLine())
731 669 if len(possibilities) > 1:
732 670 cur_slice = self.text_ctrl.getCurrentLine()
733 671 self.text_ctrl.write('\n')
734 672 self.text_ctrl.writeCompletion(possibilities)
735 673 self.text_ctrl.write('\n')
736 674
737 675 self.text_ctrl.showPrompt()
738 676 self.text_ctrl.write(cur_slice)
739 677 self.text_ctrl.changeLine(completed or cur_slice)
740 678
741 679 return
742 680 event.Skip()
743 681
744 #---------------------------- Hook Section --------------------------------------------
682 #------------------------ Hook Section -----------------------------------
745 683 def updateHistoryTracker(self,command_line):
746 684 '''
747 685 Default history tracker (does nothing)
748 686 '''
749 687 pass
750 688
751 689 def setHistoryTrackerHook(self,func):
752 690 '''
753 691 Define a new history tracker
754 692 '''
755 693 self.updateHistoryTracker = func
694
756 695 def updateStatusTracker(self,status):
757 696 '''
758 697 Default status tracker (does nothing)
759 698 '''
760 699 pass
761 700
762 701 def setStatusTrackerHook(self,func):
763 702 '''
764 703 Define a new status tracker
765 704 '''
766 705 self.updateStatusTracker = func
767 706
@@ -1,472 +1,471 b''
1 1 #!/usr/bin/python
2 2 # -*- coding: iso-8859-15 -*-
3 3 '''
4 4 Provides IPython remote instance.
5 5
6 6 @author: Laurent Dufrechou
7 7 laurent.dufrechou _at_ gmail.com
8 8 @license: BSD
9 9
10 10 All rights reserved. This program and the accompanying materials are made
11 11 available under the terms of the BSD which accompanies this distribution, and
12 12 is available at U{http://www.opensource.org/licenses/bsd-license.php}
13 13 '''
14 14
15 15 __version__ = 0.9
16 16 __author__ = "Laurent Dufrechou"
17 17 __email__ = "laurent.dufrechou _at_ gmail.com"
18 18 __license__ = "BSD"
19 19
20 20 import re
21 21 import sys
22 22 import os
23 23 import locale
24 24 import time
25 25 import pydoc,__builtin__,site
26 26 from thread_ex import ThreadEx
27 27 from StringIO import StringIO
28 28
29 29 try:
30 30 import IPython
31 31 except Exception,e:
32 32 raise "Error importing IPython (%s)" % str(e)
33 33
34 34 ##############################################################################
35 35 class _Helper(object):
36 36 """Redefine the built-in 'help'.
37 37 This is a wrapper around pydoc.help (with a twist).
38 38 """
39 39
40 40 def __init__(self,pager):
41 41 self._pager = pager
42 42
43 43 def __repr__(self):
44 44 return "Type help() for interactive help, " \
45 45 "or help(object) for help about object."
46 46
47 47 def __call__(self, *args, **kwds):
48 48 class DummyWriter(object):
49 49 def __init__(self,pager):
50 50 self._pager = pager
51 51
52 52 def write(self,data):
53 53 self._pager(data)
54 54
55 55 import pydoc
56 56 pydoc.help.output = DummyWriter(self._pager)
57 57 pydoc.help.interact = lambda :1
58 58
59 59 return pydoc.help(*args, **kwds)
60 60
61 61
62 62 ##############################################################################
63 63 class _CodeExecutor(ThreadEx):
64 64
65 65 def __init__(self, instance, after):
66 66 ThreadEx.__init__(self)
67 67 self.instance = instance
68 68 self._afterExecute=after
69 69
70 70 def run(self):
71 71 try:
72 72 self.instance._doc_text = None
73 73 self.instance._help_text = None
74 74 self.instance._execute()
75 75 # used for uper class to generate event after execution
76 76 self._afterExecute()
77 77
78 78 except KeyboardInterrupt:
79 79 pass
80 80
81 81
82 82 ##############################################################################
83 83 class NonBlockingIPShell(object):
84 84 '''
85 85 Create an IPython instance, running the commands in a separate,
86 86 non-blocking thread.
87 87 This allows embedding in any GUI without blockage.
88 88
89 89 Note: The ThreadEx class supports asynchroneous function call
90 90 via raise_exc()
91 91 '''
92 92
93 def __init__(self,argv
94 =[],user_ns={},user_global_ns=None,
93 def __init__(self,argv=[],user_ns={},user_global_ns=None,
95 94 cin=None, cout=None, cerr=None,
96 95 ask_exit_handler=None):
97 96 '''
98 97 @param argv: Command line options for IPython
99 98 @type argv: list
100 99 @param user_ns: User namespace.
101 100 @type user_ns: dictionary
102 101 @param user_global_ns: User global namespace.
103 102 @type user_global_ns: dictionary.
104 103 @param cin: Console standard input.
105 104 @type cin: IO stream
106 105 @param cout: Console standard output.
107 106 @type cout: IO stream
108 107 @param cerr: Console standard error.
109 108 @type cerr: IO stream
110 109 @param exit_handler: Replacement for builtin exit() function
111 110 @type exit_handler: function
112 111 @param time_loop: Define the sleep time between two thread's loop
113 112 @type int
114 113 '''
115 114 #first we redefine in/out/error functions of IPython
116 115 if cin:
117 116 IPython.Shell.Term.cin = cin
118 117 if cout:
119 118 IPython.Shell.Term.cout = cout
120 119 if cerr:
121 120 IPython.Shell.Term.cerr = cerr
122 121
123 122 # This is to get rid of the blockage that accurs during
124 123 # IPython.Shell.InteractiveShell.user_setup()
125 124 IPython.iplib.raw_input = lambda x: None
126 125
127 126 self._term = IPython.genutils.IOTerm(cin=cin, cout=cout, cerr=cerr)
128 127
129 128 excepthook = sys.excepthook
130 129
131 130 self._IP = IPython.Shell.make_IPython(
132 131 argv,user_ns=user_ns,
133 132 user_global_ns=user_global_ns,
134 133 embedded=True,
135 134 shell_class=IPython.Shell.InteractiveShell)
136 135
137 136 #we replace IPython default encoding by wx locale encoding
138 137 loc = locale.getpreferredencoding()
139 138 if loc:
140 139 self._IP.stdin_encoding = loc
141 140 #we replace the ipython default pager by our pager
142 141 self._IP.set_hook('show_in_pager',self._pager)
143 142
144 143 #we replace the ipython default shell command caller by our shell handler
145 144 self._IP.set_hook('shell_hook',self._shell)
146 145
147 146 #we replace the ipython default input command caller by our method
148 147 IPython.iplib.raw_input_original = self._raw_input
149 148 #we replace the ipython default exit command by our method
150 149 self._IP.exit = self._setAskExit
151 150 #we modify Exit and Quit Magic
152 151 ip = IPython.ipapi.get()
153 152 ip.expose_magic('Exit', self._setDoExit)
154 153 ip.expose_magic('Quit', self._setDoExit)
155 154 #we replace the help command
156 155 self._IP.user_ns['help'] = _Helper(self._pager_help)
157 156
158 157 sys.excepthook = excepthook
159 158
160 159 #vars used by _execute
161 160 self._iter_more = 0
162 161 self._history_level = 0
163 162 self._complete_sep = re.compile('[\s\{\}\[\]\(\)]')
164 163 self._prompt = str(self._IP.outputcache.prompt1).strip()
165 164
166 165 #thread working vars
167 166 self._line_to_execute = ''
168 167
169 168 #vars that will be checked by GUI loop to handle thread states...
170 169 #will be replaced later by PostEvent GUI funtions...
171 170 self._doc_text = None
172 171 self._help_text = None
173 172 self._ask_exit = False
174 173 self._add_button = None
175 174
176 175 #----------------------- Thread management section ----------------------
177 176 def doExecute(self,line):
178 177 """
179 178 Tell the thread to process the 'line' command
180 179 """
181 180
182 181 self._line_to_execute = line
183 182
184 183 self.ce = _CodeExecutor(self,self._afterExecute)
185 184 self.ce.start()
186 185
187 186 #----------------------- IPython management section ----------------------
188 187 def getAskExit(self):
189 188 '''
190 189 returns the _ask_exit variable that can be checked by GUI to see if
191 190 IPython request an exit handling
192 191 '''
193 192 return self._ask_exit
194 193
195 194 def clearAskExit(self):
196 195 '''
197 196 clear the _ask_exit var when GUI as handled the request.
198 197 '''
199 198 self._ask_exit = False
200 199
201 200 def getDocText(self):
202 201 """
203 202 Returns the output of the processing that need to be paged (if any)
204 203
205 204 @return: The std output string.
206 205 @rtype: string
207 206 """
208 207 return self._doc_text
209 208
210 209 def getHelpText(self):
211 210 """
212 211 Returns the output of the processing that need to be paged via help pager(if any)
213 212
214 213 @return: The std output string.
215 214 @rtype: string
216 215 """
217 216 return self._help_text
218 217
219 218 def getBanner(self):
220 219 """
221 220 Returns the IPython banner for useful info on IPython instance
222 221
223 222 @return: The banner string.
224 223 @rtype: string
225 224 """
226 225 return self._IP.BANNER
227 226
228 227 def getPromptCount(self):
229 228 """
230 229 Returns the prompt number.
231 230 Each time a user execute a line in the IPython shell the prompt count is increased
232 231
233 232 @return: The prompt number
234 233 @rtype: int
235 234 """
236 235 return self._IP.outputcache.prompt_count
237 236
238 237 def getPrompt(self):
239 238 """
240 239 Returns current prompt inside IPython instance
241 240 (Can be In [...]: ot ...:)
242 241
243 242 @return: The current prompt.
244 243 @rtype: string
245 244 """
246 245 return self._prompt
247 246
248 247 def getIndentation(self):
249 248 """
250 249 Returns the current indentation level
251 250 Usefull to put the caret at the good start position if we want to do autoindentation.
252 251
253 252 @return: The indentation level.
254 253 @rtype: int
255 254 """
256 255 return self._IP.indent_current_nsp
257 256
258 257 def updateNamespace(self, ns_dict):
259 258 '''
260 259 Add the current dictionary to the shell namespace.
261 260
262 261 @param ns_dict: A dictionary of symbol-values.
263 262 @type ns_dict: dictionary
264 263 '''
265 264 self._IP.user_ns.update(ns_dict)
266 265
267 266 def complete(self, line):
268 267 '''
269 268 Returns an auto completed line and/or posibilities for completion.
270 269
271 270 @param line: Given line so far.
272 271 @type line: string
273 272
274 273 @return: Line completed as for as possible,
275 274 and possible further completions.
276 275 @rtype: tuple
277 276 '''
278 277 split_line = self._complete_sep.split(line)
279 278 possibilities = self._IP.complete(split_line[-1])
280 279 if possibilities:
281 280
282 281 def _commonPrefix(str1, str2):
283 282 '''
284 283 Reduction function. returns common prefix of two given strings.
285 284
286 285 @param str1: First string.
287 286 @type str1: string
288 287 @param str2: Second string
289 288 @type str2: string
290 289
291 290 @return: Common prefix to both strings.
292 291 @rtype: string
293 292 '''
294 293 for i in range(len(str1)):
295 294 if not str2.startswith(str1[:i+1]):
296 295 return str1[:i]
297 296 return str1
298 297 common_prefix = reduce(_commonPrefix, possibilities)
299 298 completed = line[:-len(split_line[-1])]+common_prefix
300 299 else:
301 300 completed = line
302 301 return completed, possibilities
303 302
304 303 def historyBack(self):
305 304 '''
306 305 Provides one history command back.
307 306
308 307 @return: The command string.
309 308 @rtype: string
310 309 '''
311 310 history = ''
312 311 #the below while loop is used to suppress empty history lines
313 312 while((history == '' or history == '\n') and self._history_level >0):
314 313 if self._history_level>=1:
315 314 self._history_level -= 1
316 315 history = self._getHistory()
317 316 return history
318 317
319 318 def historyForward(self):
320 319 '''
321 320 Provides one history command forward.
322 321
323 322 @return: The command string.
324 323 @rtype: string
325 324 '''
326 325 history = ''
327 326 #the below while loop is used to suppress empty history lines
328 327 while((history == '' or history == '\n') and self._history_level <= self._getHistoryMaxIndex()):
329 328 if self._history_level < self._getHistoryMaxIndex():
330 329 self._history_level += 1
331 330 history = self._getHistory()
332 331 else:
333 332 if self._history_level == self._getHistoryMaxIndex():
334 333 history = self._getHistory()
335 334 self._history_level += 1
336 335 else:
337 336 history = ''
338 337 return history
339 338
340 339 def initHistoryIndex(self):
341 340 '''
342 341 set history to last command entered
343 342 '''
344 343 self._history_level = self._getHistoryMaxIndex()+1
345 344
346 345 #----------------------- IPython PRIVATE management section --------------
347 346 def _afterExecute(self):
348 347 '''
349 348 Can be redefined to generate post event after excution is done
350 349 '''
351 350 pass
352 351
353 352 def _setAskExit(self):
354 353 '''
355 354 set the _ask_exit variable that can be checked by GUI to see if
356 355 IPython request an exit handling
357 356 '''
358 357 self._ask_exit = True
359 358
360 359 def _setDoExit(self, toto, arg):
361 360 '''
362 361 set the _do_exit variable that can be checked by GUI to see if
363 362 IPython do a direct exit of the app
364 363 '''
365 364 self._do_exit = True
366 365
367 366 def _getHistoryMaxIndex(self):
368 367 '''
369 368 returns the max length of the history buffer
370 369
371 370 @return: history length
372 371 @rtype: int
373 372 '''
374 373 return len(self._IP.input_hist_raw)-1
375 374
376 375 def _getHistory(self):
377 376 '''
378 377 Get's the command string of the current history level.
379 378
380 379 @return: Historic command stri
381 380 @rtype: string
382 381 '''
383 382 rv = self._IP.input_hist_raw[self._history_level].strip('\n')
384 383 return rv
385 384
386 385 def _pager_help(self,text):
387 386 '''
388 387 This function is used as a callback replacment to IPython help pager function
389 388
390 389 It puts the 'text' value inside the self._help_text string that can be retrived via getHelpText
391 390 function.
392 391 '''
393 392 if self._help_text == None:
394 393 self._help_text = text
395 394 else:
396 395 self._help_text += text
397 396
398 397 def _pager(self,IP,text):
399 398 '''
400 399 This function is used as a callback replacment to IPython pager function
401 400
402 401 It puts the 'text' value inside the self._doc_text string that can be retrived via getDocText
403 402 function.
404 403 '''
405 404 self._doc_text = text
406 405
407 406 def _raw_input(self, prompt=''):
408 407 '''
409 408 Custom raw_input() replacement. Get's current line from console buffer.
410 409
411 410 @param prompt: Prompt to print. Here for compatability as replacement.
412 411 @type prompt: string
413 412
414 413 @return: The current command line text.
415 414 @rtype: string
416 415 '''
417 416 return self._line_to_execute
418 417
419 418 def _execute(self):
420 419 '''
421 420 Executes the current line provided by the shell object.
422 421 '''
423 422 orig_stdout = sys.stdout
424 423 sys.stdout = IPython.Shell.Term.cout
425 424
426 425 try:
427 426 line = self._IP.raw_input(None, self._iter_more)
428 427 if self._IP.autoindent:
429 428 self._IP.readline_startup_hook(None)
430 429
431 430 except KeyboardInterrupt:
432 431 self._IP.write('\nKeyboardInterrupt\n')
433 432 self._IP.resetbuffer()
434 433 # keep cache in sync with the prompt counter:
435 434 self._IP.outputcache.prompt_count -= 1
436 435
437 436 if self._IP.autoindent:
438 437 self._IP.indent_current_nsp = 0
439 438 self._iter_more = 0
440 439 except:
441 440 self._IP.showtraceback()
442 441 else:
443 442 self._iter_more = self._IP.push(line)
444 443 if (self._IP.SyntaxTB.last_syntax_error and
445 444 self._IP.rc.autoedit_syntax):
446 445 self._IP.edit_syntax_error()
447 446 if self._iter_more:
448 447 self._prompt = str(self._IP.outputcache.prompt2).strip()
449 448 if self._IP.autoindent:
450 449 self._IP.readline_startup_hook(self._IP.pre_readline)
451 450 else:
452 451 self._prompt = str(self._IP.outputcache.prompt1).strip()
453 452 self._IP.indent_current_nsp = 0 #we set indentation to 0
454 453 sys.stdout = orig_stdout
455 454
456 455 def _shell(self, ip, cmd):
457 456 '''
458 457 Replacement method to allow shell commands without them blocking.
459 458
460 459 @param ip: Ipython instance, same as self._IP
461 460 @type cmd: Ipython instance
462 461 @param cmd: Shell command to execute.
463 462 @type cmd: string
464 463 '''
465 464 stdin, stdout = os.popen4(cmd)
466 465 result = stdout.read().decode('cp437').encode(locale.getpreferredencoding())
467 466 #we use print command because the shell command is called inside IPython instance and thus is
468 467 #redirected to thread cout
469 468 #"\x01\x1b[1;36m\x02" <-- add colour to the text...
470 469 print "\x01\x1b[1;36m\x02"+result
471 470 stdout.close()
472 471 stdin.close()
General Comments 0
You need to be logged in to leave comments. Login now