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