1 """PyCrust Shell is the gui text control in which a user interacts and types
2 in commands to be sent to the interpreter. This particular shell is based on
3 wxPython's wxStyledTextCtrl.
6 __author__
= "Patrick K. O'Brien <pobrien@orbtech.com>"
8 __date__
= "July 1, 2001"
9 __version__
= "$Revision$"[11:-2]
11 from wxPython
.wx
import *
12 from wxPython
.stc
import *
18 from version
import VERSION
20 if wxPlatform
== '__WXMSW__':
21 faces
= { 'times' : 'Times New Roman',
22 'mono' : 'Courier New',
23 'helv' : 'Lucida Console',
24 'lucida' : 'Lucida Console',
25 'other' : 'Comic Sans MS',
31 faces
= { 'times' : 'Times',
34 'other' : 'new century schoolbook',
41 class Shell(wxStyledTextCtrl
):
42 """PyCrust Shell based on wxStyledTextCtrl."""
43 name
= 'PyCrust Shell'
44 revision
= __version__
45 def __init__(self
, parent
, id, introText
='', locals=None, interp
=None):
46 """Create a PyCrust Shell object."""
47 wxStyledTextCtrl
.__init
__(self
, parent
, id, style
=wxCLIP_CHILDREN
)
48 self
.introText
= introText
49 # Keep track of the most recent prompt starting and ending positions.
50 self
.promptPos
= [0, 0]
51 # Keep track of multi-line commands.
53 # Assign handlers for keyboard events.
54 EVT_KEY_DOWN(self
, self
.OnKeyDown
)
55 EVT_CHAR(self
, self
.OnChar
)
56 # Create a default interpreter if one isn't provided.
58 from interpreter
import Interpreter
59 from pseudo
import PseudoFileIn
, PseudoFileOut
, PseudoFileErr
60 self
.stdin
= PseudoFileIn(self
.readIn
)
61 self
.stdout
= PseudoFileOut(self
.writeOut
)
62 self
.stderr
= PseudoFileErr(self
.writeErr
)
63 # Override the default locals so we have something interesting.
64 self
.locals = {'__name__': 'PyCrust',
65 '__doc__': 'PyCrust, The Python Shell.',
66 '__version__': VERSION
,
68 # Add the dictionary that was passed in.
70 self
.locals.update(locals)
71 self
.interp
= Interpreter(locals=self
.locals,
79 # Configure various defaults and user preferences.
83 self
.showIntro(self
.introText
)
88 self
.setBuiltinKeywords()
97 # Do this last so the user has complete control over their
98 # environment. They can override anything they want.
100 self
.execStartupScript(self
.interp
.startupScript
)
111 """Configure shell based on user preferences."""
112 self
.SetMarginType(1, wxSTC_MARGIN_NUMBER
)
113 self
.SetMarginWidth(1, 40)
115 self
.SetLexer(wxSTC_LEX_PYTHON
)
116 self
.SetKeyWords(0, ' '.join(keyword
.kwlist
))
118 self
.setStyles(faces
)
119 self
.SetViewWhiteSpace(0)
122 # Do we want to automatically pop up command completion options?
123 self
.autoComplete
= 1
124 self
.autoCompleteIncludeMagic
= 1
125 self
.autoCompleteIncludeSingle
= 1
126 self
.autoCompleteIncludeDouble
= 1
127 self
.autoCompleteCaseInsensitive
= 1
128 self
.AutoCompSetIgnoreCase(self
.autoCompleteCaseInsensitive
)
129 # De we want to automatically pop up command argument help?
131 self
.CallTipSetBackground(wxColour(255, 255, 232))
133 def showIntro(self
, text
=''):
134 """Display introductory text in the shell."""
136 if text
[-1] != '\n': text
+= '\n'
139 self
.write(self
.interp
.introText
)
140 except AttributeError:
143 def setBuiltinKeywords(self
):
144 """Create pseudo keywords as part of builtins.
146 This is a rather clever hack that sets "close", "exit" and "quit"
147 to a PseudoKeyword object so that we can make them do what we want.
148 In this case what we want is to call our self.quit() method.
149 The user can type "close", "exit" or "quit" without the final parens.
152 from pseudo
import PseudoKeyword
153 __builtin__
.close
= __builtin__
.exit
= __builtin__
.quit
= \
154 PseudoKeyword(self
.quit
)
157 """Quit the application."""
159 # XXX Good enough for now but later we want to send a close event.
161 # In the close event handler we can prompt to make sure they want to quit.
162 # Other applications, like PythonCard, may choose to hide rather than
163 # quit so we should just post the event and let the surrounding app
164 # decide what it wants to do.
165 self
.write('Click on the close button to leave the application.')
167 def setLocalShell(self
):
168 """Add 'shell' to locals."""
169 self
.interp
.locals['shell'] = self
171 def execStartupScript(self
, startupScript
):
172 """Execute the user's PYTHONSTARTUP script if they have one."""
173 if startupScript
and os
.path
.isfile(startupScript
):
174 startupText
= 'Startup script executed: ' + startupScript
175 self
.push('print %s;execfile(%s)' % \
176 (`startupText`
, `startupScript`
))
180 def setStyles(self
, faces
):
181 """Configure font size, typeface and color for lexer."""
184 self
.StyleSetSpec(wxSTC_STYLE_DEFAULT
, "face:%(mono)s,size:%(size)d" % faces
)
189 self
.StyleSetSpec(wxSTC_STYLE_LINENUMBER
, "back:#C0C0C0,face:%(mono)s,size:%(lnsize)d" % faces
)
190 self
.StyleSetSpec(wxSTC_STYLE_CONTROLCHAR
, "face:%(mono)s" % faces
)
191 self
.StyleSetSpec(wxSTC_STYLE_BRACELIGHT
, "fore:#0000FF,back:#FFFF88")
192 self
.StyleSetSpec(wxSTC_STYLE_BRACEBAD
, "fore:#FF0000,back:#FFFF88")
195 self
.StyleSetSpec(wxSTC_P_DEFAULT
, "face:%(mono)s" % faces
)
196 self
.StyleSetSpec(wxSTC_P_COMMENTLINE
, "fore:#007F00,face:%(mono)s" % faces
)
197 self
.StyleSetSpec(wxSTC_P_NUMBER
, "")
198 self
.StyleSetSpec(wxSTC_P_STRING
, "fore:#7F007F,face:%(mono)s" % faces
)
199 self
.StyleSetSpec(wxSTC_P_CHARACTER
, "fore:#7F007F,face:%(mono)s" % faces
)
200 self
.StyleSetSpec(wxSTC_P_WORD
, "fore:#00007F,bold")
201 self
.StyleSetSpec(wxSTC_P_TRIPLE
, "fore:#7F0000")
202 self
.StyleSetSpec(wxSTC_P_TRIPLEDOUBLE
, "fore:#000033,back:#FFFFE8")
203 self
.StyleSetSpec(wxSTC_P_CLASSNAME
, "fore:#0000FF,bold")
204 self
.StyleSetSpec(wxSTC_P_DEFNAME
, "fore:#007F7F,bold")
205 self
.StyleSetSpec(wxSTC_P_OPERATOR
, "")
206 self
.StyleSetSpec(wxSTC_P_IDENTIFIER
, "")
207 self
.StyleSetSpec(wxSTC_P_COMMENTBLOCK
, "fore:#7F7F7F")
208 self
.StyleSetSpec(wxSTC_P_STRINGEOL
, "fore:#000000,face:%(mono)s,back:#E0C0E0,eolfilled" % faces
)
210 def OnKeyDown(self
, event
):
211 """Key down event handler.
213 The main goal here is to not allow modifications to previous
216 key
= event
.KeyCode()
217 currpos
= self
.GetCurrentPos()
218 stoppos
= self
.promptPos
[1]
219 # If the auto-complete window is up let it do its thing.
220 if self
.AutoCompActive():
222 # Return is used to submit a command to the interpreter.
223 elif key
== WXK_RETURN
:
224 if self
.CallTipActive
: self
.CallTipCancel()
226 # Home needs to be aware of the prompt.
227 elif key
== WXK_HOME
:
228 if currpos
>= stoppos
:
229 self
.SetCurrentPos(stoppos
)
230 self
.SetAnchor(stoppos
)
233 # Basic navigation keys should work anywhere.
234 elif key
in (WXK_END
, WXK_LEFT
, WXK_RIGHT
, WXK_UP
, WXK_DOWN
, \
235 WXK_PRIOR
, WXK_NEXT
):
237 # Don't backspace over the latest prompt.
238 elif key
== WXK_BACK
:
239 if currpos
> stoppos
:
241 # Only allow these keys after the latest prompt.
242 elif key
in (WXK_TAB
, WXK_DELETE
):
243 if currpos
>= stoppos
:
245 # Don't toggle between insert mode and overwrite mode.
246 elif key
== WXK_INSERT
:
251 def OnChar(self
, event
):
252 """Keypress event handler.
254 The main goal here is to not allow modifications to previous
257 key
= event
.KeyCode()
258 currpos
= self
.GetCurrentPos()
259 stoppos
= self
.promptPos
[1]
260 if currpos
>= stoppos
:
262 # "." The dot or period key activates auto completion.
263 # Get the command between the prompt and the cursor.
264 # Add a dot to the end of the command.
265 command
= self
.GetTextRange(stoppos
, currpos
) + '.'
267 if self
.autoComplete
: self
.autoCompleteShow(command
)
269 # "(" The left paren activates a call tip and cancels
270 # an active auto completion.
271 if self
.AutoCompActive(): self
.AutoCompCancel()
272 # Get the command between the prompt and the cursor.
273 # Add the '(' to the end of the command.
274 command
= self
.GetTextRange(stoppos
, currpos
) + '('
276 if self
.autoCallTip
: self
.autoCallTipShow(command
)
278 # Allow the normal event handling to take place.
283 def setStatusText(self
, text
):
284 """Display status information."""
286 # This method will most likely be replaced by the enclosing app
287 # to do something more interesting, like write to a status bar.
290 def processLine(self
):
291 """Process the line of text at which the user hit Enter."""
293 # The user hit ENTER and we need to decide what to do. They could be
294 # sitting on any line in the shell.
296 # Grab information about the current line.
297 thepos
= self
.GetCurrentPos()
298 theline
= self
.GetCurrentLine()
299 thetext
= self
.GetCurLine()[0]
300 command
= self
.getCommand(thetext
)
301 # Go to the very bottom of the text.
302 endpos
= self
.GetTextLength()
303 self
.SetCurrentPos(endpos
)
304 endline
= self
.GetCurrentLine()
305 # If they hit RETURN on the last line, execute the command.
306 if theline
== endline
:
308 # Otherwise, replace the last line with the new line.
310 # If the new line contains a command (even an invalid one).
312 startpos
= self
.PositionFromLine(endline
)
313 self
.SetSelection(startpos
, endpos
)
314 self
.ReplaceSelection('')
317 # Otherwise, put the cursor back where we started.
319 self
.SetCurrentPos(thepos
)
320 self
.SetAnchor(thepos
)
322 def getCommand(self
, text
):
323 """Extract a command from text which may include a shell prompt.
325 The command may not necessarily be valid Python syntax.
328 # XXX Need to extract real prompts here. Need to keep track of the
329 # prompt every time a command is issued. Do this in the interpreter
330 # with a line number, prompt, command dictionary. For the history, perhaps.
336 # Strip the prompt off the front of text leaving just the command.
337 if text
[:ps1size
] == ps1
:
338 command
= text
[ps1size
:]
339 elif text
[:ps2size
] == ps2
:
340 command
= text
[ps2size
:]
345 def push(self
, command
):
346 """Send command to the interpreter for execution."""
348 self
.more
= self
.interp
.push(command
)
350 # Keep the undo feature from undoing previous responses. The only
351 # thing that can be undone is stuff typed after the prompt, before
352 # hitting enter. After they hit enter it becomes permanent.
353 self
.EmptyUndoBuffer()
355 def write(self
, text
):
356 """Display text in the shell."""
358 self
.EnsureCaretVisible()
359 #self.ScrollToColumn(0)
362 """Display appropriate prompt for the context, either ps1 or ps2.
364 If this is a continuation line, autoindent as necessary.
367 prompt
= str(sys
.ps2
)
369 prompt
= str(sys
.ps1
)
370 pos
= self
.GetCurLine()[1]
371 if pos
> 0: self
.write('\n')
372 self
.promptPos
[0] = self
.GetCurrentPos()
374 self
.promptPos
[1] = self
.GetCurrentPos()
375 # XXX Add some autoindent magic here if more.
377 self
.write('\t') # Temporary hack indentation.
378 self
.EnsureCaretVisible()
379 self
.ScrollToColumn(0)
382 """Replacement for stdin."""
383 prompt
= 'Please enter your response:'
384 dialog
= wxTextEntryDialog(None, prompt
, \
385 'Input Dialog (Standard)', '')
387 if dialog
.ShowModal() == wxID_OK
:
388 text
= dialog
.GetValue()
389 self
.write(text
+ '\n')
395 def readRaw(self
, prompt
='Please enter your response:'):
396 """Replacement for raw_input."""
397 dialog
= wxTextEntryDialog(None, prompt
, \
398 'Input Dialog (Raw)', '')
400 if dialog
.ShowModal() == wxID_OK
:
401 text
= dialog
.GetValue()
407 def ask(self
, prompt
='Please enter your response:'):
408 """Get response from the user."""
409 return raw_input(prompt
=prompt
)
412 """Halt execution pending a response from the user."""
413 self
.ask('Press enter to continue:')
416 """Delete all text from the shell."""
419 def run(self
, command
, prompt
=1, verbose
=1):
420 """Execute command within the shell as if it was typed in directly.
421 >>> shell.run('print "this"')
426 command
= command
.rstrip()
427 if prompt
: self
.prompt()
428 if verbose
: self
.write(command
)
431 def runfile(self
, filename
):
432 """Execute all commands in file as if they were typed into the shell."""
433 file = open(filename
)
436 for command
in file.readlines():
437 if command
[:6] == 'shell.': # Run shell methods silently.
438 self
.run(command
, prompt
=0, verbose
=0)
440 self
.run(command
, prompt
=0, verbose
=1)
444 def autoCompleteShow(self
, command
):
445 """Display auto-completion popup list."""
446 list = self
.interp
.getAutoCompleteList(command
, \
447 includeMagic
=self
.autoCompleteIncludeMagic
, \
448 includeSingle
=self
.autoCompleteIncludeSingle
, \
449 includeDouble
=self
.autoCompleteIncludeDouble
)
451 options
= ' '.join(list)
453 self
.AutoCompShow(offset
, options
)
455 def autoCallTipShow(self
, command
):
456 """Display argument spec and docstring in a popup bubble thingie."""
457 if self
.CallTipActive
: self
.CallTipCancel()
458 tip
= self
.interp
.getCallTip(command
)
460 offset
= self
.GetCurrentPos()
461 self
.CallTipShow(offset
, tip
)
463 def writeOut(self
, text
):
464 """Replacement for stdout."""
467 def writeErr(self
, text
):
468 """Replacement for stderr."""
472 """Return true if text is selected and can be cut."""
473 return self
.GetSelectionStart() != self
.GetSelectionEnd()
476 """Return true if text is selected and can be copied."""
477 return self
.GetSelectionStart() != self
.GetSelectionEnd()