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))
236 self
.SetEndAtLastLine(false
)
238 def showIntro(self
, text
=''):
239 """Display introductory text in the shell."""
241 if not text
.endswith(os
.linesep
): text
+= os
.linesep
244 self
.write(self
.interp
.introText
)
245 except AttributeError:
247 wxCallAfter(self
.ScrollToLine
, 0)
249 def setBuiltinKeywords(self
):
250 """Create pseudo keywords as part of builtins.
252 This simply sets "close", "exit" and "quit" to a helpful string.
255 __builtin__
.close
= __builtin__
.exit
= __builtin__
.quit
= \
256 'Click on the close button to leave the application.'
259 """Quit the application."""
261 # XXX Good enough for now but later we want to send a close event.
263 # In the close event handler we can make sure they want to quit.
264 # Other applications, like PythonCard, may choose to hide rather than
265 # quit so we should just post the event and let the surrounding app
266 # decide what it wants to do.
267 self
.write('Click on the close button to leave the application.')
269 def setLocalShell(self
):
270 """Add 'shell' to locals as reference to ShellFacade instance."""
271 self
.interp
.locals['shell'] = ShellFacade(other
=self
)
273 def execStartupScript(self
, startupScript
):
274 """Execute the user's PYTHONSTARTUP script if they have one."""
275 if startupScript
and os
.path
.isfile(startupScript
):
276 startupText
= 'Startup script executed: ' + startupScript
277 self
.push('print %s;execfile(%s)' % \
278 (`startupText`
, `startupScript`
))
282 def setStyles(self
, faces
):
283 """Configure font size, typeface and color for lexer."""
286 self
.StyleSetSpec(wxSTC_STYLE_DEFAULT
, "face:%(mono)s,size:%(size)d,back:%(backcol)s" % faces
)
291 self
.StyleSetSpec(wxSTC_STYLE_LINENUMBER
, "back:#C0C0C0,face:%(mono)s,size:%(lnsize)d" % faces
)
292 self
.StyleSetSpec(wxSTC_STYLE_CONTROLCHAR
, "face:%(mono)s" % faces
)
293 self
.StyleSetSpec(wxSTC_STYLE_BRACELIGHT
, "fore:#0000FF,back:#FFFF88")
294 self
.StyleSetSpec(wxSTC_STYLE_BRACEBAD
, "fore:#FF0000,back:#FFFF88")
297 self
.StyleSetSpec(wxSTC_P_DEFAULT
, "face:%(mono)s" % faces
)
298 self
.StyleSetSpec(wxSTC_P_COMMENTLINE
, "fore:#007F00,face:%(mono)s" % faces
)
299 self
.StyleSetSpec(wxSTC_P_NUMBER
, "")
300 self
.StyleSetSpec(wxSTC_P_STRING
, "fore:#7F007F,face:%(mono)s" % faces
)
301 self
.StyleSetSpec(wxSTC_P_CHARACTER
, "fore:#7F007F,face:%(mono)s" % faces
)
302 self
.StyleSetSpec(wxSTC_P_WORD
, "fore:#00007F,bold")
303 self
.StyleSetSpec(wxSTC_P_TRIPLE
, "fore:#7F0000")
304 self
.StyleSetSpec(wxSTC_P_TRIPLEDOUBLE
, "fore:#000033,back:#FFFFE8")
305 self
.StyleSetSpec(wxSTC_P_CLASSNAME
, "fore:#0000FF,bold")
306 self
.StyleSetSpec(wxSTC_P_DEFNAME
, "fore:#007F7F,bold")
307 self
.StyleSetSpec(wxSTC_P_OPERATOR
, "")
308 self
.StyleSetSpec(wxSTC_P_IDENTIFIER
, "")
309 self
.StyleSetSpec(wxSTC_P_COMMENTBLOCK
, "fore:#7F7F7F")
310 self
.StyleSetSpec(wxSTC_P_STRINGEOL
, "fore:#000000,face:%(mono)s,back:#E0C0E0,eolfilled" % faces
)
312 def OnUpdateUI(self
, evt
):
313 """Check for matching braces."""
317 caretPos
= self
.GetCurrentPos()
319 charBefore
= self
.GetCharAt(caretPos
- 1)
320 #*** Patch to fix bug in wxSTC for wxPython < 2.3.3.
322 charBefore
= 32 # Mimic a space.
324 styleBefore
= self
.GetStyleAt(caretPos
- 1)
327 if charBefore
and chr(charBefore
) in '[]{}()' \
328 and styleBefore
== wxSTC_P_OPERATOR
:
329 braceAtCaret
= caretPos
- 1
333 charAfter
= self
.GetCharAt(caretPos
)
334 #*** Patch to fix bug in wxSTC for wxPython < 2.3.3.
336 charAfter
= 32 # Mimic a space.
338 styleAfter
= self
.GetStyleAt(caretPos
)
339 if charAfter
and chr(charAfter
) in '[]{}()' \
340 and styleAfter
== wxSTC_P_OPERATOR
:
341 braceAtCaret
= caretPos
343 if braceAtCaret
>= 0:
344 braceOpposite
= self
.BraceMatch(braceAtCaret
)
346 if braceAtCaret
!= -1 and braceOpposite
== -1:
347 self
.BraceBadLight(braceAtCaret
)
349 self
.BraceHighlight(braceAtCaret
, braceOpposite
)
351 def OnChar(self
, event
):
352 """Keypress event handler.
354 Only receives an event if OnKeyDown calls event.Skip() for
355 the corresponding event."""
357 # Prevent modification of previously submitted commands/responses.
358 if not self
.CanEdit():
360 key
= event
.KeyCode()
361 currpos
= self
.GetCurrentPos()
362 stoppos
= self
.promptPosEnd
363 # Return (Enter) needs to be ignored in this handler.
364 if key
== WXK_RETURN
:
366 elif key
in self
.autoCompleteKeys
:
367 # Usually the dot (period) key activates auto completion.
368 # Get the command between the prompt and the cursor.
369 # Add the autocomplete character to the end of the command.
370 command
= self
.GetTextRange(stoppos
, currpos
) + chr(key
)
372 if self
.autoComplete
: self
.autoCompleteShow(command
)
373 elif key
== ord('('):
374 # The left paren activates a call tip and cancels
375 # an active auto completion.
376 if self
.AutoCompActive(): self
.AutoCompCancel()
377 # Get the command between the prompt and the cursor.
378 # Add the '(' to the end of the command.
379 self
.ReplaceSelection('')
380 command
= self
.GetTextRange(stoppos
, currpos
) + '('
382 if self
.autoCallTip
: self
.autoCallTipShow(command
)
384 # Allow the normal event handling to take place.
387 def OnKeyDown(self
, event
):
388 """Key down event handler."""
390 # Prevent modification of previously submitted commands/responses.
391 key
= event
.KeyCode()
392 controlDown
= event
.ControlDown()
393 altDown
= event
.AltDown()
394 shiftDown
= event
.ShiftDown()
395 currpos
= self
.GetCurrentPos()
396 endpos
= self
.GetTextLength()
397 selecting
= self
.GetSelectionStart() != self
.GetSelectionEnd()
398 # Return (Enter) is used to submit a command to the interpreter.
399 if not controlDown
and key
== WXK_RETURN
:
400 if self
.AutoCompActive(): self
.AutoCompCancel()
401 if self
.CallTipActive(): self
.CallTipCancel()
403 # Ctrl+Return (Cntrl+Enter) is used to insert a line break.
404 elif controlDown
and key
== WXK_RETURN
:
405 if self
.AutoCompActive(): self
.AutoCompCancel()
406 if self
.CallTipActive(): self
.CallTipCancel()
407 if currpos
== endpos
:
410 self
.insertLineBreak()
411 # If the auto-complete window is up let it do its thing.
412 elif self
.AutoCompActive():
414 # Let Ctrl-Alt-* get handled normally.
415 elif controlDown
and altDown
:
417 # Clear the current, unexecuted command.
418 elif key
== WXK_ESCAPE
:
419 if self
.CallTipActive():
423 # Cut to the clipboard.
424 elif (controlDown
and key
in (ord('X'), ord('x'))) \
425 or (shiftDown
and key
== WXK_DELETE
):
427 # Copy to the clipboard.
428 elif controlDown
and not shiftDown \
429 and key
in (ord('C'), ord('c'), WXK_INSERT
):
431 # Copy to the clipboard, including prompts.
432 elif controlDown
and shiftDown \
433 and key
in (ord('C'), ord('c'), WXK_INSERT
):
434 self
.CopyWithPrompts()
435 # Home needs to be aware of the prompt.
436 elif key
== WXK_HOME
:
437 home
= self
.promptPosEnd
439 self
.SetCurrentPos(home
)
440 if not selecting
and not shiftDown
:
442 self
.EnsureCaretVisible()
446 # The following handlers modify text, so we need to see if there
447 # is a selection that includes text prior to the prompt.
449 # Don't modify a selection with text prior to the prompt.
450 elif selecting
and key
not in NAVKEYS
and not self
.CanEdit():
452 # Paste from the clipboard.
453 elif (controlDown
and not shiftDown \
454 and key
in (ord('V'), ord('v'))) \
455 or (shiftDown
and not controlDown
and key
== WXK_INSERT
):
457 # Paste from the clipboard, run commands.
458 elif controlDown
and shiftDown \
459 and key
in (ord('V'), ord('v')):
461 # Replace with the previous command from the history buffer.
462 elif (controlDown
and key
== WXK_UP
) \
463 or (altDown
and key
in (ord('P'), ord('p'))):
464 self
.OnHistoryReplace(step
=+1)
465 # Replace with the next command from the history buffer.
466 elif (controlDown
and key
== WXK_DOWN
) \
467 or (altDown
and key
in (ord('N'), ord('n'))):
468 self
.OnHistoryReplace(step
=-1)
469 # Insert the previous command from the history buffer.
470 elif (shiftDown
and key
== WXK_UP
) and self
.CanEdit():
471 self
.OnHistoryInsert(step
=+1)
472 # Insert the next command from the history buffer.
473 elif (shiftDown
and key
== WXK_DOWN
) and self
.CanEdit():
474 self
.OnHistoryInsert(step
=-1)
475 # Search up the history for the text in front of the cursor.
477 self
.OnHistorySearch()
478 # Don't backspace over the latest non-continuation prompt.
479 elif key
== WXK_BACK
:
480 if selecting
and self
.CanEdit():
482 elif currpos
> self
.promptPosEnd
:
484 # Only allow these keys after the latest prompt.
485 elif key
in (WXK_TAB
, WXK_DELETE
):
488 # Don't toggle between insert mode and overwrite mode.
489 elif key
== WXK_INSERT
:
491 # Don't allow line deletion.
492 elif controlDown
and key
in (ord('L'), ord('l')):
494 # Don't allow line transposition.
495 elif controlDown
and key
in (ord('T'), ord('t')):
497 # Basic navigation keys should work anywhere.
500 # Protect the readonly portion of the shell.
501 elif not self
.CanEdit():
506 def clearCommand(self
):
507 """Delete the current, unexecuted command."""
508 startpos
= self
.promptPosEnd
509 endpos
= self
.GetTextLength()
510 self
.SetSelection(startpos
, endpos
)
511 self
.ReplaceSelection('')
514 def OnHistoryReplace(self
, step
):
515 """Replace with the previous/next command from the history buffer."""
517 self
.replaceFromHistory(step
)
519 def replaceFromHistory(self
, step
):
520 """Replace selection with command from the history buffer."""
521 self
.ReplaceSelection('')
522 newindex
= self
.historyIndex
+ step
523 if -1 <= newindex
<= len(self
.history
):
524 self
.historyIndex
= newindex
525 if 0 <= newindex
<= len(self
.history
)-1:
526 command
= self
.history
[self
.historyIndex
]
527 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
528 self
.ReplaceSelection(command
)
530 def OnHistoryInsert(self
, step
):
531 """Insert the previous/next command from the history buffer."""
532 if not self
.CanEdit():
534 startpos
= self
.GetCurrentPos()
535 self
.replaceFromHistory(step
)
536 endpos
= self
.GetCurrentPos()
537 self
.SetSelection(endpos
, startpos
)
539 def OnHistorySearch(self
):
540 """Search up the history buffer for the text in front of the cursor."""
541 if not self
.CanEdit():
543 startpos
= self
.GetCurrentPos()
544 # The text up to the cursor is what we search for.
545 numCharsAfterCursor
= self
.GetTextLength() - startpos
546 searchText
= self
.getCommand(rstrip
=0)
547 if numCharsAfterCursor
> 0:
548 searchText
= searchText
[:-numCharsAfterCursor
]
551 # Search upwards from the current history position and loop back
552 # to the beginning if we don't find anything.
553 if (self
.historyIndex
<= -1) \
554 or (self
.historyIndex
>= len(self
.history
)-2):
555 searchOrder
= range(len(self
.history
))
557 searchOrder
= range(self
.historyIndex
+1, len(self
.history
)) + \
558 range(self
.historyIndex
)
559 for i
in searchOrder
:
560 command
= self
.history
[i
]
561 if command
[:len(searchText
)] == searchText
:
562 # Replace the current selection with the one we've found.
563 self
.ReplaceSelection(command
[len(searchText
):])
564 endpos
= self
.GetCurrentPos()
565 self
.SetSelection(endpos
, startpos
)
566 # We've now warped into middle of the history.
567 self
.historyIndex
= i
570 def setStatusText(self
, text
):
571 """Display status information."""
573 # This method will most likely be replaced by the enclosing app
574 # to do something more interesting, like write to a status bar.
577 def insertLineBreak(self
):
578 """Insert a new line break."""
580 self
.write(os
.linesep
)
584 def processLine(self
):
585 """Process the line of text at which the user hit Enter."""
587 # The user hit ENTER and we need to decide what to do. They could be
588 # sitting on any line in the shell.
590 thepos
= self
.GetCurrentPos()
591 startpos
= self
.promptPosEnd
592 endpos
= self
.GetTextLength()
593 # If they hit RETURN inside the current command, execute the command.
595 self
.SetCurrentPos(endpos
)
597 command
= self
.GetTextRange(startpos
, endpos
)
598 lines
= command
.split(os
.linesep
+ sys
.ps2
)
599 lines
= [line
.rstrip() for line
in lines
]
600 command
= '\n'.join(lines
)
601 if self
.reader
.isreading
:
603 # Match the behavior of the standard Python shell when
604 # the user hits return without entering a value.
606 self
.reader
.input = command
607 self
.write(os
.linesep
)
610 # Or replace the current command with the other command.
612 # If the line contains a command (even an invalid one).
613 if self
.getCommand(rstrip
=0):
614 command
= self
.getMultilineCommand()
617 # Otherwise, put the cursor back where we started.
619 self
.SetCurrentPos(thepos
)
620 self
.SetAnchor(thepos
)
622 def getMultilineCommand(self
, rstrip
=1):
623 """Extract a multi-line command from the editor.
625 The command may not necessarily be valid Python syntax."""
626 # XXX Need to extract real prompts here. Need to keep track of the
627 # prompt every time a command is issued.
632 # This is a total hack job, but it works.
633 text
= self
.GetCurLine()[0]
634 line
= self
.GetCurrentLine()
635 while text
[:ps2size
] == ps2
and line
> 0:
638 text
= self
.GetCurLine()[0]
639 if text
[:ps1size
] == ps1
:
640 line
= self
.GetCurrentLine()
642 startpos
= self
.GetCurrentPos() + ps1size
645 while self
.GetCurLine()[0][:ps2size
] == ps2
:
648 stoppos
= self
.GetCurrentPos()
649 command
= self
.GetTextRange(startpos
, stoppos
)
650 command
= command
.replace(os
.linesep
+ sys
.ps2
, '\n')
651 command
= command
.rstrip()
652 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
656 command
= command
.rstrip()
659 def getCommand(self
, text
=None, rstrip
=1):
660 """Extract a command from text which may include a shell prompt.
662 The command may not necessarily be valid Python syntax."""
664 text
= self
.GetCurLine()[0]
665 # Strip the prompt off the front of text leaving just the command.
666 command
= self
.lstripPrompt(text
)
668 command
= '' # Real commands have prompts.
670 command
= command
.rstrip()
673 def lstripPrompt(self
, text
):
674 """Return text without a leading prompt."""
679 # Strip the prompt off the front of text.
680 if text
[:ps1size
] == ps1
:
681 text
= text
[ps1size
:]
682 elif text
[:ps2size
] == ps2
:
683 text
= text
[ps2size
:]
686 def push(self
, command
):
687 """Send command to the interpreter for execution."""
688 self
.write(os
.linesep
)
689 busy
= wxBusyCursor()
690 self
.more
= self
.interp
.push(command
)
693 self
.addHistory(command
.rstrip())
696 def addHistory(self
, command
):
697 """Add command to the command history."""
698 # Reset the history position.
699 self
.historyIndex
= -1
700 # Insert this command into the history, unless it's a blank
701 # line or the same as the last command.
703 and (len(self
.history
) == 0 or command
!= self
.history
[0]):
704 self
.history
.insert(0, command
)
706 def write(self
, text
):
707 """Display text in the shell.
709 Replace line endings with OS-specific endings."""
710 text
= self
.fixLineEndings(text
)
712 self
.EnsureCaretVisible()
714 def fixLineEndings(self
, text
):
715 """Return text with line endings replaced by OS-specific endings."""
716 lines
= text
.split('\r\n')
717 for l
in range(len(lines
)):
718 chunks
= lines
[l
].split('\r')
719 for c
in range(len(chunks
)):
720 chunks
[c
] = os
.linesep
.join(chunks
[c
].split('\n'))
721 lines
[l
] = os
.linesep
.join(chunks
)
722 text
= os
.linesep
.join(lines
)
726 """Display appropriate prompt for the context, either ps1, ps2 or ps3.
728 If this is a continuation line, autoindent as necessary."""
729 isreading
= self
.reader
.isreading
732 prompt
= str(sys
.ps3
)
734 prompt
= str(sys
.ps2
)
736 prompt
= str(sys
.ps1
)
737 pos
= self
.GetCurLine()[1]
742 self
.write(os
.linesep
)
744 self
.promptPosStart
= self
.GetCurrentPos()
748 self
.promptPosEnd
= self
.GetCurrentPos()
749 # Keep the undo feature from undoing previous responses.
750 self
.EmptyUndoBuffer()
751 # XXX Add some autoindent magic here if more.
753 self
.write(' '*4) # Temporary hack indentation.
754 self
.EnsureCaretVisible()
755 self
.ScrollToColumn(0)
758 """Replacement for stdin.readline()."""
764 while not reader
.input:
772 def raw_input(self
, prompt
=''):
773 """Return string based on user input."""
776 return self
.readline()
778 def ask(self
, prompt
='Please enter your response:'):
779 """Get response from the user using a dialog box."""
780 dialog
= wxTextEntryDialog(None, prompt
, \
781 'Input Dialog (Raw)', '')
783 if dialog
.ShowModal() == wxID_OK
:
784 text
= dialog
.GetValue()
791 """Halt execution pending a response from the user."""
792 self
.ask('Press enter to continue:')
795 """Delete all text from the shell."""
798 def run(self
, command
, prompt
=1, verbose
=1):
799 """Execute command within the shell as if it was typed in directly.
800 >>> shell.run('print "this"')
805 # Go to the very bottom of the text.
806 endpos
= self
.GetTextLength()
807 self
.SetCurrentPos(endpos
)
808 command
= command
.rstrip()
809 if prompt
: self
.prompt()
810 if verbose
: self
.write(command
)
813 def runfile(self
, filename
):
814 """Execute all commands in file as if they were typed into the shell."""
815 file = open(filename
)
818 for command
in file.readlines():
819 if command
[:6] == 'shell.': # Run shell methods silently.
820 self
.run(command
, prompt
=0, verbose
=0)
822 self
.run(command
, prompt
=0, verbose
=1)
826 def autoCompleteShow(self
, command
):
827 """Display auto-completion popup list."""
828 list = self
.interp
.getAutoCompleteList(command
,
829 includeMagic
=self
.autoCompleteIncludeMagic
,
830 includeSingle
=self
.autoCompleteIncludeSingle
,
831 includeDouble
=self
.autoCompleteIncludeDouble
)
833 options
= ' '.join(list)
835 self
.AutoCompShow(offset
, options
)
837 def autoCallTipShow(self
, command
):
838 """Display argument spec and docstring in a popup bubble thingie."""
839 if self
.CallTipActive
: self
.CallTipCancel()
840 (name
, argspec
, tip
) = self
.interp
.getCallTip(command
)
842 startpos
= self
.GetCurrentPos()
843 self
.write(argspec
+ ')')
844 endpos
= self
.GetCurrentPos()
845 self
.SetSelection(endpos
, startpos
)
847 curpos
= self
.GetCurrentPos()
848 tippos
= curpos
- (len(name
) + 1)
849 fallback
= curpos
- self
.GetColumn(curpos
)
850 # In case there isn't enough room, only go back to the fallback.
851 tippos
= max(tippos
, fallback
)
852 self
.CallTipShow(tippos
, tip
)
854 def writeOut(self
, text
):
855 """Replacement for stdout."""
858 def writeErr(self
, text
):
859 """Replacement for stderr."""
862 def redirectStdin(self
, redirect
=1):
863 """If redirect is true then sys.stdin will come from the shell."""
865 sys
.stdin
= self
.reader
867 sys
.stdin
= self
.stdin
869 def redirectStdout(self
, redirect
=1):
870 """If redirect is true then sys.stdout will go to the shell."""
872 sys
.stdout
= PseudoFileOut(self
.writeOut
)
874 sys
.stdout
= self
.stdout
876 def redirectStderr(self
, redirect
=1):
877 """If redirect is true then sys.stderr will go to the shell."""
879 sys
.stderr
= PseudoFileErr(self
.writeErr
)
881 sys
.stderr
= self
.stderr
884 """Return true if text is selected and can be cut."""
885 if self
.GetSelectionStart() != self
.GetSelectionEnd() \
886 and self
.GetSelectionStart() >= self
.promptPosEnd \
887 and self
.GetSelectionEnd() >= self
.promptPosEnd
:
893 """Return true if text is selected and can be copied."""
894 return self
.GetSelectionStart() != self
.GetSelectionEnd()
897 """Return true if a paste should succeed."""
898 if self
.CanEdit() and wxStyledTextCtrl
.CanPaste(self
):
904 """Return true if editing should succeed."""
905 if self
.GetSelectionStart() != self
.GetSelectionEnd():
906 if self
.GetSelectionStart() >= self
.promptPosEnd \
907 and self
.GetSelectionEnd() >= self
.promptPosEnd
:
912 return self
.GetCurrentPos() >= self
.promptPosEnd
915 """Remove selection and place it on the clipboard."""
916 if self
.CanCut() and self
.CanCopy():
917 if self
.AutoCompActive(): self
.AutoCompCancel()
918 if self
.CallTipActive
: self
.CallTipCancel()
920 self
.ReplaceSelection('')
923 """Copy selection and place it on the clipboard."""
925 command
= self
.GetSelectedText()
926 command
= command
.replace(os
.linesep
+ sys
.ps2
, os
.linesep
)
927 command
= command
.replace(os
.linesep
+ sys
.ps1
, os
.linesep
)
928 command
= self
.lstripPrompt(text
=command
)
929 data
= wxTextDataObject(command
)
930 if wxTheClipboard
.Open():
931 wxTheClipboard
.SetData(data
)
932 wxTheClipboard
.Close()
934 def CopyWithPrompts(self
):
935 """Copy selection, including prompts, and place it on the clipboard."""
937 command
= self
.GetSelectedText()
938 data
= wxTextDataObject(command
)
939 if wxTheClipboard
.Open():
940 wxTheClipboard
.SetData(data
)
941 wxTheClipboard
.Close()
944 """Replace selection with clipboard contents."""
945 if self
.CanPaste() and wxTheClipboard
.Open():
946 if wxTheClipboard
.IsSupported(wxDataFormat(wxDF_TEXT
)):
947 data
= wxTextDataObject()
948 if wxTheClipboard
.GetData(data
):
949 self
.ReplaceSelection('')
950 command
= data
.GetText()
951 command
= command
.rstrip()
952 command
= self
.fixLineEndings(command
)
953 command
= self
.lstripPrompt(text
=command
)
954 command
= command
.replace(os
.linesep
+ sys
.ps2
, '\n')
955 command
= command
.replace(os
.linesep
, '\n')
956 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
958 wxTheClipboard
.Close()
960 def PasteAndRun(self
):
961 """Replace selection with clipboard contents, run commands."""
962 if wxTheClipboard
.Open():
963 if wxTheClipboard
.IsSupported(wxDataFormat(wxDF_TEXT
)):
964 data
= wxTextDataObject()
965 if wxTheClipboard
.GetData(data
):
966 endpos
= self
.GetTextLength()
967 self
.SetCurrentPos(endpos
)
968 startpos
= self
.promptPosEnd
969 self
.SetSelection(startpos
, endpos
)
970 self
.ReplaceSelection('')
971 text
= data
.GetText()
973 text
= self
.fixLineEndings(text
)
974 text
= self
.lstripPrompt(text
=text
)
975 text
= text
.replace(os
.linesep
+ sys
.ps1
, '\n')
976 text
= text
.replace(os
.linesep
+ sys
.ps2
, '\n')
977 text
= text
.replace(os
.linesep
, '\n')
978 lines
= text
.split('\n')
982 if line
.strip() != '' and line
.lstrip() == line
:
985 # Add the previous command to the list.
986 commands
.append(command
)
987 # Start a new command, which may be multiline.
990 # Multiline command. Add to the command.
993 commands
.append(command
)
994 for command
in commands
:
995 command
= command
.replace('\n', os
.linesep
+ sys
.ps2
)
998 wxTheClipboard
.Close()
1000 def wrap(self
, wrap
=1):
1001 """Sets whether text is word wrapped."""
1003 self
.SetWrapMode(wrap
)
1004 except AttributeError:
1005 return 'Wrapping is not available in this version of PyCrust.'
1007 def zoom(self
, points
=0):
1008 """Set the zoom level.
1010 This number of points is added to the size of all fonts.
1011 It may be positive to magnify or negative to reduce."""
1012 self
.SetZoom(points
)
1015 wxID_SELECTALL
= NewId() # This *should* be defined by wxPython.
1016 ID_AUTOCOMP
= NewId()
1017 ID_AUTOCOMP_SHOW
= NewId()
1018 ID_AUTOCOMP_INCLUDE_MAGIC
= NewId()
1019 ID_AUTOCOMP_INCLUDE_SINGLE
= NewId()
1020 ID_AUTOCOMP_INCLUDE_DOUBLE
= NewId()
1021 ID_CALLTIPS
= NewId()
1022 ID_CALLTIPS_SHOW
= NewId()
1026 """Mixin class to add standard menu items."""
1028 def createMenus(self
):
1029 m
= self
.fileMenu
= wxMenu()
1031 m
.Append(wxID_EXIT
, 'E&xit', 'Exit PyCrust')
1033 m
= self
.editMenu
= wxMenu()
1034 m
.Append(wxID_UNDO
, '&Undo \tCtrl+Z', 'Undo the last action')
1035 m
.Append(wxID_REDO
, '&Redo \tCtrl+Y', 'Redo the last undone action')
1037 m
.Append(wxID_CUT
, 'Cu&t \tCtrl+X', 'Cut the selection')
1038 m
.Append(wxID_COPY
, '&Copy \tCtrl+C', 'Copy the selection')
1039 m
.Append(wxID_PASTE
, '&Paste \tCtrl+V', 'Paste')
1041 m
.Append(wxID_CLEAR
, 'Cle&ar', 'Delete the selection')
1042 m
.Append(wxID_SELECTALL
, 'Select A&ll', 'Select all text')
1044 m
= self
.autocompMenu
= wxMenu()
1045 m
.Append(ID_AUTOCOMP_SHOW
, 'Show Auto Completion', \
1046 'Show auto completion during dot syntax', 1)
1047 m
.Append(ID_AUTOCOMP_INCLUDE_MAGIC
, 'Include Magic Attributes', \
1048 'Include attributes visible to __getattr__ and __setattr__', 1)
1049 m
.Append(ID_AUTOCOMP_INCLUDE_SINGLE
, 'Include Single Underscores', \
1050 'Include attibutes prefixed by a single underscore', 1)
1051 m
.Append(ID_AUTOCOMP_INCLUDE_DOUBLE
, 'Include Double Underscores', \
1052 'Include attibutes prefixed by a double underscore', 1)
1054 m
= self
.calltipsMenu
= wxMenu()
1055 m
.Append(ID_CALLTIPS_SHOW
, 'Show Call Tips', \
1056 'Show call tips with argument specifications', 1)
1058 m
= self
.optionsMenu
= wxMenu()
1059 m
.AppendMenu(ID_AUTOCOMP
, '&Auto Completion', self
.autocompMenu
, \
1060 'Auto Completion Options')
1061 m
.AppendMenu(ID_CALLTIPS
, '&Call Tips', self
.calltipsMenu
, \
1064 m
= self
.helpMenu
= wxMenu()
1066 m
.Append(wxID_ABOUT
, '&About...', 'About PyCrust')
1068 b
= self
.menuBar
= wxMenuBar()
1069 b
.Append(self
.fileMenu
, '&File')
1070 b
.Append(self
.editMenu
, '&Edit')
1071 b
.Append(self
.optionsMenu
, '&Options')
1072 b
.Append(self
.helpMenu
, '&Help')
1075 EVT_MENU(self
, wxID_EXIT
, self
.OnExit
)
1076 EVT_MENU(self
, wxID_UNDO
, self
.OnUndo
)
1077 EVT_MENU(self
, wxID_REDO
, self
.OnRedo
)
1078 EVT_MENU(self
, wxID_CUT
, self
.OnCut
)
1079 EVT_MENU(self
, wxID_COPY
, self
.OnCopy
)
1080 EVT_MENU(self
, wxID_PASTE
, self
.OnPaste
)
1081 EVT_MENU(self
, wxID_CLEAR
, self
.OnClear
)
1082 EVT_MENU(self
, wxID_SELECTALL
, self
.OnSelectAll
)
1083 EVT_MENU(self
, wxID_ABOUT
, self
.OnAbout
)
1084 EVT_MENU(self
, ID_AUTOCOMP_SHOW
, \
1085 self
.OnAutoCompleteShow
)
1086 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_MAGIC
, \
1087 self
.OnAutoCompleteIncludeMagic
)
1088 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_SINGLE
, \
1089 self
.OnAutoCompleteIncludeSingle
)
1090 EVT_MENU(self
, ID_AUTOCOMP_INCLUDE_DOUBLE
, \
1091 self
.OnAutoCompleteIncludeDouble
)
1092 EVT_MENU(self
, ID_CALLTIPS_SHOW
, \
1093 self
.OnCallTipsShow
)
1095 EVT_UPDATE_UI(self
, wxID_UNDO
, self
.OnUpdateMenu
)
1096 EVT_UPDATE_UI(self
, wxID_REDO
, self
.OnUpdateMenu
)
1097 EVT_UPDATE_UI(self
, wxID_CUT
, self
.OnUpdateMenu
)
1098 EVT_UPDATE_UI(self
, wxID_COPY
, self
.OnUpdateMenu
)
1099 EVT_UPDATE_UI(self
, wxID_PASTE
, self
.OnUpdateMenu
)
1100 EVT_UPDATE_UI(self
, wxID_CLEAR
, self
.OnUpdateMenu
)
1101 EVT_UPDATE_UI(self
, ID_AUTOCOMP_SHOW
, self
.OnUpdateMenu
)
1102 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_MAGIC
, self
.OnUpdateMenu
)
1103 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_SINGLE
, self
.OnUpdateMenu
)
1104 EVT_UPDATE_UI(self
, ID_AUTOCOMP_INCLUDE_DOUBLE
, self
.OnUpdateMenu
)
1105 EVT_UPDATE_UI(self
, ID_CALLTIPS_SHOW
, self
.OnUpdateMenu
)
1107 def OnExit(self
, event
):
1110 def OnUndo(self
, event
):
1113 def OnRedo(self
, event
):
1116 def OnCut(self
, event
):
1119 def OnCopy(self
, event
):
1122 def OnPaste(self
, event
):
1125 def OnClear(self
, event
):
1128 def OnSelectAll(self
, event
):
1129 self
.shell
.SelectAll()
1131 def OnAbout(self
, event
):
1132 """Display an About PyCrust window."""
1134 title
= 'About PyCrust'
1135 text
= 'PyCrust %s\n\n' % VERSION
+ \
1136 'Yet another Python shell, only flakier.\n\n' + \
1137 'Half-baked by Patrick K. O\'Brien,\n' + \
1138 'the other half is still in the oven.\n\n' + \
1139 'Shell Revision: %s\n' % self
.shell
.revision
+ \
1140 'Interpreter Revision: %s\n\n' % self
.shell
.interp
.revision
+ \
1141 'Python Version: %s\n' % sys
.version
.split()[0] + \
1142 'wxPython Version: %s\n' % wx
.__version
__ + \
1143 'Platform: %s\n' % sys
.platform
1144 dialog
= wxMessageDialog(self
, text
, title
, wxOK | wxICON_INFORMATION
)
1148 def OnAutoCompleteShow(self
, event
):
1149 self
.shell
.autoComplete
= event
.IsChecked()
1151 def OnAutoCompleteIncludeMagic(self
, event
):
1152 self
.shell
.autoCompleteIncludeMagic
= event
.IsChecked()
1154 def OnAutoCompleteIncludeSingle(self
, event
):
1155 self
.shell
.autoCompleteIncludeSingle
= event
.IsChecked()
1157 def OnAutoCompleteIncludeDouble(self
, event
):
1158 self
.shell
.autoCompleteIncludeDouble
= event
.IsChecked()
1160 def OnCallTipsShow(self
, event
):
1161 self
.shell
.autoCallTip
= event
.IsChecked()
1163 def OnUpdateMenu(self
, event
):
1164 """Update menu items based on current status."""
1167 event
.Enable(self
.shell
.CanUndo())
1168 elif id == wxID_REDO
:
1169 event
.Enable(self
.shell
.CanRedo())
1170 elif id == wxID_CUT
:
1171 event
.Enable(self
.shell
.CanCut())
1172 elif id == wxID_COPY
:
1173 event
.Enable(self
.shell
.CanCopy())
1174 elif id == wxID_PASTE
:
1175 event
.Enable(self
.shell
.CanPaste())
1176 elif id == wxID_CLEAR
:
1177 event
.Enable(self
.shell
.CanCut())
1178 elif id == ID_AUTOCOMP_SHOW
:
1179 event
.Check(self
.shell
.autoComplete
)
1180 elif id == ID_AUTOCOMP_INCLUDE_MAGIC
:
1181 event
.Check(self
.shell
.autoCompleteIncludeMagic
)
1182 elif id == ID_AUTOCOMP_INCLUDE_SINGLE
:
1183 event
.Check(self
.shell
.autoCompleteIncludeSingle
)
1184 elif id == ID_AUTOCOMP_INCLUDE_DOUBLE
:
1185 event
.Check(self
.shell
.autoCompleteIncludeDouble
)
1186 elif id == ID_CALLTIPS_SHOW
:
1187 event
.Check(self
.shell
.autoCallTip
)
1190 class ShellFrame(wxFrame
, ShellMenu
):
1191 """Frame containing the PyCrust shell component."""
1193 name
= 'PyCrust Shell Frame'
1194 revision
= __revision__
1196 def __init__(self
, parent
=None, id=-1, title
='PyShell', \
1197 pos
=wxDefaultPosition
, size
=wxDefaultSize
, \
1198 style
=wxDEFAULT_FRAME_STYLE
, locals=None, \
1199 InterpClass
=None, *args
, **kwds
):
1200 """Create a PyCrust ShellFrame instance."""
1201 wxFrame
.__init
__(self
, parent
, id, title
, pos
, size
, style
)
1202 intro
= 'Welcome To PyCrust %s - The Flakiest Python Shell' % VERSION
1203 intro
+= '\nSponsored by Orbtech - Your source for Python programming expertise.'
1204 self
.CreateStatusBar()
1205 self
.SetStatusText(intro
.replace('\n', ', '))
1207 self
.SetIcon(images
.getPyCrustIcon())
1208 self
.shell
= Shell(parent
=self
, id=-1, introText
=intro
, \
1209 locals=locals, InterpClass
=InterpClass
, \
1211 # Override the shell so that status messages go to the status bar.
1212 self
.shell
.setStatusText
= self
.SetStatusText
1214 EVT_CLOSE(self
, self
.OnCloseWindow
)
1216 def OnCloseWindow(self
, event
):
1217 self
.shell
.destroy()