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 Development Services"""
7 __author__
= "Patrick K. O'Brien <pobrien@orbtech.com>"
9 __version__
= "$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
= __version__
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
= __version__
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 # Configure various defaults and user preferences.
181 # Display the introductory banner information.
182 try: self
.showIntro(introText
)
184 # Assign some pseudo keywords to the interpreter's namespace.
185 try: self
.setBuiltinKeywords()
187 # Add 'shell' to the interpreter's local namespace.
188 try: self
.setLocalShell()
190 # Do this last so the user has complete control over their
191 # environment. They can override anything they want.
192 try: self
.execStartupScript(self
.interp
.startupScript
)
199 """Configure shell based on user preferences."""
200 self
.SetMarginType(1, wxSTC_MARGIN_NUMBER
)
201 self
.SetMarginWidth(1, 40)
203 self
.SetLexer(wxSTC_LEX_PYTHON
)
204 self
.SetKeyWords(0, ' '.join(keyword
.kwlist
))
206 self
.setStyles(faces
)
207 self
.SetViewWhiteSpace(0)
210 # Do we want to automatically pop up command completion options?
211 self
.autoComplete
= 1
212 self
.autoCompleteIncludeMagic
= 1
213 self
.autoCompleteIncludeSingle
= 1
214 self
.autoCompleteIncludeDouble
= 1
215 self
.autoCompleteCaseInsensitive
= 1
216 self
.AutoCompSetIgnoreCase(self
.autoCompleteCaseInsensitive
)
217 # Do we want to automatically pop up command argument help?
219 self
.CallTipSetBackground(wxColour(255, 255, 232))
221 def showIntro(self
, text
=''):
222 """Display introductory text in the shell."""
224 if not text
.endswith(os
.linesep
): text
+= os
.linesep
227 self
.write(self
.interp
.introText
)
228 except AttributeError:
231 def setBuiltinKeywords(self
):
232 """Create pseudo keywords as part of builtins.
234 This is a rather clever hack that sets "close", "exit" and "quit"
235 to a PseudoKeyword object so that we can make them do what we want.
236 In this case what we want is to call our self.quit() method.
237 The user can type "close", "exit" or "quit" without the final parens.
239 ## POB: This is having some weird side-effects so I'm taking it out.
240 ## import __builtin__
241 ## from pseudo import PseudoKeyword
242 ## __builtin__.close = __builtin__.exit = __builtin__.quit = \
243 ## PseudoKeyword(self.quit)
245 from pseudo
import PseudoKeyword
246 __builtin__
.close
= __builtin__
.exit
= __builtin__
.quit
= \
247 'Click on the close button to leave the application.'
250 """Quit the application."""
252 # XXX Good enough for now but later we want to send a close event.
254 # In the close event handler we can make sure they want to quit.
255 # Other applications, like PythonCard, may choose to hide rather than
256 # quit so we should just post the event and let the surrounding app
257 # decide what it wants to do.
258 self
.write('Click on the close button to leave the application.')
260 def setLocalShell(self
):
261 """Add 'shell' to locals as reference to ShellFacade instance."""
262 self
.interp
.locals['shell'] = ShellFacade(other
=self
)
264 def execStartupScript(self
, startupScript
):
265 """Execute the user's PYTHONSTARTUP script if they have one."""
266 if startupScript
and os
.path
.isfile(startupScript
):
267 startupText
= 'Startup script executed: ' + startupScript
268 self
.push('print %s;execfile(%s)' % \
269 (`startupText`
, `startupScript`
))
273 def setStyles(self
, faces
):
274 """Configure font size, typeface and color for lexer."""
277 self
.StyleSetSpec(wxSTC_STYLE_DEFAULT
, "face:%(mono)s,size:%(size)d,back:%(backcol)s" % faces
)
282 self
.StyleSetSpec(wxSTC_STYLE_LINENUMBER
, "back:#C0C0C0,face:%(mono)s,size:%(lnsize)d" % faces
)
283 self
.StyleSetSpec(wxSTC_STYLE_CONTROLCHAR
, "face:%(mono)s" % faces
)
284 self
.StyleSetSpec(wxSTC_STYLE_BRACELIGHT
, "fore:#0000FF,back:#FFFF88")
285 self
.StyleSetSpec(wxSTC_STYLE_BRACEBAD
, "fore:#FF0000,back:#FFFF88")
288 self
.StyleSetSpec(wxSTC_P_DEFAULT
, "face:%(mono)s" % faces
)
289 self
.StyleSetSpec(wxSTC_P_COMMENTLINE
, "fore:#007F00,face:%(mono)s" % faces
)
290 self
.StyleSetSpec(wxSTC_P_NUMBER
, "")
291 self
.StyleSetSpec(wxSTC_P_STRING
, "fore:#7F007F,face:%(mono)s" % faces
)
292 self
.StyleSetSpec(wxSTC_P_CHARACTER
, "fore:#7F007F,face:%(mono)s" % faces
)
293 self
.StyleSetSpec(wxSTC_P_WORD
, "fore:#00007F,bold")
294 self
.StyleSetSpec(wxSTC_P_TRIPLE
, "fore:#7F0000")
295 self
.StyleSetSpec(wxSTC_P_TRIPLEDOUBLE
, "fore:#000033,back:#FFFFE8")
296 self
.StyleSetSpec(wxSTC_P_CLASSNAME
, "fore:#0000FF,bold")
297 self
.StyleSetSpec(wxSTC_P_DEFNAME
, "fore:#007F7F,bold")
298 self
.StyleSetSpec(wxSTC_P_OPERATOR
, "")
299 self
.StyleSetSpec(wxSTC_P_IDENTIFIER
, "")
300 self
.StyleSetSpec(wxSTC_P_COMMENTBLOCK
, "fore:#7F7F7F")
301 self
.StyleSetSpec(wxSTC_P_STRINGEOL
, "fore:#000000,face:%(mono)s,back:#E0C0E0,eolfilled" % faces
)
303 def OnChar(self
, event
):
304 """Keypress event handler.
306 Prevents modification of previously submitted commands/responses."""
307 if not self
.CanEdit():
309 key
= event
.KeyCode()
310 currpos
= self
.GetCurrentPos()
311 stoppos
= self
.promptPosEnd
313 # The dot or period key activates auto completion.
314 # Get the command between the prompt and the cursor.
315 # Add a dot to the end of the command.
316 command
= self
.GetTextRange(stoppos
, currpos
) + '.'
318 if self
.autoComplete
: self
.autoCompleteShow(command
)
319 elif key
== ord('('):
320 # The left paren activates a call tip and cancels
321 # an active auto completion.
322 if self
.AutoCompActive(): self
.AutoCompCancel()
323 # Get the command between the prompt and the cursor.
324 # Add the '(' to the end of the command.
325 command
= self
.GetTextRange(stoppos
, currpos
) + '('
327 if self
.autoCallTip
: self
.autoCallTipShow(command
)
329 # Allow the normal event handling to take place.
332 def OnKeyDown(self
, event
):
333 """Key down event handler.
335 Prevents modification of previously submitted commands/responses."""
336 key
= event
.KeyCode()
337 controlDown
= event
.ControlDown()
338 altDown
= event
.AltDown()
339 shiftDown
= event
.ShiftDown()
340 currpos
= self
.GetCurrentPos()
341 endpos
= self
.GetTextLength()
342 # Return (Enter) is used to submit a command to the interpreter.
343 if not controlDown
and key
== WXK_RETURN
:
344 if self
.AutoCompActive(): self
.AutoCompCancel()
345 if self
.CallTipActive(): self
.CallTipCancel()
347 # Ctrl+Return (Cntrl+Enter) is used to insert a line break.
348 elif controlDown
and key
== WXK_RETURN
:
349 if self
.AutoCompActive(): self
.AutoCompCancel()
350 if self
.CallTipActive(): self
.CallTipCancel()
351 if currpos
== endpos
:
354 self
.insertLineBreak()
355 # If the auto-complete window is up let it do its thing.
356 elif self
.AutoCompActive():
358 # Let Ctrl-Alt-* get handled normally.
359 elif controlDown
and altDown
:
361 # Clear the current, unexecuted command.
362 elif key
== WXK_ESCAPE
:
363 if self
.CallTipActive():
367 # Cut to the clipboard.
368 elif (controlDown
and key
in (ord('X'), ord('x'))) \
369 or (shiftDown
and key
== WXK_DELETE
):
371 # Copy to the clipboard.
372 elif controlDown
and not shiftDown \
373 and key
in (ord('C'), ord('c'), WXK_INSERT
):
375 # Copy to the clipboard, including prompts.
376 elif controlDown
and shiftDown \
377 and key
in (ord('C'), ord('c'), WXK_INSERT
):
378 self
.CopyWithPrompts()
379 # Paste from the clipboard.
380 elif (controlDown
and not shiftDown \
381 and key
in (ord('V'), ord('v'))) \
382 or (shiftDown
and not controlDown
and key
== WXK_INSERT
):
384 # Paste from the clipboard, run commands.
385 elif controlDown
and shiftDown \
386 and key
in (ord('V'), ord('v')):
388 # Replace with the previous command from the history buffer.
389 elif (controlDown
and key
== WXK_UP
) \
390 or (altDown
and key
in (ord('P'), ord('p'))):
391 self
.OnHistoryReplace(step
=+1)
392 # Replace with the next command from the history buffer.
393 elif (controlDown
and key
== WXK_DOWN
) \
394 or (altDown
and key
in (ord('N'), ord('n'))):
395 self
.OnHistoryReplace(step
=-1)
396 # Insert the previous command from the history buffer.
397 elif (shiftDown
and key
== WXK_UP
):
398 self
.OnHistoryInsert(step
=+1)
399 # Insert the next command from the history buffer.
400 elif (shiftDown
and key
== WXK_DOWN
):
401 self
.OnHistoryInsert(step
=-1)
402 # Search up the history for the text in front of the cursor.
404 self
.OnHistorySearch()
405 # Home needs to be aware of the prompt.
406 elif key
== WXK_HOME
:
407 home
= self
.promptPosEnd
409 if event
.ShiftDown():
410 # Select text from current position to end of prompt.
411 self
.SetSelection(self
.GetCurrentPos(), home
)
413 self
.SetCurrentPos(home
)
415 self
.EnsureCaretVisible()
418 # Basic navigation keys should work anywhere.
419 elif key
in (WXK_END
, WXK_LEFT
, WXK_RIGHT
, WXK_UP
, WXK_DOWN
, \
420 WXK_PRIOR
, WXK_NEXT
):
422 # Don't backspace over the latest non-continuation prompt.
423 elif key
== WXK_BACK
:
424 if currpos
> self
.promptPosEnd
:
426 # Only allow these keys after the latest prompt.
427 elif key
in (WXK_TAB
, WXK_DELETE
):
430 # Don't toggle between insert mode and overwrite mode.
431 elif key
== WXK_INSERT
:
433 # Don't allow line deletion.
434 elif controlDown
and key
in (ord('L'), ord('l')):
436 # Don't allow line transposition.
437 elif controlDown
and key
in (ord('T'), ord('t')):
439 # Protect the readonly portion of the shell.
440 elif not self
.CanEdit():
445 def clearCommand(self
):
446 """Delete the current, unexecuted command."""
447 startpos
= self
.promptPosEnd
448 endpos
= self
.GetTextLength()
449 self
.SetSelection(startpos
, endpos
)
450 self
.ReplaceSelection('')
453 def OnHistoryReplace(self
, step
):
454 """Replace with the previous/next command from the history buffer."""
456 self
.replaceFromHistory(step
)
458 def replaceFromHistory(self
, step
):
459 """Replace selection with command from the history buffer."""
460 self
.ReplaceSelection('')
461 newindex
= self
.historyIndex
+ step
462 if -1 <= newindex
<= len(self
.history
):
463 self
.historyIndex
= newindex
464 if 0 <= newindex
<= len(self
.history
)-1:
465 command
= self
.history
[self
.historyIndex
]
466 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
467 self
.ReplaceSelection(command
)
469 def OnHistoryInsert(self
, step
):
470 """Insert the previous/next command from the history buffer."""
471 if not self
.CanEdit():
473 startpos
= self
.GetCurrentPos()
474 self
.replaceFromHistory(step
)
475 endpos
= self
.GetCurrentPos()
476 self
.SetSelection(endpos
, startpos
)
478 def OnHistorySearch(self
):
479 """Search up the history buffer for the text in front of the cursor."""
480 if not self
.CanEdit():
482 startpos
= self
.GetCurrentPos()
483 # The text up to the cursor is what we search for.
484 numCharsAfterCursor
= self
.GetTextLength() - startpos
485 searchText
= self
.getCommand(rstrip
=0)
486 if numCharsAfterCursor
> 0:
487 searchText
= searchText
[:-numCharsAfterCursor
]
490 # Search upwards from the current history position and loop back
491 # to the beginning if we don't find anything.
492 if (self
.historyIndex
<= -1) \
493 or (self
.historyIndex
>= len(self
.history
)-2):
494 searchOrder
= range(len(self
.history
))
496 searchOrder
= range(self
.historyIndex
+1, len(self
.history
)) + \
497 range(self
.historyIndex
)
498 for i
in searchOrder
:
499 command
= self
.history
[i
]
500 if command
[:len(searchText
)] == searchText
:
501 # Replace the current selection with the one we've found.
502 self
.ReplaceSelection(command
[len(searchText
):])
503 endpos
= self
.GetCurrentPos()
504 self
.SetSelection(endpos
, startpos
)
505 # We've now warped into middle of the history.
506 self
.historyIndex
= i
509 def setStatusText(self
, text
):
510 """Display status information."""
512 # This method will most likely be replaced by the enclosing app
513 # to do something more interesting, like write to a status bar.
516 def insertLineBreak(self
):
517 """Insert a new line break."""
519 self
.write(os
.linesep
)
523 def processLine(self
):
524 """Process the line of text at which the user hit Enter."""
526 # The user hit ENTER and we need to decide what to do. They could be
527 # sitting on any line in the shell.
529 thepos
= self
.GetCurrentPos()
530 startpos
= self
.promptPosEnd
531 endpos
= self
.GetTextLength()
532 # If they hit RETURN inside the current command, execute the command.
534 self
.SetCurrentPos(endpos
)
536 command
= self
.GetTextRange(startpos
, endpos
)
537 lines
= command
.split(os
.linesep
+ sys
.ps2
)
538 lines
= [line
.rstrip() for line
in lines
]
539 command
= '\n'.join(lines
)
541 # Or replace the current command with the other command.
543 # If the line contains a command (even an invalid one).
544 if self
.getCommand(rstrip
=0):
545 command
= self
.getMultilineCommand()
548 # Otherwise, put the cursor back where we started.
550 self
.SetCurrentPos(thepos
)
551 self
.SetAnchor(thepos
)
553 def getMultilineCommand(self
, rstrip
=1):
554 """Extract a multi-line command from the editor.
556 The command may not necessarily be valid Python syntax."""
557 # XXX Need to extract real prompts here. Need to keep track of the
558 # prompt every time a command is issued.
563 # This is a total hack job, but it works.
564 text
= self
.GetCurLine()[0]
565 line
= self
.GetCurrentLine()
566 while text
[:ps2size
] == ps2
and line
> 0:
569 text
= self
.GetCurLine()[0]
570 if text
[:ps1size
] == ps1
:
571 line
= self
.GetCurrentLine()
573 startpos
= self
.GetCurrentPos() + ps1size
576 while self
.GetCurLine()[0][:ps2size
] == ps2
:
579 stoppos
= self
.GetCurrentPos()
580 command
= self
.GetTextRange(startpos
, stoppos
)
581 command
= command
.replace(os
.linesep
+ sys
.ps2
, '\n')
582 command
= command
.rstrip()
583 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
587 command
= command
.rstrip()
590 def getCommand(self
, text
=None, rstrip
=1):
591 """Extract a command from text which may include a shell prompt.
593 The command may not necessarily be valid Python syntax."""
595 text
= self
.GetCurLine()[0]
596 # Strip the prompt off the front of text leaving just the command.
597 command
= self
.lstripPrompt(text
)
599 command
= '' # Real commands have prompts.
601 command
= command
.rstrip()
604 def lstripPrompt(self
, text
):
605 """Return text without a leading prompt."""
610 # Strip the prompt off the front of text.
611 if text
[:ps1size
] == ps1
:
612 text
= text
[ps1size
:]
613 elif text
[:ps2size
] == ps2
:
614 text
= text
[ps2size
:]
617 def push(self
, command
):
618 """Send command to the interpreter for execution."""
619 self
.write(os
.linesep
)
620 self
.more
= self
.interp
.push(command
)
622 self
.addHistory(command
.rstrip())
625 def addHistory(self
, command
):
626 """Add command to the command history."""
627 # Reset the history position.
628 self
.historyIndex
= -1
629 # Insert this command into the history, unless it's a blank
630 # line or the same as the last command.
632 and (len(self
.history
) == 0 or command
!= self
.history
[0]):
633 self
.history
.insert(0, command
)
635 def write(self
, text
):
636 """Display text in the shell.
638 Replace line endings with OS-specific endings."""
639 text
= self
.fixLineEndings(text
)
641 self
.EnsureCaretVisible()
643 def fixLineEndings(self
, text
):
644 """Return text with line endings replaced by OS-specific endings."""
645 lines
= text
.split('\r\n')
646 for l
in range(len(lines
)):
647 chunks
= lines
[l
].split('\r')
648 for c
in range(len(chunks
)):
649 chunks
[c
] = os
.linesep
.join(chunks
[c
].split('\n'))
650 lines
[l
] = os
.linesep
.join(chunks
)
651 text
= os
.linesep
.join(lines
)
655 """Display appropriate prompt for the context, either ps1 or ps2.
657 If this is a continuation line, autoindent as necessary."""
659 prompt
= str(sys
.ps2
)
661 prompt
= str(sys
.ps1
)
662 pos
= self
.GetCurLine()[1]
663 if pos
> 0: self
.write(os
.linesep
)
665 self
.promptPosStart
= self
.GetCurrentPos()
668 self
.promptPosEnd
= self
.GetCurrentPos()
669 # Keep the undo feature from undoing previous responses.
670 self
.EmptyUndoBuffer()
671 # XXX Add some autoindent magic here if more.
673 self
.write(' '*4) # Temporary hack indentation.
674 self
.EnsureCaretVisible()
675 self
.ScrollToColumn(0)
678 """Replacement for stdin."""
679 prompt
= 'Please enter your response:'
680 dialog
= wxTextEntryDialog(None, prompt
, \
681 'Input Dialog (Standard)', '')
683 if dialog
.ShowModal() == wxID_OK
:
684 text
= dialog
.GetValue()
685 self
.write(text
+ os
.linesep
)
691 def readRaw(self
, prompt
='Please enter your response:'):
692 """Replacement for raw_input."""
693 dialog
= wxTextEntryDialog(None, prompt
, \
694 'Input Dialog (Raw)', '')
696 if dialog
.ShowModal() == wxID_OK
:
697 text
= dialog
.GetValue()
703 def ask(self
, prompt
='Please enter your response:'):
704 """Get response from the user."""
705 return raw_input(prompt
=prompt
)
708 """Halt execution pending a response from the user."""
709 self
.ask('Press enter to continue:')
712 """Delete all text from the shell."""
715 def run(self
, command
, prompt
=1, verbose
=1):
716 """Execute command within the shell as if it was typed in directly.
717 >>> shell.run('print "this"')
722 # Go to the very bottom of the text.
723 endpos
= self
.GetTextLength()
724 self
.SetCurrentPos(endpos
)
725 command
= command
.rstrip()
726 if prompt
: self
.prompt()
727 if verbose
: self
.write(command
)
730 def runfile(self
, filename
):
731 """Execute all commands in file as if they were typed into the shell."""
732 file = open(filename
)
735 for command
in file.readlines():
736 if command
[:6] == 'shell.': # Run shell methods silently.
737 self
.run(command
, prompt
=0, verbose
=0)
739 self
.run(command
, prompt
=0, verbose
=1)
743 def autoCompleteShow(self
, command
):
744 """Display auto-completion popup list."""
745 list = self
.interp
.getAutoCompleteList(command
, \
746 includeMagic
=self
.autoCompleteIncludeMagic
, \
747 includeSingle
=self
.autoCompleteIncludeSingle
, \
748 includeDouble
=self
.autoCompleteIncludeDouble
)
750 options
= ' '.join(list)
752 self
.AutoCompShow(offset
, options
)
754 def autoCallTipShow(self
, command
):
755 """Display argument spec and docstring in a popup bubble thingie."""
756 if self
.CallTipActive
: self
.CallTipCancel()
757 (name
, argspec
, tip
) = self
.interp
.getCallTip(command
)
759 startpos
= self
.GetCurrentPos()
760 self
.write(argspec
+ ')')
761 endpos
= self
.GetCurrentPos()
762 self
.SetSelection(endpos
, startpos
)
764 curpos
= self
.GetCurrentPos()
765 tippos
= curpos
- (len(name
) + 1)
766 fallback
= curpos
- self
.GetColumn(curpos
)
767 # In case there isn't enough room, only go back to the fallback.
768 tippos
= max(tippos
, fallback
)
769 self
.CallTipShow(tippos
, tip
)
771 def writeOut(self
, text
):
772 """Replacement for stdout."""
775 def writeErr(self
, text
):
776 """Replacement for stderr."""
779 def redirectStdin(self
, redirect
=1):
780 """If redirect is true then sys.stdin will come from the shell."""
782 sys
.stdin
= PseudoFileIn(self
.readIn
)
784 sys
.stdin
= self
.stdin
786 def redirectStdout(self
, redirect
=1):
787 """If redirect is true then sys.stdout will go to the shell."""
789 sys
.stdout
= PseudoFileOut(self
.writeOut
)
791 sys
.stdout
= self
.stdout
793 def redirectStderr(self
, redirect
=1):
794 """If redirect is true then sys.stderr will go to the shell."""
796 sys
.stderr
= PseudoFileErr(self
.writeErr
)
798 sys
.stderr
= self
.stderr
801 """Return true if text is selected and can be cut."""
802 if self
.GetSelectionStart() != self
.GetSelectionEnd() \
803 and self
.GetSelectionStart() >= self
.promptPosEnd \
804 and self
.GetSelectionEnd() >= self
.promptPosEnd
:
810 """Return true if text is selected and can be copied."""
811 return self
.GetSelectionStart() != self
.GetSelectionEnd()
814 """Return true if a paste should succeed."""
815 if self
.CanEdit() and wxStyledTextCtrl
.CanPaste(self
):
821 """Return true if editing should succeed."""
822 return self
.GetCurrentPos() >= self
.promptPosEnd
825 """Remove selection and place it on the clipboard."""
826 if self
.CanCut() and self
.CanCopy():
827 if self
.AutoCompActive(): self
.AutoCompCancel()
828 if self
.CallTipActive
: self
.CallTipCancel()
830 self
.ReplaceSelection('')
833 """Copy selection and place it on the clipboard."""
835 command
= self
.GetSelectedText()
836 command
= command
.replace(os
.linesep
+ sys
.ps2
, os
.linesep
)
837 command
= command
.replace(os
.linesep
+ sys
.ps1
, os
.linesep
)
838 command
= self
.lstripPrompt(text
=command
)
839 data
= wxTextDataObject(command
)
840 if wxTheClipboard
.Open():
841 wxTheClipboard
.SetData(data
)
842 wxTheClipboard
.Close()
844 def CopyWithPrompts(self
):
845 """Copy selection, including prompts, and place it on the clipboard."""
847 command
= self
.GetSelectedText()
848 data
= wxTextDataObject(command
)
849 if wxTheClipboard
.Open():
850 wxTheClipboard
.SetData(data
)
851 wxTheClipboard
.Close()
854 """Replace selection with clipboard contents."""
855 if self
.CanPaste() and wxTheClipboard
.Open():
856 if wxTheClipboard
.IsSupported(wxDataFormat(wxDF_TEXT
)):
857 data
= wxTextDataObject()
858 if wxTheClipboard
.GetData(data
):
859 self
.ReplaceSelection('')
860 command
= data
.GetText()
861 command
= command
.rstrip()
862 command
= self
.fixLineEndings(command
)
863 command
= self
.lstripPrompt(text
=command
)
864 command
= command
.replace(os
.linesep
+ sys
.ps2
, '\n')
865 command
= command
.replace(os
.linesep
, '\n')
866 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
868 wxTheClipboard
.Close()
870 def PasteAndRun(self
):
871 """Replace selection with clipboard contents, run commands."""
872 if wxTheClipboard
.Open():
873 if wxTheClipboard
.IsSupported(wxDataFormat(wxDF_TEXT
)):
874 data
= wxTextDataObject()
875 if wxTheClipboard
.GetData(data
):
876 endpos
= self
.GetTextLength()
877 self
.SetCurrentPos(endpos
)
878 startpos
= self
.promptPosEnd
879 self
.SetSelection(startpos
, endpos
)
880 self
.ReplaceSelection('')
881 text
= data
.GetText()
883 text
= self
.fixLineEndings(text
)
884 text
= self
.lstripPrompt(text
=text
)
885 text
= text
.replace(os
.linesep
+ sys
.ps1
, '\n')
886 text
= text
.replace(os
.linesep
+ sys
.ps2
, '\n')
887 text
= text
.replace(os
.linesep
, '\n')
888 lines
= text
.split('\n')
892 if line
.strip() != '' and line
.lstrip() == line
:
895 # Add the previous command to the list.
896 commands
.append(command
)
897 # Start a new command, which may be multiline.
900 # Multiline command. Add to the command.
903 commands
.append(command
)
904 for command
in commands
:
905 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
908 wxTheClipboard
.Close()
911 wxID_SELECTALL
= NewId() # This *should* be defined by wxPython.
912 ID_AUTOCOMP
= NewId()
913 ID_AUTOCOMP_SHOW
= NewId()
914 ID_AUTOCOMP_INCLUDE_MAGIC
= NewId()
915 ID_AUTOCOMP_INCLUDE_SINGLE
= NewId()
916 ID_AUTOCOMP_INCLUDE_DOUBLE
= NewId()
917 ID_CALLTIPS
= NewId()
918 ID_CALLTIPS_SHOW
= NewId()
922 """Mixin class to add standard menu items."""
924 def createMenus(self
):
925 m
= self
.fileMenu
= wxMenu()
927 m
.Append(wxID_EXIT
, 'E&xit', 'Exit PyCrust')
929 m
= self
.editMenu
= wxMenu()
930 m
.Append(wxID_UNDO
, '&Undo \tCtrl+Z', 'Undo the last action')
931 m
.Append(wxID_REDO
, '&Redo \tCtrl+Y', 'Redo the last undone action')
933 m
.Append(wxID_CUT
, 'Cu&t \tCtrl+X', 'Cut the selection')
934 m
.Append(wxID_COPY
, '&Copy \tCtrl+C', 'Copy the selection')
935 m
.Append(wxID_PASTE
, '&Paste \tCtrl+V', 'Paste')
937 m
.Append(wxID_CLEAR
, 'Cle&ar', 'Delete the selection')
938 m
.Append(wxID_SELECTALL
, 'Select A&ll', 'Select all text')
940 m
= self
.autocompMenu
= wxMenu()
941 m
.Append(ID_AUTOCOMP_SHOW
, 'Show Auto Completion', \
942 'Show auto completion during dot syntax', \
944 m
.Append(ID_AUTOCOMP_INCLUDE_MAGIC
, 'Include Magic Attributes', \
945 'Include attributes visible to __getattr__ and __setattr__', \
947 m
.Append(ID_AUTOCOMP_INCLUDE_SINGLE
, 'Include Single Underscores', \
948 'Include attibutes prefixed by a single underscore', \
950 m
.Append(ID_AUTOCOMP_INCLUDE_DOUBLE
, 'Include Double Underscores', \
951 'Include attibutes prefixed by a double underscore', \
954 m
= self
.calltipsMenu
= wxMenu()
955 m
.Append(ID_CALLTIPS_SHOW
, 'Show Call Tips', \
956 'Show call tips with argument specifications', kind
=wxITEM_CHECK
)
958 m
= self
.optionsMenu
= wxMenu()
959 m
.AppendMenu(ID_AUTOCOMP
, '&Auto Completion', self
.autocompMenu
, \
960 'Auto Completion Options')
961 m
.AppendMenu(ID_CALLTIPS
, '&Call Tips', self
.calltipsMenu
, \
964 m
= self
.helpMenu
= wxMenu()
966 m
.Append(wxID_ABOUT
, '&About...', 'About PyCrust')
968 b
= self
.menuBar
= wxMenuBar()
969 b
.Append(self
.fileMenu
, '&File')
970 b
.Append(self
.editMenu
, '&Edit')
971 b
.Append(self
.optionsMenu
, '&Options')
972 b
.Append(self
.helpMenu
, '&Help')
975 EVT_MENU(self
, wxID_EXIT
, self
.OnExit
)
976 EVT_MENU(self
, wxID_UNDO
, self
.OnUndo
)
977 EVT_MENU(self
, wxID_REDO
, self
.OnRedo
)
978 EVT_MENU(self
, wxID_CUT
, self
.OnCut
)
979 EVT_MENU(self
, wxID_COPY
, self
.OnCopy
)
980 EVT_MENU(self
, wxID_PASTE
, self
.OnPaste
)
981 EVT_MENU(self
, wxID_CLEAR
, self
.OnClear
)
982 EVT_MENU(self
, wxID_SELECTALL
, self
.OnSelectAll
)
983 EVT_MENU(self
, wxID_ABOUT
, self
.OnAbout
)
984 EVT_MENU(self
, ID_AUTOCOMP_SHOW
, \
985 self
.OnAutoCompleteShow
)
986 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_MAGIC
, \
987 self
.OnAutoCompleteIncludeMagic
)
988 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_SINGLE
, \
989 self
.OnAutoCompleteIncludeSingle
)
990 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_DOUBLE
, \
991 self
.OnAutoCompleteIncludeDouble
)
992 EVT_MENU(self
, ID_CALLTIPS_SHOW
, \
995 EVT_UPDATE_UI(self
, wxID_UNDO
, self
.OnUpdateMenu
)
996 EVT_UPDATE_UI(self
, wxID_REDO
, self
.OnUpdateMenu
)
997 EVT_UPDATE_UI(self
, wxID_CUT
, self
.OnUpdateMenu
)
998 EVT_UPDATE_UI(self
, wxID_COPY
, self
.OnUpdateMenu
)
999 EVT_UPDATE_UI(self
, wxID_PASTE
, self
.OnUpdateMenu
)
1000 EVT_UPDATE_UI(self
, wxID_CLEAR
, self
.OnUpdateMenu
)
1001 EVT_UPDATE_UI(self
, ID_AUTOCOMP_SHOW
, self
.OnUpdateMenu
)
1002 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_MAGIC
, self
.OnUpdateMenu
)
1003 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_SINGLE
, self
.OnUpdateMenu
)
1004 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_DOUBLE
, self
.OnUpdateMenu
)
1005 EVT_UPDATE_UI(self
, ID_CALLTIPS_SHOW
, self
.OnUpdateMenu
)
1007 def OnExit(self
, event
):
1010 def OnUndo(self
, event
):
1013 def OnRedo(self
, event
):
1016 def OnCut(self
, event
):
1019 def OnCopy(self
, event
):
1022 def OnPaste(self
, event
):
1025 def OnClear(self
, event
):
1028 def OnSelectAll(self
, event
):
1029 self
.shell
.SelectAll()
1031 def OnAbout(self
, event
):
1032 """Display an About PyCrust window."""
1034 title
= 'About PyCrust'
1035 text
= 'PyCrust %s\n\n' % VERSION
+ \
1036 'Yet another Python shell, only flakier.\n\n' + \
1037 'Half-baked by Patrick K. O\'Brien,\n' + \
1038 'the other half is still in the oven.\n\n' + \
1039 'Shell Revision: %s\n' % self
.shell
.revision
+ \
1040 'Interpreter Revision: %s\n\n' % self
.shell
.interp
.revision
+ \
1041 'Python Version: %s\n' % sys
.version
.split()[0] + \
1042 'wxPython Version: %s\n' % wx
.__version
__ + \
1043 'Platform: %s\n' % sys
.platform
1044 dialog
= wxMessageDialog(self
, text
, title
, wxOK | wxICON_INFORMATION
)
1048 def OnAutoCompleteShow(self
, event
):
1049 self
.shell
.autoComplete
= event
.IsChecked()
1051 def OnAutoCompleteIncludeMagic(self
, event
):
1052 self
.shell
.autoCompleteIncludeMagic
= event
.IsChecked()
1054 def OnAutoCompleteIncludeSingle(self
, event
):
1055 self
.shell
.autoCompleteIncludeSingle
= event
.IsChecked()
1057 def OnAutoCompleteIncludeDouble(self
, event
):
1058 self
.shell
.autoCompleteIncludeDouble
= event
.IsChecked()
1060 def OnCallTipsShow(self
, event
):
1061 self
.shell
.autoCallTip
= event
.IsChecked()
1063 def OnUpdateMenu(self
, event
):
1064 """Update menu items based on current status."""
1067 event
.Enable(self
.shell
.CanUndo())
1068 elif id == wxID_REDO
:
1069 event
.Enable(self
.shell
.CanRedo())
1070 elif id == wxID_CUT
:
1071 event
.Enable(self
.shell
.CanCut())
1072 elif id == wxID_COPY
:
1073 event
.Enable(self
.shell
.CanCopy())
1074 elif id == wxID_PASTE
:
1075 event
.Enable(self
.shell
.CanPaste())
1076 elif id == wxID_CLEAR
:
1077 event
.Enable(self
.shell
.CanCut())
1078 elif id == ID_AUTOCOMP_SHOW
:
1079 event
.Check(self
.shell
.autoComplete
)
1080 elif id == ID_AUTOCOMP_INCLUDE_MAGIC
:
1081 event
.Check(self
.shell
.autoCompleteIncludeMagic
)
1082 elif id == ID_AUTOCOMP_INCLUDE_SINGLE
:
1083 event
.Check(self
.shell
.autoCompleteIncludeSingle
)
1084 elif id == ID_AUTOCOMP_INCLUDE_DOUBLE
:
1085 event
.Check(self
.shell
.autoCompleteIncludeDouble
)
1086 elif id == ID_CALLTIPS_SHOW
:
1087 event
.Check(self
.shell
.autoCallTip
)
1090 class ShellFrame(wxFrame
, ShellMenu
):
1091 """Frame containing the PyCrust shell component."""
1093 name
= 'PyCrust Shell Frame'
1094 revision
= __version__
1096 def __init__(self
, parent
=None, id=-1, title
='PyShell', \
1097 pos
=wxDefaultPosition
, size
=wxDefaultSize
, \
1098 style
=wxDEFAULT_FRAME_STYLE
, locals=None, \
1099 InterpClass
=None, *args
, **kwds
):
1100 """Create a PyCrust ShellFrame instance."""
1101 wxFrame
.__init
__(self
, parent
, id, title
, pos
, size
, style
)
1102 intro
= 'Welcome To PyCrust %s - The Flakiest Python Shell' % VERSION
1103 intro
+= '\nSponsored by Orbtech - Your Source For Python Development Services'
1104 self
.CreateStatusBar()
1105 self
.SetStatusText(intro
.replace('\n', ', '))
1106 if wxPlatform
== '__WXMSW__':
1108 filename
= os
.path
.join(os
.path
.dirname(__file__
), 'PyCrust.ico')
1109 icon
= wxIcon(filename
, wxBITMAP_TYPE_ICO
)
1111 self
.shell
= Shell(parent
=self
, id=-1, introText
=intro
, \
1112 locals=locals, InterpClass
=InterpClass
, \
1114 # Override the shell so that status messages go to the status bar.
1115 self
.shell
.setStatusText
= self
.SetStatusText