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