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