]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wxPython/lib/PyCrust/shell.py
b96ea802184329570a22d9af71d8b0b8bec90997
[wxWidgets.git] / wxPython / wxPython / lib / PyCrust / shell.py
1 """The PyCrust Shell is an interactive text control in which a user types in
2 commands to be sent to the interpreter. This particular shell is based on
3 wxPython's wxStyledTextCtrl. The latest files are always available at the
4 SourceForge project page at http://sourceforge.net/projects/pycrust/.
5 Sponsored by Orbtech - Your source for Python programming expertise."""
6
7 __author__ = "Patrick K. O'Brien <pobrien@orbtech.com>"
8 __cvsid__ = "$Id$"
9 __revision__ = "$Revision$"[11:-2]
10
11 from wxPython.wx import *
12 from wxPython.stc import *
13 import keyword
14 import os
15 import sys
16 from pseudo import PseudoFileIn
17 from pseudo import PseudoFileOut
18 from pseudo import PseudoFileErr
19 from version import VERSION
20
21
22 if wxPlatform == '__WXMSW__':
23 faces = { 'times' : 'Times New Roman',
24 'mono' : 'Courier New',
25 'helv' : 'Lucida Console',
26 'lucida' : 'Lucida Console',
27 'other' : 'Comic Sans MS',
28 'size' : 10,
29 'lnsize' : 9,
30 'backcol': '#FFFFFF',
31 }
32 # Versions of wxPython prior to 2.3.2 had a sizing bug on Win platform.
33 # The font was 2 points too large. So we need to reduce the font size.
34 if (wxMAJOR_VERSION, wxMINOR_VERSION, wxRELEASE_NUMBER) < (2, 3, 2):
35 faces['size'] -= 2
36 faces['lnsize'] -= 2
37 else: # GTK
38 faces = { 'times' : 'Times',
39 'mono' : 'Courier',
40 'helv' : 'Helvetica',
41 'other' : 'new century schoolbook',
42 'size' : 12,
43 'lnsize' : 10,
44 'backcol': '#FFFFFF',
45 }
46
47
48 class ShellFacade:
49 """Simplified interface to all shell-related functionality.
50
51 This is a semi-transparent facade, in that all attributes of other are
52 still accessible, even though only some are visible to the user."""
53
54 name = 'PyCrust Shell Interface'
55 revision = __revision__
56
57 def __init__(self, other):
58 """Create a ShellFacade instance."""
59 methods = ['ask',
60 'clear',
61 'pause',
62 'prompt',
63 'quit',
64 'redirectStderr',
65 'redirectStdin',
66 'redirectStdout',
67 'run',
68 'runfile',
69 ]
70 for method in methods:
71 self.__dict__[method] = getattr(other, method)
72 d = self.__dict__
73 d['other'] = other
74 d['helpText'] = \
75 """
76 * Key bindings:
77 Home Go to the beginning of the command or line.
78 Shift+Home Select to the beginning of the command or line.
79 Shift+End Select to the end of the line.
80 End Go to the end of the line.
81 Ctrl+C Copy selected text, removing prompts.
82 Ctrl+Shift+C Copy selected text, retaining prompts.
83 Ctrl+X Cut selected text.
84 Ctrl+V Paste from clipboard.
85 Ctrl+Shift+V Paste and run multiple commands from clipboard.
86 Ctrl+Up Arrow Retrieve Previous History item.
87 Alt+P Retrieve Previous History item.
88 Ctrl+Down Arrow Retrieve Next History item.
89 Alt+N Retrieve Next History item.
90 Shift+Up Arrow Insert Previous History item.
91 Shift+Down Arrow Insert Next History item.
92 F8 Command-completion of History item.
93 (Type a few characters of a previous command and then press F8.)
94 """
95
96 def help(self):
97 """Display some useful information about how to use the shell."""
98 self.write(self.helpText)
99
100 def __getattr__(self, name):
101 if hasattr(self.other, name):
102 return getattr(self.other, name)
103 else:
104 raise AttributeError, name
105
106 def __setattr__(self, name, value):
107 if self.__dict__.has_key(name):
108 self.__dict__[name] = value
109 elif hasattr(self.other, name):
110 return setattr(self.other, name, value)
111 else:
112 raise AttributeError, name
113
114 def _getAttributeNames(self):
115 """Return list of magic attributes to extend introspection."""
116 list = ['autoCallTip',
117 'autoComplete',
118 'autoCompleteCaseInsensitive',
119 'autoCompleteIncludeDouble',
120 'autoCompleteIncludeMagic',
121 'autoCompleteIncludeSingle',
122 ]
123 list.sort()
124 return list
125
126
127 class Shell(wxStyledTextCtrl):
128 """PyCrust Shell based on wxStyledTextCtrl."""
129
130 name = 'PyCrust Shell'
131 revision = __revision__
132
133 def __init__(self, parent, id=-1, pos=wxDefaultPosition, \
134 size=wxDefaultSize, style=wxCLIP_CHILDREN, introText='', \
135 locals=None, InterpClass=None, *args, **kwds):
136 """Create a PyCrust Shell instance."""
137 wxStyledTextCtrl.__init__(self, parent, id, pos, size, style)
138 # Grab these so they can be restored by self.redirect* methods.
139 self.stdin = sys.stdin
140 self.stdout = sys.stdout
141 self.stderr = sys.stderr
142 # Add the current working directory "." to the search path.
143 sys.path.insert(0, os.curdir)
144 # Import a default interpreter class if one isn't provided.
145 if InterpClass == None:
146 from interpreter import Interpreter
147 else:
148 Interpreter = InterpClass
149 # Create default locals so we have something interesting.
150 shellLocals = {'__name__': 'PyCrust-Shell',
151 '__doc__': 'PyCrust-Shell, The PyCrust Python Shell.',
152 '__version__': VERSION,
153 }
154 # Add the dictionary that was passed in.
155 if locals:
156 shellLocals.update(locals)
157 self.interp = Interpreter(locals=shellLocals, \
158 rawin=self.readRaw, \
159 stdin=PseudoFileIn(self.readIn), \
160 stdout=PseudoFileOut(self.writeOut), \
161 stderr=PseudoFileErr(self.writeErr), \
162 *args, **kwds)
163 # Keep track of the last non-continuation prompt positions.
164 self.promptPosStart = 0
165 self.promptPosEnd = 0
166 # Keep track of multi-line commands.
167 self.more = 0
168 # Create the command history. Commands are added into the front of
169 # the list (ie. at index 0) as they are entered. self.historyIndex
170 # is the current position in the history; it gets incremented as you
171 # retrieve the previous command, decremented as you retrieve the
172 # next, and reset when you hit Enter. self.historyIndex == -1 means
173 # you're on the current command, not in the history.
174 self.history = []
175 self.historyIndex = -1
176 # Assign handlers for keyboard events.
177 EVT_KEY_DOWN(self, self.OnKeyDown)
178 EVT_CHAR(self, self.OnChar)
179 # Assign handlers for wxSTC events.
180 EVT_STC_UPDATEUI(self, id, self.OnUpdateUI)
181 # Configure various defaults and user preferences.
182 self.config()
183 # Display the introductory banner information.
184 try: self.showIntro(introText)
185 except: pass
186 # Assign some pseudo keywords to the interpreter's namespace.
187 try: self.setBuiltinKeywords()
188 except: pass
189 # Add 'shell' to the interpreter's local namespace.
190 try: self.setLocalShell()
191 except: pass
192 # Do this last so the user has complete control over their
193 # environment. They can override anything they want.
194 try: self.execStartupScript(self.interp.startupScript)
195 except: pass
196
197 def destroy(self):
198 del self.interp
199
200 def config(self):
201 """Configure shell based on user preferences."""
202 self.SetMarginType(1, wxSTC_MARGIN_NUMBER)
203 self.SetMarginWidth(1, 40)
204
205 self.SetLexer(wxSTC_LEX_PYTHON)
206 self.SetKeyWords(0, ' '.join(keyword.kwlist))
207
208 self.setStyles(faces)
209 self.SetViewWhiteSpace(0)
210 self.SetTabWidth(4)
211 self.SetUseTabs(0)
212 # Do we want to automatically pop up command completion options?
213 self.autoComplete = 1
214 self.autoCompleteIncludeMagic = 1
215 self.autoCompleteIncludeSingle = 1
216 self.autoCompleteIncludeDouble = 1
217 self.autoCompleteCaseInsensitive = 1
218 self.AutoCompSetIgnoreCase(self.autoCompleteCaseInsensitive)
219 # Do we want to automatically pop up command argument help?
220 self.autoCallTip = 1
221 self.CallTipSetBackground(wxColour(255, 255, 232))
222
223 def showIntro(self, text=''):
224 """Display introductory text in the shell."""
225 if text:
226 if not text.endswith(os.linesep): text += os.linesep
227 self.write(text)
228 try:
229 self.write(self.interp.introText)
230 except AttributeError:
231 pass
232
233 def setBuiltinKeywords(self):
234 """Create pseudo keywords as part of builtins.
235
236 This simply sets "close", "exit" and "quit" to a helpful string.
237 """
238 import __builtin__
239 __builtin__.close = __builtin__.exit = __builtin__.quit = \
240 'Click on the close button to leave the application.'
241
242 def quit(self):
243 """Quit the application."""
244
245 # XXX Good enough for now but later we want to send a close event.
246
247 # In the close event handler we can make sure they want to quit.
248 # Other applications, like PythonCard, may choose to hide rather than
249 # quit so we should just post the event and let the surrounding app
250 # decide what it wants to do.
251 self.write('Click on the close button to leave the application.')
252
253 def setLocalShell(self):
254 """Add 'shell' to locals as reference to ShellFacade instance."""
255 self.interp.locals['shell'] = ShellFacade(other=self)
256
257 def execStartupScript(self, startupScript):
258 """Execute the user's PYTHONSTARTUP script if they have one."""
259 if startupScript and os.path.isfile(startupScript):
260 startupText = 'Startup script executed: ' + startupScript
261 self.push('print %s;execfile(%s)' % \
262 (`startupText`, `startupScript`))
263 else:
264 self.push('')
265
266 def setStyles(self, faces):
267 """Configure font size, typeface and color for lexer."""
268
269 # Default style
270 self.StyleSetSpec(wxSTC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d,back:%(backcol)s" % faces)
271
272 self.StyleClearAll()
273
274 # Built in styles
275 self.StyleSetSpec(wxSTC_STYLE_LINENUMBER, "back:#C0C0C0,face:%(mono)s,size:%(lnsize)d" % faces)
276 self.StyleSetSpec(wxSTC_STYLE_CONTROLCHAR, "face:%(mono)s" % faces)
277 self.StyleSetSpec(wxSTC_STYLE_BRACELIGHT, "fore:#0000FF,back:#FFFF88")
278 self.StyleSetSpec(wxSTC_STYLE_BRACEBAD, "fore:#FF0000,back:#FFFF88")
279
280 # Python styles
281 self.StyleSetSpec(wxSTC_P_DEFAULT, "face:%(mono)s" % faces)
282 self.StyleSetSpec(wxSTC_P_COMMENTLINE, "fore:#007F00,face:%(mono)s" % faces)
283 self.StyleSetSpec(wxSTC_P_NUMBER, "")
284 self.StyleSetSpec(wxSTC_P_STRING, "fore:#7F007F,face:%(mono)s" % faces)
285 self.StyleSetSpec(wxSTC_P_CHARACTER, "fore:#7F007F,face:%(mono)s" % faces)
286 self.StyleSetSpec(wxSTC_P_WORD, "fore:#00007F,bold")
287 self.StyleSetSpec(wxSTC_P_TRIPLE, "fore:#7F0000")
288 self.StyleSetSpec(wxSTC_P_TRIPLEDOUBLE, "fore:#000033,back:#FFFFE8")
289 self.StyleSetSpec(wxSTC_P_CLASSNAME, "fore:#0000FF,bold")
290 self.StyleSetSpec(wxSTC_P_DEFNAME, "fore:#007F7F,bold")
291 self.StyleSetSpec(wxSTC_P_OPERATOR, "")
292 self.StyleSetSpec(wxSTC_P_IDENTIFIER, "")
293 self.StyleSetSpec(wxSTC_P_COMMENTBLOCK, "fore:#7F7F7F")
294 self.StyleSetSpec(wxSTC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eolfilled" % faces)
295
296 def OnUpdateUI(self, evt):
297 """Check for matching braces."""
298 braceAtCaret = -1
299 braceOpposite = -1
300 charBefore = None
301 caretPos = self.GetCurrentPos()
302 if caretPos > 0:
303 charBefore = self.GetCharAt(caretPos - 1)
304 styleBefore = self.GetStyleAt(caretPos - 1)
305
306 # Check before.
307 if charBefore and chr(charBefore) in '[]{}()' \
308 and styleBefore == wxSTC_P_OPERATOR:
309 braceAtCaret = caretPos - 1
310
311 # Check after.
312 if braceAtCaret < 0:
313 charAfter = self.GetCharAt(caretPos)
314 styleAfter = self.GetStyleAt(caretPos)
315 if charAfter and chr(charAfter) in '[]{}()' \
316 and styleAfter == wxSTC_P_OPERATOR:
317 braceAtCaret = caretPos
318
319 if braceAtCaret >= 0:
320 braceOpposite = self.BraceMatch(braceAtCaret)
321
322 if braceAtCaret != -1 and braceOpposite == -1:
323 self.BraceBadLight(braceAtCaret)
324 else:
325 self.BraceHighlight(braceAtCaret, braceOpposite)
326
327 def OnChar(self, event):
328 """Keypress event handler.
329
330 Prevents modification of previously submitted commands/responses."""
331 if not self.CanEdit():
332 return
333 key = event.KeyCode()
334 currpos = self.GetCurrentPos()
335 stoppos = self.promptPosEnd
336 if key == ord('.'):
337 # The dot or period key activates auto completion.
338 # Get the command between the prompt and the cursor.
339 # Add a dot to the end of the command.
340 command = self.GetTextRange(stoppos, currpos) + '.'
341 self.write('.')
342 if self.autoComplete: self.autoCompleteShow(command)
343 elif key == ord('('):
344 # The left paren activates a call tip and cancels
345 # an active auto completion.
346 if self.AutoCompActive(): self.AutoCompCancel()
347 # Get the command between the prompt and the cursor.
348 # Add the '(' to the end of the command.
349 self.ReplaceSelection('')
350 command = self.GetTextRange(stoppos, currpos) + '('
351 self.write('(')
352 if self.autoCallTip: self.autoCallTipShow(command)
353 else:
354 # Allow the normal event handling to take place.
355 event.Skip()
356
357 def OnKeyDown(self, event):
358 """Key down event handler.
359
360 Prevents modification of previously submitted commands/responses."""
361 key = event.KeyCode()
362 controlDown = event.ControlDown()
363 altDown = event.AltDown()
364 shiftDown = event.ShiftDown()
365 currpos = self.GetCurrentPos()
366 endpos = self.GetTextLength()
367 # Return (Enter) is used to submit a command to the interpreter.
368 if not controlDown and key == WXK_RETURN:
369 if self.AutoCompActive(): self.AutoCompCancel()
370 if self.CallTipActive(): self.CallTipCancel()
371 self.processLine()
372 # Ctrl+Return (Cntrl+Enter) is used to insert a line break.
373 elif controlDown and key == WXK_RETURN:
374 if self.AutoCompActive(): self.AutoCompCancel()
375 if self.CallTipActive(): self.CallTipCancel()
376 if currpos == endpos:
377 self.processLine()
378 else:
379 self.insertLineBreak()
380 # If the auto-complete window is up let it do its thing.
381 elif self.AutoCompActive():
382 event.Skip()
383 # Let Ctrl-Alt-* get handled normally.
384 elif controlDown and altDown:
385 event.Skip()
386 # Clear the current, unexecuted command.
387 elif key == WXK_ESCAPE:
388 if self.CallTipActive():
389 event.Skip()
390 else:
391 self.clearCommand()
392 # Cut to the clipboard.
393 elif (controlDown and key in (ord('X'), ord('x'))) \
394 or (shiftDown and key == WXK_DELETE):
395 self.Cut()
396 # Copy to the clipboard.
397 elif controlDown and not shiftDown \
398 and key in (ord('C'), ord('c'), WXK_INSERT):
399 self.Copy()
400 # Copy to the clipboard, including prompts.
401 elif controlDown and shiftDown \
402 and key in (ord('C'), ord('c'), WXK_INSERT):
403 self.CopyWithPrompts()
404 # Paste from the clipboard.
405 elif (controlDown and not shiftDown \
406 and key in (ord('V'), ord('v'))) \
407 or (shiftDown and not controlDown and key == WXK_INSERT):
408 self.Paste()
409 # Paste from the clipboard, run commands.
410 elif controlDown and shiftDown \
411 and key in (ord('V'), ord('v')):
412 self.PasteAndRun()
413 # Replace with the previous command from the history buffer.
414 elif (controlDown and key == WXK_UP) \
415 or (altDown and key in (ord('P'), ord('p'))):
416 self.OnHistoryReplace(step=+1)
417 # Replace with the next command from the history buffer.
418 elif (controlDown and key == WXK_DOWN) \
419 or (altDown and key in (ord('N'), ord('n'))):
420 self.OnHistoryReplace(step=-1)
421 # Insert the previous command from the history buffer.
422 elif (shiftDown and key == WXK_UP):
423 self.OnHistoryInsert(step=+1)
424 # Insert the next command from the history buffer.
425 elif (shiftDown and key == WXK_DOWN):
426 self.OnHistoryInsert(step=-1)
427 # Search up the history for the text in front of the cursor.
428 elif key == WXK_F8:
429 self.OnHistorySearch()
430 # Home needs to be aware of the prompt.
431 elif key == WXK_HOME:
432 home = self.promptPosEnd
433 if currpos >= home:
434 if event.ShiftDown():
435 # Select text from current position to end of prompt.
436 self.SetSelection(self.GetCurrentPos(), home)
437 else:
438 self.SetCurrentPos(home)
439 self.SetAnchor(home)
440 self.EnsureCaretVisible()
441 else:
442 event.Skip()
443 # Basic navigation keys should work anywhere.
444 elif key in (WXK_END, WXK_LEFT, WXK_RIGHT, WXK_UP, WXK_DOWN, \
445 WXK_PRIOR, WXK_NEXT):
446 event.Skip()
447 # Don't backspace over the latest non-continuation prompt.
448 elif key == WXK_BACK:
449 if currpos > self.promptPosEnd:
450 event.Skip()
451 # Only allow these keys after the latest prompt.
452 elif key in (WXK_TAB, WXK_DELETE):
453 if self.CanEdit():
454 event.Skip()
455 # Don't toggle between insert mode and overwrite mode.
456 elif key == WXK_INSERT:
457 pass
458 # Don't allow line deletion.
459 elif controlDown and key in (ord('L'), ord('l')):
460 pass
461 # Don't allow line transposition.
462 elif controlDown and key in (ord('T'), ord('t')):
463 pass
464 # Protect the readonly portion of the shell.
465 elif not self.CanEdit():
466 pass
467 else:
468 event.Skip()
469
470 def clearCommand(self):
471 """Delete the current, unexecuted command."""
472 startpos = self.promptPosEnd
473 endpos = self.GetTextLength()
474 self.SetSelection(startpos, endpos)
475 self.ReplaceSelection('')
476 self.more = 0
477
478 def OnHistoryReplace(self, step):
479 """Replace with the previous/next command from the history buffer."""
480 self.clearCommand()
481 self.replaceFromHistory(step)
482
483 def replaceFromHistory(self, step):
484 """Replace selection with command from the history buffer."""
485 self.ReplaceSelection('')
486 newindex = self.historyIndex + step
487 if -1 <= newindex <= len(self.history):
488 self.historyIndex = newindex
489 if 0 <= newindex <= len(self.history)-1:
490 command = self.history[self.historyIndex]
491 command = command.replace('\n', os.linesep + sys.ps2)
492 self.ReplaceSelection(command)
493
494 def OnHistoryInsert(self, step):
495 """Insert the previous/next command from the history buffer."""
496 if not self.CanEdit():
497 return
498 startpos = self.GetCurrentPos()
499 self.replaceFromHistory(step)
500 endpos = self.GetCurrentPos()
501 self.SetSelection(endpos, startpos)
502
503 def OnHistorySearch(self):
504 """Search up the history buffer for the text in front of the cursor."""
505 if not self.CanEdit():
506 return
507 startpos = self.GetCurrentPos()
508 # The text up to the cursor is what we search for.
509 numCharsAfterCursor = self.GetTextLength() - startpos
510 searchText = self.getCommand(rstrip=0)
511 if numCharsAfterCursor > 0:
512 searchText = searchText[:-numCharsAfterCursor]
513 if not searchText:
514 return
515 # Search upwards from the current history position and loop back
516 # to the beginning if we don't find anything.
517 if (self.historyIndex <= -1) \
518 or (self.historyIndex >= len(self.history)-2):
519 searchOrder = range(len(self.history))
520 else:
521 searchOrder = range(self.historyIndex+1, len(self.history)) + \
522 range(self.historyIndex)
523 for i in searchOrder:
524 command = self.history[i]
525 if command[:len(searchText)] == searchText:
526 # Replace the current selection with the one we've found.
527 self.ReplaceSelection(command[len(searchText):])
528 endpos = self.GetCurrentPos()
529 self.SetSelection(endpos, startpos)
530 # We've now warped into middle of the history.
531 self.historyIndex = i
532 break
533
534 def setStatusText(self, text):
535 """Display status information."""
536
537 # This method will most likely be replaced by the enclosing app
538 # to do something more interesting, like write to a status bar.
539 print text
540
541 def insertLineBreak(self):
542 """Insert a new line break."""
543 if self.CanEdit():
544 self.write(os.linesep)
545 self.more = 1
546 self.prompt()
547
548 def processLine(self):
549 """Process the line of text at which the user hit Enter."""
550
551 # The user hit ENTER and we need to decide what to do. They could be
552 # sitting on any line in the shell.
553
554 thepos = self.GetCurrentPos()
555 startpos = self.promptPosEnd
556 endpos = self.GetTextLength()
557 # If they hit RETURN inside the current command, execute the command.
558 if self.CanEdit():
559 self.SetCurrentPos(endpos)
560 self.interp.more = 0
561 command = self.GetTextRange(startpos, endpos)
562 lines = command.split(os.linesep + sys.ps2)
563 lines = [line.rstrip() for line in lines]
564 command = '\n'.join(lines)
565 self.push(command)
566 # Or replace the current command with the other command.
567 else:
568 # If the line contains a command (even an invalid one).
569 if self.getCommand(rstrip=0):
570 command = self.getMultilineCommand()
571 self.clearCommand()
572 self.write(command)
573 # Otherwise, put the cursor back where we started.
574 else:
575 self.SetCurrentPos(thepos)
576 self.SetAnchor(thepos)
577
578 def getMultilineCommand(self, rstrip=1):
579 """Extract a multi-line command from the editor.
580
581 The command may not necessarily be valid Python syntax."""
582 # XXX Need to extract real prompts here. Need to keep track of the
583 # prompt every time a command is issued.
584 ps1 = str(sys.ps1)
585 ps1size = len(ps1)
586 ps2 = str(sys.ps2)
587 ps2size = len(ps2)
588 # This is a total hack job, but it works.
589 text = self.GetCurLine()[0]
590 line = self.GetCurrentLine()
591 while text[:ps2size] == ps2 and line > 0:
592 line -= 1
593 self.GotoLine(line)
594 text = self.GetCurLine()[0]
595 if text[:ps1size] == ps1:
596 line = self.GetCurrentLine()
597 self.GotoLine(line)
598 startpos = self.GetCurrentPos() + ps1size
599 line += 1
600 self.GotoLine(line)
601 while self.GetCurLine()[0][:ps2size] == ps2:
602 line += 1
603 self.GotoLine(line)
604 stoppos = self.GetCurrentPos()
605 command = self.GetTextRange(startpos, stoppos)
606 command = command.replace(os.linesep + sys.ps2, '\n')
607 command = command.rstrip()
608 command = command.replace('\n', os.linesep + sys.ps2)
609 else:
610 command = ''
611 if rstrip:
612 command = command.rstrip()
613 return command
614
615 def getCommand(self, text=None, rstrip=1):
616 """Extract a command from text which may include a shell prompt.
617
618 The command may not necessarily be valid Python syntax."""
619 if not text:
620 text = self.GetCurLine()[0]
621 # Strip the prompt off the front of text leaving just the command.
622 command = self.lstripPrompt(text)
623 if command == text:
624 command = '' # Real commands have prompts.
625 if rstrip:
626 command = command.rstrip()
627 return command
628
629 def lstripPrompt(self, text):
630 """Return text without a leading prompt."""
631 ps1 = str(sys.ps1)
632 ps1size = len(ps1)
633 ps2 = str(sys.ps2)
634 ps2size = len(ps2)
635 # Strip the prompt off the front of text.
636 if text[:ps1size] == ps1:
637 text = text[ps1size:]
638 elif text[:ps2size] == ps2:
639 text = text[ps2size:]
640 return text
641
642 def push(self, command):
643 """Send command to the interpreter for execution."""
644 self.write(os.linesep)
645 self.more = self.interp.push(command)
646 if not self.more:
647 self.addHistory(command.rstrip())
648 self.prompt()
649
650 def addHistory(self, command):
651 """Add command to the command history."""
652 # Reset the history position.
653 self.historyIndex = -1
654 # Insert this command into the history, unless it's a blank
655 # line or the same as the last command.
656 if command != '' \
657 and (len(self.history) == 0 or command != self.history[0]):
658 self.history.insert(0, command)
659
660 def write(self, text):
661 """Display text in the shell.
662
663 Replace line endings with OS-specific endings."""
664 text = self.fixLineEndings(text)
665 self.AddText(text)
666 self.EnsureCaretVisible()
667
668 def fixLineEndings(self, text):
669 """Return text with line endings replaced by OS-specific endings."""
670 lines = text.split('\r\n')
671 for l in range(len(lines)):
672 chunks = lines[l].split('\r')
673 for c in range(len(chunks)):
674 chunks[c] = os.linesep.join(chunks[c].split('\n'))
675 lines[l] = os.linesep.join(chunks)
676 text = os.linesep.join(lines)
677 return text
678
679 def prompt(self):
680 """Display appropriate prompt for the context, either ps1 or ps2.
681
682 If this is a continuation line, autoindent as necessary."""
683 if self.more:
684 prompt = str(sys.ps2)
685 else:
686 prompt = str(sys.ps1)
687 pos = self.GetCurLine()[1]
688 if pos > 0: self.write(os.linesep)
689 if not self.more:
690 self.promptPosStart = self.GetCurrentPos()
691 self.write(prompt)
692 if not self.more:
693 self.promptPosEnd = self.GetCurrentPos()
694 # Keep the undo feature from undoing previous responses.
695 self.EmptyUndoBuffer()
696 # XXX Add some autoindent magic here if more.
697 if self.more:
698 self.write(' '*4) # Temporary hack indentation.
699 self.EnsureCaretVisible()
700 self.ScrollToColumn(0)
701
702 def readIn(self):
703 """Replacement for stdin."""
704 prompt = 'Please enter your response:'
705 dialog = wxTextEntryDialog(None, prompt, \
706 'Input Dialog (Standard)', '')
707 try:
708 if dialog.ShowModal() == wxID_OK:
709 text = dialog.GetValue()
710 self.write(text + os.linesep)
711 return text
712 finally:
713 dialog.Destroy()
714 return ''
715
716 def readRaw(self, prompt='Please enter your response:'):
717 """Replacement for raw_input."""
718 dialog = wxTextEntryDialog(None, prompt, \
719 'Input Dialog (Raw)', '')
720 try:
721 if dialog.ShowModal() == wxID_OK:
722 text = dialog.GetValue()
723 return text
724 finally:
725 dialog.Destroy()
726 return ''
727
728 def ask(self, prompt='Please enter your response:'):
729 """Get response from the user."""
730 return raw_input(prompt=prompt)
731
732 def pause(self):
733 """Halt execution pending a response from the user."""
734 self.ask('Press enter to continue:')
735
736 def clear(self):
737 """Delete all text from the shell."""
738 self.ClearAll()
739
740 def run(self, command, prompt=1, verbose=1):
741 """Execute command within the shell as if it was typed in directly.
742 >>> shell.run('print "this"')
743 >>> print "this"
744 this
745 >>>
746 """
747 # Go to the very bottom of the text.
748 endpos = self.GetTextLength()
749 self.SetCurrentPos(endpos)
750 command = command.rstrip()
751 if prompt: self.prompt()
752 if verbose: self.write(command)
753 self.push(command)
754
755 def runfile(self, filename):
756 """Execute all commands in file as if they were typed into the shell."""
757 file = open(filename)
758 try:
759 self.prompt()
760 for command in file.readlines():
761 if command[:6] == 'shell.': # Run shell methods silently.
762 self.run(command, prompt=0, verbose=0)
763 else:
764 self.run(command, prompt=0, verbose=1)
765 finally:
766 file.close()
767
768 def autoCompleteShow(self, command):
769 """Display auto-completion popup list."""
770 list = self.interp.getAutoCompleteList(command,
771 includeMagic=self.autoCompleteIncludeMagic,
772 includeSingle=self.autoCompleteIncludeSingle,
773 includeDouble=self.autoCompleteIncludeDouble)
774 if list:
775 options = ' '.join(list)
776 offset = 0
777 self.AutoCompShow(offset, options)
778
779 def autoCallTipShow(self, command):
780 """Display argument spec and docstring in a popup bubble thingie."""
781 if self.CallTipActive: self.CallTipCancel()
782 (name, argspec, tip) = self.interp.getCallTip(command)
783 if argspec:
784 startpos = self.GetCurrentPos()
785 self.write(argspec + ')')
786 endpos = self.GetCurrentPos()
787 self.SetSelection(endpos, startpos)
788 if tip:
789 curpos = self.GetCurrentPos()
790 tippos = curpos - (len(name) + 1)
791 fallback = curpos - self.GetColumn(curpos)
792 # In case there isn't enough room, only go back to the fallback.
793 tippos = max(tippos, fallback)
794 self.CallTipShow(tippos, tip)
795
796 def writeOut(self, text):
797 """Replacement for stdout."""
798 self.write(text)
799
800 def writeErr(self, text):
801 """Replacement for stderr."""
802 self.write(text)
803
804 def redirectStdin(self, redirect=1):
805 """If redirect is true then sys.stdin will come from the shell."""
806 if redirect:
807 sys.stdin = PseudoFileIn(self.readIn)
808 else:
809 sys.stdin = self.stdin
810
811 def redirectStdout(self, redirect=1):
812 """If redirect is true then sys.stdout will go to the shell."""
813 if redirect:
814 sys.stdout = PseudoFileOut(self.writeOut)
815 else:
816 sys.stdout = self.stdout
817
818 def redirectStderr(self, redirect=1):
819 """If redirect is true then sys.stderr will go to the shell."""
820 if redirect:
821 sys.stderr = PseudoFileErr(self.writeErr)
822 else:
823 sys.stderr = self.stderr
824
825 def CanCut(self):
826 """Return true if text is selected and can be cut."""
827 if self.GetSelectionStart() != self.GetSelectionEnd() \
828 and self.GetSelectionStart() >= self.promptPosEnd \
829 and self.GetSelectionEnd() >= self.promptPosEnd:
830 return 1
831 else:
832 return 0
833
834 def CanCopy(self):
835 """Return true if text is selected and can be copied."""
836 return self.GetSelectionStart() != self.GetSelectionEnd()
837
838 def CanPaste(self):
839 """Return true if a paste should succeed."""
840 if self.CanEdit() and wxStyledTextCtrl.CanPaste(self):
841 return 1
842 else:
843 return 0
844
845 def CanEdit(self):
846 """Return true if editing should succeed."""
847 return self.GetCurrentPos() >= self.promptPosEnd
848
849 def Cut(self):
850 """Remove selection and place it on the clipboard."""
851 if self.CanCut() and self.CanCopy():
852 if self.AutoCompActive(): self.AutoCompCancel()
853 if self.CallTipActive: self.CallTipCancel()
854 self.Copy()
855 self.ReplaceSelection('')
856
857 def Copy(self):
858 """Copy selection and place it on the clipboard."""
859 if self.CanCopy():
860 command = self.GetSelectedText()
861 command = command.replace(os.linesep + sys.ps2, os.linesep)
862 command = command.replace(os.linesep + sys.ps1, os.linesep)
863 command = self.lstripPrompt(text=command)
864 data = wxTextDataObject(command)
865 if wxTheClipboard.Open():
866 wxTheClipboard.SetData(data)
867 wxTheClipboard.Close()
868
869 def CopyWithPrompts(self):
870 """Copy selection, including prompts, and place it on the clipboard."""
871 if self.CanCopy():
872 command = self.GetSelectedText()
873 data = wxTextDataObject(command)
874 if wxTheClipboard.Open():
875 wxTheClipboard.SetData(data)
876 wxTheClipboard.Close()
877
878 def Paste(self):
879 """Replace selection with clipboard contents."""
880 if self.CanPaste() and wxTheClipboard.Open():
881 if wxTheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)):
882 data = wxTextDataObject()
883 if wxTheClipboard.GetData(data):
884 self.ReplaceSelection('')
885 command = data.GetText()
886 command = command.rstrip()
887 command = self.fixLineEndings(command)
888 command = self.lstripPrompt(text=command)
889 command = command.replace(os.linesep + sys.ps2, '\n')
890 command = command.replace(os.linesep, '\n')
891 command = command.replace('\n', os.linesep + sys.ps2)
892 self.write(command)
893 wxTheClipboard.Close()
894
895 def PasteAndRun(self):
896 """Replace selection with clipboard contents, run commands."""
897 if wxTheClipboard.Open():
898 if wxTheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)):
899 data = wxTextDataObject()
900 if wxTheClipboard.GetData(data):
901 endpos = self.GetTextLength()
902 self.SetCurrentPos(endpos)
903 startpos = self.promptPosEnd
904 self.SetSelection(startpos, endpos)
905 self.ReplaceSelection('')
906 text = data.GetText()
907 text = text.strip()
908 text = self.fixLineEndings(text)
909 text = self.lstripPrompt(text=text)
910 text = text.replace(os.linesep + sys.ps1, '\n')
911 text = text.replace(os.linesep + sys.ps2, '\n')
912 text = text.replace(os.linesep, '\n')
913 lines = text.split('\n')
914 commands = []
915 command = ''
916 for line in lines:
917 if line.strip() != '' and line.lstrip() == line:
918 # New command.
919 if command:
920 # Add the previous command to the list.
921 commands.append(command)
922 # Start a new command, which may be multiline.
923 command = line
924 else:
925 # Multiline command. Add to the command.
926 command += '\n'
927 command += line
928 commands.append(command)
929 for command in commands:
930 command = command.replace('\n', os.linesep + sys.ps2)
931 self.write(command)
932 self.processLine()
933 wxTheClipboard.Close()
934
935
936 wxID_SELECTALL = NewId() # This *should* be defined by wxPython.
937 ID_AUTOCOMP = NewId()
938 ID_AUTOCOMP_SHOW = NewId()
939 ID_AUTOCOMP_INCLUDE_MAGIC = NewId()
940 ID_AUTOCOMP_INCLUDE_SINGLE = NewId()
941 ID_AUTOCOMP_INCLUDE_DOUBLE = NewId()
942 ID_CALLTIPS = NewId()
943 ID_CALLTIPS_SHOW = NewId()
944
945
946 class ShellMenu:
947 """Mixin class to add standard menu items."""
948
949 def createMenus(self):
950 m = self.fileMenu = wxMenu()
951 m.AppendSeparator()
952 m.Append(wxID_EXIT, 'E&xit', 'Exit PyCrust')
953
954 m = self.editMenu = wxMenu()
955 m.Append(wxID_UNDO, '&Undo \tCtrl+Z', 'Undo the last action')
956 m.Append(wxID_REDO, '&Redo \tCtrl+Y', 'Redo the last undone action')
957 m.AppendSeparator()
958 m.Append(wxID_CUT, 'Cu&t \tCtrl+X', 'Cut the selection')
959 m.Append(wxID_COPY, '&Copy \tCtrl+C', 'Copy the selection')
960 m.Append(wxID_PASTE, '&Paste \tCtrl+V', 'Paste')
961 m.AppendSeparator()
962 m.Append(wxID_CLEAR, 'Cle&ar', 'Delete the selection')
963 m.Append(wxID_SELECTALL, 'Select A&ll', 'Select all text')
964
965 m = self.autocompMenu = wxMenu()
966 m.Append(ID_AUTOCOMP_SHOW, 'Show Auto Completion', \
967 'Show auto completion during dot syntax', 1)
968 m.Append(ID_AUTOCOMP_INCLUDE_MAGIC, 'Include Magic Attributes', \
969 'Include attributes visible to __getattr__ and __setattr__', 1)
970 m.Append(ID_AUTOCOMP_INCLUDE_SINGLE, 'Include Single Underscores', \
971 'Include attibutes prefixed by a single underscore', 1)
972 m.Append(ID_AUTOCOMP_INCLUDE_DOUBLE, 'Include Double Underscores', \
973 'Include attibutes prefixed by a double underscore', 1)
974
975 m = self.calltipsMenu = wxMenu()
976 m.Append(ID_CALLTIPS_SHOW, 'Show Call Tips', \
977 'Show call tips with argument specifications', 1)
978
979 m = self.optionsMenu = wxMenu()
980 m.AppendMenu(ID_AUTOCOMP, '&Auto Completion', self.autocompMenu, \
981 'Auto Completion Options')
982 m.AppendMenu(ID_CALLTIPS, '&Call Tips', self.calltipsMenu, \
983 'Call Tip Options')
984
985 m = self.helpMenu = wxMenu()
986 m.AppendSeparator()
987 m.Append(wxID_ABOUT, '&About...', 'About PyCrust')
988
989 b = self.menuBar = wxMenuBar()
990 b.Append(self.fileMenu, '&File')
991 b.Append(self.editMenu, '&Edit')
992 b.Append(self.optionsMenu, '&Options')
993 b.Append(self.helpMenu, '&Help')
994 self.SetMenuBar(b)
995
996 EVT_MENU(self, wxID_EXIT, self.OnExit)
997 EVT_MENU(self, wxID_UNDO, self.OnUndo)
998 EVT_MENU(self, wxID_REDO, self.OnRedo)
999 EVT_MENU(self, wxID_CUT, self.OnCut)
1000 EVT_MENU(self, wxID_COPY, self.OnCopy)
1001 EVT_MENU(self, wxID_PASTE, self.OnPaste)
1002 EVT_MENU(self, wxID_CLEAR, self.OnClear)
1003 EVT_MENU(self, wxID_SELECTALL, self.OnSelectAll)
1004 EVT_MENU(self, wxID_ABOUT, self.OnAbout)
1005 EVT_MENU(self, ID_AUTOCOMP_SHOW, \
1006 self.OnAutoCompleteShow)
1007 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_MAGIC, \
1008 self.OnAutoCompleteIncludeMagic)
1009 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_SINGLE, \
1010 self.OnAutoCompleteIncludeSingle)
1011 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_DOUBLE, \
1012 self.OnAutoCompleteIncludeDouble)
1013 EVT_MENU(self, ID_CALLTIPS_SHOW, \
1014 self.OnCallTipsShow)
1015
1016 EVT_UPDATE_UI(self, wxID_UNDO, self.OnUpdateMenu)
1017 EVT_UPDATE_UI(self, wxID_REDO, self.OnUpdateMenu)
1018 EVT_UPDATE_UI(self, wxID_CUT, self.OnUpdateMenu)
1019 EVT_UPDATE_UI(self, wxID_COPY, self.OnUpdateMenu)
1020 EVT_UPDATE_UI(self, wxID_PASTE, self.OnUpdateMenu)
1021 EVT_UPDATE_UI(self, wxID_CLEAR, self.OnUpdateMenu)
1022 EVT_UPDATE_UI(self, ID_AUTOCOMP_SHOW, self.OnUpdateMenu)
1023 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_MAGIC, self.OnUpdateMenu)
1024 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_SINGLE, self.OnUpdateMenu)
1025 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_DOUBLE, self.OnUpdateMenu)
1026 EVT_UPDATE_UI(self, ID_CALLTIPS_SHOW, self.OnUpdateMenu)
1027
1028 def OnExit(self, event):
1029 self.Close(true)
1030
1031 def OnUndo(self, event):
1032 self.shell.Undo()
1033
1034 def OnRedo(self, event):
1035 self.shell.Redo()
1036
1037 def OnCut(self, event):
1038 self.shell.Cut()
1039
1040 def OnCopy(self, event):
1041 self.shell.Copy()
1042
1043 def OnPaste(self, event):
1044 self.shell.Paste()
1045
1046 def OnClear(self, event):
1047 self.shell.Clear()
1048
1049 def OnSelectAll(self, event):
1050 self.shell.SelectAll()
1051
1052 def OnAbout(self, event):
1053 """Display an About PyCrust window."""
1054 import sys
1055 title = 'About PyCrust'
1056 text = 'PyCrust %s\n\n' % VERSION + \
1057 'Yet another Python shell, only flakier.\n\n' + \
1058 'Half-baked by Patrick K. O\'Brien,\n' + \
1059 'the other half is still in the oven.\n\n' + \
1060 'Shell Revision: %s\n' % self.shell.revision + \
1061 'Interpreter Revision: %s\n\n' % self.shell.interp.revision + \
1062 'Python Version: %s\n' % sys.version.split()[0] + \
1063 'wxPython Version: %s\n' % wx.__version__ + \
1064 'Platform: %s\n' % sys.platform
1065 dialog = wxMessageDialog(self, text, title, wxOK | wxICON_INFORMATION)
1066 dialog.ShowModal()
1067 dialog.Destroy()
1068
1069 def OnAutoCompleteShow(self, event):
1070 self.shell.autoComplete = event.IsChecked()
1071
1072 def OnAutoCompleteIncludeMagic(self, event):
1073 self.shell.autoCompleteIncludeMagic = event.IsChecked()
1074
1075 def OnAutoCompleteIncludeSingle(self, event):
1076 self.shell.autoCompleteIncludeSingle = event.IsChecked()
1077
1078 def OnAutoCompleteIncludeDouble(self, event):
1079 self.shell.autoCompleteIncludeDouble = event.IsChecked()
1080
1081 def OnCallTipsShow(self, event):
1082 self.shell.autoCallTip = event.IsChecked()
1083
1084 def OnUpdateMenu(self, event):
1085 """Update menu items based on current status."""
1086 id = event.GetId()
1087 if id == wxID_UNDO:
1088 event.Enable(self.shell.CanUndo())
1089 elif id == wxID_REDO:
1090 event.Enable(self.shell.CanRedo())
1091 elif id == wxID_CUT:
1092 event.Enable(self.shell.CanCut())
1093 elif id == wxID_COPY:
1094 event.Enable(self.shell.CanCopy())
1095 elif id == wxID_PASTE:
1096 event.Enable(self.shell.CanPaste())
1097 elif id == wxID_CLEAR:
1098 event.Enable(self.shell.CanCut())
1099 elif id == ID_AUTOCOMP_SHOW:
1100 event.Check(self.shell.autoComplete)
1101 elif id == ID_AUTOCOMP_INCLUDE_MAGIC:
1102 event.Check(self.shell.autoCompleteIncludeMagic)
1103 elif id == ID_AUTOCOMP_INCLUDE_SINGLE:
1104 event.Check(self.shell.autoCompleteIncludeSingle)
1105 elif id == ID_AUTOCOMP_INCLUDE_DOUBLE:
1106 event.Check(self.shell.autoCompleteIncludeDouble)
1107 elif id == ID_CALLTIPS_SHOW:
1108 event.Check(self.shell.autoCallTip)
1109
1110
1111 class ShellFrame(wxFrame, ShellMenu):
1112 """Frame containing the PyCrust shell component."""
1113
1114 name = 'PyCrust Shell Frame'
1115 revision = __revision__
1116
1117 def __init__(self, parent=None, id=-1, title='PyShell', \
1118 pos=wxDefaultPosition, size=wxDefaultSize, \
1119 style=wxDEFAULT_FRAME_STYLE, locals=None, \
1120 InterpClass=None, *args, **kwds):
1121 """Create a PyCrust ShellFrame instance."""
1122 wxFrame.__init__(self, parent, id, title, pos, size, style)
1123 intro = 'Welcome To PyCrust %s - The Flakiest Python Shell' % VERSION
1124 intro += '\nSponsored by Orbtech - Your source for Python programming expertise.'
1125 self.CreateStatusBar()
1126 self.SetStatusText(intro.replace('\n', ', '))
1127
1128 import os
1129 filename = os.path.join(os.path.dirname(__file__), 'PyCrust.ico')
1130 icon = wxIcon(filename, wxBITMAP_TYPE_ICO)
1131 self.SetIcon(icon)
1132
1133 self.shell = Shell(parent=self, id=-1, introText=intro, \
1134 locals=locals, InterpClass=InterpClass, \
1135 *args, **kwds)
1136 # Override the shell so that status messages go to the status bar.
1137 self.shell.setStatusText = self.SetStatusText
1138 self.createMenus()
1139
1140
1141
1142