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