]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wxPython/lib/PyCrust/shell.py
3f96dd3f44a1491c654fbc5eef42a78240284345
[wxWidgets.git] / wxPython / wxPython / lib / PyCrust / shell.py
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."""
6
7 __author__ = "Patrick K. O'Brien <pobrien@orbtech.com>"
8 __cvsid__ = "$Id$"
9 __revision__ = "$Revision$"[11:-2]
10
11 from wxPython.wx import *
12 from wxPython.stc import *
13 import keyword
14 import os
15 import sys
16 from pseudo import PseudoFileIn
17 from pseudo import PseudoFileOut
18 from pseudo import PseudoFileErr
19 from version import VERSION
20
21
22 NAVKEYS = (WXK_END, WXK_LEFT, WXK_RIGHT, WXK_UP, WXK_DOWN, WXK_PRIOR, WXK_NEXT)
23
24 if wxPlatform == '__WXMSW__':
25 faces = { 'times' : 'Times New Roman',
26 'mono' : 'Courier New',
27 'helv' : 'Lucida Console',
28 'lucida' : 'Lucida Console',
29 'other' : 'Comic Sans MS',
30 'size' : 10,
31 'lnsize' : 9,
32 'backcol': '#FFFFFF',
33 }
34 # Versions of wxPython prior to 2.3.2 had a sizing bug on Win platform.
35 # The font was 2 points too large. So we need to reduce the font size.
36 if (wxMAJOR_VERSION, wxMINOR_VERSION, wxRELEASE_NUMBER) < (2, 3, 2):
37 faces['size'] -= 2
38 faces['lnsize'] -= 2
39 else: # GTK
40 faces = { 'times' : 'Times',
41 'mono' : 'Courier',
42 'helv' : 'Helvetica',
43 'other' : 'new century schoolbook',
44 'size' : 12,
45 'lnsize' : 10,
46 'backcol': '#FFFFFF',
47 }
48
49
50 class ShellFacade:
51 """Simplified interface to all shell-related functionality.
52
53 This is a semi-transparent facade, in that all attributes of other are
54 still accessible, even though only some are visible to the user."""
55
56 name = 'PyCrust Shell Interface'
57 revision = __revision__
58
59 def __init__(self, other):
60 """Create a ShellFacade instance."""
61 methods = ['ask',
62 'clear',
63 'pause',
64 'prompt',
65 'quit',
66 'redirectStderr',
67 'redirectStdin',
68 'redirectStdout',
69 'run',
70 'runfile',
71 'wrap',
72 'zoom',
73 ]
74 for method in methods:
75 self.__dict__[method] = getattr(other, method)
76 d = self.__dict__
77 d['other'] = other
78 d['helpText'] = \
79 """
80 * Key bindings:
81 Home Go to the beginning of the command or line.
82 Shift+Home Select to the beginning of the command or line.
83 Shift+End Select to the end of the line.
84 End Go to the end of the line.
85 Ctrl+C Copy selected text, removing prompts.
86 Ctrl+Shift+C Copy selected text, retaining prompts.
87 Ctrl+X Cut selected text.
88 Ctrl+V Paste from clipboard.
89 Ctrl+Shift+V Paste and run multiple commands from clipboard.
90 Ctrl+Up Arrow Retrieve Previous History item.
91 Alt+P Retrieve Previous History item.
92 Ctrl+Down Arrow Retrieve Next History item.
93 Alt+N Retrieve Next History item.
94 Shift+Up Arrow Insert Previous History item.
95 Shift+Down Arrow Insert Next History item.
96 F8 Command-completion of History item.
97 (Type a few characters of a previous command and then press F8.)
98 """
99
100 def help(self):
101 """Display some useful information about how to use the shell."""
102 self.write(self.helpText)
103
104 def __getattr__(self, name):
105 if hasattr(self.other, name):
106 return getattr(self.other, name)
107 else:
108 raise AttributeError, name
109
110 def __setattr__(self, name, value):
111 if self.__dict__.has_key(name):
112 self.__dict__[name] = value
113 elif hasattr(self.other, name):
114 return setattr(self.other, name, value)
115 else:
116 raise AttributeError, name
117
118 def _getAttributeNames(self):
119 """Return list of magic attributes to extend introspection."""
120 list = ['autoCallTip',
121 'autoComplete',
122 'autoCompleteCaseInsensitive',
123 'autoCompleteIncludeDouble',
124 'autoCompleteIncludeMagic',
125 'autoCompleteIncludeSingle',
126 ]
127 list.sort()
128 return list
129
130
131 class Shell(wxStyledTextCtrl):
132 """PyCrust Shell based on wxStyledTextCtrl."""
133
134 name = 'PyCrust Shell'
135 revision = __revision__
136
137 def __init__(self, parent, id=-1, pos=wxDefaultPosition, \
138 size=wxDefaultSize, style=wxCLIP_CHILDREN, introText='', \
139 locals=None, InterpClass=None, *args, **kwds):
140 """Create a PyCrust Shell instance."""
141 wxStyledTextCtrl.__init__(self, parent, id, pos, size, style)
142 # Grab these so they can be restored by self.redirect* methods.
143 self.stdin = sys.stdin
144 self.stdout = sys.stdout
145 self.stderr = sys.stderr
146 # Add the current working directory "." to the search path.
147 sys.path.insert(0, os.curdir)
148 # Import a default interpreter class if one isn't provided.
149 if InterpClass == None:
150 from interpreter import Interpreter
151 else:
152 Interpreter = InterpClass
153 # Create default locals so we have something interesting.
154 shellLocals = {'__name__': 'PyCrust-Shell',
155 '__doc__': 'PyCrust-Shell, The PyCrust Python Shell.',
156 '__version__': VERSION,
157 }
158 # Add the dictionary that was passed in.
159 if locals:
160 shellLocals.update(locals)
161 self.interp = Interpreter(locals=shellLocals, \
162 rawin=self.readRaw, \
163 stdin=PseudoFileIn(self.readIn), \
164 stdout=PseudoFileOut(self.writeOut), \
165 stderr=PseudoFileErr(self.writeErr), \
166 *args, **kwds)
167 # Find out for which keycodes the interpreter will autocomplete.
168 self.autoCompleteKeys = self.interp.getAutoCompleteKeys()
169 # Keep track of the last non-continuation prompt positions.
170 self.promptPosStart = 0
171 self.promptPosEnd = 0
172 # Keep track of multi-line commands.
173 self.more = 0
174 # Create the command history. Commands are added into the front of
175 # the list (ie. at index 0) as they are entered. self.historyIndex
176 # is the current position in the history; it gets incremented as you
177 # retrieve the previous command, decremented as you retrieve the
178 # next, and reset when you hit Enter. self.historyIndex == -1 means
179 # you're on the current command, not in the history.
180 self.history = []
181 self.historyIndex = -1
182 # Assign handlers for keyboard events.
183 EVT_KEY_DOWN(self, self.OnKeyDown)
184 EVT_CHAR(self, self.OnChar)
185 # Assign handlers for wxSTC events.
186 EVT_STC_UPDATEUI(self, id, self.OnUpdateUI)
187 # Configure various defaults and user preferences.
188 self.config()
189 # Display the introductory banner information.
190 try: self.showIntro(introText)
191 except: pass
192 # Assign some pseudo keywords to the interpreter's namespace.
193 try: self.setBuiltinKeywords()
194 except: pass
195 # Add 'shell' to the interpreter's local namespace.
196 try: self.setLocalShell()
197 except: pass
198 # Do this last so the user has complete control over their
199 # environment. They can override anything they want.
200 try: self.execStartupScript(self.interp.startupScript)
201 except: pass
202
203 def destroy(self):
204 del self.interp
205
206 def config(self):
207 """Configure shell based on user preferences."""
208 self.SetMarginType(1, wxSTC_MARGIN_NUMBER)
209 self.SetMarginWidth(1, 40)
210
211 self.SetLexer(wxSTC_LEX_PYTHON)
212 self.SetKeyWords(0, ' '.join(keyword.kwlist))
213
214 self.setStyles(faces)
215 self.SetViewWhiteSpace(0)
216 self.SetTabWidth(4)
217 self.SetUseTabs(0)
218 # Do we want to automatically pop up command completion options?
219 self.autoComplete = 1
220 self.autoCompleteIncludeMagic = 1
221 self.autoCompleteIncludeSingle = 1
222 self.autoCompleteIncludeDouble = 1
223 self.autoCompleteCaseInsensitive = 1
224 self.AutoCompSetIgnoreCase(self.autoCompleteCaseInsensitive)
225 # Do we want to automatically pop up command argument help?
226 self.autoCallTip = 1
227 self.CallTipSetBackground(wxColour(255, 255, 232))
228 self.wrap()
229
230 def showIntro(self, text=''):
231 """Display introductory text in the shell."""
232 if text:
233 if not text.endswith(os.linesep): text += os.linesep
234 self.write(text)
235 try:
236 self.write(self.interp.introText)
237 except AttributeError:
238 pass
239
240 def setBuiltinKeywords(self):
241 """Create pseudo keywords as part of builtins.
242
243 This simply sets "close", "exit" and "quit" to a helpful string.
244 """
245 import __builtin__
246 __builtin__.close = __builtin__.exit = __builtin__.quit = \
247 'Click on the close button to leave the application.'
248
249 def quit(self):
250 """Quit the application."""
251
252 # XXX Good enough for now but later we want to send a close event.
253
254 # In the close event handler we can make sure they want to quit.
255 # Other applications, like PythonCard, may choose to hide rather than
256 # quit so we should just post the event and let the surrounding app
257 # decide what it wants to do.
258 self.write('Click on the close button to leave the application.')
259
260 def setLocalShell(self):
261 """Add 'shell' to locals as reference to ShellFacade instance."""
262 self.interp.locals['shell'] = ShellFacade(other=self)
263
264 def execStartupScript(self, startupScript):
265 """Execute the user's PYTHONSTARTUP script if they have one."""
266 if startupScript and os.path.isfile(startupScript):
267 startupText = 'Startup script executed: ' + startupScript
268 self.push('print %s;execfile(%s)' % \
269 (`startupText`, `startupScript`))
270 else:
271 self.push('')
272
273 def setStyles(self, faces):
274 """Configure font size, typeface and color for lexer."""
275
276 # Default style
277 self.StyleSetSpec(wxSTC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d,back:%(backcol)s" % faces)
278
279 self.StyleClearAll()
280
281 # Built in styles
282 self.StyleSetSpec(wxSTC_STYLE_LINENUMBER, "back:#C0C0C0,face:%(mono)s,size:%(lnsize)d" % faces)
283 self.StyleSetSpec(wxSTC_STYLE_CONTROLCHAR, "face:%(mono)s" % faces)
284 self.StyleSetSpec(wxSTC_STYLE_BRACELIGHT, "fore:#0000FF,back:#FFFF88")
285 self.StyleSetSpec(wxSTC_STYLE_BRACEBAD, "fore:#FF0000,back:#FFFF88")
286
287 # Python styles
288 self.StyleSetSpec(wxSTC_P_DEFAULT, "face:%(mono)s" % faces)
289 self.StyleSetSpec(wxSTC_P_COMMENTLINE, "fore:#007F00,face:%(mono)s" % faces)
290 self.StyleSetSpec(wxSTC_P_NUMBER, "")
291 self.StyleSetSpec(wxSTC_P_STRING, "fore:#7F007F,face:%(mono)s" % faces)
292 self.StyleSetSpec(wxSTC_P_CHARACTER, "fore:#7F007F,face:%(mono)s" % faces)
293 self.StyleSetSpec(wxSTC_P_WORD, "fore:#00007F,bold")
294 self.StyleSetSpec(wxSTC_P_TRIPLE, "fore:#7F0000")
295 self.StyleSetSpec(wxSTC_P_TRIPLEDOUBLE, "fore:#000033,back:#FFFFE8")
296 self.StyleSetSpec(wxSTC_P_CLASSNAME, "fore:#0000FF,bold")
297 self.StyleSetSpec(wxSTC_P_DEFNAME, "fore:#007F7F,bold")
298 self.StyleSetSpec(wxSTC_P_OPERATOR, "")
299 self.StyleSetSpec(wxSTC_P_IDENTIFIER, "")
300 self.StyleSetSpec(wxSTC_P_COMMENTBLOCK, "fore:#7F7F7F")
301 self.StyleSetSpec(wxSTC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eolfilled" % faces)
302
303 def OnUpdateUI(self, evt):
304 """Check for matching braces."""
305 braceAtCaret = -1
306 braceOpposite = -1
307 charBefore = None
308 caretPos = self.GetCurrentPos()
309 if caretPos > 0:
310 charBefore = self.GetCharAt(caretPos - 1)
311 #*** Patch to fix bug in wxSTC for wxPython < 2.3.3.
312 if charBefore < 0:
313 charBefore = 32 # Mimic a space.
314 #***
315 styleBefore = self.GetStyleAt(caretPos - 1)
316
317 # Check before.
318 if charBefore and chr(charBefore) in '[]{}()' \
319 and styleBefore == wxSTC_P_OPERATOR:
320 braceAtCaret = caretPos - 1
321
322 # Check after.
323 if braceAtCaret < 0:
324 charAfter = self.GetCharAt(caretPos)
325 #*** Patch to fix bug in wxSTC for wxPython < 2.3.3.
326 if charAfter < 0:
327 charAfter = 32 # Mimic a space.
328 #***
329 styleAfter = self.GetStyleAt(caretPos)
330 if charAfter and chr(charAfter) in '[]{}()' \
331 and styleAfter == wxSTC_P_OPERATOR:
332 braceAtCaret = caretPos
333
334 if braceAtCaret >= 0:
335 braceOpposite = self.BraceMatch(braceAtCaret)
336
337 if braceAtCaret != -1 and braceOpposite == -1:
338 self.BraceBadLight(braceAtCaret)
339 else:
340 self.BraceHighlight(braceAtCaret, braceOpposite)
341
342 def OnChar(self, event):
343 """Keypress event handler."""
344
345 # Prevent modification of previously submitted commands/responses.
346 if not self.CanEdit():
347 return
348 key = event.KeyCode()
349 currpos = self.GetCurrentPos()
350 stoppos = self.promptPosEnd
351 if key in self.autoCompleteKeys:
352 # Usually the dot (period) key activates auto completion.
353 # Get the command between the prompt and the cursor.
354 # Add the autocomplete character to the end of the command.
355 command = self.GetTextRange(stoppos, currpos) + chr(key)
356 self.write(chr(key))
357 if self.autoComplete: self.autoCompleteShow(command)
358 elif key == ord('('):
359 # The left paren activates a call tip and cancels
360 # an active auto completion.
361 if self.AutoCompActive(): self.AutoCompCancel()
362 # Get the command between the prompt and the cursor.
363 # Add the '(' to the end of the command.
364 self.ReplaceSelection('')
365 command = self.GetTextRange(stoppos, currpos) + '('
366 self.write('(')
367 if self.autoCallTip: self.autoCallTipShow(command)
368 else:
369 # Allow the normal event handling to take place.
370 event.Skip()
371
372 def OnKeyDown(self, event):
373 """Key down event handler."""
374
375 # Prevent modification of previously submitted commands/responses.
376 key = event.KeyCode()
377 controlDown = event.ControlDown()
378 altDown = event.AltDown()
379 shiftDown = event.ShiftDown()
380 currpos = self.GetCurrentPos()
381 endpos = self.GetTextLength()
382 # Return (Enter) is used to submit a command to the interpreter.
383 if not controlDown and key == WXK_RETURN:
384 if self.AutoCompActive(): self.AutoCompCancel()
385 if self.CallTipActive(): self.CallTipCancel()
386 self.processLine()
387 # Ctrl+Return (Cntrl+Enter) is used to insert a line break.
388 elif controlDown and key == WXK_RETURN:
389 if self.AutoCompActive(): self.AutoCompCancel()
390 if self.CallTipActive(): self.CallTipCancel()
391 if currpos == endpos:
392 self.processLine()
393 else:
394 self.insertLineBreak()
395 # If the auto-complete window is up let it do its thing.
396 elif self.AutoCompActive():
397 event.Skip()
398 # Let Ctrl-Alt-* get handled normally.
399 elif controlDown and altDown:
400 event.Skip()
401 # Clear the current, unexecuted command.
402 elif key == WXK_ESCAPE:
403 if self.CallTipActive():
404 event.Skip()
405 else:
406 self.clearCommand()
407 # Cut to the clipboard.
408 elif (controlDown and key in (ord('X'), ord('x'))) \
409 or (shiftDown and key == WXK_DELETE):
410 self.Cut()
411 # Copy to the clipboard.
412 elif controlDown and not shiftDown \
413 and key in (ord('C'), ord('c'), WXK_INSERT):
414 self.Copy()
415 # Copy to the clipboard, including prompts.
416 elif controlDown and shiftDown \
417 and key in (ord('C'), ord('c'), WXK_INSERT):
418 self.CopyWithPrompts()
419 # Home needs to be aware of the prompt.
420 elif key == WXK_HOME:
421 home = self.promptPosEnd
422 if currpos > home:
423 selecting = self.GetSelectionStart() != self.GetSelectionEnd()
424 self.SetCurrentPos(home)
425 if not selecting and not shiftDown:
426 self.SetAnchor(home)
427 self.EnsureCaretVisible()
428 else:
429 event.Skip()
430 #
431 # The following handlers modify text, so we need to see if there
432 # is a selection that includes text prior to the prompt.
433 #
434 # Don't modify a selection with text prior to the prompt.
435 elif self.GetSelectionStart() != self.GetSelectionEnd()\
436 and key not in NAVKEYS and not self.CanEdit():
437 pass
438 # Paste from the clipboard.
439 elif (controlDown and not shiftDown \
440 and key in (ord('V'), ord('v'))) \
441 or (shiftDown and not controlDown and key == WXK_INSERT):
442 self.Paste()
443 # Paste from the clipboard, run commands.
444 elif controlDown and shiftDown \
445 and key in (ord('V'), ord('v')):
446 self.PasteAndRun()
447 # Replace with the previous command from the history buffer.
448 elif (controlDown and key == WXK_UP) \
449 or (altDown and key in (ord('P'), ord('p'))):
450 self.OnHistoryReplace(step=+1)
451 # Replace with the next command from the history buffer.
452 elif (controlDown and key == WXK_DOWN) \
453 or (altDown and key in (ord('N'), ord('n'))):
454 self.OnHistoryReplace(step=-1)
455 # Insert the previous command from the history buffer.
456 elif (shiftDown and key == WXK_UP) and self.CanEdit():
457 self.OnHistoryInsert(step=+1)
458 # Insert the next command from the history buffer.
459 elif (shiftDown and key == WXK_DOWN) and self.CanEdit():
460 self.OnHistoryInsert(step=-1)
461 # Search up the history for the text in front of the cursor.
462 elif key == WXK_F8:
463 self.OnHistorySearch()
464 # Don't backspace over the latest non-continuation prompt.
465 elif key == WXK_BACK:
466 if self.GetSelectionStart() != self.GetSelectionEnd()\
467 and self.CanEdit():
468 event.Skip()
469 elif currpos > self.promptPosEnd:
470 event.Skip()
471 # Only allow these keys after the latest prompt.
472 elif key in (WXK_TAB, WXK_DELETE):
473 if self.CanEdit():
474 event.Skip()
475 # Don't toggle between insert mode and overwrite mode.
476 elif key == WXK_INSERT:
477 pass
478 # Don't allow line deletion.
479 elif controlDown and key in (ord('L'), ord('l')):
480 pass
481 # Don't allow line transposition.
482 elif controlDown and key in (ord('T'), ord('t')):
483 pass
484 # Basic navigation keys should work anywhere.
485 elif key in NAVKEYS:
486 event.Skip()
487 # Protect the readonly portion of the shell.
488 elif not self.CanEdit():
489 pass
490 else:
491 event.Skip()
492
493 def clearCommand(self):
494 """Delete the current, unexecuted command."""
495 startpos = self.promptPosEnd
496 endpos = self.GetTextLength()
497 self.SetSelection(startpos, endpos)
498 self.ReplaceSelection('')
499 self.more = 0
500
501 def OnHistoryReplace(self, step):
502 """Replace with the previous/next command from the history buffer."""
503 self.clearCommand()
504 self.replaceFromHistory(step)
505
506 def replaceFromHistory(self, step):
507 """Replace selection with command from the history buffer."""
508 self.ReplaceSelection('')
509 newindex = self.historyIndex + step
510 if -1 <= newindex <= len(self.history):
511 self.historyIndex = newindex
512 if 0 <= newindex <= len(self.history)-1:
513 command = self.history[self.historyIndex]
514 command = command.replace('\n', os.linesep + sys.ps2)
515 self.ReplaceSelection(command)
516
517 def OnHistoryInsert(self, step):
518 """Insert the previous/next command from the history buffer."""
519 if not self.CanEdit():
520 return
521 startpos = self.GetCurrentPos()
522 self.replaceFromHistory(step)
523 endpos = self.GetCurrentPos()
524 self.SetSelection(endpos, startpos)
525
526 def OnHistorySearch(self):
527 """Search up the history buffer for the text in front of the cursor."""
528 if not self.CanEdit():
529 return
530 startpos = self.GetCurrentPos()
531 # The text up to the cursor is what we search for.
532 numCharsAfterCursor = self.GetTextLength() - startpos
533 searchText = self.getCommand(rstrip=0)
534 if numCharsAfterCursor > 0:
535 searchText = searchText[:-numCharsAfterCursor]
536 if not searchText:
537 return
538 # Search upwards from the current history position and loop back
539 # to the beginning if we don't find anything.
540 if (self.historyIndex <= -1) \
541 or (self.historyIndex >= len(self.history)-2):
542 searchOrder = range(len(self.history))
543 else:
544 searchOrder = range(self.historyIndex+1, len(self.history)) + \
545 range(self.historyIndex)
546 for i in searchOrder:
547 command = self.history[i]
548 if command[:len(searchText)] == searchText:
549 # Replace the current selection with the one we've found.
550 self.ReplaceSelection(command[len(searchText):])
551 endpos = self.GetCurrentPos()
552 self.SetSelection(endpos, startpos)
553 # We've now warped into middle of the history.
554 self.historyIndex = i
555 break
556
557 def setStatusText(self, text):
558 """Display status information."""
559
560 # This method will most likely be replaced by the enclosing app
561 # to do something more interesting, like write to a status bar.
562 print text
563
564 def insertLineBreak(self):
565 """Insert a new line break."""
566 if self.CanEdit():
567 self.write(os.linesep)
568 self.more = 1
569 self.prompt()
570
571 def processLine(self):
572 """Process the line of text at which the user hit Enter."""
573
574 # The user hit ENTER and we need to decide what to do. They could be
575 # sitting on any line in the shell.
576
577 thepos = self.GetCurrentPos()
578 startpos = self.promptPosEnd
579 endpos = self.GetTextLength()
580 # If they hit RETURN inside the current command, execute the command.
581 if self.CanEdit():
582 self.SetCurrentPos(endpos)
583 self.interp.more = 0
584 command = self.GetTextRange(startpos, endpos)
585 lines = command.split(os.linesep + sys.ps2)
586 lines = [line.rstrip() for line in lines]
587 command = '\n'.join(lines)
588 self.push(command)
589 # Or replace the current command with the other command.
590 else:
591 # If the line contains a command (even an invalid one).
592 if self.getCommand(rstrip=0):
593 command = self.getMultilineCommand()
594 self.clearCommand()
595 self.write(command)
596 # Otherwise, put the cursor back where we started.
597 else:
598 self.SetCurrentPos(thepos)
599 self.SetAnchor(thepos)
600
601 def getMultilineCommand(self, rstrip=1):
602 """Extract a multi-line command from the editor.
603
604 The command may not necessarily be valid Python syntax."""
605 # XXX Need to extract real prompts here. Need to keep track of the
606 # prompt every time a command is issued.
607 ps1 = str(sys.ps1)
608 ps1size = len(ps1)
609 ps2 = str(sys.ps2)
610 ps2size = len(ps2)
611 # This is a total hack job, but it works.
612 text = self.GetCurLine()[0]
613 line = self.GetCurrentLine()
614 while text[:ps2size] == ps2 and line > 0:
615 line -= 1
616 self.GotoLine(line)
617 text = self.GetCurLine()[0]
618 if text[:ps1size] == ps1:
619 line = self.GetCurrentLine()
620 self.GotoLine(line)
621 startpos = self.GetCurrentPos() + ps1size
622 line += 1
623 self.GotoLine(line)
624 while self.GetCurLine()[0][:ps2size] == ps2:
625 line += 1
626 self.GotoLine(line)
627 stoppos = self.GetCurrentPos()
628 command = self.GetTextRange(startpos, stoppos)
629 command = command.replace(os.linesep + sys.ps2, '\n')
630 command = command.rstrip()
631 command = command.replace('\n', os.linesep + sys.ps2)
632 else:
633 command = ''
634 if rstrip:
635 command = command.rstrip()
636 return command
637
638 def getCommand(self, text=None, rstrip=1):
639 """Extract a command from text which may include a shell prompt.
640
641 The command may not necessarily be valid Python syntax."""
642 if not text:
643 text = self.GetCurLine()[0]
644 # Strip the prompt off the front of text leaving just the command.
645 command = self.lstripPrompt(text)
646 if command == text:
647 command = '' # Real commands have prompts.
648 if rstrip:
649 command = command.rstrip()
650 return command
651
652 def lstripPrompt(self, text):
653 """Return text without a leading prompt."""
654 ps1 = str(sys.ps1)
655 ps1size = len(ps1)
656 ps2 = str(sys.ps2)
657 ps2size = len(ps2)
658 # Strip the prompt off the front of text.
659 if text[:ps1size] == ps1:
660 text = text[ps1size:]
661 elif text[:ps2size] == ps2:
662 text = text[ps2size:]
663 return text
664
665 def push(self, command):
666 """Send command to the interpreter for execution."""
667 busy = wxBusyCursor()
668 self.write(os.linesep)
669 self.more = self.interp.push(command)
670 del busy
671 if not self.more:
672 self.addHistory(command.rstrip())
673 self.prompt()
674
675 def addHistory(self, command):
676 """Add command to the command history."""
677 # Reset the history position.
678 self.historyIndex = -1
679 # Insert this command into the history, unless it's a blank
680 # line or the same as the last command.
681 if command != '' \
682 and (len(self.history) == 0 or command != self.history[0]):
683 self.history.insert(0, command)
684
685 def write(self, text):
686 """Display text in the shell.
687
688 Replace line endings with OS-specific endings."""
689 text = self.fixLineEndings(text)
690 self.AddText(text)
691 self.EnsureCaretVisible()
692
693 def fixLineEndings(self, text):
694 """Return text with line endings replaced by OS-specific endings."""
695 lines = text.split('\r\n')
696 for l in range(len(lines)):
697 chunks = lines[l].split('\r')
698 for c in range(len(chunks)):
699 chunks[c] = os.linesep.join(chunks[c].split('\n'))
700 lines[l] = os.linesep.join(chunks)
701 text = os.linesep.join(lines)
702 return text
703
704 def prompt(self):
705 """Display appropriate prompt for the context, either ps1 or ps2.
706
707 If this is a continuation line, autoindent as necessary."""
708 if self.more:
709 prompt = str(sys.ps2)
710 else:
711 prompt = str(sys.ps1)
712 pos = self.GetCurLine()[1]
713 if pos > 0: self.write(os.linesep)
714 if not self.more:
715 self.promptPosStart = self.GetCurrentPos()
716 self.write(prompt)
717 if not self.more:
718 self.promptPosEnd = self.GetCurrentPos()
719 # Keep the undo feature from undoing previous responses.
720 self.EmptyUndoBuffer()
721 # XXX Add some autoindent magic here if more.
722 if self.more:
723 self.write(' '*4) # Temporary hack indentation.
724 self.EnsureCaretVisible()
725 self.ScrollToColumn(0)
726
727 def readIn(self):
728 """Replacement for stdin."""
729 prompt = 'Please enter your response:'
730 dialog = wxTextEntryDialog(None, prompt, \
731 'Input Dialog (Standard)', '')
732 try:
733 if dialog.ShowModal() == wxID_OK:
734 text = dialog.GetValue()
735 self.write(text + os.linesep)
736 return text
737 finally:
738 dialog.Destroy()
739 return ''
740
741 def readRaw(self, prompt='Please enter your response:'):
742 """Replacement for raw_input."""
743 dialog = wxTextEntryDialog(None, prompt, \
744 'Input Dialog (Raw)', '')
745 try:
746 if dialog.ShowModal() == wxID_OK:
747 text = dialog.GetValue()
748 return text
749 finally:
750 dialog.Destroy()
751 return ''
752
753 def ask(self, prompt='Please enter your response:'):
754 """Get response from the user."""
755 return raw_input(prompt=prompt)
756
757 def pause(self):
758 """Halt execution pending a response from the user."""
759 self.ask('Press enter to continue:')
760
761 def clear(self):
762 """Delete all text from the shell."""
763 self.ClearAll()
764
765 def run(self, command, prompt=1, verbose=1):
766 """Execute command within the shell as if it was typed in directly.
767 >>> shell.run('print "this"')
768 >>> print "this"
769 this
770 >>>
771 """
772 # Go to the very bottom of the text.
773 endpos = self.GetTextLength()
774 self.SetCurrentPos(endpos)
775 command = command.rstrip()
776 if prompt: self.prompt()
777 if verbose: self.write(command)
778 self.push(command)
779
780 def runfile(self, filename):
781 """Execute all commands in file as if they were typed into the shell."""
782 file = open(filename)
783 try:
784 self.prompt()
785 for command in file.readlines():
786 if command[:6] == 'shell.': # Run shell methods silently.
787 self.run(command, prompt=0, verbose=0)
788 else:
789 self.run(command, prompt=0, verbose=1)
790 finally:
791 file.close()
792
793 def autoCompleteShow(self, command):
794 """Display auto-completion popup list."""
795 list = self.interp.getAutoCompleteList(command,
796 includeMagic=self.autoCompleteIncludeMagic,
797 includeSingle=self.autoCompleteIncludeSingle,
798 includeDouble=self.autoCompleteIncludeDouble)
799 if list:
800 options = ' '.join(list)
801 offset = 0
802 self.AutoCompShow(offset, options)
803
804 def autoCallTipShow(self, command):
805 """Display argument spec and docstring in a popup bubble thingie."""
806 if self.CallTipActive: self.CallTipCancel()
807 (name, argspec, tip) = self.interp.getCallTip(command)
808 if argspec:
809 startpos = self.GetCurrentPos()
810 self.write(argspec + ')')
811 endpos = self.GetCurrentPos()
812 self.SetSelection(endpos, startpos)
813 if tip:
814 curpos = self.GetCurrentPos()
815 tippos = curpos - (len(name) + 1)
816 fallback = curpos - self.GetColumn(curpos)
817 # In case there isn't enough room, only go back to the fallback.
818 tippos = max(tippos, fallback)
819 self.CallTipShow(tippos, tip)
820
821 def writeOut(self, text):
822 """Replacement for stdout."""
823 self.write(text)
824
825 def writeErr(self, text):
826 """Replacement for stderr."""
827 self.write(text)
828
829 def redirectStdin(self, redirect=1):
830 """If redirect is true then sys.stdin will come from the shell."""
831 if redirect:
832 sys.stdin = PseudoFileIn(self.readIn)
833 else:
834 sys.stdin = self.stdin
835
836 def redirectStdout(self, redirect=1):
837 """If redirect is true then sys.stdout will go to the shell."""
838 if redirect:
839 sys.stdout = PseudoFileOut(self.writeOut)
840 else:
841 sys.stdout = self.stdout
842
843 def redirectStderr(self, redirect=1):
844 """If redirect is true then sys.stderr will go to the shell."""
845 if redirect:
846 sys.stderr = PseudoFileErr(self.writeErr)
847 else:
848 sys.stderr = self.stderr
849
850 def CanCut(self):
851 """Return true if text is selected and can be cut."""
852 if self.GetSelectionStart() != self.GetSelectionEnd() \
853 and self.GetSelectionStart() >= self.promptPosEnd \
854 and self.GetSelectionEnd() >= self.promptPosEnd:
855 return 1
856 else:
857 return 0
858
859 def CanCopy(self):
860 """Return true if text is selected and can be copied."""
861 return self.GetSelectionStart() != self.GetSelectionEnd()
862
863 def CanPaste(self):
864 """Return true if a paste should succeed."""
865 if self.CanEdit() and wxStyledTextCtrl.CanPaste(self):
866 return 1
867 else:
868 return 0
869
870 def CanEdit(self):
871 """Return true if editing should succeed."""
872 if self.GetSelectionStart() != self.GetSelectionEnd():
873 if self.GetSelectionStart() >= self.promptPosEnd \
874 and self.GetSelectionEnd() >= self.promptPosEnd:
875 return 1
876 else:
877 return 0
878 else:
879 return self.GetCurrentPos() >= self.promptPosEnd
880
881 def Cut(self):
882 """Remove selection and place it on the clipboard."""
883 if self.CanCut() and self.CanCopy():
884 if self.AutoCompActive(): self.AutoCompCancel()
885 if self.CallTipActive: self.CallTipCancel()
886 self.Copy()
887 self.ReplaceSelection('')
888
889 def Copy(self):
890 """Copy selection and place it on the clipboard."""
891 if self.CanCopy():
892 command = self.GetSelectedText()
893 command = command.replace(os.linesep + sys.ps2, os.linesep)
894 command = command.replace(os.linesep + sys.ps1, os.linesep)
895 command = self.lstripPrompt(text=command)
896 data = wxTextDataObject(command)
897 if wxTheClipboard.Open():
898 wxTheClipboard.SetData(data)
899 wxTheClipboard.Close()
900
901 def CopyWithPrompts(self):
902 """Copy selection, including prompts, and place it on the clipboard."""
903 if self.CanCopy():
904 command = self.GetSelectedText()
905 data = wxTextDataObject(command)
906 if wxTheClipboard.Open():
907 wxTheClipboard.SetData(data)
908 wxTheClipboard.Close()
909
910 def Paste(self):
911 """Replace selection with clipboard contents."""
912 if self.CanPaste() and wxTheClipboard.Open():
913 if wxTheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)):
914 data = wxTextDataObject()
915 if wxTheClipboard.GetData(data):
916 self.ReplaceSelection('')
917 command = data.GetText()
918 command = command.rstrip()
919 command = self.fixLineEndings(command)
920 command = self.lstripPrompt(text=command)
921 command = command.replace(os.linesep + sys.ps2, '\n')
922 command = command.replace(os.linesep, '\n')
923 command = command.replace('\n', os.linesep + sys.ps2)
924 self.write(command)
925 wxTheClipboard.Close()
926
927 def PasteAndRun(self):
928 """Replace selection with clipboard contents, run commands."""
929 if wxTheClipboard.Open():
930 if wxTheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)):
931 data = wxTextDataObject()
932 if wxTheClipboard.GetData(data):
933 endpos = self.GetTextLength()
934 self.SetCurrentPos(endpos)
935 startpos = self.promptPosEnd
936 self.SetSelection(startpos, endpos)
937 self.ReplaceSelection('')
938 text = data.GetText()
939 text = text.strip()
940 text = self.fixLineEndings(text)
941 text = self.lstripPrompt(text=text)
942 text = text.replace(os.linesep + sys.ps1, '\n')
943 text = text.replace(os.linesep + sys.ps2, '\n')
944 text = text.replace(os.linesep, '\n')
945 lines = text.split('\n')
946 commands = []
947 command = ''
948 for line in lines:
949 if line.strip() != '' and line.lstrip() == line:
950 # New command.
951 if command:
952 # Add the previous command to the list.
953 commands.append(command)
954 # Start a new command, which may be multiline.
955 command = line
956 else:
957 # Multiline command. Add to the command.
958 command += '\n'
959 command += line
960 commands.append(command)
961 for command in commands:
962 command = command.replace('\n', os.linesep + sys.ps2)
963 self.write(command)
964 self.processLine()
965 wxTheClipboard.Close()
966
967 def wrap(self, wrap=1):
968 """Sets whether text is word wrapped."""
969 try:
970 self.SetWrapMode(wrap)
971 except AttributeError:
972 return 'Wrapping is not available in this version of PyCrust.'
973
974 def zoom(self, points=0):
975 """Set the zoom level.
976
977 This number of points is added to the size of all fonts.
978 It may be positive to magnify or negative to reduce."""
979 self.SetZoom(points)
980
981
982 wxID_SELECTALL = NewId() # This *should* be defined by wxPython.
983 ID_AUTOCOMP = NewId()
984 ID_AUTOCOMP_SHOW = NewId()
985 ID_AUTOCOMP_INCLUDE_MAGIC = NewId()
986 ID_AUTOCOMP_INCLUDE_SINGLE = NewId()
987 ID_AUTOCOMP_INCLUDE_DOUBLE = NewId()
988 ID_CALLTIPS = NewId()
989 ID_CALLTIPS_SHOW = NewId()
990
991
992 class ShellMenu:
993 """Mixin class to add standard menu items."""
994
995 def createMenus(self):
996 m = self.fileMenu = wxMenu()
997 m.AppendSeparator()
998 m.Append(wxID_EXIT, 'E&xit', 'Exit PyCrust')
999
1000 m = self.editMenu = wxMenu()
1001 m.Append(wxID_UNDO, '&Undo \tCtrl+Z', 'Undo the last action')
1002 m.Append(wxID_REDO, '&Redo \tCtrl+Y', 'Redo the last undone action')
1003 m.AppendSeparator()
1004 m.Append(wxID_CUT, 'Cu&t \tCtrl+X', 'Cut the selection')
1005 m.Append(wxID_COPY, '&Copy \tCtrl+C', 'Copy the selection')
1006 m.Append(wxID_PASTE, '&Paste \tCtrl+V', 'Paste')
1007 m.AppendSeparator()
1008 m.Append(wxID_CLEAR, 'Cle&ar', 'Delete the selection')
1009 m.Append(wxID_SELECTALL, 'Select A&ll', 'Select all text')
1010
1011 m = self.autocompMenu = wxMenu()
1012 m.Append(ID_AUTOCOMP_SHOW, 'Show Auto Completion', \
1013 'Show auto completion during dot syntax', 1)
1014 m.Append(ID_AUTOCOMP_INCLUDE_MAGIC, 'Include Magic Attributes', \
1015 'Include attributes visible to __getattr__ and __setattr__', 1)
1016 m.Append(ID_AUTOCOMP_INCLUDE_SINGLE, 'Include Single Underscores', \
1017 'Include attibutes prefixed by a single underscore', 1)
1018 m.Append(ID_AUTOCOMP_INCLUDE_DOUBLE, 'Include Double Underscores', \
1019 'Include attibutes prefixed by a double underscore', 1)
1020
1021 m = self.calltipsMenu = wxMenu()
1022 m.Append(ID_CALLTIPS_SHOW, 'Show Call Tips', \
1023 'Show call tips with argument specifications', 1)
1024
1025 m = self.optionsMenu = wxMenu()
1026 m.AppendMenu(ID_AUTOCOMP, '&Auto Completion', self.autocompMenu, \
1027 'Auto Completion Options')
1028 m.AppendMenu(ID_CALLTIPS, '&Call Tips', self.calltipsMenu, \
1029 'Call Tip Options')
1030
1031 m = self.helpMenu = wxMenu()
1032 m.AppendSeparator()
1033 m.Append(wxID_ABOUT, '&About...', 'About PyCrust')
1034
1035 b = self.menuBar = wxMenuBar()
1036 b.Append(self.fileMenu, '&File')
1037 b.Append(self.editMenu, '&Edit')
1038 b.Append(self.optionsMenu, '&Options')
1039 b.Append(self.helpMenu, '&Help')
1040 self.SetMenuBar(b)
1041
1042 EVT_MENU(self, wxID_EXIT, self.OnExit)
1043 EVT_MENU(self, wxID_UNDO, self.OnUndo)
1044 EVT_MENU(self, wxID_REDO, self.OnRedo)
1045 EVT_MENU(self, wxID_CUT, self.OnCut)
1046 EVT_MENU(self, wxID_COPY, self.OnCopy)
1047 EVT_MENU(self, wxID_PASTE, self.OnPaste)
1048 EVT_MENU(self, wxID_CLEAR, self.OnClear)
1049 EVT_MENU(self, wxID_SELECTALL, self.OnSelectAll)
1050 EVT_MENU(self, wxID_ABOUT, self.OnAbout)
1051 EVT_MENU(self, ID_AUTOCOMP_SHOW, \
1052 self.OnAutoCompleteShow)
1053 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_MAGIC, \
1054 self.OnAutoCompleteIncludeMagic)
1055 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_SINGLE, \
1056 self.OnAutoCompleteIncludeSingle)
1057 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_DOUBLE, \
1058 self.OnAutoCompleteIncludeDouble)
1059 EVT_MENU(self, ID_CALLTIPS_SHOW, \
1060 self.OnCallTipsShow)
1061
1062 EVT_UPDATE_UI(self, wxID_UNDO, self.OnUpdateMenu)
1063 EVT_UPDATE_UI(self, wxID_REDO, self.OnUpdateMenu)
1064 EVT_UPDATE_UI(self, wxID_CUT, self.OnUpdateMenu)
1065 EVT_UPDATE_UI(self, wxID_COPY, self.OnUpdateMenu)
1066 EVT_UPDATE_UI(self, wxID_PASTE, self.OnUpdateMenu)
1067 EVT_UPDATE_UI(self, wxID_CLEAR, self.OnUpdateMenu)
1068 EVT_UPDATE_UI(self, ID_AUTOCOMP_SHOW, self.OnUpdateMenu)
1069 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_MAGIC, self.OnUpdateMenu)
1070 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_SINGLE, self.OnUpdateMenu)
1071 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_DOUBLE, self.OnUpdateMenu)
1072 EVT_UPDATE_UI(self, ID_CALLTIPS_SHOW, self.OnUpdateMenu)
1073
1074 def OnExit(self, event):
1075 self.Close(true)
1076
1077 def OnUndo(self, event):
1078 self.shell.Undo()
1079
1080 def OnRedo(self, event):
1081 self.shell.Redo()
1082
1083 def OnCut(self, event):
1084 self.shell.Cut()
1085
1086 def OnCopy(self, event):
1087 self.shell.Copy()
1088
1089 def OnPaste(self, event):
1090 self.shell.Paste()
1091
1092 def OnClear(self, event):
1093 self.shell.Clear()
1094
1095 def OnSelectAll(self, event):
1096 self.shell.SelectAll()
1097
1098 def OnAbout(self, event):
1099 """Display an About PyCrust window."""
1100 import sys
1101 title = 'About PyCrust'
1102 text = 'PyCrust %s\n\n' % VERSION + \
1103 'Yet another Python shell, only flakier.\n\n' + \
1104 'Half-baked by Patrick K. O\'Brien,\n' + \
1105 'the other half is still in the oven.\n\n' + \
1106 'Shell Revision: %s\n' % self.shell.revision + \
1107 'Interpreter Revision: %s\n\n' % self.shell.interp.revision + \
1108 'Python Version: %s\n' % sys.version.split()[0] + \
1109 'wxPython Version: %s\n' % wx.__version__ + \
1110 'Platform: %s\n' % sys.platform
1111 dialog = wxMessageDialog(self, text, title, wxOK | wxICON_INFORMATION)
1112 dialog.ShowModal()
1113 dialog.Destroy()
1114
1115 def OnAutoCompleteShow(self, event):
1116 self.shell.autoComplete = event.IsChecked()
1117
1118 def OnAutoCompleteIncludeMagic(self, event):
1119 self.shell.autoCompleteIncludeMagic = event.IsChecked()
1120
1121 def OnAutoCompleteIncludeSingle(self, event):
1122 self.shell.autoCompleteIncludeSingle = event.IsChecked()
1123
1124 def OnAutoCompleteIncludeDouble(self, event):
1125 self.shell.autoCompleteIncludeDouble = event.IsChecked()
1126
1127 def OnCallTipsShow(self, event):
1128 self.shell.autoCallTip = event.IsChecked()
1129
1130 def OnUpdateMenu(self, event):
1131 """Update menu items based on current status."""
1132 id = event.GetId()
1133 if id == wxID_UNDO:
1134 event.Enable(self.shell.CanUndo())
1135 elif id == wxID_REDO:
1136 event.Enable(self.shell.CanRedo())
1137 elif id == wxID_CUT:
1138 event.Enable(self.shell.CanCut())
1139 elif id == wxID_COPY:
1140 event.Enable(self.shell.CanCopy())
1141 elif id == wxID_PASTE:
1142 event.Enable(self.shell.CanPaste())
1143 elif id == wxID_CLEAR:
1144 event.Enable(self.shell.CanCut())
1145 elif id == ID_AUTOCOMP_SHOW:
1146 event.Check(self.shell.autoComplete)
1147 elif id == ID_AUTOCOMP_INCLUDE_MAGIC:
1148 event.Check(self.shell.autoCompleteIncludeMagic)
1149 elif id == ID_AUTOCOMP_INCLUDE_SINGLE:
1150 event.Check(self.shell.autoCompleteIncludeSingle)
1151 elif id == ID_AUTOCOMP_INCLUDE_DOUBLE:
1152 event.Check(self.shell.autoCompleteIncludeDouble)
1153 elif id == ID_CALLTIPS_SHOW:
1154 event.Check(self.shell.autoCallTip)
1155
1156
1157 class ShellFrame(wxFrame, ShellMenu):
1158 """Frame containing the PyCrust shell component."""
1159
1160 name = 'PyCrust Shell Frame'
1161 revision = __revision__
1162
1163 def __init__(self, parent=None, id=-1, title='PyShell', \
1164 pos=wxDefaultPosition, size=wxDefaultSize, \
1165 style=wxDEFAULT_FRAME_STYLE, locals=None, \
1166 InterpClass=None, *args, **kwds):
1167 """Create a PyCrust ShellFrame instance."""
1168 wxFrame.__init__(self, parent, id, title, pos, size, style)
1169 intro = 'Welcome To PyCrust %s - The Flakiest Python Shell' % VERSION
1170 intro += '\nSponsored by Orbtech - Your source for Python programming expertise.'
1171 self.CreateStatusBar()
1172 self.SetStatusText(intro.replace('\n', ', '))
1173 import images
1174 self.SetIcon(images.getPyCrustIcon())
1175 self.shell = Shell(parent=self, id=-1, introText=intro, \
1176 locals=locals, InterpClass=InterpClass, \
1177 *args, **kwds)
1178 # Override the shell so that status messages go to the status bar.
1179 self.shell.setStatusText = self.SetStatusText
1180 self.createMenus()
1181 EVT_CLOSE(self, self.OnCloseWindow)
1182
1183 def OnCloseWindow(self, event):
1184 self.shell.destroy()
1185 self.Destroy()
1186