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
21 sys
.ps3
= '<-- ' # Input prompt.
23 NAVKEYS
= (WXK_END
, WXK_LEFT
, WXK_RIGHT
, WXK_UP
, WXK_DOWN
, WXK_PRIOR
, WXK_NEXT
)
25 if wxPlatform
== '__WXMSW__':
26 faces
= { 'times' : 'Times New Roman',
27 'mono' : 'Courier New',
28 'helv' : 'Lucida Console',
29 'lucida' : 'Lucida Console',
30 'other' : 'Comic Sans MS',
35 # Versions of wxPython prior to 2.3.2 had a sizing bug on Win platform.
36 # The font was 2 points too large. So we need to reduce the font size.
37 if (wxMAJOR_VERSION
, wxMINOR_VERSION
, wxRELEASE_NUMBER
) < (2, 3, 2):
41 faces
= { 'times' : 'Times',
44 'other' : 'new century schoolbook',
52 """Simplified interface to all shell-related functionality.
54 This is a semi-transparent facade, in that all attributes of other are
55 still accessible, even though only some are visible to the user."""
57 name
= 'PyCrust Shell Interface'
58 revision
= __revision__
60 def __init__(self
, other
):
61 """Create a ShellFacade instance."""
75 for method
in methods
:
76 self
.__dict
__[method
] = getattr(other
, method
)
82 Home Go to the beginning of the command or line.
83 Shift+Home Select to the beginning of the command or line.
84 Shift+End Select to the end of the line.
85 End Go to the end of the line.
86 Ctrl+C Copy selected text, removing prompts.
87 Ctrl+Shift+C Copy selected text, retaining prompts.
88 Ctrl+X Cut selected text.
89 Ctrl+V Paste from clipboard.
90 Ctrl+Shift+V Paste and run multiple commands from clipboard.
91 Ctrl+Up Arrow Retrieve Previous History item.
92 Alt+P Retrieve Previous History item.
93 Ctrl+Down Arrow Retrieve Next History item.
94 Alt+N Retrieve Next History item.
95 Shift+Up Arrow Insert Previous History item.
96 Shift+Down Arrow Insert Next History item.
97 F8 Command-completion of History item.
98 (Type a few characters of a previous command and then press F8.)
102 """Display some useful information about how to use the shell."""
103 self
.write(self
.helpText
)
105 def __getattr__(self
, name
):
106 if hasattr(self
.other
, name
):
107 return getattr(self
.other
, name
)
109 raise AttributeError, name
111 def __setattr__(self
, name
, value
):
112 if self
.__dict
__.has_key(name
):
113 self
.__dict
__[name
] = value
114 elif hasattr(self
.other
, name
):
115 return setattr(self
.other
, name
, value
)
117 raise AttributeError, name
119 def _getAttributeNames(self
):
120 """Return list of magic attributes to extend introspection."""
121 list = ['autoCallTip',
123 'autoCompleteCaseInsensitive',
124 'autoCompleteIncludeDouble',
125 'autoCompleteIncludeMagic',
126 'autoCompleteIncludeSingle',
132 class Shell(wxStyledTextCtrl
):
133 """PyCrust Shell based on wxStyledTextCtrl."""
135 name
= 'PyCrust Shell'
136 revision
= __revision__
138 def __init__(self
, parent
, id=-1, pos
=wxDefaultPosition
, \
139 size
=wxDefaultSize
, style
=wxCLIP_CHILDREN
, introText
='', \
140 locals=None, InterpClass
=None, *args
, **kwds
):
141 """Create a PyCrust Shell instance."""
142 wxStyledTextCtrl
.__init
__(self
, parent
, id, pos
, size
, style
)
143 # Grab these so they can be restored by self.redirect* methods.
144 self
.stdin
= sys
.stdin
145 self
.stdout
= sys
.stdout
146 self
.stderr
= sys
.stderr
147 # Add the current working directory "." to the search path.
148 sys
.path
.insert(0, os
.curdir
)
149 # Import a default interpreter class if one isn't provided.
150 if InterpClass
== None:
151 from interpreter
import Interpreter
153 Interpreter
= InterpClass
154 # Create default locals so we have something interesting.
155 shellLocals
= {'__name__': 'PyCrust-Shell',
156 '__doc__': 'PyCrust-Shell, The PyCrust Python Shell.',
157 '__version__': VERSION
,
159 # Add the dictionary that was passed in.
161 shellLocals
.update(locals)
162 # Create a replacement for stdin.
163 self
.reader
= PseudoFileIn(self
.readline
)
164 self
.reader
.input = ''
165 self
.reader
.isreading
= 0
166 # Set up the interpreter.
167 self
.interp
= Interpreter(locals=shellLocals
, \
168 rawin
=self
.raw_input, \
170 stdout
=PseudoFileOut(self
.writeOut
), \
171 stderr
=PseudoFileErr(self
.writeErr
), \
173 # Find out for which keycodes the interpreter will autocomplete.
174 self
.autoCompleteKeys
= self
.interp
.getAutoCompleteKeys()
175 # Keep track of the last non-continuation prompt positions.
176 self
.promptPosStart
= 0
177 self
.promptPosEnd
= 0
178 # Keep track of multi-line commands.
180 # Create the command history. Commands are added into the front of
181 # the list (ie. at index 0) as they are entered. self.historyIndex
182 # is the current position in the history; it gets incremented as you
183 # retrieve the previous command, decremented as you retrieve the
184 # next, and reset when you hit Enter. self.historyIndex == -1 means
185 # you're on the current command, not in the history.
187 self
.historyIndex
= -1
188 # Assign handlers for keyboard events.
189 EVT_KEY_DOWN(self
, self
.OnKeyDown
)
190 EVT_CHAR(self
, self
.OnChar
)
191 # Assign handlers for wxSTC events.
192 EVT_STC_UPDATEUI(self
, id, self
.OnUpdateUI
)
193 # Configure various defaults and user preferences.
195 # Display the introductory banner information.
196 try: self
.showIntro(introText
)
198 # Assign some pseudo keywords to the interpreter's namespace.
199 try: self
.setBuiltinKeywords()
201 # Add 'shell' to the interpreter's local namespace.
202 try: self
.setLocalShell()
204 # Do this last so the user has complete control over their
205 # environment. They can override anything they want.
206 try: self
.execStartupScript(self
.interp
.startupScript
)
214 """Configure shell based on user preferences."""
215 self
.SetMarginType(1, wxSTC_MARGIN_NUMBER
)
216 self
.SetMarginWidth(1, 40)
218 self
.SetLexer(wxSTC_LEX_PYTHON
)
219 self
.SetKeyWords(0, ' '.join(keyword
.kwlist
))
221 self
.setStyles(faces
)
222 self
.SetViewWhiteSpace(0)
225 # Do we want to automatically pop up command completion options?
226 self
.autoComplete
= 1
227 self
.autoCompleteIncludeMagic
= 1
228 self
.autoCompleteIncludeSingle
= 1
229 self
.autoCompleteIncludeDouble
= 1
230 self
.autoCompleteCaseInsensitive
= 1
231 self
.AutoCompSetIgnoreCase(self
.autoCompleteCaseInsensitive
)
232 # Do we want to automatically pop up command argument help?
234 self
.CallTipSetBackground(wxColour(255, 255, 232))
237 self
.SetEndAtLastLine(false
)
238 except AttributeError:
241 def showIntro(self
, text
=''):
242 """Display introductory text in the shell."""
244 if not text
.endswith(os
.linesep
): text
+= os
.linesep
247 self
.write(self
.interp
.introText
)
248 except AttributeError:
250 wxCallAfter(self
.ScrollToLine
, 0)
252 def setBuiltinKeywords(self
):
253 """Create pseudo keywords as part of builtins.
255 This simply sets "close", "exit" and "quit" to a helpful string.
258 __builtin__
.close
= __builtin__
.exit
= __builtin__
.quit
= \
259 'Click on the close button to leave the application.'
262 """Quit the application."""
264 # XXX Good enough for now but later we want to send a close event.
266 # In the close event handler we can make sure they want to quit.
267 # Other applications, like PythonCard, may choose to hide rather than
268 # quit so we should just post the event and let the surrounding app
269 # decide what it wants to do.
270 self
.write('Click on the close button to leave the application.')
272 def setLocalShell(self
):
273 """Add 'shell' to locals as reference to ShellFacade instance."""
274 self
.interp
.locals['shell'] = ShellFacade(other
=self
)
276 def execStartupScript(self
, startupScript
):
277 """Execute the user's PYTHONSTARTUP script if they have one."""
278 if startupScript
and os
.path
.isfile(startupScript
):
279 startupText
= 'Startup script executed: ' + startupScript
280 self
.push('print %s;execfile(%s)' % \
281 (`startupText`
, `startupScript`
))
285 def setStyles(self
, faces
):
286 """Configure font size, typeface and color for lexer."""
289 self
.StyleSetSpec(wxSTC_STYLE_DEFAULT
, "face:%(mono)s,size:%(size)d,back:%(backcol)s" % faces
)
294 self
.StyleSetSpec(wxSTC_STYLE_LINENUMBER
, "back:#C0C0C0,face:%(mono)s,size:%(lnsize)d" % faces
)
295 self
.StyleSetSpec(wxSTC_STYLE_CONTROLCHAR
, "face:%(mono)s" % faces
)
296 self
.StyleSetSpec(wxSTC_STYLE_BRACELIGHT
, "fore:#0000FF,back:#FFFF88")
297 self
.StyleSetSpec(wxSTC_STYLE_BRACEBAD
, "fore:#FF0000,back:#FFFF88")
300 self
.StyleSetSpec(wxSTC_P_DEFAULT
, "face:%(mono)s" % faces
)
301 self
.StyleSetSpec(wxSTC_P_COMMENTLINE
, "fore:#007F00,face:%(mono)s" % faces
)
302 self
.StyleSetSpec(wxSTC_P_NUMBER
, "")
303 self
.StyleSetSpec(wxSTC_P_STRING
, "fore:#7F007F,face:%(mono)s" % faces
)
304 self
.StyleSetSpec(wxSTC_P_CHARACTER
, "fore:#7F007F,face:%(mono)s" % faces
)
305 self
.StyleSetSpec(wxSTC_P_WORD
, "fore:#00007F,bold")
306 self
.StyleSetSpec(wxSTC_P_TRIPLE
, "fore:#7F0000")
307 self
.StyleSetSpec(wxSTC_P_TRIPLEDOUBLE
, "fore:#000033,back:#FFFFE8")
308 self
.StyleSetSpec(wxSTC_P_CLASSNAME
, "fore:#0000FF,bold")
309 self
.StyleSetSpec(wxSTC_P_DEFNAME
, "fore:#007F7F,bold")
310 self
.StyleSetSpec(wxSTC_P_OPERATOR
, "")
311 self
.StyleSetSpec(wxSTC_P_IDENTIFIER
, "")
312 self
.StyleSetSpec(wxSTC_P_COMMENTBLOCK
, "fore:#7F7F7F")
313 self
.StyleSetSpec(wxSTC_P_STRINGEOL
, "fore:#000000,face:%(mono)s,back:#E0C0E0,eolfilled" % faces
)
315 def OnUpdateUI(self
, evt
):
316 """Check for matching braces."""
320 caretPos
= self
.GetCurrentPos()
322 charBefore
= self
.GetCharAt(caretPos
- 1)
323 #*** Patch to fix bug in wxSTC for wxPython < 2.3.3.
325 charBefore
= 32 # Mimic a space.
327 styleBefore
= self
.GetStyleAt(caretPos
- 1)
330 if charBefore
and chr(charBefore
) in '[]{}()' \
331 and styleBefore
== wxSTC_P_OPERATOR
:
332 braceAtCaret
= caretPos
- 1
336 charAfter
= self
.GetCharAt(caretPos
)
337 #*** Patch to fix bug in wxSTC for wxPython < 2.3.3.
339 charAfter
= 32 # Mimic a space.
341 styleAfter
= self
.GetStyleAt(caretPos
)
342 if charAfter
and chr(charAfter
) in '[]{}()' \
343 and styleAfter
== wxSTC_P_OPERATOR
:
344 braceAtCaret
= caretPos
346 if braceAtCaret
>= 0:
347 braceOpposite
= self
.BraceMatch(braceAtCaret
)
349 if braceAtCaret
!= -1 and braceOpposite
== -1:
350 self
.BraceBadLight(braceAtCaret
)
352 self
.BraceHighlight(braceAtCaret
, braceOpposite
)
354 def OnChar(self
, event
):
355 """Keypress event handler.
357 Only receives an event if OnKeyDown calls event.Skip() for
358 the corresponding event."""
360 # Prevent modification of previously submitted commands/responses.
361 if not self
.CanEdit():
363 key
= event
.KeyCode()
364 currpos
= self
.GetCurrentPos()
365 stoppos
= self
.promptPosEnd
366 # Return (Enter) needs to be ignored in this handler.
367 if key
== WXK_RETURN
:
369 elif key
in self
.autoCompleteKeys
:
370 # Usually the dot (period) key activates auto completion.
371 # Get the command between the prompt and the cursor.
372 # Add the autocomplete character to the end of the command.
373 command
= self
.GetTextRange(stoppos
, currpos
) + chr(key
)
375 if self
.autoComplete
: self
.autoCompleteShow(command
)
376 elif key
== ord('('):
377 # The left paren activates a call tip and cancels
378 # an active auto completion.
379 if self
.AutoCompActive(): self
.AutoCompCancel()
380 # Get the command between the prompt and the cursor.
381 # Add the '(' to the end of the command.
382 self
.ReplaceSelection('')
383 command
= self
.GetTextRange(stoppos
, currpos
) + '('
385 if self
.autoCallTip
: self
.autoCallTipShow(command
)
387 # Allow the normal event handling to take place.
390 def OnKeyDown(self
, event
):
391 """Key down event handler."""
393 # Prevent modification of previously submitted commands/responses.
394 key
= event
.KeyCode()
395 controlDown
= event
.ControlDown()
396 altDown
= event
.AltDown()
397 shiftDown
= event
.ShiftDown()
398 currpos
= self
.GetCurrentPos()
399 endpos
= self
.GetTextLength()
400 selecting
= self
.GetSelectionStart() != self
.GetSelectionEnd()
401 # Return (Enter) is used to submit a command to the interpreter.
402 if not controlDown
and key
== WXK_RETURN
:
403 if self
.AutoCompActive(): self
.AutoCompCancel()
404 if self
.CallTipActive(): self
.CallTipCancel()
406 # Ctrl+Return (Cntrl+Enter) is used to insert a line break.
407 elif controlDown
and key
== WXK_RETURN
:
408 if self
.AutoCompActive(): self
.AutoCompCancel()
409 if self
.CallTipActive(): self
.CallTipCancel()
410 if currpos
== endpos
:
413 self
.insertLineBreak()
414 # If the auto-complete window is up let it do its thing.
415 elif self
.AutoCompActive():
417 # Let Ctrl-Alt-* get handled normally.
418 elif controlDown
and altDown
:
420 # Clear the current, unexecuted command.
421 elif key
== WXK_ESCAPE
:
422 if self
.CallTipActive():
426 # Cut to the clipboard.
427 elif (controlDown
and key
in (ord('X'), ord('x'))) \
428 or (shiftDown
and key
== WXK_DELETE
):
430 # Copy to the clipboard.
431 elif controlDown
and not shiftDown \
432 and key
in (ord('C'), ord('c'), WXK_INSERT
):
434 # Copy to the clipboard, including prompts.
435 elif controlDown
and shiftDown \
436 and key
in (ord('C'), ord('c'), WXK_INSERT
):
437 self
.CopyWithPrompts()
438 # Home needs to be aware of the prompt.
439 elif key
== WXK_HOME
:
440 home
= self
.promptPosEnd
442 self
.SetCurrentPos(home
)
443 if not selecting
and not shiftDown
:
445 self
.EnsureCaretVisible()
449 # The following handlers modify text, so we need to see if there
450 # is a selection that includes text prior to the prompt.
452 # Don't modify a selection with text prior to the prompt.
453 elif selecting
and key
not in NAVKEYS
and not self
.CanEdit():
455 # Paste from the clipboard.
456 elif (controlDown
and not shiftDown \
457 and key
in (ord('V'), ord('v'))) \
458 or (shiftDown
and not controlDown
and key
== WXK_INSERT
):
460 # Paste from the clipboard, run commands.
461 elif controlDown
and shiftDown \
462 and key
in (ord('V'), ord('v')):
464 # Replace with the previous command from the history buffer.
465 elif (controlDown
and key
== WXK_UP
) \
466 or (altDown
and key
in (ord('P'), ord('p'))):
467 self
.OnHistoryReplace(step
=+1)
468 # Replace with the next command from the history buffer.
469 elif (controlDown
and key
== WXK_DOWN
) \
470 or (altDown
and key
in (ord('N'), ord('n'))):
471 self
.OnHistoryReplace(step
=-1)
472 # Insert the previous command from the history buffer.
473 elif (shiftDown
and key
== WXK_UP
) and self
.CanEdit():
474 self
.OnHistoryInsert(step
=+1)
475 # Insert the next command from the history buffer.
476 elif (shiftDown
and key
== WXK_DOWN
) and self
.CanEdit():
477 self
.OnHistoryInsert(step
=-1)
478 # Search up the history for the text in front of the cursor.
480 self
.OnHistorySearch()
481 # Don't backspace over the latest non-continuation prompt.
482 elif key
== WXK_BACK
:
483 if selecting
and self
.CanEdit():
485 elif currpos
> self
.promptPosEnd
:
487 # Only allow these keys after the latest prompt.
488 elif key
in (WXK_TAB
, WXK_DELETE
):
491 # Don't toggle between insert mode and overwrite mode.
492 elif key
== WXK_INSERT
:
494 # Don't allow line deletion.
495 elif controlDown
and key
in (ord('L'), ord('l')):
497 # Don't allow line transposition.
498 elif controlDown
and key
in (ord('T'), ord('t')):
500 # Basic navigation keys should work anywhere.
503 # Protect the readonly portion of the shell.
504 elif not self
.CanEdit():
509 def clearCommand(self
):
510 """Delete the current, unexecuted command."""
511 startpos
= self
.promptPosEnd
512 endpos
= self
.GetTextLength()
513 self
.SetSelection(startpos
, endpos
)
514 self
.ReplaceSelection('')
517 def OnHistoryReplace(self
, step
):
518 """Replace with the previous/next command from the history buffer."""
520 self
.replaceFromHistory(step
)
522 def replaceFromHistory(self
, step
):
523 """Replace selection with command from the history buffer."""
524 self
.ReplaceSelection('')
525 newindex
= self
.historyIndex
+ step
526 if -1 <= newindex
<= len(self
.history
):
527 self
.historyIndex
= newindex
528 if 0 <= newindex
<= len(self
.history
)-1:
529 command
= self
.history
[self
.historyIndex
]
530 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
531 self
.ReplaceSelection(command
)
533 def OnHistoryInsert(self
, step
):
534 """Insert the previous/next command from the history buffer."""
535 if not self
.CanEdit():
537 startpos
= self
.GetCurrentPos()
538 self
.replaceFromHistory(step
)
539 endpos
= self
.GetCurrentPos()
540 self
.SetSelection(endpos
, startpos
)
542 def OnHistorySearch(self
):
543 """Search up the history buffer for the text in front of the cursor."""
544 if not self
.CanEdit():
546 startpos
= self
.GetCurrentPos()
547 # The text up to the cursor is what we search for.
548 numCharsAfterCursor
= self
.GetTextLength() - startpos
549 searchText
= self
.getCommand(rstrip
=0)
550 if numCharsAfterCursor
> 0:
551 searchText
= searchText
[:-numCharsAfterCursor
]
554 # Search upwards from the current history position and loop back
555 # to the beginning if we don't find anything.
556 if (self
.historyIndex
<= -1) \
557 or (self
.historyIndex
>= len(self
.history
)-2):
558 searchOrder
= range(len(self
.history
))
560 searchOrder
= range(self
.historyIndex
+1, len(self
.history
)) + \
561 range(self
.historyIndex
)
562 for i
in searchOrder
:
563 command
= self
.history
[i
]
564 if command
[:len(searchText
)] == searchText
:
565 # Replace the current selection with the one we've found.
566 self
.ReplaceSelection(command
[len(searchText
):])
567 endpos
= self
.GetCurrentPos()
568 self
.SetSelection(endpos
, startpos
)
569 # We've now warped into middle of the history.
570 self
.historyIndex
= i
573 def setStatusText(self
, text
):
574 """Display status information."""
576 # This method will most likely be replaced by the enclosing app
577 # to do something more interesting, like write to a status bar.
580 def insertLineBreak(self
):
581 """Insert a new line break."""
583 self
.write(os
.linesep
)
587 def processLine(self
):
588 """Process the line of text at which the user hit Enter."""
590 # The user hit ENTER and we need to decide what to do. They could be
591 # sitting on any line in the shell.
593 thepos
= self
.GetCurrentPos()
594 startpos
= self
.promptPosEnd
595 endpos
= self
.GetTextLength()
596 # If they hit RETURN inside the current command, execute the command.
598 self
.SetCurrentPos(endpos
)
600 command
= self
.GetTextRange(startpos
, endpos
)
601 lines
= command
.split(os
.linesep
+ sys
.ps2
)
602 lines
= [line
.rstrip() for line
in lines
]
603 command
= '\n'.join(lines
)
604 if self
.reader
.isreading
:
606 # Match the behavior of the standard Python shell when
607 # the user hits return without entering a value.
609 self
.reader
.input = command
610 self
.write(os
.linesep
)
613 # Or replace the current command with the other command.
615 # If the line contains a command (even an invalid one).
616 if self
.getCommand(rstrip
=0):
617 command
= self
.getMultilineCommand()
620 # Otherwise, put the cursor back where we started.
622 self
.SetCurrentPos(thepos
)
623 self
.SetAnchor(thepos
)
625 def getMultilineCommand(self
, rstrip
=1):
626 """Extract a multi-line command from the editor.
628 The command may not necessarily be valid Python syntax."""
629 # XXX Need to extract real prompts here. Need to keep track of the
630 # prompt every time a command is issued.
635 # This is a total hack job, but it works.
636 text
= self
.GetCurLine()[0]
637 line
= self
.GetCurrentLine()
638 while text
[:ps2size
] == ps2
and line
> 0:
641 text
= self
.GetCurLine()[0]
642 if text
[:ps1size
] == ps1
:
643 line
= self
.GetCurrentLine()
645 startpos
= self
.GetCurrentPos() + ps1size
648 while self
.GetCurLine()[0][:ps2size
] == ps2
:
651 stoppos
= self
.GetCurrentPos()
652 command
= self
.GetTextRange(startpos
, stoppos
)
653 command
= command
.replace(os
.linesep
+ sys
.ps2
, '\n')
654 command
= command
.rstrip()
655 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
659 command
= command
.rstrip()
662 def getCommand(self
, text
=None, rstrip
=1):
663 """Extract a command from text which may include a shell prompt.
665 The command may not necessarily be valid Python syntax."""
667 text
= self
.GetCurLine()[0]
668 # Strip the prompt off the front of text leaving just the command.
669 command
= self
.lstripPrompt(text
)
671 command
= '' # Real commands have prompts.
673 command
= command
.rstrip()
676 def lstripPrompt(self
, text
):
677 """Return text without a leading prompt."""
682 # Strip the prompt off the front of text.
683 if text
[:ps1size
] == ps1
:
684 text
= text
[ps1size
:]
685 elif text
[:ps2size
] == ps2
:
686 text
= text
[ps2size
:]
689 def push(self
, command
):
690 """Send command to the interpreter for execution."""
691 self
.write(os
.linesep
)
692 busy
= wxBusyCursor()
693 self
.more
= self
.interp
.push(command
)
696 self
.addHistory(command
.rstrip())
699 def addHistory(self
, command
):
700 """Add command to the command history."""
701 # Reset the history position.
702 self
.historyIndex
= -1
703 # Insert this command into the history, unless it's a blank
704 # line or the same as the last command.
706 and (len(self
.history
) == 0 or command
!= self
.history
[0]):
707 self
.history
.insert(0, command
)
709 def write(self
, text
):
710 """Display text in the shell.
712 Replace line endings with OS-specific endings."""
713 text
= self
.fixLineEndings(text
)
715 self
.EnsureCaretVisible()
717 def fixLineEndings(self
, text
):
718 """Return text with line endings replaced by OS-specific endings."""
719 lines
= text
.split('\r\n')
720 for l
in range(len(lines
)):
721 chunks
= lines
[l
].split('\r')
722 for c
in range(len(chunks
)):
723 chunks
[c
] = os
.linesep
.join(chunks
[c
].split('\n'))
724 lines
[l
] = os
.linesep
.join(chunks
)
725 text
= os
.linesep
.join(lines
)
729 """Display appropriate prompt for the context, either ps1, ps2 or ps3.
731 If this is a continuation line, autoindent as necessary."""
732 isreading
= self
.reader
.isreading
735 prompt
= str(sys
.ps3
)
737 prompt
= str(sys
.ps2
)
739 prompt
= str(sys
.ps1
)
740 pos
= self
.GetCurLine()[1]
745 self
.write(os
.linesep
)
747 self
.promptPosStart
= self
.GetCurrentPos()
751 self
.promptPosEnd
= self
.GetCurrentPos()
752 # Keep the undo feature from undoing previous responses.
753 self
.EmptyUndoBuffer()
754 # XXX Add some autoindent magic here if more.
756 self
.write(' '*4) # Temporary hack indentation.
757 self
.EnsureCaretVisible()
758 self
.ScrollToColumn(0)
761 """Replacement for stdin.readline()."""
767 while not reader
.input:
775 def raw_input(self
, prompt
=''):
776 """Return string based on user input."""
779 return self
.readline()
781 def ask(self
, prompt
='Please enter your response:'):
782 """Get response from the user using a dialog box."""
783 dialog
= wxTextEntryDialog(None, prompt
, \
784 'Input Dialog (Raw)', '')
786 if dialog
.ShowModal() == wxID_OK
:
787 text
= dialog
.GetValue()
794 """Halt execution pending a response from the user."""
795 self
.ask('Press enter to continue:')
798 """Delete all text from the shell."""
801 def run(self
, command
, prompt
=1, verbose
=1):
802 """Execute command within the shell as if it was typed in directly.
803 >>> shell.run('print "this"')
808 # Go to the very bottom of the text.
809 endpos
= self
.GetTextLength()
810 self
.SetCurrentPos(endpos
)
811 command
= command
.rstrip()
812 if prompt
: self
.prompt()
813 if verbose
: self
.write(command
)
816 def runfile(self
, filename
):
817 """Execute all commands in file as if they were typed into the shell."""
818 file = open(filename
)
821 for command
in file.readlines():
822 if command
[:6] == 'shell.': # Run shell methods silently.
823 self
.run(command
, prompt
=0, verbose
=0)
825 self
.run(command
, prompt
=0, verbose
=1)
829 def autoCompleteShow(self
, command
):
830 """Display auto-completion popup list."""
831 list = self
.interp
.getAutoCompleteList(command
,
832 includeMagic
=self
.autoCompleteIncludeMagic
,
833 includeSingle
=self
.autoCompleteIncludeSingle
,
834 includeDouble
=self
.autoCompleteIncludeDouble
)
836 options
= ' '.join(list)
838 self
.AutoCompShow(offset
, options
)
840 def autoCallTipShow(self
, command
):
841 """Display argument spec and docstring in a popup bubble thingie."""
842 if self
.CallTipActive
: self
.CallTipCancel()
843 (name
, argspec
, tip
) = self
.interp
.getCallTip(command
)
845 startpos
= self
.GetCurrentPos()
846 self
.write(argspec
+ ')')
847 endpos
= self
.GetCurrentPos()
848 self
.SetSelection(endpos
, startpos
)
850 curpos
= self
.GetCurrentPos()
851 tippos
= curpos
- (len(name
) + 1)
852 fallback
= curpos
- self
.GetColumn(curpos
)
853 # In case there isn't enough room, only go back to the fallback.
854 tippos
= max(tippos
, fallback
)
855 self
.CallTipShow(tippos
, tip
)
857 def writeOut(self
, text
):
858 """Replacement for stdout."""
861 def writeErr(self
, text
):
862 """Replacement for stderr."""
865 def redirectStdin(self
, redirect
=1):
866 """If redirect is true then sys.stdin will come from the shell."""
868 sys
.stdin
= self
.reader
870 sys
.stdin
= self
.stdin
872 def redirectStdout(self
, redirect
=1):
873 """If redirect is true then sys.stdout will go to the shell."""
875 sys
.stdout
= PseudoFileOut(self
.writeOut
)
877 sys
.stdout
= self
.stdout
879 def redirectStderr(self
, redirect
=1):
880 """If redirect is true then sys.stderr will go to the shell."""
882 sys
.stderr
= PseudoFileErr(self
.writeErr
)
884 sys
.stderr
= self
.stderr
887 """Return true if text is selected and can be cut."""
888 if self
.GetSelectionStart() != self
.GetSelectionEnd() \
889 and self
.GetSelectionStart() >= self
.promptPosEnd \
890 and self
.GetSelectionEnd() >= self
.promptPosEnd
:
896 """Return true if text is selected and can be copied."""
897 return self
.GetSelectionStart() != self
.GetSelectionEnd()
900 """Return true if a paste should succeed."""
901 if self
.CanEdit() and wxStyledTextCtrl
.CanPaste(self
):
907 """Return true if editing should succeed."""
908 if self
.GetSelectionStart() != self
.GetSelectionEnd():
909 if self
.GetSelectionStart() >= self
.promptPosEnd \
910 and self
.GetSelectionEnd() >= self
.promptPosEnd
:
915 return self
.GetCurrentPos() >= self
.promptPosEnd
918 """Remove selection and place it on the clipboard."""
919 if self
.CanCut() and self
.CanCopy():
920 if self
.AutoCompActive(): self
.AutoCompCancel()
921 if self
.CallTipActive
: self
.CallTipCancel()
923 self
.ReplaceSelection('')
926 """Copy selection and place it on the clipboard."""
928 command
= self
.GetSelectedText()
929 command
= command
.replace(os
.linesep
+ sys
.ps2
, os
.linesep
)
930 command
= command
.replace(os
.linesep
+ sys
.ps1
, os
.linesep
)
931 command
= self
.lstripPrompt(text
=command
)
932 data
= wxTextDataObject(command
)
933 if wxTheClipboard
.Open():
934 wxTheClipboard
.SetData(data
)
935 wxTheClipboard
.Close()
937 def CopyWithPrompts(self
):
938 """Copy selection, including prompts, and place it on the clipboard."""
940 command
= self
.GetSelectedText()
941 data
= wxTextDataObject(command
)
942 if wxTheClipboard
.Open():
943 wxTheClipboard
.SetData(data
)
944 wxTheClipboard
.Close()
947 """Replace selection with clipboard contents."""
948 if self
.CanPaste() and wxTheClipboard
.Open():
949 if wxTheClipboard
.IsSupported(wxDataFormat(wxDF_TEXT
)):
950 data
= wxTextDataObject()
951 if wxTheClipboard
.GetData(data
):
952 self
.ReplaceSelection('')
953 command
= data
.GetText()
954 command
= command
.rstrip()
955 command
= self
.fixLineEndings(command
)
956 command
= self
.lstripPrompt(text
=command
)
957 command
= command
.replace(os
.linesep
+ sys
.ps2
, '\n')
958 command
= command
.replace(os
.linesep
, '\n')
959 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
961 wxTheClipboard
.Close()
963 def PasteAndRun(self
):
964 """Replace selection with clipboard contents, run commands."""
965 if wxTheClipboard
.Open():
966 if wxTheClipboard
.IsSupported(wxDataFormat(wxDF_TEXT
)):
967 data
= wxTextDataObject()
968 if wxTheClipboard
.GetData(data
):
969 endpos
= self
.GetTextLength()
970 self
.SetCurrentPos(endpos
)
971 startpos
= self
.promptPosEnd
972 self
.SetSelection(startpos
, endpos
)
973 self
.ReplaceSelection('')
974 text
= data
.GetText()
976 text
= self
.fixLineEndings(text
)
977 text
= self
.lstripPrompt(text
=text
)
978 text
= text
.replace(os
.linesep
+ sys
.ps1
, '\n')
979 text
= text
.replace(os
.linesep
+ sys
.ps2
, '\n')
980 text
= text
.replace(os
.linesep
, '\n')
981 lines
= text
.split('\n')
985 if line
.strip() != '' and line
.lstrip() == line
:
988 # Add the previous command to the list.
989 commands
.append(command
)
990 # Start a new command, which may be multiline.
993 # Multiline command. Add to the command.
996 commands
.append(command
)
997 for command
in commands
:
998 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
1001 wxTheClipboard
.Close()
1003 def wrap(self
, wrap
=1):
1004 """Sets whether text is word wrapped."""
1006 self
.SetWrapMode(wrap
)
1007 except AttributeError:
1008 return 'Wrapping is not available in this version of PyCrust.'
1010 def zoom(self
, points
=0):
1011 """Set the zoom level.
1013 This number of points is added to the size of all fonts.
1014 It may be positive to magnify or negative to reduce."""
1015 self
.SetZoom(points
)
1018 wxID_SELECTALL
= NewId() # This *should* be defined by wxPython.
1019 ID_AUTOCOMP
= NewId()
1020 ID_AUTOCOMP_SHOW
= NewId()
1021 ID_AUTOCOMP_INCLUDE_MAGIC
= NewId()
1022 ID_AUTOCOMP_INCLUDE_SINGLE
= NewId()
1023 ID_AUTOCOMP_INCLUDE_DOUBLE
= NewId()
1024 ID_CALLTIPS
= NewId()
1025 ID_CALLTIPS_SHOW
= NewId()
1029 """Mixin class to add standard menu items."""
1031 def createMenus(self
):
1032 m
= self
.fileMenu
= wxMenu()
1034 m
.Append(wxID_EXIT
, 'E&xit', 'Exit PyCrust')
1036 m
= self
.editMenu
= wxMenu()
1037 m
.Append(wxID_UNDO
, '&Undo \tCtrl+Z', 'Undo the last action')
1038 m
.Append(wxID_REDO
, '&Redo \tCtrl+Y', 'Redo the last undone action')
1040 m
.Append(wxID_CUT
, 'Cu&t \tCtrl+X', 'Cut the selection')
1041 m
.Append(wxID_COPY
, '&Copy \tCtrl+C', 'Copy the selection')
1042 m
.Append(wxID_PASTE
, '&Paste \tCtrl+V', 'Paste')
1044 m
.Append(wxID_CLEAR
, 'Cle&ar', 'Delete the selection')
1045 m
.Append(wxID_SELECTALL
, 'Select A&ll', 'Select all text')
1047 m
= self
.autocompMenu
= wxMenu()
1048 m
.Append(ID_AUTOCOMP_SHOW
, 'Show Auto Completion', \
1049 'Show auto completion during dot syntax', 1)
1050 m
.Append(ID_AUTOCOMP_INCLUDE_MAGIC
, 'Include Magic Attributes', \
1051 'Include attributes visible to __getattr__ and __setattr__', 1)
1052 m
.Append(ID_AUTOCOMP_INCLUDE_SINGLE
, 'Include Single Underscores', \
1053 'Include attibutes prefixed by a single underscore', 1)
1054 m
.Append(ID_AUTOCOMP_INCLUDE_DOUBLE
, 'Include Double Underscores', \
1055 'Include attibutes prefixed by a double underscore', 1)
1057 m
= self
.calltipsMenu
= wxMenu()
1058 m
.Append(ID_CALLTIPS_SHOW
, 'Show Call Tips', \
1059 'Show call tips with argument specifications', 1)
1061 m
= self
.optionsMenu
= wxMenu()
1062 m
.AppendMenu(ID_AUTOCOMP
, '&Auto Completion', self
.autocompMenu
, \
1063 'Auto Completion Options')
1064 m
.AppendMenu(ID_CALLTIPS
, '&Call Tips', self
.calltipsMenu
, \
1067 m
= self
.helpMenu
= wxMenu()
1069 m
.Append(wxID_ABOUT
, '&About...', 'About PyCrust')
1071 b
= self
.menuBar
= wxMenuBar()
1072 b
.Append(self
.fileMenu
, '&File')
1073 b
.Append(self
.editMenu
, '&Edit')
1074 b
.Append(self
.optionsMenu
, '&Options')
1075 b
.Append(self
.helpMenu
, '&Help')
1078 EVT_MENU(self
, wxID_EXIT
, self
.OnExit
)
1079 EVT_MENU(self
, wxID_UNDO
, self
.OnUndo
)
1080 EVT_MENU(self
, wxID_REDO
, self
.OnRedo
)
1081 EVT_MENU(self
, wxID_CUT
, self
.OnCut
)
1082 EVT_MENU(self
, wxID_COPY
, self
.OnCopy
)
1083 EVT_MENU(self
, wxID_PASTE
, self
.OnPaste
)
1084 EVT_MENU(self
, wxID_CLEAR
, self
.OnClear
)
1085 EVT_MENU(self
, wxID_SELECTALL
, self
.OnSelectAll
)
1086 EVT_MENU(self
, wxID_ABOUT
, self
.OnAbout
)
1087 EVT_MENU(self
, ID_AUTOCOMP_SHOW
, \
1088 self
.OnAutoCompleteShow
)
1089 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_MAGIC
, \
1090 self
.OnAutoCompleteIncludeMagic
)
1091 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_SINGLE
, \
1092 self
.OnAutoCompleteIncludeSingle
)
1093 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_DOUBLE
, \
1094 self
.OnAutoCompleteIncludeDouble
)
1095 EVT_MENU(self
, ID_CALLTIPS_SHOW
, \
1096 self
.OnCallTipsShow
)
1098 EVT_UPDATE_UI(self
, wxID_UNDO
, self
.OnUpdateMenu
)
1099 EVT_UPDATE_UI(self
, wxID_REDO
, self
.OnUpdateMenu
)
1100 EVT_UPDATE_UI(self
, wxID_CUT
, self
.OnUpdateMenu
)
1101 EVT_UPDATE_UI(self
, wxID_COPY
, self
.OnUpdateMenu
)
1102 EVT_UPDATE_UI(self
, wxID_PASTE
, self
.OnUpdateMenu
)
1103 EVT_UPDATE_UI(self
, wxID_CLEAR
, self
.OnUpdateMenu
)
1104 EVT_UPDATE_UI(self
, ID_AUTOCOMP_SHOW
, self
.OnUpdateMenu
)
1105 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_MAGIC
, self
.OnUpdateMenu
)
1106 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_SINGLE
, self
.OnUpdateMenu
)
1107 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_DOUBLE
, self
.OnUpdateMenu
)
1108 EVT_UPDATE_UI(self
, ID_CALLTIPS_SHOW
, self
.OnUpdateMenu
)
1110 def OnExit(self
, event
):
1113 def OnUndo(self
, event
):
1116 def OnRedo(self
, event
):
1119 def OnCut(self
, event
):
1122 def OnCopy(self
, event
):
1125 def OnPaste(self
, event
):
1128 def OnClear(self
, event
):
1131 def OnSelectAll(self
, event
):
1132 self
.shell
.SelectAll()
1134 def OnAbout(self
, event
):
1135 """Display an About PyCrust window."""
1137 title
= 'About PyCrust'
1138 text
= 'PyCrust %s\n\n' % VERSION
+ \
1139 'Yet another Python shell, only flakier.\n\n' + \
1140 'Half-baked by Patrick K. O\'Brien,\n' + \
1141 'the other half is still in the oven.\n\n' + \
1142 'Shell Revision: %s\n' % self
.shell
.revision
+ \
1143 'Interpreter Revision: %s\n\n' % self
.shell
.interp
.revision
+ \
1144 'Python Version: %s\n' % sys
.version
.split()[0] + \
1145 'wxPython Version: %s\n' % wx
.__version
__ + \
1146 'Platform: %s\n' % sys
.platform
1147 dialog
= wxMessageDialog(self
, text
, title
, wxOK | wxICON_INFORMATION
)
1151 def OnAutoCompleteShow(self
, event
):
1152 self
.shell
.autoComplete
= event
.IsChecked()
1154 def OnAutoCompleteIncludeMagic(self
, event
):
1155 self
.shell
.autoCompleteIncludeMagic
= event
.IsChecked()
1157 def OnAutoCompleteIncludeSingle(self
, event
):
1158 self
.shell
.autoCompleteIncludeSingle
= event
.IsChecked()
1160 def OnAutoCompleteIncludeDouble(self
, event
):
1161 self
.shell
.autoCompleteIncludeDouble
= event
.IsChecked()
1163 def OnCallTipsShow(self
, event
):
1164 self
.shell
.autoCallTip
= event
.IsChecked()
1166 def OnUpdateMenu(self
, event
):
1167 """Update menu items based on current status."""
1170 event
.Enable(self
.shell
.CanUndo())
1171 elif id == wxID_REDO
:
1172 event
.Enable(self
.shell
.CanRedo())
1173 elif id == wxID_CUT
:
1174 event
.Enable(self
.shell
.CanCut())
1175 elif id == wxID_COPY
:
1176 event
.Enable(self
.shell
.CanCopy())
1177 elif id == wxID_PASTE
:
1178 event
.Enable(self
.shell
.CanPaste())
1179 elif id == wxID_CLEAR
:
1180 event
.Enable(self
.shell
.CanCut())
1181 elif id == ID_AUTOCOMP_SHOW
:
1182 event
.Check(self
.shell
.autoComplete
)
1183 elif id == ID_AUTOCOMP_INCLUDE_MAGIC
:
1184 event
.Check(self
.shell
.autoCompleteIncludeMagic
)
1185 elif id == ID_AUTOCOMP_INCLUDE_SINGLE
:
1186 event
.Check(self
.shell
.autoCompleteIncludeSingle
)
1187 elif id == ID_AUTOCOMP_INCLUDE_DOUBLE
:
1188 event
.Check(self
.shell
.autoCompleteIncludeDouble
)
1189 elif id == ID_CALLTIPS_SHOW
:
1190 event
.Check(self
.shell
.autoCallTip
)
1193 class ShellFrame(wxFrame
, ShellMenu
):
1194 """Frame containing the PyCrust shell component."""
1196 name
= 'PyCrust Shell Frame'
1197 revision
= __revision__
1199 def __init__(self
, parent
=None, id=-1, title
='PyShell', \
1200 pos
=wxDefaultPosition
, size
=wxDefaultSize
, \
1201 style
=wxDEFAULT_FRAME_STYLE
, locals=None, \
1202 InterpClass
=None, *args
, **kwds
):
1203 """Create a PyCrust ShellFrame instance."""
1204 wxFrame
.__init
__(self
, parent
, id, title
, pos
, size
, style
)
1205 intro
= 'Welcome To PyCrust %s - The Flakiest Python Shell' % VERSION
1206 intro
+= '\nSponsored by Orbtech - Your source for Python programming expertise.'
1207 self
.CreateStatusBar()
1208 self
.SetStatusText(intro
.replace('\n', ', '))
1210 self
.SetIcon(images
.getPyCrustIcon())
1211 self
.shell
= Shell(parent
=self
, id=-1, introText
=intro
, \
1212 locals=locals, InterpClass
=InterpClass
, \
1214 # Override the shell so that status messages go to the status bar.
1215 self
.shell
.setStatusText
= self
.SetStatusText
1217 EVT_CLOSE(self
, self
.OnCloseWindow
)
1219 def OnCloseWindow(self
, event
):
1220 self
.shell
.destroy()