##// END OF EJS Templates
'magic' command handling
laurent.dufrechou -
Show More
@@ -1,1057 +1,1058 b''
1 1 #!/usr/bin/python
2 2 # -*- coding: iso-8859-15 -*-
3 3 '''
4 4 Provides IPython WX console widget.
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 ThreadEx import Thread
37 37 from StringIO import StringIO
38 38
39 39 try:
40 40 import IPython
41 41 except Exception,e:
42 42 raise "Error importing IPython (%s)" % str(e)
43 43
44 44 class IterableIPShell(Thread):
45 45 '''
46 46 Create an IPython instance inside a dedicated thread.
47 47 Does not start a blocking event loop, instead allow single iterations.
48 48 This allows embedding in any GUI without blockage.
49 49 The thread is a slave one, in that it doesn't interact directly with the GUI.
50 50 Note Thread class comes from ThreadEx that supports asynchroneous function call
51 51 via raise_exc()
52 52 '''
53 53
54 54 def __init__(self,argv=[],user_ns=None,user_global_ns=None,
55 55 cin=None, cout=None, cerr=None,
56 56 exit_handler=None,time_loop = 0.1):
57 57 '''
58 58 @param argv: Command line options for IPython
59 59 @type argv: list
60 60 @param user_ns: User namespace.
61 61 @type user_ns: dictionary
62 62 @param user_global_ns: User global namespace.
63 63 @type user_global_ns: dictionary.
64 64 @param cin: Console standard input.
65 65 @type cin: IO stream
66 66 @param cout: Console standard output.
67 67 @type cout: IO stream
68 68 @param cerr: Console standard error.
69 69 @type cerr: IO stream
70 70 @param exit_handler: Replacement for builtin exit() function
71 71 @type exit_handler: function
72 72 @param time_loop: Define the sleep time between two thread's loop
73 73 @type int
74 74 '''
75 75 Thread.__init__(self)
76 76
77 77 #first we redefine in/out/error functions of IPython
78 78 if cin:
79 79 IPython.Shell.Term.cin = cin
80 80 if cout:
81 81 IPython.Shell.Term.cout = cout
82 82 if cerr:
83 83 IPython.Shell.Term.cerr = cerr
84 84
85 85 # This is to get rid of the blockage that accurs during
86 86 # IPython.Shell.InteractiveShell.user_setup()
87 87 IPython.iplib.raw_input = lambda x: None
88 88
89 89 self._term = IPython.genutils.IOTerm(cin=cin, cout=cout, cerr=cerr)
90 90
91 91 excepthook = sys.excepthook
92 92 self._IP = IPython.Shell.make_IPython(
93 93 argv,user_ns=user_ns,
94 94 user_global_ns=user_global_ns,
95 95 embedded=True,
96 96 shell_class=IPython.Shell.InteractiveShell)
97 97
98 98 #we replace IPython default encoding by wx locale encoding
99 99 loc = locale.getpreferredencoding()
100 100 if loc:
101 101 self._IP.stdin_encoding = loc
102 102 #we replace the ipython default pager by our pager
103 103 #FIXME: add a pager callback to IPython
104 104 IPython.OInspect.page = self._pager
105 105 IPython.genutils.page = self._pager
106 106 IPython.iplib.page = self._pager
107 IPython.Magic.page = self._pager
107 108
108 109 #we replace the ipython default shell command caller by our shell handler
109 110 #FIXME: any better solution welcome
110 111 IPython.genutils.shell_ori = self._shell #needed by windows
111 112 self._IP.system = self._shell #needed for linux
112 113 #we replace the ipython default input command caller by our method
113 114 IPython.iplib.raw_input_original = self._raw_input
114 115 #we replace the ipython default exit command by our method
115 116 self._IP.exit = self._setAskExit
116 117
117 118 sys.excepthook = excepthook
118 119
119 120 self._iter_more = 0
120 121 self._history_level = 0
121 122 self._complete_sep = re.compile('[\s\{\}\[\]\(\)]')
122 123 self._prompt = str(self._IP.outputcache.prompt1).strip()
123 124
124 125 #thread working vars
125 126 self._terminate = False
126 127 self._time_loop = time_loop
127 128 self._has_doc = False
128 129 self._do_execute = False
129 130 self._line_to_execute = ''
130 131 self._doc_text = None
131 132 self._ask_exit = False
132 133
133 134 #----------------------- Thread management section ----------------------
134 135 def run (self):
135 136 """
136 137 Thread main loop
137 138 The thread will run until self._terminate will be set to True via shutdown() function
138 139 Command processing can be interrupted with Instance.raise_exc(KeyboardInterrupt) call in the
139 140 GUI thread.
140 141 """
141 142 while(not self._terminate):
142 143 try:
143 144 if self._do_execute:
144 145 self._doc_text = None
145 146 self._execute()
146 147 self._do_execute = False
147 148
148 149 except KeyboardInterrupt:
149 150 pass
150 151
151 152 time.sleep(self._time_loop)
152 153
153 154 def shutdown(self):
154 155 """
155 156 Shutdown the tread
156 157 """
157 158 self._terminate = True
158 159
159 160 def doExecute(self,line):
160 161 """
161 162 Tell the thread to process the 'line' command
162 163 """
163 164 self._do_execute = True
164 165 self._line_to_execute = line
165 166
166 167 def isExecuteDone(self):
167 168 """
168 169 Returns the processing state
169 170 """
170 171 return not self._do_execute
171 172
172 173 #----------------------- IPython management section ----------------------
173 174 def getAskExit(self):
174 175 '''
175 176 returns the _ask_exit variable that can be checked by GUI to see if
176 177 IPython request an exit handling
177 178 '''
178 179 return self._ask_exit
179 180
180 181 def clearAskExit(self):
181 182 '''
182 183 clear the _ask_exit var when GUI as handled the request.
183 184 '''
184 185 self._ask_exit = False
185 186
186 187 def getDocText(self):
187 188 """
188 189 Returns the output of the processing that need to be paged (if any)
189 190
190 191 @return: The std output string.
191 192 @rtype: string
192 193 """
193 194 return self._doc_text
194 195
195 196 def getBanner(self):
196 197 """
197 198 Returns the IPython banner for useful info on IPython instance
198 199
199 200 @return: The banner string.
200 201 @rtype: string
201 202 """
202 203 return self._IP.BANNER
203 204
204 205 def getPromptCount(self):
205 206 """
206 207 Returns the prompt number.
207 208 Each time a user execute a line in the IPython shell the prompt count is increased
208 209
209 210 @return: The prompt number
210 211 @rtype: int
211 212 """
212 213 return self._IP.outputcache.prompt_count
213 214
214 215 def getPrompt(self):
215 216 """
216 217 Returns current prompt inside IPython instance
217 218 (Can be In [...]: ot ...:)
218 219
219 220 @return: The current prompt.
220 221 @rtype: string
221 222 """
222 223 return self._prompt
223 224
224 225 def getIndentation(self):
225 226 """
226 227 Returns the current indentation level
227 228 Usefull to put the caret at the good start position if we want to do autoindentation.
228 229
229 230 @return: The indentation level.
230 231 @rtype: int
231 232 """
232 233 return self._IP.indent_current_nsp
233 234
234 235 def updateNamespace(self, ns_dict):
235 236 '''
236 237 Add the current dictionary to the shell namespace.
237 238
238 239 @param ns_dict: A dictionary of symbol-values.
239 240 @type ns_dict: dictionary
240 241 '''
241 242 self._IP.user_ns.update(ns_dict)
242 243
243 244 def complete(self, line):
244 245 '''
245 246 Returns an auto completed line and/or posibilities for completion.
246 247
247 248 @param line: Given line so far.
248 249 @type line: string
249 250
250 251 @return: Line completed as for as possible,
251 252 and possible further completions.
252 253 @rtype: tuple
253 254 '''
254 255 split_line = self._complete_sep.split(line)
255 256 possibilities = self._IP.complete(split_line[-1])
256 257 if possibilities:
257 258
258 259 def _commonPrefix(str1, str2):
259 260 '''
260 261 Reduction function. returns common prefix of two given strings.
261 262
262 263 @param str1: First string.
263 264 @type str1: string
264 265 @param str2: Second string
265 266 @type str2: string
266 267
267 268 @return: Common prefix to both strings.
268 269 @rtype: string
269 270 '''
270 271 for i in range(len(str1)):
271 272 if not str2.startswith(str1[:i+1]):
272 273 return str1[:i]
273 274 return str1
274 275 common_prefix = reduce(_commonPrefix, possibilities)
275 276 completed = line[:-len(split_line[-1])]+common_prefix
276 277 else:
277 278 completed = line
278 279 return completed, possibilities
279 280
280 281 def historyBack(self):
281 282 '''
282 283 Provides one history command back.
283 284
284 285 @return: The command string.
285 286 @rtype: string
286 287 '''
287 288 history = ''
288 289 #the below while loop is used to suppress empty history lines
289 290 while((history == '' or history == '\n') and self._history_level >0):
290 291 if self._history_level>=1:
291 292 self._history_level -= 1
292 293 history = self._getHistory()
293 294 return history
294 295
295 296 def historyForward(self):
296 297 '''
297 298 Provides one history command forward.
298 299
299 300 @return: The command string.
300 301 @rtype: string
301 302 '''
302 303 history = ''
303 304 #the below while loop is used to suppress empty history lines
304 305 while((history == '' or history == '\n') and self._history_level <= self._getHistoryMaxIndex()):
305 306 if self._history_level < self._getHistoryMaxIndex():
306 307 self._history_level += 1
307 308 history = self._getHistory()
308 309 else:
309 310 if self._history_level == self._getHistoryMaxIndex():
310 311 history = self._getHistory()
311 312 self._history_level += 1
312 313 else:
313 314 history = ''
314 315 return history
315 316
316 317 def initHistoryIndex(self):
317 318 '''
318 319 set history to last command entered
319 320 '''
320 321 self._history_level = self._getHistoryMaxIndex()+1
321 322
322 323 #----------------------- IPython PRIVATE management section ----------------------
323 324 def _setAskExit(self):
324 325 '''
325 326 set the _ask_exit variable that can be cjhecked by GUI to see if
326 327 IPython request an exit handling
327 328 '''
328 329 self._ask_exit = True
329 330
330 331 def _getHistoryMaxIndex(self):
331 332 '''
332 333 returns the max length of the history buffer
333 334
334 335 @return: history length
335 336 @rtype: int
336 337 '''
337 338 return len(self._IP.input_hist_raw)-1
338 339
339 340 def _getHistory(self):
340 341 '''
341 342 Get's the command string of the current history level.
342 343
343 344 @return: Historic command string.
344 345 @rtype: string
345 346 '''
346 347 rv = self._IP.input_hist_raw[self._history_level].strip('\n')
347 348 return rv
348 349
349 350 def _pager(self,text,start=0,screen_lines=0,pager_cmd = None):
350 351 '''
351 352 This function is used as a callback replacment to IPython pager function
352 353
353 354 It puts the 'text' value inside the self._doc_text string that can be retrived via getDocText
354 355 function.
355 356 '''
356 357 self._doc_text = text
357 358
358 359 def _raw_input(self, prompt=''):
359 360 '''
360 361 Custom raw_input() replacement. Get's current line from console buffer.
361 362
362 363 @param prompt: Prompt to print. Here for compatability as replacement.
363 364 @type prompt: string
364 365
365 366 @return: The current command line text.
366 367 @rtype: string
367 368 '''
368 369 return self._line_to_execute
369 370
370 371 def _execute(self):
371 372 '''
372 373 Executes the current line provided by the shell object.
373 374 '''
374 375 orig_stdout = sys.stdout
375 376 sys.stdout = IPython.Shell.Term.cout
376 377
377 378 try:
378 379 line = self._IP.raw_input(None, self._iter_more)
379 380 if self._IP.autoindent:
380 381 self._IP.readline_startup_hook(None)
381 382
382 383 except KeyboardInterrupt:
383 384 self._IP.write('\nKeyboardInterrupt\n')
384 385 self._IP.resetbuffer()
385 386 # keep cache in sync with the prompt counter:
386 387 self._IP.outputcache.prompt_count -= 1
387 388
388 389 if self._IP.autoindent:
389 390 self._IP.indent_current_nsp = 0
390 391 self._iter_more = 0
391 392 except:
392 393 self._IP.showtraceback()
393 394 else:
394 395 self._iter_more = self._IP.push(line)
395 396 if (self._IP.SyntaxTB.last_syntax_error and
396 397 self._IP.rc.autoedit_syntax):
397 398 self._IP.edit_syntax_error()
398 399 if self._iter_more:
399 400 self._prompt = str(self._IP.outputcache.prompt2).strip()
400 401 if self._IP.autoindent:
401 402 self._IP.readline_startup_hook(self._IP.pre_readline)
402 403 else:
403 404 self._prompt = str(self._IP.outputcache.prompt1).strip()
404 405 self._IP.indent_current_nsp = 0 #we set indentation to 0
405 406 sys.stdout = orig_stdout
406 407
407 408 def _shell(self, cmd,verbose=0,debug=0,header=''):
408 409 '''
409 410 Replacement method to allow shell commands without them blocking.
410 411
411 412 @param cmd: Shell command to execute.
412 413 @type cmd: string
413 414 @param verbose: Verbosity
414 415 @type verbose: integer
415 416 @param debug: Debug level
416 417 @type debug: integer
417 418 @param header: Header to be printed before output
418 419 @type header: string
419 420 '''
420 421 if verbose or debug: print header+cmd
421 422 # flush stdout so we don't mangle python's buffering
422 423 if not debug:
423 424 stdin, stdout = os.popen4(cmd)
424 425 result = stdout.read().decode('cp437').encode(locale.getpreferredencoding())
425 426 #we use print command because the shell command is called inside IPython instance and thus is
426 427 #redirected to thread cout
427 428 #"\x01\x1b[1;36m\x02" <-- add colour to the text...
428 429 print "\x01\x1b[1;36m\x02"+result
429 430 stdout.close()
430 431 stdin.close()
431 432
432 433 class WxConsoleView(stc.StyledTextCtrl):
433 434 '''
434 435 Specialized styled text control view for console-like workflow.
435 436 We use here a scintilla frontend thus it can be reused in any GUI taht supports
436 437 scintilla with less work.
437 438
438 439 @cvar ANSI_COLORS_BLACK: Mapping of terminal colors to X11 names.(with Black background)
439 440 @type ANSI_COLORS_BLACK: dictionary
440 441
441 442 @cvar ANSI_COLORS_WHITE: Mapping of terminal colors to X11 names.(with White background)
442 443 @type ANSI_COLORS_WHITE: dictionary
443 444
444 445 @ivar color_pat: Regex of terminal color pattern
445 446 @type color_pat: _sre.SRE_Pattern
446 447 '''
447 448 ANSI_STYLES_BLACK ={'0;30': [0,'WHITE'], '0;31': [1,'RED'],
448 449 '0;32': [2,'GREEN'], '0;33': [3,'BROWN'],
449 450 '0;34': [4,'BLUE'], '0;35': [5,'PURPLE'],
450 451 '0;36': [6,'CYAN'], '0;37': [7,'LIGHT GREY'],
451 452 '1;30': [8,'DARK GREY'], '1;31': [9,'RED'],
452 453 '1;32': [10,'SEA GREEN'], '1;33': [11,'YELLOW'],
453 454 '1;34': [12,'LIGHT BLUE'], '1;35': [13,'MEDIUM VIOLET RED'],
454 455 '1;36': [14,'LIGHT STEEL BLUE'], '1;37': [15,'YELLOW']}
455 456
456 457 ANSI_STYLES_WHITE ={'0;30': [0,'BLACK'], '0;31': [1,'RED'],
457 458 '0;32': [2,'GREEN'], '0;33': [3,'BROWN'],
458 459 '0;34': [4,'BLUE'], '0;35': [5,'PURPLE'],
459 460 '0;36': [6,'CYAN'], '0;37': [7,'LIGHT GREY'],
460 461 '1;30': [8,'DARK GREY'], '1;31': [9,'RED'],
461 462 '1;32': [10,'SEA GREEN'], '1;33': [11,'YELLOW'],
462 463 '1;34': [12,'LIGHT BLUE'], '1;35': [13,'MEDIUM VIOLET RED'],
463 464 '1;36': [14,'LIGHT STEEL BLUE'], '1;37': [15,'YELLOW']}
464 465
465 466 def __init__(self,parent,prompt,intro="",background_color="BLACK",pos=wx.DefaultPosition, ID = -1, size=wx.DefaultSize,
466 467 style=0):
467 468 '''
468 469 Initialize console view.
469 470
470 471 @param parent: Parent widget
471 472 @param prompt: User specified prompt
472 473 @type intro: string
473 474 @param intro: User specified startup introduction string
474 475 @type intro: string
475 476 @param background_color: Can be BLACK or WHITE
476 477 @type background_color: string
477 478 @param other: init param of styledTextControl (can be used as-is)
478 479 '''
479 480 stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style)
480 481
481 482 ####### Scintilla configuration ##################################################
482 483
483 484 # Ctrl + B or Ctrl + N can be used to zoomin/zoomout the text inside the widget
484 485 self.CmdKeyAssign(ord('B'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
485 486 self.CmdKeyAssign(ord('N'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
486 487
487 488 #we define platform specific fonts
488 489 if wx.Platform == '__WXMSW__':
489 490 faces = { 'times': 'Times New Roman',
490 491 'mono' : 'Courier New',
491 492 'helv' : 'Arial',
492 493 'other': 'Comic Sans MS',
493 494 'size' : 10,
494 495 'size2': 8,
495 496 }
496 497 elif wx.Platform == '__WXMAC__':
497 498 faces = { 'times': 'Times New Roman',
498 499 'mono' : 'Monaco',
499 500 'helv' : 'Arial',
500 501 'other': 'Comic Sans MS',
501 502 'size' : 10,
502 503 'size2': 8,
503 504 }
504 505 else:
505 506 faces = { 'times': 'Times',
506 507 'mono' : 'Courier',
507 508 'helv' : 'Helvetica',
508 509 'other': 'new century schoolbook',
509 510 'size' : 10,
510 511 'size2': 8,
511 512 }
512 513
513 514 #We draw a line at position 80
514 515 self.SetEdgeMode(stc.STC_EDGE_LINE)
515 516 self.SetEdgeColumn(80)
516 517 self.SetEdgeColour(wx.LIGHT_GREY)
517 518
518 519 #self.SetViewWhiteSpace(True)
519 520 #self.SetViewEOL(True)
520 521 self.SetEOLMode(stc.STC_EOL_CRLF)
521 522 #self.SetWrapMode(stc.STC_WRAP_CHAR)
522 523 #self.SetWrapMode(stc.STC_WRAP_WORD)
523 524 self.SetBufferedDraw(True)
524 525 #self.SetUseAntiAliasing(True)
525 526 self.SetLayoutCache(stc.STC_CACHE_PAGE)
526 527
527 528 self.EnsureCaretVisible()
528 529
529 530 self.SetMargins(3,3) #text is moved away from border with 3px
530 531 # Suppressing Scintilla margins
531 532 self.SetMarginWidth(0,0)
532 533 self.SetMarginWidth(1,0)
533 534 self.SetMarginWidth(2,0)
534 535
535 536 # make some styles
536 537 if background_color != "BLACK":
537 538 self.background_color = "WHITE"
538 539 self.SetCaretForeground("BLACK")
539 540 self.ANSI_STYLES = self.ANSI_STYLES_WHITE
540 541 else:
541 542 self.background_color = background_color
542 543 self.SetCaretForeground("WHITE")
543 544 self.ANSI_STYLES = self.ANSI_STYLES_BLACK
544 545
545 546 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "fore:%s,back:%s,size:%d,face:%s" % (self.ANSI_STYLES['0;30'][1],
546 547 self.background_color,
547 548 faces['size'], faces['mono']))
548 549 self.StyleClearAll()
549 550 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, "fore:#FF0000,back:#0000FF,bold")
550 551 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, "fore:#000000,back:#FF0000,bold")
551 552
552 553 for style in self.ANSI_STYLES.values():
553 554 self.StyleSetSpec(style[0], "bold,fore:%s" % style[1])
554 555
555 556 #######################################################################
556 557
557 558 self.indent = 0
558 559 self.prompt_count = 0
559 560 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
560 561
561 562 self.write(intro)
562 563 self.setPrompt(prompt)
563 564 self.showPrompt()
564 565
565 566 self.Bind(wx.EVT_KEY_DOWN, self._onKeypress, self)
566 567 #self.Bind(stc.EVT_STC_UPDATEUI, self.OnUpdateUI)
567 568
568 569 def write(self, text):
569 570 '''
570 571 Write given text to buffer.
571 572
572 573 @param text: Text to append.
573 574 @type text: string
574 575 '''
575 576 segments = self.color_pat.split(text)
576 577 segment = segments.pop(0)
577 578 self.StartStyling(self.getCurrentLineEnd(),0xFF)
578 579 self.AppendText(segment)
579 580
580 581 if segments:
581 582 ansi_tags = self.color_pat.findall(text)
582 583
583 584 for tag in ansi_tags:
584 585 i = segments.index(tag)
585 586 self.StartStyling(self.getCurrentLineEnd(),0xFF)
586 587 self.AppendText(segments[i+1])
587 588
588 589 if tag != '0':
589 590 self.SetStyling(len(segments[i+1]),self.ANSI_STYLES[tag][0])
590 591
591 592 segments.pop(i)
592 593
593 594 self.moveCursor(self.getCurrentLineEnd())
594 595
595 596 def getPromptLen(self):
596 597 '''
597 598 Return the length of current prompt
598 599 '''
599 600 return len(str(self.prompt_count)) + 7
600 601
601 602 def setPrompt(self,prompt):
602 603 self.prompt = prompt
603 604
604 605 def setIndentation(self,indentation):
605 606 self.indent = indentation
606 607
607 608 def setPromptCount(self,count):
608 609 self.prompt_count = count
609 610
610 611 def showPrompt(self):
611 612 '''
612 613 Prints prompt at start of line.
613 614
614 615 @param prompt: Prompt to print.
615 616 @type prompt: string
616 617 '''
617 618 self.write(self.prompt)
618 619 #now we update the position of end of prompt
619 620 self.current_start = self.getCurrentLineEnd()
620 621
621 622 autoindent = self.indent*' '
622 623 autoindent = autoindent.replace(' ','\t')
623 624 self.write(autoindent)
624 625
625 626 def changeLine(self, text):
626 627 '''
627 628 Replace currently entered command line with given text.
628 629
629 630 @param text: Text to use as replacement.
630 631 @type text: string
631 632 '''
632 633 self.SetSelection(self.getCurrentPromptStart(),self.getCurrentLineEnd())
633 634 self.ReplaceSelection(text)
634 635 self.moveCursor(self.getCurrentLineEnd())
635 636
636 637 def getCurrentPromptStart(self):
637 638 return self.current_start
638 639
639 640 def getCurrentLineStart(self):
640 641 return self.GotoLine(self.LineFromPosition(self.GetCurrentPos()))
641 642
642 643 def getCurrentLineEnd(self):
643 644 return self.GetLength()
644 645
645 646 def getCurrentLine(self):
646 647 '''
647 648 Get text in current command line.
648 649
649 650 @return: Text of current command line.
650 651 @rtype: string
651 652 '''
652 653 return self.GetTextRange(self.getCurrentPromptStart(),
653 654 self.getCurrentLineEnd())
654 655
655 656 def showReturned(self, text):
656 657 '''
657 658 Show returned text from last command and print new prompt.
658 659
659 660 @param text: Text to show.
660 661 @type text: string
661 662 '''
662 663 self.write('\n'+text)
663 664 if text:
664 665 self.write('\n')
665 666 self.showPrompt()
666 667
667 668 def moveCursorOnNewValidKey(self):
668 669 #If cursor is at wrong position put it at last line...
669 670 if self.GetCurrentPos() < self.getCurrentPromptStart():
670 671 self.GotoPos(self.getCurrentPromptStart())
671 672
672 673 def removeFromTo(self,from_pos,to_pos):
673 674 if from_pos < to_pos:
674 675 self.SetSelection(from_pos,to_pos)
675 676 self.DeleteBack()
676 677
677 678 def removeCurrentLine(self):
678 679 self.LineDelete()
679 680
680 681 def moveCursor(self,position):
681 682 self.GotoPos(position)
682 683
683 684 def getCursorPos(self):
684 685 return self.GetCurrentPos()
685 686
686 687 def selectFromTo(self,from_pos,to_pos):
687 688 self.SetSelectionStart(from_pos)
688 689 self.SetSelectionEnd(to_pos)
689 690
690 691 def writeHistory(self,history):
691 692 self.removeFromTo(self.getCurrentPromptStart(),self.getCurrentLineEnd())
692 693 self.changeLine(history)
693 694
694 695 def writeCompletion(self, possibilities):
695 696 max_len = len(max(possibilities,key=len))
696 697 max_symbol =' '*max_len
697 698
698 699 #now we check how much symbol we can put on a line...
699 700 cursor_pos = self.getCursorPos()
700 701 test_buffer = max_symbol + ' '*4
701 702 current_lines = self.GetLineCount()
702 703
703 704 allowed_symbols = 80/len(test_buffer)
704 705 if allowed_symbols == 0:
705 706 allowed_symbols = 1
706 707
707 708 pos = 1
708 709 buf = ''
709 710 for symbol in possibilities:
710 711 #buf += symbol+'\n'#*spaces)
711 712 if pos<allowed_symbols:
712 713 spaces = max_len - len(symbol) + 4
713 714 buf += symbol+' '*spaces
714 715 pos += 1
715 716 else:
716 717 buf+=symbol+'\n'
717 718 pos = 1
718 719 self.write(buf)
719 720
720 721 def _onKeypress(self, event, skip=True):
721 722 '''
722 723 Key press callback used for correcting behavior for console-like
723 724 interfaces. For example 'home' should go to prompt, not to begining of
724 725 line.
725 726
726 727 @param widget: Widget that key press accored in.
727 728 @type widget: gtk.Widget
728 729 @param event: Event object
729 730 @type event: gtk.gdk.Event
730 731
731 732 @return: Return True if event as been catched.
732 733 @rtype: boolean
733 734 '''
734 735
735 736 if event.GetKeyCode() == wx.WXK_HOME:
736 737 if event.Modifiers == wx.MOD_NONE:
737 738 self.moveCursorOnNewValidKey()
738 739 self.moveCursor(self.getCurrentPromptStart())
739 740 return True
740 741 elif event.Modifiers == wx.MOD_SHIFT:
741 742 self.moveCursorOnNewValidKey()
742 743 self.selectFromTo(self.getCurrentPromptStart(),self.getCursorPos())
743 744 return True
744 745 else:
745 746 return False
746 747
747 748 elif event.GetKeyCode() == wx.WXK_LEFT:
748 749 if event.Modifiers == wx.MOD_NONE:
749 750 self.moveCursorOnNewValidKey()
750 751
751 752 self.moveCursor(self.getCursorPos()-1)
752 753 if self.getCursorPos() < self.getCurrentPromptStart():
753 754 self.moveCursor(self.getCurrentPromptStart())
754 755 return True
755 756
756 757 elif event.GetKeyCode() == wx.WXK_BACK:
757 758 self.moveCursorOnNewValidKey()
758 759 if self.getCursorPos() > self.getCurrentPromptStart():
759 760 self.removeFromTo(self.getCursorPos()-1,self.getCursorPos())
760 761 return True
761 762
762 763 if skip:
763 764 if event.GetKeyCode() not in [wx.WXK_PAGEUP,wx.WXK_PAGEDOWN] and event.Modifiers == wx.MOD_NONE:
764 765 self.moveCursorOnNewValidKey()
765 766
766 767 event.Skip()
767 768 return True
768 769 return False
769 770
770 771 def OnUpdateUI(self, evt):
771 772 # check for matching braces
772 773 braceAtCaret = -1
773 774 braceOpposite = -1
774 775 charBefore = None
775 776 caretPos = self.GetCurrentPos()
776 777
777 778 if caretPos > 0:
778 779 charBefore = self.GetCharAt(caretPos - 1)
779 780 styleBefore = self.GetStyleAt(caretPos - 1)
780 781
781 782 # check before
782 783 if charBefore and chr(charBefore) in "[]{}()" and styleBefore == stc.STC_P_OPERATOR:
783 784 braceAtCaret = caretPos - 1
784 785
785 786 # check after
786 787 if braceAtCaret < 0:
787 788 charAfter = self.GetCharAt(caretPos)
788 789 styleAfter = self.GetStyleAt(caretPos)
789 790
790 791 if charAfter and chr(charAfter) in "[]{}()" and styleAfter == stc.STC_P_OPERATOR:
791 792 braceAtCaret = caretPos
792 793
793 794 if braceAtCaret >= 0:
794 795 braceOpposite = self.BraceMatch(braceAtCaret)
795 796
796 797 if braceAtCaret != -1 and braceOpposite == -1:
797 798 self.BraceBadLight(braceAtCaret)
798 799 else:
799 800 self.BraceHighlight(braceAtCaret, braceOpposite)
800 801 #pt = self.PointFromPosition(braceOpposite)
801 802 #self.Refresh(True, wxRect(pt.x, pt.y, 5,5))
802 803 #print pt
803 804 #self.Refresh(False)
804 805
805 806 class WxIPythonViewPanel(wx.Panel):
806 807 '''
807 808 This is wx.Panel that embbed the IPython Thread and the wx.StyledTextControl
808 809 If you want to port this to any other GUI toolkit, just replace the WxConsoleView
809 810 by YOURGUIConsoleView and make YOURGUIIPythonView derivate from whatever container you want.
810 811 I've choosed to derivate from a wx.Panel because it seems to be ore usefull
811 812 Any idea to make it more 'genric' welcomed.
812 813 '''
813 814 def __init__(self,parent,exit_handler=None,intro=None,background_color="BLACK"):
814 815 '''
815 816 Initialize.
816 817 Instanciate an IPython thread.
817 818 Instanciate a WxConsoleView.
818 819 Redirect I/O to console.
819 820 '''
820 821 wx.Panel.__init__(self,parent,-1)
821 822
822 823 ### IPython thread instanciation ###
823 824 self.cout = StringIO()
824 825 self.IP = IterableIPShell(cout=self.cout,cerr=self.cout,
825 826 exit_handler = exit_handler,
826 827 time_loop = 0.1)
827 828 self.IP.start()
828 829
829 830 ### IPython wx console view instanciation ###
830 831 #If user didn't defined an intro text, we create one for him
831 832 #If you really wnat an empty intrp just call wxIPythonViewPanel with intro=''
832 833 if intro == None:
833 834 welcome_text = "Welcome to WxIPython Shell.\n\n"
834 835 welcome_text+= self.IP.getBanner()
835 836 welcome_text+= "!command -> Execute command in shell\n"
836 837 welcome_text+= "TAB -> Autocompletion\n"
837 838
838 839 self.text_ctrl = WxConsoleView(self,
839 840 self.IP.getPrompt(),
840 841 intro=welcome_text,
841 842 background_color=background_color)
842 843
843 844 self.text_ctrl.Bind(wx.EVT_KEY_DOWN, self.keyPress, self.text_ctrl)
844 845
845 846 ### making the layout of the panel ###
846 847 sizer = wx.BoxSizer(wx.VERTICAL)
847 848 sizer.Add(self.text_ctrl, 1, wx.EXPAND)
848 849 self.SetAutoLayout(True)
849 850 sizer.Fit(self)
850 851 sizer.SetSizeHints(self)
851 852 self.SetSizer(sizer)
852 853 #and we focus on the widget :)
853 854 self.SetFocus()
854 855
855 856 ### below are the thread communication variable ###
856 857 # the IPython thread is managed via unidirectional communication.
857 858 # It's a thread slave that can't interact by itself with the GUI.
858 859 # When the GUI event loop is done runStateMachine() is called and the thread sate is then
859 860 # managed.
860 861
861 862 #Initialize the state machine #kept for information
862 863 #self.states = ['IDLE',
863 864 # 'DO_EXECUTE_LINE',
864 865 # 'WAIT_END_OF_EXECUTION',
865 866 # 'SHOW_DOC',
866 867 # 'SHOW_PROMPT']
867 868
868 869 self.cur_state = 'IDLE'
869 870 self.pager_state = 'DONE'
870 871 #wx.CallAfter(self.runStateMachine)
871 872
872 873 # This creates a new Event class and a EVT binder function
873 874 (self.AskExitEvent, EVT_ASK_EXIT) = wx.lib.newevent.NewEvent()
874 875
875 876 self.Bind(wx.EVT_IDLE, self.runStateMachine)
876 877 self.Bind(EVT_ASK_EXIT, exit_handler)
877 878
878 879 def __del__(self):
879 880 self.IP.shutdown()
880 881 self.IP.join()
881 882 WxConsoleView.__del__()
882 883
883 884 #---------------------------- IPython Thread Management ---------------------------------------
884 885 def runStateMachine(self,event):
885 886 #print >>self.sys_stdout,"state:",self.cur_state
886 887 self.updateStatusTracker(self.cur_state)
887 888
888 889 if self.cur_state == 'DO_EXECUTE_LINE':
889 890 #print >>self.sys_stdout,"command:",self.getCurrentLine()
890 891 self.IP.doExecute(self.text_ctrl.getCurrentLine().replace('\t',' '*4))
891 892 self.updateHistoryTracker(self.text_ctrl.getCurrentLine())
892 893 self.cur_state = 'WAIT_END_OF_EXECUTION'
893 894
894 895 if self.cur_state == 'WAIT_END_OF_EXECUTION':
895 896 if self.IP.isExecuteDone():
896 897 self.doc = self.IP.getDocText()
897 898 if self.IP.getAskExit():
898 899 evt = self.AskExitEvent()
899 900 wx.PostEvent(self, evt)
900 901 self.IP.clearAskExit()
901 902 if self.doc:
902 903 self.pager_state = 'INIT'
903 904 self.cur_state = 'SHOW_DOC'
904 905 else:
905 906 self.cur_state = 'SHOW_PROMPT'
906 907
907 908 if self.cur_state == 'SHOW_PROMPT':
908 909 self.text_ctrl.setPrompt(self.IP.getPrompt())
909 910 self.text_ctrl.setIndentation(self.IP.getIndentation())
910 911 self.text_ctrl.setPromptCount(self.IP.getPromptCount())
911 912 rv = self.cout.getvalue()
912 913 if rv: rv = rv.strip('\n')
913 914 self.text_ctrl.showReturned(rv)
914 915 self.cout.truncate(0)
915 916 self.IP.initHistoryIndex()
916 917 self.cur_state = 'IDLE'
917 918
918 919 if self.cur_state == 'SHOW_DOC':
919 920 self.pager(self.doc)
920 921 if self.pager_state == 'DONE':
921 922 self.cur_state = 'SHOW_PROMPT'
922 923
923 924 event.Skip()
924 925
925 926 #---------------------------- IPython pager ---------------------------------------
926 927 def pager(self,text):#,start=0,screen_lines=0,pager_cmd = None):
927 928 if self.pager_state == 'WAITING':
928 929 #print >>self.sys_stdout,"PAGER waiting"
929 930 return
930 931
931 932 if self.pager_state == 'INIT':
932 933 #print >>self.sys_stdout,"PAGER state:",self.pager_state
933 934 self.pager_lines = text[7:].split('\n')
934 935 self.pager_nb_lines = len(self.pager_lines)
935 936 self.pager_index = 0
936 937 self.pager_do_remove = False
937 938 self.text_ctrl.write('\n')
938 939 self.pager_state = 'PROCESS_LINES'
939 940
940 941 if self.pager_state == 'PROCESS_LINES':
941 942 #print >>self.sys_stdout,"PAGER state:",self.pager_state
942 943 if self.pager_do_remove == True:
943 944 self.text_ctrl.removeCurrentLine()
944 945 self.pager_do_remove = False
945 946
946 947 if self.pager_nb_lines > 10:
947 948 #print >>self.sys_stdout,"PAGER processing 10 lines"
948 949 if self.pager_index > 0:
949 950 self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n')
950 951 else:
951 952 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n')
952 953
953 954 for line in self.pager_lines[self.pager_index+1:self.pager_index+9]:
954 955 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n')
955 956 self.pager_index += 10
956 957 self.pager_nb_lines -= 10
957 958 self.text_ctrl.write("--- Push Enter to continue or 'Q' to quit---")
958 959 self.pager_do_remove = True
959 960 self.pager_state = 'WAITING'
960 961 return
961 962 else:
962 963 #print >>self.sys_stdout,"PAGER processing last lines"
963 964 if self.pager_nb_lines > 0:
964 965 if self.pager_index > 0:
965 966 self.text_ctrl.write(">\x01\x1b[1;36m\x02"+self.pager_lines[self.pager_index]+'\n')
966 967 else:
967 968 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+self.pager_lines[self.pager_index]+'\n')
968 969
969 970 self.pager_index += 1
970 971 self.pager_nb_lines -= 1
971 972 if self.pager_nb_lines > 0:
972 973 for line in self.pager_lines[self.pager_index:]:
973 974 self.text_ctrl.write("\x01\x1b[1;36m\x02 "+line+'\n')
974 975 self.pager_nb_lines = 0
975 976 self.pager_state = 'DONE'
976 977
977 978 #---------------------------- Key Handler --------------------------------------------
978 979 def keyPress(self, event):
979 980 '''
980 981 Key press callback with plenty of shell goodness, like history,
981 982 autocompletions, etc.
982 983 '''
983 984
984 985 if event.GetKeyCode() == ord('C'):
985 986 if event.Modifiers == wx.MOD_CONTROL:
986 987 if self.cur_state == 'WAIT_END_OF_EXECUTION':
987 988 #we raise an exception inside the IPython thread container
988 989 self.IP.raise_exc(KeyboardInterrupt)
989 990 return
990 991
991 992 if event.KeyCode == wx.WXK_RETURN:
992 993 if self.cur_state == 'IDLE':
993 994 #we change the state ot the state machine
994 995 self.cur_state = 'DO_EXECUTE_LINE'
995 996 return
996 997 if self.pager_state == 'WAITING':
997 998 self.pager_state = 'PROCESS_LINES'
998 999 return
999 1000
1000 1001 if event.GetKeyCode() in [ord('q'),ord('Q')]:
1001 1002 if self.pager_state == 'WAITING':
1002 1003 self.pager_state = 'DONE'
1003 1004 return
1004 1005
1005 1006 #scroll_position = self.text_ctrl.GetScrollPos(wx.VERTICAL)
1006 1007 if self.cur_state == 'IDLE':
1007 1008 if event.KeyCode == wx.WXK_UP:
1008 1009 history = self.IP.historyBack()
1009 1010 self.text_ctrl.writeHistory(history)
1010 1011 return
1011 1012 if event.KeyCode == wx.WXK_DOWN:
1012 1013 history = self.IP.historyForward()
1013 1014 self.text_ctrl.writeHistory(history)
1014 1015 return
1015 1016 if event.KeyCode == wx.WXK_TAB:
1016 1017 #if line empty we disable tab completion
1017 1018 if not self.text_ctrl.getCurrentLine().strip():
1018 1019 self.text_ctrl.write('\t')
1019 1020 return
1020 1021 completed, possibilities = self.IP.complete(self.text_ctrl.getCurrentLine())
1021 1022 if len(possibilities) > 1:
1022 1023 cur_slice = self.text_ctrl.getCurrentLine()
1023 1024 self.text_ctrl.write('\n')
1024 1025 self.text_ctrl.writeCompletion(possibilities)
1025 1026 self.text_ctrl.write('\n')
1026 1027
1027 1028 self.text_ctrl.showPrompt()
1028 1029 self.text_ctrl.write(cur_slice)
1029 1030 self.text_ctrl.changeLine(completed or cur_slice)
1030 1031
1031 1032 return
1032 1033 event.Skip()
1033 1034
1034 1035 #---------------------------- Hook Section --------------------------------------------
1035 1036 def updateHistoryTracker(self,command_line):
1036 1037 '''
1037 1038 Default history tracker (does nothing)
1038 1039 '''
1039 1040 pass
1040 1041
1041 1042 def setHistoryTrackerHook(self,func):
1042 1043 '''
1043 1044 Define a new history tracker
1044 1045 '''
1045 1046 self.updateHistoryTracker = func
1046 1047 def updateStatusTracker(self,status):
1047 1048 '''
1048 1049 Default status tracker (does nothing)
1049 1050 '''
1050 1051 pass
1051 1052
1052 1053 def setStatusTrackerHook(self,func):
1053 1054 '''
1054 1055 Define a new status tracker
1055 1056 '''
1056 1057 self.updateStatusTracker = func
1057 1058
General Comments 0
You need to be logged in to leave comments. Login now