]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wxPython/lib/PyCrust/shell.py
693c9408e5fe05e5d02e1bd14b4ecf1dfd21e9be
[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 if not self.more:
671 self.addHistory(command.rstrip())
672 self.prompt()
673
674 def addHistory(self, command):
675 """Add command to the command history."""
676 # Reset the history position.
677 self.historyIndex = -1
678 # Insert this command into the history, unless it's a blank
679 # line or the same as the last command.
680 if command != '' \
681 and (len(self.history) == 0 or command != self.history[0]):
682 self.history.insert(0, command)
683
684 def write(self, text):
685 """Display text in the shell.
686
687 Replace line endings with OS-specific endings."""
688 text = self.fixLineEndings(text)
689 self.AddText(text)
690 self.EnsureCaretVisible()
691
692 def fixLineEndings(self, text):
693 """Return text with line endings replaced by OS-specific endings."""
694 lines = text.split('\r\n')
695 for l in range(len(lines)):
696 chunks = lines[l].split('\r')
697 for c in range(len(chunks)):
698 chunks[c] = os.linesep.join(chunks[c].split('\n'))
699 lines[l] = os.linesep.join(chunks)
700 text = os.linesep.join(lines)
701 return text
702
703 def prompt(self):
704 """Display appropriate prompt for the context, either ps1 or ps2.
705
706 If this is a continuation line, autoindent as necessary."""
707 if self.more:
708 prompt = str(sys.ps2)
709 else:
710 prompt = str(sys.ps1)
711 pos = self.GetCurLine()[1]
712 if pos > 0: self.write(os.linesep)
713 if not self.more:
714 self.promptPosStart = self.GetCurrentPos()
715 self.write(prompt)
716 if not self.more:
717 self.promptPosEnd = self.GetCurrentPos()
718 # Keep the undo feature from undoing previous responses.
719 self.EmptyUndoBuffer()
720 # XXX Add some autoindent magic here if more.
721 if self.more:
722 self.write(' '*4) # Temporary hack indentation.
723 self.EnsureCaretVisible()
724 self.ScrollToColumn(0)
725
726 def readIn(self):
727 """Replacement for stdin."""
728 prompt = 'Please enter your response:'
729 dialog = wxTextEntryDialog(None, prompt, \
730 'Input Dialog (Standard)', '')
731 try:
732 if dialog.ShowModal() == wxID_OK:
733 text = dialog.GetValue()
734 self.write(text + os.linesep)
735 return text
736 finally:
737 dialog.Destroy()
738 return ''
739
740 def readRaw(self, prompt='Please enter your response:'):
741 """Replacement for raw_input."""
742 dialog = wxTextEntryDialog(None, prompt, \
743 'Input Dialog (Raw)', '')
744 try:
745 if dialog.ShowModal() == wxID_OK:
746 text = dialog.GetValue()
747 return text
748 finally:
749 dialog.Destroy()
750 return ''
751
752 def ask(self, prompt='Please enter your response:'):
753 """Get response from the user."""
754 return raw_input(prompt=prompt)
755
756 def pause(self):
757 """Halt execution pending a response from the user."""
758 self.ask('Press enter to continue:')
759
760 def clear(self):
761 """Delete all text from the shell."""
762 self.ClearAll()
763
764 def run(self, command, prompt=1, verbose=1):
765 """Execute command within the shell as if it was typed in directly.
766 >>> shell.run('print "this"')
767 >>> print "this"
768 this
769 >>>
770 """
771 # Go to the very bottom of the text.
772 endpos = self.GetTextLength()
773 self.SetCurrentPos(endpos)
774 command = command.rstrip()
775 if prompt: self.prompt()
776 if verbose: self.write(command)
777 self.push(command)
778
779 def runfile(self, filename):
780 """Execute all commands in file as if they were typed into the shell."""
781 file = open(filename)
782 try:
783 self.prompt()
784 for command in file.readlines():
785 if command[:6] == 'shell.': # Run shell methods silently.
786 self.run(command, prompt=0, verbose=0)
787 else:
788 self.run(command, prompt=0, verbose=1)
789 finally:
790 file.close()
791
792 def autoCompleteShow(self, command):
793 """Display auto-completion popup list."""
794 list = self.interp.getAutoCompleteList(command,
795 includeMagic=self.autoCompleteIncludeMagic,
796 includeSingle=self.autoCompleteIncludeSingle,
797 includeDouble=self.autoCompleteIncludeDouble)
798 if list:
799 options = ' '.join(list)
800 offset = 0
801 self.AutoCompShow(offset, options)
802
803 def autoCallTipShow(self, command):
804 """Display argument spec and docstring in a popup bubble thingie."""
805 if self.CallTipActive: self.CallTipCancel()
806 (name, argspec, tip) = self.interp.getCallTip(command)
807 if argspec:
808 startpos = self.GetCurrentPos()
809 self.write(argspec + ')')
810 endpos = self.GetCurrentPos()
811 self.SetSelection(endpos, startpos)
812 if tip:
813 curpos = self.GetCurrentPos()
814 tippos = curpos - (len(name) + 1)
815 fallback = curpos - self.GetColumn(curpos)
816 # In case there isn't enough room, only go back to the fallback.
817 tippos = max(tippos, fallback)
818 self.CallTipShow(tippos, tip)
819
820 def writeOut(self, text):
821 """Replacement for stdout."""
822 self.write(text)
823
824 def writeErr(self, text):
825 """Replacement for stderr."""
826 self.write(text)
827
828 def redirectStdin(self, redirect=1):
829 """If redirect is true then sys.stdin will come from the shell."""
830 if redirect:
831 sys.stdin = PseudoFileIn(self.readIn)
832 else:
833 sys.stdin = self.stdin
834
835 def redirectStdout(self, redirect=1):
836 """If redirect is true then sys.stdout will go to the shell."""
837 if redirect:
838 sys.stdout = PseudoFileOut(self.writeOut)
839 else:
840 sys.stdout = self.stdout
841
842 def redirectStderr(self, redirect=1):
843 """If redirect is true then sys.stderr will go to the shell."""
844 if redirect:
845 sys.stderr = PseudoFileErr(self.writeErr)
846 else:
847 sys.stderr = self.stderr
848
849 def CanCut(self):
850 """Return true if text is selected and can be cut."""
851 if self.GetSelectionStart() != self.GetSelectionEnd() \
852 and self.GetSelectionStart() >= self.promptPosEnd \
853 and self.GetSelectionEnd() >= self.promptPosEnd:
854 return 1
855 else:
856 return 0
857
858 def CanCopy(self):
859 """Return true if text is selected and can be copied."""
860 return self.GetSelectionStart() != self.GetSelectionEnd()
861
862 def CanPaste(self):
863 """Return true if a paste should succeed."""
864 if self.CanEdit() and wxStyledTextCtrl.CanPaste(self):
865 return 1
866 else:
867 return 0
868
869 def CanEdit(self):
870 """Return true if editing should succeed."""
871 if self.GetSelectionStart() != self.GetSelectionEnd():
872 if self.GetSelectionStart() >= self.promptPosEnd \
873 and self.GetSelectionEnd() >= self.promptPosEnd:
874 return 1
875 else:
876 return 0
877 else:
878 return self.GetCurrentPos() >= self.promptPosEnd
879
880 def Cut(self):
881 """Remove selection and place it on the clipboard."""
882 if self.CanCut() and self.CanCopy():
883 if self.AutoCompActive(): self.AutoCompCancel()
884 if self.CallTipActive: self.CallTipCancel()
885 self.Copy()
886 self.ReplaceSelection('')
887
888 def Copy(self):
889 """Copy selection and place it on the clipboard."""
890 if self.CanCopy():
891 command = self.GetSelectedText()
892 command = command.replace(os.linesep + sys.ps2, os.linesep)
893 command = command.replace(os.linesep + sys.ps1, os.linesep)
894 command = self.lstripPrompt(text=command)
895 data = wxTextDataObject(command)
896 if wxTheClipboard.Open():
897 wxTheClipboard.SetData(data)
898 wxTheClipboard.Close()
899
900 def CopyWithPrompts(self):
901 """Copy selection, including prompts, and place it on the clipboard."""
902 if self.CanCopy():
903 command = self.GetSelectedText()
904 data = wxTextDataObject(command)
905 if wxTheClipboard.Open():
906 wxTheClipboard.SetData(data)
907 wxTheClipboard.Close()
908
909 def Paste(self):
910 """Replace selection with clipboard contents."""
911 if self.CanPaste() and wxTheClipboard.Open():
912 if wxTheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)):
913 data = wxTextDataObject()
914 if wxTheClipboard.GetData(data):
915 self.ReplaceSelection('')
916 command = data.GetText()
917 command = command.rstrip()
918 command = self.fixLineEndings(command)
919 command = self.lstripPrompt(text=command)
920 command = command.replace(os.linesep + sys.ps2, '\n')
921 command = command.replace(os.linesep, '\n')
922 command = command.replace('\n', os.linesep + sys.ps2)
923 self.write(command)
924 wxTheClipboard.Close()
925
926 def PasteAndRun(self):
927 """Replace selection with clipboard contents, run commands."""
928 if wxTheClipboard.Open():
929 if wxTheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)):
930 data = wxTextDataObject()
931 if wxTheClipboard.GetData(data):
932 endpos = self.GetTextLength()
933 self.SetCurrentPos(endpos)
934 startpos = self.promptPosEnd
935 self.SetSelection(startpos, endpos)
936 self.ReplaceSelection('')
937 text = data.GetText()
938 text = text.strip()
939 text = self.fixLineEndings(text)
940 text = self.lstripPrompt(text=text)
941 text = text.replace(os.linesep + sys.ps1, '\n')
942 text = text.replace(os.linesep + sys.ps2, '\n')
943 text = text.replace(os.linesep, '\n')
944 lines = text.split('\n')
945 commands = []
946 command = ''
947 for line in lines:
948 if line.strip() != '' and line.lstrip() == line:
949 # New command.
950 if command:
951 # Add the previous command to the list.
952 commands.append(command)
953 # Start a new command, which may be multiline.
954 command = line
955 else:
956 # Multiline command. Add to the command.
957 command += '\n'
958 command += line
959 commands.append(command)
960 for command in commands:
961 command = command.replace('\n', os.linesep + sys.ps2)
962 self.write(command)
963 self.processLine()
964 wxTheClipboard.Close()
965
966 def wrap(self, wrap=1):
967 """Sets whether text is word wrapped."""
968 try:
969 self.SetWrapMode(wrap)
970 except AttributeError:
971 return 'Wrapping is not available in this version of PyCrust.'
972
973 def zoom(self, points=0):
974 """Set the zoom level.
975
976 This number of points is added to the size of all fonts.
977 It may be positive to magnify or negative to reduce."""
978 self.SetZoom(points)
979
980
981 wxID_SELECTALL = NewId() # This *should* be defined by wxPython.
982 ID_AUTOCOMP = NewId()
983 ID_AUTOCOMP_SHOW = NewId()
984 ID_AUTOCOMP_INCLUDE_MAGIC = NewId()
985 ID_AUTOCOMP_INCLUDE_SINGLE = NewId()
986 ID_AUTOCOMP_INCLUDE_DOUBLE = NewId()
987 ID_CALLTIPS = NewId()
988 ID_CALLTIPS_SHOW = NewId()
989
990
991 class ShellMenu:
992 """Mixin class to add standard menu items."""
993
994 def createMenus(self):
995 m = self.fileMenu = wxMenu()
996 m.AppendSeparator()
997 m.Append(wxID_EXIT, 'E&xit', 'Exit PyCrust')
998
999 m = self.editMenu = wxMenu()
1000 m.Append(wxID_UNDO, '&Undo \tCtrl+Z', 'Undo the last action')
1001 m.Append(wxID_REDO, '&Redo \tCtrl+Y', 'Redo the last undone action')
1002 m.AppendSeparator()
1003 m.Append(wxID_CUT, 'Cu&t \tCtrl+X', 'Cut the selection')
1004 m.Append(wxID_COPY, '&Copy \tCtrl+C', 'Copy the selection')
1005 m.Append(wxID_PASTE, '&Paste \tCtrl+V', 'Paste')
1006 m.AppendSeparator()
1007 m.Append(wxID_CLEAR, 'Cle&ar', 'Delete the selection')
1008 m.Append(wxID_SELECTALL, 'Select A&ll', 'Select all text')
1009
1010 m = self.autocompMenu = wxMenu()
1011 m.Append(ID_AUTOCOMP_SHOW, 'Show Auto Completion', \
1012 'Show auto completion during dot syntax', 1)
1013 m.Append(ID_AUTOCOMP_INCLUDE_MAGIC, 'Include Magic Attributes', \
1014 'Include attributes visible to __getattr__ and __setattr__', 1)
1015 m.Append(ID_AUTOCOMP_INCLUDE_SINGLE, 'Include Single Underscores', \
1016 'Include attibutes prefixed by a single underscore', 1)
1017 m.Append(ID_AUTOCOMP_INCLUDE_DOUBLE, 'Include Double Underscores', \
1018 'Include attibutes prefixed by a double underscore', 1)
1019
1020 m = self.calltipsMenu = wxMenu()
1021 m.Append(ID_CALLTIPS_SHOW, 'Show Call Tips', \
1022 'Show call tips with argument specifications', 1)
1023
1024 m = self.optionsMenu = wxMenu()
1025 m.AppendMenu(ID_AUTOCOMP, '&Auto Completion', self.autocompMenu, \
1026 'Auto Completion Options')
1027 m.AppendMenu(ID_CALLTIPS, '&Call Tips', self.calltipsMenu, \
1028 'Call Tip Options')
1029
1030 m = self.helpMenu = wxMenu()
1031 m.AppendSeparator()
1032 m.Append(wxID_ABOUT, '&About...', 'About PyCrust')
1033
1034 b = self.menuBar = wxMenuBar()
1035 b.Append(self.fileMenu, '&File')
1036 b.Append(self.editMenu, '&Edit')
1037 b.Append(self.optionsMenu, '&Options')
1038 b.Append(self.helpMenu, '&Help')
1039 self.SetMenuBar(b)
1040
1041 EVT_MENU(self, wxID_EXIT, self.OnExit)
1042 EVT_MENU(self, wxID_UNDO, self.OnUndo)
1043 EVT_MENU(self, wxID_REDO, self.OnRedo)
1044 EVT_MENU(self, wxID_CUT, self.OnCut)
1045 EVT_MENU(self, wxID_COPY, self.OnCopy)
1046 EVT_MENU(self, wxID_PASTE, self.OnPaste)
1047 EVT_MENU(self, wxID_CLEAR, self.OnClear)
1048 EVT_MENU(self, wxID_SELECTALL, self.OnSelectAll)
1049 EVT_MENU(self, wxID_ABOUT, self.OnAbout)
1050 EVT_MENU(self, ID_AUTOCOMP_SHOW, \
1051 self.OnAutoCompleteShow)
1052 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_MAGIC, \
1053 self.OnAutoCompleteIncludeMagic)
1054 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_SINGLE, \
1055 self.OnAutoCompleteIncludeSingle)
1056 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_DOUBLE, \
1057 self.OnAutoCompleteIncludeDouble)
1058 EVT_MENU(self, ID_CALLTIPS_SHOW, \
1059 self.OnCallTipsShow)
1060
1061 EVT_UPDATE_UI(self, wxID_UNDO, self.OnUpdateMenu)
1062 EVT_UPDATE_UI(self, wxID_REDO, self.OnUpdateMenu)
1063 EVT_UPDATE_UI(self, wxID_CUT, self.OnUpdateMenu)
1064 EVT_UPDATE_UI(self, wxID_COPY, self.OnUpdateMenu)
1065 EVT_UPDATE_UI(self, wxID_PASTE, self.OnUpdateMenu)
1066 EVT_UPDATE_UI(self, wxID_CLEAR, self.OnUpdateMenu)
1067 EVT_UPDATE_UI(self, ID_AUTOCOMP_SHOW, self.OnUpdateMenu)
1068 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_MAGIC, self.OnUpdateMenu)
1069 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_SINGLE, self.OnUpdateMenu)
1070 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_DOUBLE, self.OnUpdateMenu)
1071 EVT_UPDATE_UI(self, ID_CALLTIPS_SHOW, self.OnUpdateMenu)
1072
1073 def OnExit(self, event):
1074 self.Close(true)
1075
1076 def OnUndo(self, event):
1077 self.shell.Undo()
1078
1079 def OnRedo(self, event):
1080 self.shell.Redo()
1081
1082 def OnCut(self, event):
1083 self.shell.Cut()
1084
1085 def OnCopy(self, event):
1086 self.shell.Copy()
1087
1088 def OnPaste(self, event):
1089 self.shell.Paste()
1090
1091 def OnClear(self, event):
1092 self.shell.Clear()
1093
1094 def OnSelectAll(self, event):
1095 self.shell.SelectAll()
1096
1097 def OnAbout(self, event):
1098 """Display an About PyCrust window."""
1099 import sys
1100 title = 'About PyCrust'
1101 text = 'PyCrust %s\n\n' % VERSION + \
1102 'Yet another Python shell, only flakier.\n\n' + \
1103 'Half-baked by Patrick K. O\'Brien,\n' + \
1104 'the other half is still in the oven.\n\n' + \
1105 'Shell Revision: %s\n' % self.shell.revision + \
1106 'Interpreter Revision: %s\n\n' % self.shell.interp.revision + \
1107 'Python Version: %s\n' % sys.version.split()[0] + \
1108 'wxPython Version: %s\n' % wx.__version__ + \
1109 'Platform: %s\n' % sys.platform
1110 dialog = wxMessageDialog(self, text, title, wxOK | wxICON_INFORMATION)
1111 dialog.ShowModal()
1112 dialog.Destroy()
1113
1114 def OnAutoCompleteShow(self, event):
1115 self.shell.autoComplete = event.IsChecked()
1116
1117 def OnAutoCompleteIncludeMagic(self, event):
1118 self.shell.autoCompleteIncludeMagic = event.IsChecked()
1119
1120 def OnAutoCompleteIncludeSingle(self, event):
1121 self.shell.autoCompleteIncludeSingle = event.IsChecked()
1122
1123 def OnAutoCompleteIncludeDouble(self, event):
1124 self.shell.autoCompleteIncludeDouble = event.IsChecked()
1125
1126 def OnCallTipsShow(self, event):
1127 self.shell.autoCallTip = event.IsChecked()
1128
1129 def OnUpdateMenu(self, event):
1130 """Update menu items based on current status."""
1131 id = event.GetId()
1132 if id == wxID_UNDO:
1133 event.Enable(self.shell.CanUndo())
1134 elif id == wxID_REDO:
1135 event.Enable(self.shell.CanRedo())
1136 elif id == wxID_CUT:
1137 event.Enable(self.shell.CanCut())
1138 elif id == wxID_COPY:
1139 event.Enable(self.shell.CanCopy())
1140 elif id == wxID_PASTE:
1141 event.Enable(self.shell.CanPaste())
1142 elif id == wxID_CLEAR:
1143 event.Enable(self.shell.CanCut())
1144 elif id == ID_AUTOCOMP_SHOW:
1145 event.Check(self.shell.autoComplete)
1146 elif id == ID_AUTOCOMP_INCLUDE_MAGIC:
1147 event.Check(self.shell.autoCompleteIncludeMagic)
1148 elif id == ID_AUTOCOMP_INCLUDE_SINGLE:
1149 event.Check(self.shell.autoCompleteIncludeSingle)
1150 elif id == ID_AUTOCOMP_INCLUDE_DOUBLE:
1151 event.Check(self.shell.autoCompleteIncludeDouble)
1152 elif id == ID_CALLTIPS_SHOW:
1153 event.Check(self.shell.autoCallTip)
1154
1155
1156 class ShellFrame(wxFrame, ShellMenu):
1157 """Frame containing the PyCrust shell component."""
1158
1159 name = 'PyCrust Shell Frame'
1160 revision = __revision__
1161
1162 def __init__(self, parent=None, id=-1, title='PyShell', \
1163 pos=wxDefaultPosition, size=wxDefaultSize, \
1164 style=wxDEFAULT_FRAME_STYLE, locals=None, \
1165 InterpClass=None, *args, **kwds):
1166 """Create a PyCrust ShellFrame instance."""
1167 wxFrame.__init__(self, parent, id, title, pos, size, style)
1168 intro = 'Welcome To PyCrust %s - The Flakiest Python Shell' % VERSION
1169 intro += '\nSponsored by Orbtech - Your source for Python programming expertise.'
1170 self.CreateStatusBar()
1171 self.SetStatusText(intro.replace('\n', ', '))
1172 import images
1173 self.SetIcon(images.getPyCrustIcon())
1174 self.shell = Shell(parent=self, id=-1, introText=intro, \
1175 locals=locals, InterpClass=InterpClass, \
1176 *args, **kwds)
1177 # Override the shell so that status messages go to the status bar.
1178 self.shell.setStatusText = self.SetStatusText
1179 self.createMenus()
1180 EVT_CLOSE(self, self.OnCloseWindow)
1181
1182 def OnCloseWindow(self, event):
1183 self.shell.destroy()
1184 self.Destroy()
1185