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."""
7 __author__
= "Patrick K. O'Brien <pobrien@orbtech.com>"
9 __revision__
= "$Revision$"[11:-2]
11 from wxPython
.wx
import *
12 from wxPython
.stc
import *
16 from pseudo
import PseudoFileIn
17 from pseudo
import PseudoFileOut
18 from pseudo
import PseudoFileErr
19 from version
import VERSION
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',
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):
38 faces
= { 'times' : 'Times',
41 'other' : 'new century schoolbook',
49 """Simplified interface to all shell-related functionality.
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."""
54 name
= 'PyCrust Shell Interface'
55 revision
= __revision__
57 def __init__(self
, other
):
58 """Create a ShellFacade instance."""
70 for method
in methods
:
71 self
.__dict
__[method
] = getattr(other
, method
)
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.)
97 """Display some useful information about how to use the shell."""
98 self
.write(self
.helpText
)
100 def __getattr__(self
, name
):
101 if hasattr(self
.other
, name
):
102 return getattr(self
.other
, name
)
104 raise AttributeError, name
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
)
112 raise AttributeError, name
114 def _getAttributeNames(self
):
115 """Return list of magic attributes to extend introspection."""
116 list = ['autoCallTip',
118 'autoCompleteCaseInsensitive',
119 'autoCompleteIncludeDouble',
120 'autoCompleteIncludeMagic',
121 'autoCompleteIncludeSingle',
127 class Shell(wxStyledTextCtrl
):
128 """PyCrust Shell based on wxStyledTextCtrl."""
130 name
= 'PyCrust Shell'
131 revision
= __revision__
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
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
,
154 # Add the dictionary that was passed in.
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
), \
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.
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.
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.
183 # Display the introductory banner information.
184 try: self
.showIntro(introText
)
186 # Assign some pseudo keywords to the interpreter's namespace.
187 try: self
.setBuiltinKeywords()
189 # Add 'shell' to the interpreter's local namespace.
190 try: self
.setLocalShell()
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
)
201 """Configure shell based on user preferences."""
202 self
.SetMarginType(1, wxSTC_MARGIN_NUMBER
)
203 self
.SetMarginWidth(1, 40)
205 self
.SetLexer(wxSTC_LEX_PYTHON
)
206 self
.SetKeyWords(0, ' '.join(keyword
.kwlist
))
208 self
.setStyles(faces
)
209 self
.SetViewWhiteSpace(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?
221 self
.CallTipSetBackground(wxColour(255, 255, 232))
223 def showIntro(self
, text
=''):
224 """Display introductory text in the shell."""
226 if not text
.endswith(os
.linesep
): text
+= os
.linesep
229 self
.write(self
.interp
.introText
)
230 except AttributeError:
233 def setBuiltinKeywords(self
):
234 """Create pseudo keywords as part of builtins.
236 This simply sets "close", "exit" and "quit" to a helpful string.
239 __builtin__
.close
= __builtin__
.exit
= __builtin__
.quit
= \
240 'Click on the close button to leave the application.'
243 """Quit the application."""
245 # XXX Good enough for now but later we want to send a close event.
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.')
253 def setLocalShell(self
):
254 """Add 'shell' to locals as reference to ShellFacade instance."""
255 self
.interp
.locals['shell'] = ShellFacade(other
=self
)
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`
))
266 def setStyles(self
, faces
):
267 """Configure font size, typeface and color for lexer."""
270 self
.StyleSetSpec(wxSTC_STYLE_DEFAULT
, "face:%(mono)s,size:%(size)d,back:%(backcol)s" % faces
)
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")
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
)
296 def OnUpdateUI(self
, evt
):
297 """Check for matching braces."""
301 caretPos
= self
.GetCurrentPos()
303 charBefore
= self
.GetCharAt(caretPos
- 1)
304 styleBefore
= self
.GetStyleAt(caretPos
- 1)
307 if charBefore
and chr(charBefore
) in '[]{}()' \
308 and styleBefore
== wxSTC_P_OPERATOR
:
309 braceAtCaret
= caretPos
- 1
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
319 if braceAtCaret
>= 0:
320 braceOpposite
= self
.BraceMatch(braceAtCaret
)
322 if braceAtCaret
!= -1 and braceOpposite
== -1:
323 self
.BraceBadLight(braceAtCaret
)
325 self
.BraceHighlight(braceAtCaret
, braceOpposite
)
327 def OnChar(self
, event
):
328 """Keypress event handler.
330 Prevents modification of previously submitted commands/responses."""
331 if not self
.CanEdit():
333 key
= event
.KeyCode()
334 currpos
= self
.GetCurrentPos()
335 stoppos
= self
.promptPosEnd
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
) + '.'
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
) + '('
352 if self
.autoCallTip
: self
.autoCallTipShow(command
)
354 # Allow the normal event handling to take place.
357 def OnKeyDown(self
, event
):
358 """Key down event handler.
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()
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
:
379 self
.insertLineBreak()
380 # If the auto-complete window is up let it do its thing.
381 elif self
.AutoCompActive():
383 # Let Ctrl-Alt-* get handled normally.
384 elif controlDown
and altDown
:
386 # Clear the current, unexecuted command.
387 elif key
== WXK_ESCAPE
:
388 if self
.CallTipActive():
392 # Cut to the clipboard.
393 elif (controlDown
and key
in (ord('X'), ord('x'))) \
394 or (shiftDown
and key
== WXK_DELETE
):
396 # Copy to the clipboard.
397 elif controlDown
and not shiftDown \
398 and key
in (ord('C'), ord('c'), WXK_INSERT
):
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
):
409 # Paste from the clipboard, run commands.
410 elif controlDown
and shiftDown \
411 and key
in (ord('V'), ord('v')):
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.
429 self
.OnHistorySearch()
430 # Home needs to be aware of the prompt.
431 elif key
== WXK_HOME
:
432 home
= self
.promptPosEnd
434 if event
.ShiftDown():
435 # Select text from current position to end of prompt.
436 self
.SetSelection(self
.GetCurrentPos(), home
)
438 self
.SetCurrentPos(home
)
440 self
.EnsureCaretVisible()
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
):
447 # Don't backspace over the latest non-continuation prompt.
448 elif key
== WXK_BACK
:
449 if currpos
> self
.promptPosEnd
:
451 # Only allow these keys after the latest prompt.
452 elif key
in (WXK_TAB
, WXK_DELETE
):
455 # Don't toggle between insert mode and overwrite mode.
456 elif key
== WXK_INSERT
:
458 # Don't allow line deletion.
459 elif controlDown
and key
in (ord('L'), ord('l')):
461 # Don't allow line transposition.
462 elif controlDown
and key
in (ord('T'), ord('t')):
464 # Protect the readonly portion of the shell.
465 elif not self
.CanEdit():
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('')
478 def OnHistoryReplace(self
, step
):
479 """Replace with the previous/next command from the history buffer."""
481 self
.replaceFromHistory(step
)
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
)
494 def OnHistoryInsert(self
, step
):
495 """Insert the previous/next command from the history buffer."""
496 if not self
.CanEdit():
498 startpos
= self
.GetCurrentPos()
499 self
.replaceFromHistory(step
)
500 endpos
= self
.GetCurrentPos()
501 self
.SetSelection(endpos
, startpos
)
503 def OnHistorySearch(self
):
504 """Search up the history buffer for the text in front of the cursor."""
505 if not self
.CanEdit():
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
]
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
))
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
534 def setStatusText(self
, text
):
535 """Display status information."""
537 # This method will most likely be replaced by the enclosing app
538 # to do something more interesting, like write to a status bar.
541 def insertLineBreak(self
):
542 """Insert a new line break."""
544 self
.write(os
.linesep
)
548 def processLine(self
):
549 """Process the line of text at which the user hit Enter."""
551 # The user hit ENTER and we need to decide what to do. They could be
552 # sitting on any line in the shell.
554 thepos
= self
.GetCurrentPos()
555 startpos
= self
.promptPosEnd
556 endpos
= self
.GetTextLength()
557 # If they hit RETURN inside the current command, execute the command.
559 self
.SetCurrentPos(endpos
)
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
)
566 # Or replace the current command with the other command.
568 # If the line contains a command (even an invalid one).
569 if self
.getCommand(rstrip
=0):
570 command
= self
.getMultilineCommand()
573 # Otherwise, put the cursor back where we started.
575 self
.SetCurrentPos(thepos
)
576 self
.SetAnchor(thepos
)
578 def getMultilineCommand(self
, rstrip
=1):
579 """Extract a multi-line command from the editor.
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.
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:
594 text
= self
.GetCurLine()[0]
595 if text
[:ps1size
] == ps1
:
596 line
= self
.GetCurrentLine()
598 startpos
= self
.GetCurrentPos() + ps1size
601 while self
.GetCurLine()[0][:ps2size
] == ps2
:
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
)
612 command
= command
.rstrip()
615 def getCommand(self
, text
=None, rstrip
=1):
616 """Extract a command from text which may include a shell prompt.
618 The command may not necessarily be valid Python syntax."""
620 text
= self
.GetCurLine()[0]
621 # Strip the prompt off the front of text leaving just the command.
622 command
= self
.lstripPrompt(text
)
624 command
= '' # Real commands have prompts.
626 command
= command
.rstrip()
629 def lstripPrompt(self
, text
):
630 """Return text without a leading prompt."""
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
:]
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
)
647 self
.addHistory(command
.rstrip())
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.
657 and (len(self
.history
) == 0 or command
!= self
.history
[0]):
658 self
.history
.insert(0, command
)
660 def write(self
, text
):
661 """Display text in the shell.
663 Replace line endings with OS-specific endings."""
664 text
= self
.fixLineEndings(text
)
666 self
.EnsureCaretVisible()
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
)
680 """Display appropriate prompt for the context, either ps1 or ps2.
682 If this is a continuation line, autoindent as necessary."""
684 prompt
= str(sys
.ps2
)
686 prompt
= str(sys
.ps1
)
687 pos
= self
.GetCurLine()[1]
688 if pos
> 0: self
.write(os
.linesep
)
690 self
.promptPosStart
= self
.GetCurrentPos()
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.
698 self
.write(' '*4) # Temporary hack indentation.
699 self
.EnsureCaretVisible()
700 self
.ScrollToColumn(0)
703 """Replacement for stdin."""
704 prompt
= 'Please enter your response:'
705 dialog
= wxTextEntryDialog(None, prompt
, \
706 'Input Dialog (Standard)', '')
708 if dialog
.ShowModal() == wxID_OK
:
709 text
= dialog
.GetValue()
710 self
.write(text
+ os
.linesep
)
716 def readRaw(self
, prompt
='Please enter your response:'):
717 """Replacement for raw_input."""
718 dialog
= wxTextEntryDialog(None, prompt
, \
719 'Input Dialog (Raw)', '')
721 if dialog
.ShowModal() == wxID_OK
:
722 text
= dialog
.GetValue()
728 def ask(self
, prompt
='Please enter your response:'):
729 """Get response from the user."""
730 return raw_input(prompt
=prompt
)
733 """Halt execution pending a response from the user."""
734 self
.ask('Press enter to continue:')
737 """Delete all text from the shell."""
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"')
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
)
755 def runfile(self
, filename
):
756 """Execute all commands in file as if they were typed into the shell."""
757 file = open(filename
)
760 for command
in file.readlines():
761 if command
[:6] == 'shell.': # Run shell methods silently.
762 self
.run(command
, prompt
=0, verbose
=0)
764 self
.run(command
, prompt
=0, verbose
=1)
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
)
775 options
= ' '.join(list)
777 self
.AutoCompShow(offset
, options
)
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
)
784 startpos
= self
.GetCurrentPos()
785 self
.write(argspec
+ ')')
786 endpos
= self
.GetCurrentPos()
787 self
.SetSelection(endpos
, startpos
)
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
)
796 def writeOut(self
, text
):
797 """Replacement for stdout."""
800 def writeErr(self
, text
):
801 """Replacement for stderr."""
804 def redirectStdin(self
, redirect
=1):
805 """If redirect is true then sys.stdin will come from the shell."""
807 sys
.stdin
= PseudoFileIn(self
.readIn
)
809 sys
.stdin
= self
.stdin
811 def redirectStdout(self
, redirect
=1):
812 """If redirect is true then sys.stdout will go to the shell."""
814 sys
.stdout
= PseudoFileOut(self
.writeOut
)
816 sys
.stdout
= self
.stdout
818 def redirectStderr(self
, redirect
=1):
819 """If redirect is true then sys.stderr will go to the shell."""
821 sys
.stderr
= PseudoFileErr(self
.writeErr
)
823 sys
.stderr
= self
.stderr
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
:
835 """Return true if text is selected and can be copied."""
836 return self
.GetSelectionStart() != self
.GetSelectionEnd()
839 """Return true if a paste should succeed."""
840 if self
.CanEdit() and wxStyledTextCtrl
.CanPaste(self
):
846 """Return true if editing should succeed."""
847 return self
.GetCurrentPos() >= self
.promptPosEnd
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()
855 self
.ReplaceSelection('')
858 """Copy selection and place it on the clipboard."""
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()
869 def CopyWithPrompts(self
):
870 """Copy selection, including prompts, and place it on the clipboard."""
872 command
= self
.GetSelectedText()
873 data
= wxTextDataObject(command
)
874 if wxTheClipboard
.Open():
875 wxTheClipboard
.SetData(data
)
876 wxTheClipboard
.Close()
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
)
893 wxTheClipboard
.Close()
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()
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')
917 if line
.strip() != '' and line
.lstrip() == line
:
920 # Add the previous command to the list.
921 commands
.append(command
)
922 # Start a new command, which may be multiline.
925 # Multiline command. Add to the command.
928 commands
.append(command
)
929 for command
in commands
:
930 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
933 wxTheClipboard
.Close()
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()
947 """Mixin class to add standard menu items."""
949 def createMenus(self
):
950 m
= self
.fileMenu
= wxMenu()
952 m
.Append(wxID_EXIT
, 'E&xit', 'Exit PyCrust')
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')
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')
962 m
.Append(wxID_CLEAR
, 'Cle&ar', 'Delete the selection')
963 m
.Append(wxID_SELECTALL
, 'Select A&ll', 'Select all text')
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)
975 m
= self
.calltipsMenu
= wxMenu()
976 m
.Append(ID_CALLTIPS_SHOW
, 'Show Call Tips', \
977 'Show call tips with argument specifications', 1)
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
, \
985 m
= self
.helpMenu
= wxMenu()
987 m
.Append(wxID_ABOUT
, '&About...', 'About PyCrust')
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')
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
)
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
)
1028 def OnExit(self
, event
):
1031 def OnUndo(self
, event
):
1034 def OnRedo(self
, event
):
1037 def OnCut(self
, event
):
1040 def OnCopy(self
, event
):
1043 def OnPaste(self
, event
):
1046 def OnClear(self
, event
):
1049 def OnSelectAll(self
, event
):
1050 self
.shell
.SelectAll()
1052 def OnAbout(self
, event
):
1053 """Display an About PyCrust window."""
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
)
1069 def OnAutoCompleteShow(self
, event
):
1070 self
.shell
.autoComplete
= event
.IsChecked()
1072 def OnAutoCompleteIncludeMagic(self
, event
):
1073 self
.shell
.autoCompleteIncludeMagic
= event
.IsChecked()
1075 def OnAutoCompleteIncludeSingle(self
, event
):
1076 self
.shell
.autoCompleteIncludeSingle
= event
.IsChecked()
1078 def OnAutoCompleteIncludeDouble(self
, event
):
1079 self
.shell
.autoCompleteIncludeDouble
= event
.IsChecked()
1081 def OnCallTipsShow(self
, event
):
1082 self
.shell
.autoCallTip
= event
.IsChecked()
1084 def OnUpdateMenu(self
, event
):
1085 """Update menu items based on current status."""
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
)
1111 class ShellFrame(wxFrame
, ShellMenu
):
1112 """Frame containing the PyCrust shell component."""
1114 name
= 'PyCrust Shell Frame'
1115 revision
= __revision__
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', ', '))
1129 filename
= os
.path
.join(os
.path
.dirname(__file__
), 'PyCrust.ico')
1130 icon
= wxIcon(filename
, wxBITMAP_TYPE_ICO
)
1133 self
.shell
= Shell(parent
=self
, id=-1, introText
=intro
, \
1134 locals=locals, InterpClass
=InterpClass
, \
1136 # Override the shell so that status messages go to the status bar.
1137 self
.shell
.setStatusText
= self
.SetStatusText