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 def showIntro(self
, text
=''):
238 """Display introductory text in the shell."""
240 if not text
.endswith(os
.linesep
): text
+= os
.linesep
243 self
.write(self
.interp
.introText
)
244 except AttributeError:
247 def setBuiltinKeywords(self
):
248 """Create pseudo keywords as part of builtins.
250 This simply sets "close", "exit" and "quit" to a helpful string.
253 __builtin__
.close
= __builtin__
.exit
= __builtin__
.quit
= \
254 'Click on the close button to leave the application.'
257 """Quit the application."""
259 # XXX Good enough for now but later we want to send a close event.
261 # In the close event handler we can make sure they want to quit.
262 # Other applications, like PythonCard, may choose to hide rather than
263 # quit so we should just post the event and let the surrounding app
264 # decide what it wants to do.
265 self
.write('Click on the close button to leave the application.')
267 def setLocalShell(self
):
268 """Add 'shell' to locals as reference to ShellFacade instance."""
269 self
.interp
.locals['shell'] = ShellFacade(other
=self
)
271 def execStartupScript(self
, startupScript
):
272 """Execute the user's PYTHONSTARTUP script if they have one."""
273 if startupScript
and os
.path
.isfile(startupScript
):
274 startupText
= 'Startup script executed: ' + startupScript
275 self
.push('print %s;execfile(%s)' % \
276 (`startupText`
, `startupScript`
))
280 def setStyles(self
, faces
):
281 """Configure font size, typeface and color for lexer."""
284 self
.StyleSetSpec(wxSTC_STYLE_DEFAULT
, "face:%(mono)s,size:%(size)d,back:%(backcol)s" % faces
)
289 self
.StyleSetSpec(wxSTC_STYLE_LINENUMBER
, "back:#C0C0C0,face:%(mono)s,size:%(lnsize)d" % faces
)
290 self
.StyleSetSpec(wxSTC_STYLE_CONTROLCHAR
, "face:%(mono)s" % faces
)
291 self
.StyleSetSpec(wxSTC_STYLE_BRACELIGHT
, "fore:#0000FF,back:#FFFF88")
292 self
.StyleSetSpec(wxSTC_STYLE_BRACEBAD
, "fore:#FF0000,back:#FFFF88")
295 self
.StyleSetSpec(wxSTC_P_DEFAULT
, "face:%(mono)s" % faces
)
296 self
.StyleSetSpec(wxSTC_P_COMMENTLINE
, "fore:#007F00,face:%(mono)s" % faces
)
297 self
.StyleSetSpec(wxSTC_P_NUMBER
, "")
298 self
.StyleSetSpec(wxSTC_P_STRING
, "fore:#7F007F,face:%(mono)s" % faces
)
299 self
.StyleSetSpec(wxSTC_P_CHARACTER
, "fore:#7F007F,face:%(mono)s" % faces
)
300 self
.StyleSetSpec(wxSTC_P_WORD
, "fore:#00007F,bold")
301 self
.StyleSetSpec(wxSTC_P_TRIPLE
, "fore:#7F0000")
302 self
.StyleSetSpec(wxSTC_P_TRIPLEDOUBLE
, "fore:#000033,back:#FFFFE8")
303 self
.StyleSetSpec(wxSTC_P_CLASSNAME
, "fore:#0000FF,bold")
304 self
.StyleSetSpec(wxSTC_P_DEFNAME
, "fore:#007F7F,bold")
305 self
.StyleSetSpec(wxSTC_P_OPERATOR
, "")
306 self
.StyleSetSpec(wxSTC_P_IDENTIFIER
, "")
307 self
.StyleSetSpec(wxSTC_P_COMMENTBLOCK
, "fore:#7F7F7F")
308 self
.StyleSetSpec(wxSTC_P_STRINGEOL
, "fore:#000000,face:%(mono)s,back:#E0C0E0,eolfilled" % faces
)
310 def OnUpdateUI(self
, evt
):
311 """Check for matching braces."""
315 caretPos
= self
.GetCurrentPos()
317 charBefore
= self
.GetCharAt(caretPos
- 1)
318 #*** Patch to fix bug in wxSTC for wxPython < 2.3.3.
320 charBefore
= 32 # Mimic a space.
322 styleBefore
= self
.GetStyleAt(caretPos
- 1)
325 if charBefore
and chr(charBefore
) in '[]{}()' \
326 and styleBefore
== wxSTC_P_OPERATOR
:
327 braceAtCaret
= caretPos
- 1
331 charAfter
= self
.GetCharAt(caretPos
)
332 #*** Patch to fix bug in wxSTC for wxPython < 2.3.3.
334 charAfter
= 32 # Mimic a space.
336 styleAfter
= self
.GetStyleAt(caretPos
)
337 if charAfter
and chr(charAfter
) in '[]{}()' \
338 and styleAfter
== wxSTC_P_OPERATOR
:
339 braceAtCaret
= caretPos
341 if braceAtCaret
>= 0:
342 braceOpposite
= self
.BraceMatch(braceAtCaret
)
344 if braceAtCaret
!= -1 and braceOpposite
== -1:
345 self
.BraceBadLight(braceAtCaret
)
347 self
.BraceHighlight(braceAtCaret
, braceOpposite
)
349 def OnChar(self
, event
):
350 """Keypress event handler.
352 Only receives an event if OnKeyDown calls event.Skip() for
353 the corresponding event."""
355 # Prevent modification of previously submitted commands/responses.
356 if not self
.CanEdit():
358 key
= event
.KeyCode()
359 currpos
= self
.GetCurrentPos()
360 stoppos
= self
.promptPosEnd
361 # Return (Enter) needs to be ignored in this handler.
362 if key
== WXK_RETURN
:
364 elif key
in self
.autoCompleteKeys
:
365 # Usually the dot (period) key activates auto completion.
366 # Get the command between the prompt and the cursor.
367 # Add the autocomplete character to the end of the command.
368 command
= self
.GetTextRange(stoppos
, currpos
) + chr(key
)
370 if self
.autoComplete
: self
.autoCompleteShow(command
)
371 elif key
== ord('('):
372 # The left paren activates a call tip and cancels
373 # an active auto completion.
374 if self
.AutoCompActive(): self
.AutoCompCancel()
375 # Get the command between the prompt and the cursor.
376 # Add the '(' to the end of the command.
377 self
.ReplaceSelection('')
378 command
= self
.GetTextRange(stoppos
, currpos
) + '('
380 if self
.autoCallTip
: self
.autoCallTipShow(command
)
382 # Allow the normal event handling to take place.
385 def OnKeyDown(self
, event
):
386 """Key down event handler."""
388 # Prevent modification of previously submitted commands/responses.
389 key
= event
.KeyCode()
390 controlDown
= event
.ControlDown()
391 altDown
= event
.AltDown()
392 shiftDown
= event
.ShiftDown()
393 currpos
= self
.GetCurrentPos()
394 endpos
= self
.GetTextLength()
395 selecting
= self
.GetSelectionStart() != self
.GetSelectionEnd()
396 # Return (Enter) is used to submit a command to the interpreter.
397 if not controlDown
and key
== WXK_RETURN
:
398 if self
.AutoCompActive(): self
.AutoCompCancel()
399 if self
.CallTipActive(): self
.CallTipCancel()
401 # Ctrl+Return (Cntrl+Enter) is used to insert a line break.
402 elif controlDown
and key
== WXK_RETURN
:
403 if self
.AutoCompActive(): self
.AutoCompCancel()
404 if self
.CallTipActive(): self
.CallTipCancel()
405 if currpos
== endpos
:
408 self
.insertLineBreak()
409 # If the auto-complete window is up let it do its thing.
410 elif self
.AutoCompActive():
412 # Let Ctrl-Alt-* get handled normally.
413 elif controlDown
and altDown
:
415 # Clear the current, unexecuted command.
416 elif key
== WXK_ESCAPE
:
417 if self
.CallTipActive():
421 # Cut to the clipboard.
422 elif (controlDown
and key
in (ord('X'), ord('x'))) \
423 or (shiftDown
and key
== WXK_DELETE
):
425 # Copy to the clipboard.
426 elif controlDown
and not shiftDown \
427 and key
in (ord('C'), ord('c'), WXK_INSERT
):
429 # Copy to the clipboard, including prompts.
430 elif controlDown
and shiftDown \
431 and key
in (ord('C'), ord('c'), WXK_INSERT
):
432 self
.CopyWithPrompts()
433 # Home needs to be aware of the prompt.
434 elif key
== WXK_HOME
:
435 home
= self
.promptPosEnd
437 self
.SetCurrentPos(home
)
438 if not selecting
and not shiftDown
:
440 self
.EnsureCaretVisible()
444 # The following handlers modify text, so we need to see if there
445 # is a selection that includes text prior to the prompt.
447 # Don't modify a selection with text prior to the prompt.
448 elif selecting
and key
not in NAVKEYS
and not self
.CanEdit():
450 # Paste from the clipboard.
451 elif (controlDown
and not shiftDown \
452 and key
in (ord('V'), ord('v'))) \
453 or (shiftDown
and not controlDown
and key
== WXK_INSERT
):
455 # Paste from the clipboard, run commands.
456 elif controlDown
and shiftDown \
457 and key
in (ord('V'), ord('v')):
459 # Replace with the previous command from the history buffer.
460 elif (controlDown
and key
== WXK_UP
) \
461 or (altDown
and key
in (ord('P'), ord('p'))):
462 self
.OnHistoryReplace(step
=+1)
463 # Replace with the next command from the history buffer.
464 elif (controlDown
and key
== WXK_DOWN
) \
465 or (altDown
and key
in (ord('N'), ord('n'))):
466 self
.OnHistoryReplace(step
=-1)
467 # Insert the previous command from the history buffer.
468 elif (shiftDown
and key
== WXK_UP
) and self
.CanEdit():
469 self
.OnHistoryInsert(step
=+1)
470 # Insert the next command from the history buffer.
471 elif (shiftDown
and key
== WXK_DOWN
) and self
.CanEdit():
472 self
.OnHistoryInsert(step
=-1)
473 # Search up the history for the text in front of the cursor.
475 self
.OnHistorySearch()
476 # Don't backspace over the latest non-continuation prompt.
477 elif key
== WXK_BACK
:
478 if selecting
and self
.CanEdit():
480 elif currpos
> self
.promptPosEnd
:
482 # Only allow these keys after the latest prompt.
483 elif key
in (WXK_TAB
, WXK_DELETE
):
486 # Don't toggle between insert mode and overwrite mode.
487 elif key
== WXK_INSERT
:
489 # Don't allow line deletion.
490 elif controlDown
and key
in (ord('L'), ord('l')):
492 # Don't allow line transposition.
493 elif controlDown
and key
in (ord('T'), ord('t')):
495 # Basic navigation keys should work anywhere.
498 # Protect the readonly portion of the shell.
499 elif not self
.CanEdit():
504 def clearCommand(self
):
505 """Delete the current, unexecuted command."""
506 startpos
= self
.promptPosEnd
507 endpos
= self
.GetTextLength()
508 self
.SetSelection(startpos
, endpos
)
509 self
.ReplaceSelection('')
512 def OnHistoryReplace(self
, step
):
513 """Replace with the previous/next command from the history buffer."""
515 self
.replaceFromHistory(step
)
517 def replaceFromHistory(self
, step
):
518 """Replace selection with command from the history buffer."""
519 self
.ReplaceSelection('')
520 newindex
= self
.historyIndex
+ step
521 if -1 <= newindex
<= len(self
.history
):
522 self
.historyIndex
= newindex
523 if 0 <= newindex
<= len(self
.history
)-1:
524 command
= self
.history
[self
.historyIndex
]
525 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
526 self
.ReplaceSelection(command
)
528 def OnHistoryInsert(self
, step
):
529 """Insert the previous/next command from the history buffer."""
530 if not self
.CanEdit():
532 startpos
= self
.GetCurrentPos()
533 self
.replaceFromHistory(step
)
534 endpos
= self
.GetCurrentPos()
535 self
.SetSelection(endpos
, startpos
)
537 def OnHistorySearch(self
):
538 """Search up the history buffer for the text in front of the cursor."""
539 if not self
.CanEdit():
541 startpos
= self
.GetCurrentPos()
542 # The text up to the cursor is what we search for.
543 numCharsAfterCursor
= self
.GetTextLength() - startpos
544 searchText
= self
.getCommand(rstrip
=0)
545 if numCharsAfterCursor
> 0:
546 searchText
= searchText
[:-numCharsAfterCursor
]
549 # Search upwards from the current history position and loop back
550 # to the beginning if we don't find anything.
551 if (self
.historyIndex
<= -1) \
552 or (self
.historyIndex
>= len(self
.history
)-2):
553 searchOrder
= range(len(self
.history
))
555 searchOrder
= range(self
.historyIndex
+1, len(self
.history
)) + \
556 range(self
.historyIndex
)
557 for i
in searchOrder
:
558 command
= self
.history
[i
]
559 if command
[:len(searchText
)] == searchText
:
560 # Replace the current selection with the one we've found.
561 self
.ReplaceSelection(command
[len(searchText
):])
562 endpos
= self
.GetCurrentPos()
563 self
.SetSelection(endpos
, startpos
)
564 # We've now warped into middle of the history.
565 self
.historyIndex
= i
568 def setStatusText(self
, text
):
569 """Display status information."""
571 # This method will most likely be replaced by the enclosing app
572 # to do something more interesting, like write to a status bar.
575 def insertLineBreak(self
):
576 """Insert a new line break."""
578 self
.write(os
.linesep
)
582 def processLine(self
):
583 """Process the line of text at which the user hit Enter."""
585 # The user hit ENTER and we need to decide what to do. They could be
586 # sitting on any line in the shell.
588 thepos
= self
.GetCurrentPos()
589 startpos
= self
.promptPosEnd
590 endpos
= self
.GetTextLength()
591 # If they hit RETURN inside the current command, execute the command.
593 self
.SetCurrentPos(endpos
)
595 command
= self
.GetTextRange(startpos
, endpos
)
596 lines
= command
.split(os
.linesep
+ sys
.ps2
)
597 lines
= [line
.rstrip() for line
in lines
]
598 command
= '\n'.join(lines
)
599 if self
.reader
.isreading
:
601 # Match the behavior of the standard Python shell when
602 # the user hits return without entering a value.
604 self
.reader
.input = command
605 self
.write(os
.linesep
)
608 # Or replace the current command with the other command.
610 # If the line contains a command (even an invalid one).
611 if self
.getCommand(rstrip
=0):
612 command
= self
.getMultilineCommand()
615 # Otherwise, put the cursor back where we started.
617 self
.SetCurrentPos(thepos
)
618 self
.SetAnchor(thepos
)
620 def getMultilineCommand(self
, rstrip
=1):
621 """Extract a multi-line command from the editor.
623 The command may not necessarily be valid Python syntax."""
624 # XXX Need to extract real prompts here. Need to keep track of the
625 # prompt every time a command is issued.
630 # This is a total hack job, but it works.
631 text
= self
.GetCurLine()[0]
632 line
= self
.GetCurrentLine()
633 while text
[:ps2size
] == ps2
and line
> 0:
636 text
= self
.GetCurLine()[0]
637 if text
[:ps1size
] == ps1
:
638 line
= self
.GetCurrentLine()
640 startpos
= self
.GetCurrentPos() + ps1size
643 while self
.GetCurLine()[0][:ps2size
] == ps2
:
646 stoppos
= self
.GetCurrentPos()
647 command
= self
.GetTextRange(startpos
, stoppos
)
648 command
= command
.replace(os
.linesep
+ sys
.ps2
, '\n')
649 command
= command
.rstrip()
650 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
654 command
= command
.rstrip()
657 def getCommand(self
, text
=None, rstrip
=1):
658 """Extract a command from text which may include a shell prompt.
660 The command may not necessarily be valid Python syntax."""
662 text
= self
.GetCurLine()[0]
663 # Strip the prompt off the front of text leaving just the command.
664 command
= self
.lstripPrompt(text
)
666 command
= '' # Real commands have prompts.
668 command
= command
.rstrip()
671 def lstripPrompt(self
, text
):
672 """Return text without a leading prompt."""
677 # Strip the prompt off the front of text.
678 if text
[:ps1size
] == ps1
:
679 text
= text
[ps1size
:]
680 elif text
[:ps2size
] == ps2
:
681 text
= text
[ps2size
:]
684 def push(self
, command
):
685 """Send command to the interpreter for execution."""
686 self
.write(os
.linesep
)
687 busy
= wxBusyCursor()
688 self
.more
= self
.interp
.push(command
)
691 self
.addHistory(command
.rstrip())
694 def addHistory(self
, command
):
695 """Add command to the command history."""
696 # Reset the history position.
697 self
.historyIndex
= -1
698 # Insert this command into the history, unless it's a blank
699 # line or the same as the last command.
701 and (len(self
.history
) == 0 or command
!= self
.history
[0]):
702 self
.history
.insert(0, command
)
704 def write(self
, text
):
705 """Display text in the shell.
707 Replace line endings with OS-specific endings."""
708 text
= self
.fixLineEndings(text
)
710 self
.EnsureCaretVisible()
712 def fixLineEndings(self
, text
):
713 """Return text with line endings replaced by OS-specific endings."""
714 lines
= text
.split('\r\n')
715 for l
in range(len(lines
)):
716 chunks
= lines
[l
].split('\r')
717 for c
in range(len(chunks
)):
718 chunks
[c
] = os
.linesep
.join(chunks
[c
].split('\n'))
719 lines
[l
] = os
.linesep
.join(chunks
)
720 text
= os
.linesep
.join(lines
)
724 """Display appropriate prompt for the context, either ps1, ps2 or ps3.
726 If this is a continuation line, autoindent as necessary."""
727 isreading
= self
.reader
.isreading
730 prompt
= str(sys
.ps3
)
732 prompt
= str(sys
.ps2
)
734 prompt
= str(sys
.ps1
)
735 pos
= self
.GetCurLine()[1]
740 self
.write(os
.linesep
)
742 self
.promptPosStart
= self
.GetCurrentPos()
746 self
.promptPosEnd
= self
.GetCurrentPos()
747 # Keep the undo feature from undoing previous responses.
748 self
.EmptyUndoBuffer()
749 # XXX Add some autoindent magic here if more.
751 self
.write(' '*4) # Temporary hack indentation.
752 self
.EnsureCaretVisible()
753 self
.ScrollToColumn(0)
756 """Replacement for stdin.readline()."""
762 while not reader
.input:
770 def raw_input(self
, prompt
=''):
771 """Return string based on user input."""
774 return self
.readline()
776 def ask(self
, prompt
='Please enter your response:'):
777 """Get response from the user using a dialog box."""
778 dialog
= wxTextEntryDialog(None, prompt
, \
779 'Input Dialog (Raw)', '')
781 if dialog
.ShowModal() == wxID_OK
:
782 text
= dialog
.GetValue()
789 """Halt execution pending a response from the user."""
790 self
.ask('Press enter to continue:')
793 """Delete all text from the shell."""
796 def run(self
, command
, prompt
=1, verbose
=1):
797 """Execute command within the shell as if it was typed in directly.
798 >>> shell.run('print "this"')
803 # Go to the very bottom of the text.
804 endpos
= self
.GetTextLength()
805 self
.SetCurrentPos(endpos
)
806 command
= command
.rstrip()
807 if prompt
: self
.prompt()
808 if verbose
: self
.write(command
)
811 def runfile(self
, filename
):
812 """Execute all commands in file as if they were typed into the shell."""
813 file = open(filename
)
816 for command
in file.readlines():
817 if command
[:6] == 'shell.': # Run shell methods silently.
818 self
.run(command
, prompt
=0, verbose
=0)
820 self
.run(command
, prompt
=0, verbose
=1)
824 def autoCompleteShow(self
, command
):
825 """Display auto-completion popup list."""
826 list = self
.interp
.getAutoCompleteList(command
,
827 includeMagic
=self
.autoCompleteIncludeMagic
,
828 includeSingle
=self
.autoCompleteIncludeSingle
,
829 includeDouble
=self
.autoCompleteIncludeDouble
)
831 options
= ' '.join(list)
833 self
.AutoCompShow(offset
, options
)
835 def autoCallTipShow(self
, command
):
836 """Display argument spec and docstring in a popup bubble thingie."""
837 if self
.CallTipActive
: self
.CallTipCancel()
838 (name
, argspec
, tip
) = self
.interp
.getCallTip(command
)
840 startpos
= self
.GetCurrentPos()
841 self
.write(argspec
+ ')')
842 endpos
= self
.GetCurrentPos()
843 self
.SetSelection(endpos
, startpos
)
845 curpos
= self
.GetCurrentPos()
846 tippos
= curpos
- (len(name
) + 1)
847 fallback
= curpos
- self
.GetColumn(curpos
)
848 # In case there isn't enough room, only go back to the fallback.
849 tippos
= max(tippos
, fallback
)
850 self
.CallTipShow(tippos
, tip
)
852 def writeOut(self
, text
):
853 """Replacement for stdout."""
856 def writeErr(self
, text
):
857 """Replacement for stderr."""
860 def redirectStdin(self
, redirect
=1):
861 """If redirect is true then sys.stdin will come from the shell."""
863 sys
.stdin
= self
.reader
865 sys
.stdin
= self
.stdin
867 def redirectStdout(self
, redirect
=1):
868 """If redirect is true then sys.stdout will go to the shell."""
870 sys
.stdout
= PseudoFileOut(self
.writeOut
)
872 sys
.stdout
= self
.stdout
874 def redirectStderr(self
, redirect
=1):
875 """If redirect is true then sys.stderr will go to the shell."""
877 sys
.stderr
= PseudoFileErr(self
.writeErr
)
879 sys
.stderr
= self
.stderr
882 """Return true if text is selected and can be cut."""
883 if self
.GetSelectionStart() != self
.GetSelectionEnd() \
884 and self
.GetSelectionStart() >= self
.promptPosEnd \
885 and self
.GetSelectionEnd() >= self
.promptPosEnd
:
891 """Return true if text is selected and can be copied."""
892 return self
.GetSelectionStart() != self
.GetSelectionEnd()
895 """Return true if a paste should succeed."""
896 if self
.CanEdit() and wxStyledTextCtrl
.CanPaste(self
):
902 """Return true if editing should succeed."""
903 if self
.GetSelectionStart() != self
.GetSelectionEnd():
904 if self
.GetSelectionStart() >= self
.promptPosEnd \
905 and self
.GetSelectionEnd() >= self
.promptPosEnd
:
910 return self
.GetCurrentPos() >= self
.promptPosEnd
913 """Remove selection and place it on the clipboard."""
914 if self
.CanCut() and self
.CanCopy():
915 if self
.AutoCompActive(): self
.AutoCompCancel()
916 if self
.CallTipActive
: self
.CallTipCancel()
918 self
.ReplaceSelection('')
921 """Copy selection and place it on the clipboard."""
923 command
= self
.GetSelectedText()
924 command
= command
.replace(os
.linesep
+ sys
.ps2
, os
.linesep
)
925 command
= command
.replace(os
.linesep
+ sys
.ps1
, os
.linesep
)
926 command
= self
.lstripPrompt(text
=command
)
927 data
= wxTextDataObject(command
)
928 if wxTheClipboard
.Open():
929 wxTheClipboard
.SetData(data
)
930 wxTheClipboard
.Close()
932 def CopyWithPrompts(self
):
933 """Copy selection, including prompts, and place it on the clipboard."""
935 command
= self
.GetSelectedText()
936 data
= wxTextDataObject(command
)
937 if wxTheClipboard
.Open():
938 wxTheClipboard
.SetData(data
)
939 wxTheClipboard
.Close()
942 """Replace selection with clipboard contents."""
943 if self
.CanPaste() and wxTheClipboard
.Open():
944 if wxTheClipboard
.IsSupported(wxDataFormat(wxDF_TEXT
)):
945 data
= wxTextDataObject()
946 if wxTheClipboard
.GetData(data
):
947 self
.ReplaceSelection('')
948 command
= data
.GetText()
949 command
= command
.rstrip()
950 command
= self
.fixLineEndings(command
)
951 command
= self
.lstripPrompt(text
=command
)
952 command
= command
.replace(os
.linesep
+ sys
.ps2
, '\n')
953 command
= command
.replace(os
.linesep
, '\n')
954 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
956 wxTheClipboard
.Close()
958 def PasteAndRun(self
):
959 """Replace selection with clipboard contents, run commands."""
960 if wxTheClipboard
.Open():
961 if wxTheClipboard
.IsSupported(wxDataFormat(wxDF_TEXT
)):
962 data
= wxTextDataObject()
963 if wxTheClipboard
.GetData(data
):
964 endpos
= self
.GetTextLength()
965 self
.SetCurrentPos(endpos
)
966 startpos
= self
.promptPosEnd
967 self
.SetSelection(startpos
, endpos
)
968 self
.ReplaceSelection('')
969 text
= data
.GetText()
971 text
= self
.fixLineEndings(text
)
972 text
= self
.lstripPrompt(text
=text
)
973 text
= text
.replace(os
.linesep
+ sys
.ps1
, '\n')
974 text
= text
.replace(os
.linesep
+ sys
.ps2
, '\n')
975 text
= text
.replace(os
.linesep
, '\n')
976 lines
= text
.split('\n')
980 if line
.strip() != '' and line
.lstrip() == line
:
983 # Add the previous command to the list.
984 commands
.append(command
)
985 # Start a new command, which may be multiline.
988 # Multiline command. Add to the command.
991 commands
.append(command
)
992 for command
in commands
:
993 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
996 wxTheClipboard
.Close()
998 def wrap(self
, wrap
=1):
999 """Sets whether text is word wrapped."""
1001 self
.SetWrapMode(wrap
)
1002 except AttributeError:
1003 return 'Wrapping is not available in this version of PyCrust.'
1005 def zoom(self
, points
=0):
1006 """Set the zoom level.
1008 This number of points is added to the size of all fonts.
1009 It may be positive to magnify or negative to reduce."""
1010 self
.SetZoom(points
)
1013 wxID_SELECTALL
= NewId() # This *should* be defined by wxPython.
1014 ID_AUTOCOMP
= NewId()
1015 ID_AUTOCOMP_SHOW
= NewId()
1016 ID_AUTOCOMP_INCLUDE_MAGIC
= NewId()
1017 ID_AUTOCOMP_INCLUDE_SINGLE
= NewId()
1018 ID_AUTOCOMP_INCLUDE_DOUBLE
= NewId()
1019 ID_CALLTIPS
= NewId()
1020 ID_CALLTIPS_SHOW
= NewId()
1024 """Mixin class to add standard menu items."""
1026 def createMenus(self
):
1027 m
= self
.fileMenu
= wxMenu()
1029 m
.Append(wxID_EXIT
, 'E&xit', 'Exit PyCrust')
1031 m
= self
.editMenu
= wxMenu()
1032 m
.Append(wxID_UNDO
, '&Undo \tCtrl+Z', 'Undo the last action')
1033 m
.Append(wxID_REDO
, '&Redo \tCtrl+Y', 'Redo the last undone action')
1035 m
.Append(wxID_CUT
, 'Cu&t \tCtrl+X', 'Cut the selection')
1036 m
.Append(wxID_COPY
, '&Copy \tCtrl+C', 'Copy the selection')
1037 m
.Append(wxID_PASTE
, '&Paste \tCtrl+V', 'Paste')
1039 m
.Append(wxID_CLEAR
, 'Cle&ar', 'Delete the selection')
1040 m
.Append(wxID_SELECTALL
, 'Select A&ll', 'Select all text')
1042 m
= self
.autocompMenu
= wxMenu()
1043 m
.Append(ID_AUTOCOMP_SHOW
, 'Show Auto Completion', \
1044 'Show auto completion during dot syntax', 1)
1045 m
.Append(ID_AUTOCOMP_INCLUDE_MAGIC
, 'Include Magic Attributes', \
1046 'Include attributes visible to __getattr__ and __setattr__', 1)
1047 m
.Append(ID_AUTOCOMP_INCLUDE_SINGLE
, 'Include Single Underscores', \
1048 'Include attibutes prefixed by a single underscore', 1)
1049 m
.Append(ID_AUTOCOMP_INCLUDE_DOUBLE
, 'Include Double Underscores', \
1050 'Include attibutes prefixed by a double underscore', 1)
1052 m
= self
.calltipsMenu
= wxMenu()
1053 m
.Append(ID_CALLTIPS_SHOW
, 'Show Call Tips', \
1054 'Show call tips with argument specifications', 1)
1056 m
= self
.optionsMenu
= wxMenu()
1057 m
.AppendMenu(ID_AUTOCOMP
, '&Auto Completion', self
.autocompMenu
, \
1058 'Auto Completion Options')
1059 m
.AppendMenu(ID_CALLTIPS
, '&Call Tips', self
.calltipsMenu
, \
1062 m
= self
.helpMenu
= wxMenu()
1064 m
.Append(wxID_ABOUT
, '&About...', 'About PyCrust')
1066 b
= self
.menuBar
= wxMenuBar()
1067 b
.Append(self
.fileMenu
, '&File')
1068 b
.Append(self
.editMenu
, '&Edit')
1069 b
.Append(self
.optionsMenu
, '&Options')
1070 b
.Append(self
.helpMenu
, '&Help')
1073 EVT_MENU(self
, wxID_EXIT
, self
.OnExit
)
1074 EVT_MENU(self
, wxID_UNDO
, self
.OnUndo
)
1075 EVT_MENU(self
, wxID_REDO
, self
.OnRedo
)
1076 EVT_MENU(self
, wxID_CUT
, self
.OnCut
)
1077 EVT_MENU(self
, wxID_COPY
, self
.OnCopy
)
1078 EVT_MENU(self
, wxID_PASTE
, self
.OnPaste
)
1079 EVT_MENU(self
, wxID_CLEAR
, self
.OnClear
)
1080 EVT_MENU(self
, wxID_SELECTALL
, self
.OnSelectAll
)
1081 EVT_MENU(self
, wxID_ABOUT
, self
.OnAbout
)
1082 EVT_MENU(self
, ID_AUTOCOMP_SHOW
, \
1083 self
.OnAutoCompleteShow
)
1084 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_MAGIC
, \
1085 self
.OnAutoCompleteIncludeMagic
)
1086 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_SINGLE
, \
1087 self
.OnAutoCompleteIncludeSingle
)
1088 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_DOUBLE
, \
1089 self
.OnAutoCompleteIncludeDouble
)
1090 EVT_MENU(self
, ID_CALLTIPS_SHOW
, \
1091 self
.OnCallTipsShow
)
1093 EVT_UPDATE_UI(self
, wxID_UNDO
, self
.OnUpdateMenu
)
1094 EVT_UPDATE_UI(self
, wxID_REDO
, self
.OnUpdateMenu
)
1095 EVT_UPDATE_UI(self
, wxID_CUT
, self
.OnUpdateMenu
)
1096 EVT_UPDATE_UI(self
, wxID_COPY
, self
.OnUpdateMenu
)
1097 EVT_UPDATE_UI(self
, wxID_PASTE
, self
.OnUpdateMenu
)
1098 EVT_UPDATE_UI(self
, wxID_CLEAR
, self
.OnUpdateMenu
)
1099 EVT_UPDATE_UI(self
, ID_AUTOCOMP_SHOW
, self
.OnUpdateMenu
)
1100 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_MAGIC
, self
.OnUpdateMenu
)
1101 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_SINGLE
, self
.OnUpdateMenu
)
1102 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_DOUBLE
, self
.OnUpdateMenu
)
1103 EVT_UPDATE_UI(self
, ID_CALLTIPS_SHOW
, self
.OnUpdateMenu
)
1105 def OnExit(self
, event
):
1108 def OnUndo(self
, event
):
1111 def OnRedo(self
, event
):
1114 def OnCut(self
, event
):
1117 def OnCopy(self
, event
):
1120 def OnPaste(self
, event
):
1123 def OnClear(self
, event
):
1126 def OnSelectAll(self
, event
):
1127 self
.shell
.SelectAll()
1129 def OnAbout(self
, event
):
1130 """Display an About PyCrust window."""
1132 title
= 'About PyCrust'
1133 text
= 'PyCrust %s\n\n' % VERSION
+ \
1134 'Yet another Python shell, only flakier.\n\n' + \
1135 'Half-baked by Patrick K. O\'Brien,\n' + \
1136 'the other half is still in the oven.\n\n' + \
1137 'Shell Revision: %s\n' % self
.shell
.revision
+ \
1138 'Interpreter Revision: %s\n\n' % self
.shell
.interp
.revision
+ \
1139 'Python Version: %s\n' % sys
.version
.split()[0] + \
1140 'wxPython Version: %s\n' % wx
.__version
__ + \
1141 'Platform: %s\n' % sys
.platform
1142 dialog
= wxMessageDialog(self
, text
, title
, wxOK | wxICON_INFORMATION
)
1146 def OnAutoCompleteShow(self
, event
):
1147 self
.shell
.autoComplete
= event
.IsChecked()
1149 def OnAutoCompleteIncludeMagic(self
, event
):
1150 self
.shell
.autoCompleteIncludeMagic
= event
.IsChecked()
1152 def OnAutoCompleteIncludeSingle(self
, event
):
1153 self
.shell
.autoCompleteIncludeSingle
= event
.IsChecked()
1155 def OnAutoCompleteIncludeDouble(self
, event
):
1156 self
.shell
.autoCompleteIncludeDouble
= event
.IsChecked()
1158 def OnCallTipsShow(self
, event
):
1159 self
.shell
.autoCallTip
= event
.IsChecked()
1161 def OnUpdateMenu(self
, event
):
1162 """Update menu items based on current status."""
1165 event
.Enable(self
.shell
.CanUndo())
1166 elif id == wxID_REDO
:
1167 event
.Enable(self
.shell
.CanRedo())
1168 elif id == wxID_CUT
:
1169 event
.Enable(self
.shell
.CanCut())
1170 elif id == wxID_COPY
:
1171 event
.Enable(self
.shell
.CanCopy())
1172 elif id == wxID_PASTE
:
1173 event
.Enable(self
.shell
.CanPaste())
1174 elif id == wxID_CLEAR
:
1175 event
.Enable(self
.shell
.CanCut())
1176 elif id == ID_AUTOCOMP_SHOW
:
1177 event
.Check(self
.shell
.autoComplete
)
1178 elif id == ID_AUTOCOMP_INCLUDE_MAGIC
:
1179 event
.Check(self
.shell
.autoCompleteIncludeMagic
)
1180 elif id == ID_AUTOCOMP_INCLUDE_SINGLE
:
1181 event
.Check(self
.shell
.autoCompleteIncludeSingle
)
1182 elif id == ID_AUTOCOMP_INCLUDE_DOUBLE
:
1183 event
.Check(self
.shell
.autoCompleteIncludeDouble
)
1184 elif id == ID_CALLTIPS_SHOW
:
1185 event
.Check(self
.shell
.autoCallTip
)
1188 class ShellFrame(wxFrame
, ShellMenu
):
1189 """Frame containing the PyCrust shell component."""
1191 name
= 'PyCrust Shell Frame'
1192 revision
= __revision__
1194 def __init__(self
, parent
=None, id=-1, title
='PyShell', \
1195 pos
=wxDefaultPosition
, size
=wxDefaultSize
, \
1196 style
=wxDEFAULT_FRAME_STYLE
, locals=None, \
1197 InterpClass
=None, *args
, **kwds
):
1198 """Create a PyCrust ShellFrame instance."""
1199 wxFrame
.__init
__(self
, parent
, id, title
, pos
, size
, style
)
1200 intro
= 'Welcome To PyCrust %s - The Flakiest Python Shell' % VERSION
1201 intro
+= '\nSponsored by Orbtech - Your source for Python programming expertise.'
1202 self
.CreateStatusBar()
1203 self
.SetStatusText(intro
.replace('\n', ', '))
1205 self
.SetIcon(images
.getPyCrustIcon())
1206 self
.shell
= Shell(parent
=self
, id=-1, introText
=intro
, \
1207 locals=locals, InterpClass
=InterpClass
, \
1209 # Override the shell so that status messages go to the status bar.
1210 self
.shell
.setStatusText
= self
.SetStatusText
1212 EVT_CLOSE(self
, self
.OnCloseWindow
)
1214 def OnCloseWindow(self
, event
):
1215 self
.shell
.destroy()