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