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