02744d9e3b522a0ac4bee1232d13c2acd18c0d2a
[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 # De 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 # Hack to keep characters from entering when Alt or Control are down.
308 elif event.ControlDown() or event.AltDown():
309 pass
310 else:
311 # Allow the normal event handling to take place.
312 event.Skip()
313
314 def OnKeyDown(self, event):
315 """Key down event handler.
316
317 Prevents modification of previously submitted commands/responses."""
318 key = event.KeyCode()
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 # Cut to the clipboard.
330 elif event.ControlDown() and key in (ord('X'), ord('x')):
331 self.Cut()
332 # Copy to the clipboard.
333 elif event.ControlDown() and not event.ShiftDown() \
334 and key in (ord('C'), ord('c')):
335 self.Copy()
336 # Copy to the clipboard, including prompts.
337 elif event.ControlDown() and event.ShiftDown() \
338 and key in (ord('C'), ord('c')):
339 self.CopyWithPrompts()
340 # Paste from the clipboard.
341 elif event.ControlDown() and key in (ord('V'), ord('v')):
342 self.Paste()
343 # Retrieve the previous command from the history buffer.
344 elif (event.ControlDown() and key == WXK_UP) \
345 or (event.AltDown() and key in (ord('P'), ord('p'))):
346 self.OnHistoryRetrieve(step=+1)
347 # Retrieve the next command from the history buffer.
348 elif (event.ControlDown() and key == WXK_DOWN) \
349 or (event.AltDown() and key in (ord('N'), ord('n'))):
350 self.OnHistoryRetrieve(step=-1)
351 # Search up the history for the text in front of the cursor.
352 elif key == WXK_F8:
353 self.OnHistorySearch()
354 # Home needs to be aware of the prompt.
355 elif key == WXK_HOME:
356 if currpos >= stoppos:
357 if event.ShiftDown():
358 # Select text from current position to end of prompt.
359 self.SetSelection(self.GetCurrentPos(), stoppos)
360 else:
361 self.SetCurrentPos(stoppos)
362 self.SetAnchor(stoppos)
363 else:
364 event.Skip()
365 # Basic navigation keys should work anywhere.
366 elif key in (WXK_END, WXK_LEFT, WXK_RIGHT, WXK_UP, WXK_DOWN, \
367 WXK_PRIOR, WXK_NEXT):
368 event.Skip()
369 # Don't backspace over the latest prompt.
370 elif key == WXK_BACK:
371 if currpos > self.prompt1Pos[1]:
372 event.Skip()
373 # Only allow these keys after the latest prompt.
374 elif key in (WXK_TAB, WXK_DELETE):
375 if self.CanEdit():
376 event.Skip()
377 # Don't toggle between insert mode and overwrite mode.
378 elif key == WXK_INSERT:
379 pass
380 # Protect the readonly portion of the shell.
381 elif not self.CanEdit():
382 pass
383 else:
384 event.Skip()
385
386 def OnHistoryRetrieve(self, step):
387 """Retrieve the previous/next command from the history buffer."""
388 if not self.CanEdit():
389 return
390 startpos = self.GetCurrentPos()
391 newindex = self.historyIndex + step
392 if not (-1 <= newindex < len(self.history)):
393 return
394 self.historyIndex = newindex
395 if newindex == -1:
396 self.ReplaceSelection('')
397 else:
398 self.ReplaceSelection('')
399 command = self.history[self.historyIndex]
400 command = command.replace('\n', os.linesep + sys.ps2)
401 self.ReplaceSelection(command)
402 endpos = self.GetCurrentPos()
403 self.SetSelection(endpos, startpos)
404
405 def OnHistorySearch(self):
406 """Search up the history buffer for the text in front of the cursor."""
407 if not self.CanEdit():
408 return
409 startpos = self.GetCurrentPos()
410 # The text up to the cursor is what we search for.
411 numCharsAfterCursor = self.GetTextLength() - startpos
412 searchText = self.getCommand(rstrip=0)
413 if numCharsAfterCursor > 0:
414 searchText = searchText[:-numCharsAfterCursor]
415 if not searchText:
416 return
417 # Search upwards from the current history position and loop back
418 # to the beginning if we don't find anything.
419 if (self.historyIndex <= -1) \
420 or (self.historyIndex >= len(self.history)-2):
421 searchOrder = range(len(self.history))
422 else:
423 searchOrder = range(self.historyIndex+1, len(self.history)) + \
424 range(self.historyIndex)
425 for i in searchOrder:
426 command = self.history[i]
427 if command[:len(searchText)] == searchText:
428 # Replace the current selection with the one we've found.
429 self.ReplaceSelection(command[len(searchText):])
430 endpos = self.GetCurrentPos()
431 self.SetSelection(endpos, startpos)
432 # We've now warped into middle of the history.
433 self.historyIndex = i
434 break
435
436 def setStatusText(self, text):
437 """Display status information."""
438
439 # This method will most likely be replaced by the enclosing app
440 # to do something more interesting, like write to a status bar.
441 print text
442
443 def processLine(self):
444 """Process the line of text at which the user hit Enter."""
445
446 # The user hit ENTER and we need to decide what to do. They could be
447 # sitting on any line in the shell.
448
449 thepos = self.GetCurrentPos()
450 endpos = self.GetTextLength()
451 # If they hit RETURN at the very bottom, execute the command.
452 if thepos == endpos:
453 self.interp.more = 0
454 if self.getCommand():
455 command = self.GetTextRange(self.prompt1Pos[1], endpos)
456 else:
457 # This is a hack, now that we allow editing of previous
458 # lines, which throws off our promptPos values.
459 newend = endpos - len(self.getCommand(rstrip=0))
460 command = self.GetTextRange(self.prompt1Pos[1], newend)
461 command = command.replace(os.linesep + sys.ps2, '\n')
462 self.push(command)
463 # Or replace the current command with the other command.
464 elif thepos < self.prompt1Pos[0]:
465 theline = self.GetCurrentLine()
466 command = self.getCommand(rstrip=0)
467 # If the new line contains a command (even an invalid one).
468 if command:
469 command = self.getMultilineCommand()
470 self.SetCurrentPos(endpos)
471 startpos = self.prompt1Pos[1]
472 self.SetSelection(startpos, endpos)
473 self.ReplaceSelection('')
474 self.write(command)
475 self.more = 0
476 # Otherwise, put the cursor back where we started.
477 else:
478 self.SetCurrentPos(thepos)
479 self.SetAnchor(thepos)
480 # Or add a new line to the current single or multi-line command.
481 elif thepos > self.prompt1Pos[1]:
482 self.write(os.linesep)
483 self.more = 1
484 self.prompt()
485
486 def getMultilineCommand(self, rstrip=1):
487 """Extract a multi-line command from the editor.
488
489 The command may not necessarily be valid Python syntax."""
490 # XXX Need to extract real prompts here. Need to keep track of the
491 # prompt every time a command is issued.
492 ps1 = str(sys.ps1)
493 ps1size = len(ps1)
494 ps2 = str(sys.ps2)
495 ps2size = len(ps2)
496 # This is a total hack job, but it works.
497 text = self.GetCurLine()[0]
498 line = self.GetCurrentLine()
499 while text[:ps2size] == ps2 and line > 0:
500 line -= 1
501 self.GotoLine(line)
502 text = self.GetCurLine()[0]
503 if text[:ps1size] == ps1:
504 line = self.GetCurrentLine()
505 self.GotoLine(line)
506 startpos = self.GetCurrentPos() + ps1size
507 line += 1
508 self.GotoLine(line)
509 while self.GetCurLine()[0][:ps2size] == ps2:
510 line += 1
511 self.GotoLine(line)
512 stoppos = self.GetCurrentPos()
513 command = self.GetTextRange(startpos, stoppos)
514 command = command.replace(os.linesep + sys.ps2, '\n')
515 command = command.rstrip()
516 command = command.replace('\n', os.linesep + sys.ps2)
517 else:
518 command = ''
519 if rstrip:
520 command = command.rstrip()
521 return command
522
523 def getCommand(self, text=None, rstrip=1):
524 """Extract a command from text which may include a shell prompt.
525
526 The command may not necessarily be valid Python syntax."""
527 if not text:
528 text = self.GetCurLine()[0]
529 # Strip the prompt off the front of text leaving just the command.
530 command = self.lstripPrompt(text)
531 if command == text:
532 command = '' # Real commands have prompts.
533 if rstrip:
534 command = command.rstrip()
535 return command
536
537 def lstripPrompt(self, text):
538 """Return text without a leading prompt."""
539 ps1 = str(sys.ps1)
540 ps1size = len(ps1)
541 ps2 = str(sys.ps2)
542 ps2size = len(ps2)
543 # Strip the prompt off the front of text.
544 if text[:ps1size] == ps1:
545 text = text[ps1size:]
546 elif text[:ps2size] == ps2:
547 text = text[ps2size:]
548 return text
549
550 def push(self, command):
551 """Send command to the interpreter for execution."""
552 self.write(os.linesep)
553 self.more = self.interp.push(command)
554 if not self.more:
555 self.addHistory(command.rstrip())
556 self.prompt()
557
558 def addHistory(self, command):
559 """Add command to the command history."""
560 # Reset the history position.
561 self.historyIndex = -1
562 # Insert this command into the history, unless it's a blank
563 # line or the same as the last command.
564 if command != '' \
565 and (len(self.history) == 0 or command != self.history[0]):
566 self.history.insert(0, command)
567
568 def write(self, text):
569 """Display text in the shell.
570
571 Replace line endings with OS-specific endings."""
572 text = self.fixLineEndings(text)
573 self.AddText(text)
574 self.EnsureCaretVisible()
575
576 def fixLineEndings(self, text):
577 """Return text with line endings replaced by OS-specific endings."""
578 lines = text.split('\r\n')
579 for l in range(len(lines)):
580 chunks = lines[l].split('\r')
581 for c in range(len(chunks)):
582 chunks[c] = os.linesep.join(chunks[c].split('\n'))
583 lines[l] = os.linesep.join(chunks)
584 text = os.linesep.join(lines)
585 return text
586
587 def prompt(self):
588 """Display appropriate prompt for the context, either ps1 or ps2.
589
590 If this is a continuation line, autoindent as necessary."""
591 if self.more:
592 prompt = str(sys.ps2)
593 else:
594 prompt = str(sys.ps1)
595 pos = self.GetCurLine()[1]
596 if pos > 0: self.write(os.linesep)
597 self.promptPos[0] = self.GetCurrentPos()
598 if not self.more: self.prompt1Pos[0] = self.GetCurrentPos()
599 self.write(prompt)
600 self.promptPos[1] = self.GetCurrentPos()
601 if not self.more:
602 self.prompt1Pos[1] = self.GetCurrentPos()
603 # Keep the undo feature from undoing previous responses.
604 self.EmptyUndoBuffer()
605 # XXX Add some autoindent magic here if more.
606 if self.more:
607 self.write(' '*4) # Temporary hack indentation.
608 self.EnsureCaretVisible()
609 self.ScrollToColumn(0)
610
611 def readIn(self):
612 """Replacement for stdin."""
613 prompt = 'Please enter your response:'
614 dialog = wxTextEntryDialog(None, prompt, \
615 'Input Dialog (Standard)', '')
616 try:
617 if dialog.ShowModal() == wxID_OK:
618 text = dialog.GetValue()
619 self.write(text + os.linesep)
620 return text
621 finally:
622 dialog.Destroy()
623 return ''
624
625 def readRaw(self, prompt='Please enter your response:'):
626 """Replacement for raw_input."""
627 dialog = wxTextEntryDialog(None, prompt, \
628 'Input Dialog (Raw)', '')
629 try:
630 if dialog.ShowModal() == wxID_OK:
631 text = dialog.GetValue()
632 return text
633 finally:
634 dialog.Destroy()
635 return ''
636
637 def ask(self, prompt='Please enter your response:'):
638 """Get response from the user."""
639 return raw_input(prompt=prompt)
640
641 def pause(self):
642 """Halt execution pending a response from the user."""
643 self.ask('Press enter to continue:')
644
645 def clear(self):
646 """Delete all text from the shell."""
647 self.ClearAll()
648
649 def run(self, command, prompt=1, verbose=1):
650 """Execute command within the shell as if it was typed in directly.
651 >>> shell.run('print "this"')
652 >>> print "this"
653 this
654 >>>
655 """
656 # Go to the very bottom of the text.
657 endpos = self.GetTextLength()
658 self.SetCurrentPos(endpos)
659 command = command.rstrip()
660 if prompt: self.prompt()
661 if verbose: self.write(command)
662 self.push(command)
663
664 def runfile(self, filename):
665 """Execute all commands in file as if they were typed into the shell."""
666 file = open(filename)
667 try:
668 self.prompt()
669 for command in file.readlines():
670 if command[:6] == 'shell.': # Run shell methods silently.
671 self.run(command, prompt=0, verbose=0)
672 else:
673 self.run(command, prompt=0, verbose=1)
674 finally:
675 file.close()
676
677 def autoCompleteShow(self, command):
678 """Display auto-completion popup list."""
679 list = self.interp.getAutoCompleteList(command, \
680 includeMagic=self.autoCompleteIncludeMagic, \
681 includeSingle=self.autoCompleteIncludeSingle, \
682 includeDouble=self.autoCompleteIncludeDouble)
683 if list:
684 options = ' '.join(list)
685 offset = 0
686 self.AutoCompShow(offset, options)
687
688 def autoCallTipShow(self, command):
689 """Display argument spec and docstring in a popup bubble thingie."""
690 if self.CallTipActive: self.CallTipCancel()
691 tip = self.interp.getCallTip(command)
692 if tip:
693 offset = self.GetCurrentPos()
694 self.CallTipShow(offset, tip)
695
696 def writeOut(self, text):
697 """Replacement for stdout."""
698 self.write(text)
699
700 def writeErr(self, text):
701 """Replacement for stderr."""
702 self.write(text)
703
704 def redirectStdin(self, redirect=1):
705 """If redirect is true then sys.stdin will come from the shell."""
706 if redirect:
707 sys.stdin = PseudoFileIn(self.readIn)
708 else:
709 sys.stdin = self.stdin
710
711 def redirectStdout(self, redirect=1):
712 """If redirect is true then sys.stdout will go to the shell."""
713 if redirect:
714 sys.stdout = PseudoFileOut(self.writeOut)
715 else:
716 sys.stdout = self.stdout
717
718 def redirectStderr(self, redirect=1):
719 """If redirect is true then sys.stderr will go to the shell."""
720 if redirect:
721 sys.stderr = PseudoFileErr(self.writeErr)
722 else:
723 sys.stderr = self.stderr
724
725 def CanCut(self):
726 """Return true if text is selected and can be cut."""
727 if self.GetSelectionStart() != self.GetSelectionEnd() \
728 and self.GetSelectionStart() >= self.prompt1Pos[1] \
729 and self.GetSelectionEnd() >= self.prompt1Pos[1]:
730 return 1
731 else:
732 return 0
733
734 def CanCopy(self):
735 """Return true if text is selected and can be copied."""
736 return self.GetSelectionStart() != self.GetSelectionEnd()
737
738 def CanPaste(self):
739 """Return true if a paste should succeed."""
740 if self.CanEdit() and wxStyledTextCtrl.CanPaste(self):
741 return 1
742 else:
743 return 0
744
745 def CanEdit(self):
746 """Return true if editing should succeed."""
747 return self.GetCurrentPos() >= self.prompt1Pos[1]
748
749 def Cut(self):
750 """Remove selection and place it on the clipboard."""
751 if self.CanCut() and self.CanCopy():
752 if self.AutoCompActive(): self.AutoCompCancel()
753 if self.CallTipActive: self.CallTipCancel()
754 self.Copy()
755 self.ReplaceSelection('')
756
757 def Copy(self):
758 """Copy selection and place it on the clipboard."""
759 if self.CanCopy():
760 command = self.GetSelectedText()
761 command = command.replace(os.linesep + sys.ps2, os.linesep)
762 command = command.replace(os.linesep + sys.ps1, os.linesep)
763 command = self.lstripPrompt(text=command)
764 data = wxTextDataObject(command)
765 if wxTheClipboard.Open():
766 wxTheClipboard.SetData(data)
767 wxTheClipboard.Close()
768
769 def CopyWithPrompts(self):
770 """Copy selection, including prompts, and place it on the clipboard."""
771 if self.CanCopy():
772 command = self.GetSelectedText()
773 data = wxTextDataObject(command)
774 if wxTheClipboard.Open():
775 wxTheClipboard.SetData(data)
776 wxTheClipboard.Close()
777
778 def Paste(self):
779 """Replace selection with clipboard contents."""
780 if self.CanPaste():
781 if wxTheClipboard.Open():
782 if wxTheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)):
783 data = wxTextDataObject()
784 if wxTheClipboard.GetData(data):
785 command = data.GetText()
786 command = command.rstrip()
787 command = self.fixLineEndings(command)
788 command = self.lstripPrompt(text=command)
789 command = command.replace(os.linesep + sys.ps2, '\n')
790 command = command.replace(os.linesep, '\n')
791 command = command.replace('\n', os.linesep + sys.ps2)
792 self.ReplaceSelection('')
793 self.write(command)
794 wxTheClipboard.Close()
795
796
797 wxID_SELECTALL = NewId() # This *should* be defined by wxPython.
798 ID_AUTOCOMP = NewId()
799 ID_AUTOCOMP_SHOW = NewId()
800 ID_AUTOCOMP_INCLUDE_MAGIC = NewId()
801 ID_AUTOCOMP_INCLUDE_SINGLE = NewId()
802 ID_AUTOCOMP_INCLUDE_DOUBLE = NewId()
803 ID_CALLTIPS = NewId()
804 ID_CALLTIPS_SHOW = NewId()
805
806
807 class ShellMenu:
808 """Mixin class to add standard menu items."""
809
810 def createMenus(self):
811 m = self.fileMenu = wxMenu()
812 m.AppendSeparator()
813 m.Append(wxID_EXIT, 'E&xit', 'Exit PyCrust')
814
815 m = self.editMenu = wxMenu()
816 m.Append(wxID_UNDO, '&Undo \tCtrl+Z', 'Undo the last action')
817 m.Append(wxID_REDO, '&Redo \tCtrl+Y', 'Redo the last undone action')
818 m.AppendSeparator()
819 m.Append(wxID_CUT, 'Cu&t \tCtrl+X', 'Cut the selection')
820 m.Append(wxID_COPY, '&Copy \tCtrl+C', 'Copy the selection')
821 m.Append(wxID_PASTE, '&Paste \tCtrl+V', 'Paste')
822 m.AppendSeparator()
823 m.Append(wxID_CLEAR, 'Cle&ar', 'Delete the selection')
824 m.Append(wxID_SELECTALL, 'Select A&ll', 'Select all text')
825
826 m = self.autocompMenu = wxMenu()
827 m.Append(ID_AUTOCOMP_SHOW, 'Show Auto Completion', \
828 'Show auto completion during dot syntax', \
829 checkable=1)
830 m.Append(ID_AUTOCOMP_INCLUDE_MAGIC, 'Include Magic Attributes', \
831 'Include attributes visible to __getattr__ and __setattr__', \
832 checkable=1)
833 m.Append(ID_AUTOCOMP_INCLUDE_SINGLE, 'Include Single Underscores', \
834 'Include attibutes prefixed by a single underscore', \
835 checkable=1)
836 m.Append(ID_AUTOCOMP_INCLUDE_DOUBLE, 'Include Double Underscores', \
837 'Include attibutes prefixed by a double underscore', \
838 checkable=1)
839
840 m = self.calltipsMenu = wxMenu()
841 m.Append(ID_CALLTIPS_SHOW, 'Show Call Tips', \
842 'Show call tips with argument specifications', checkable=1)
843
844 m = self.optionsMenu = wxMenu()
845 m.AppendMenu(ID_AUTOCOMP, '&Auto Completion', self.autocompMenu, \
846 'Auto Completion Options')
847 m.AppendMenu(ID_CALLTIPS, '&Call Tips', self.calltipsMenu, \
848 'Call Tip Options')
849
850 m = self.helpMenu = wxMenu()
851 m.AppendSeparator()
852 m.Append(wxID_ABOUT, '&About...', 'About PyCrust')
853
854 b = self.menuBar = wxMenuBar()
855 b.Append(self.fileMenu, '&File')
856 b.Append(self.editMenu, '&Edit')
857 b.Append(self.optionsMenu, '&Options')
858 b.Append(self.helpMenu, '&Help')
859 self.SetMenuBar(b)
860
861 EVT_MENU(self, wxID_EXIT, self.OnExit)
862 EVT_MENU(self, wxID_UNDO, self.OnUndo)
863 EVT_MENU(self, wxID_REDO, self.OnRedo)
864 EVT_MENU(self, wxID_CUT, self.OnCut)
865 EVT_MENU(self, wxID_COPY, self.OnCopy)
866 EVT_MENU(self, wxID_PASTE, self.OnPaste)
867 EVT_MENU(self, wxID_CLEAR, self.OnClear)
868 EVT_MENU(self, wxID_SELECTALL, self.OnSelectAll)
869 EVT_MENU(self, wxID_ABOUT, self.OnAbout)
870 EVT_MENU(self, ID_AUTOCOMP_SHOW, \
871 self.OnAutoCompleteShow)
872 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_MAGIC, \
873 self.OnAutoCompleteIncludeMagic)
874 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_SINGLE, \
875 self.OnAutoCompleteIncludeSingle)
876 EVT_MENU(self, ID_AUTOCOMP_INCLUDE_DOUBLE, \
877 self.OnAutoCompleteIncludeDouble)
878 EVT_MENU(self, ID_CALLTIPS_SHOW, \
879 self.OnCallTipsShow)
880
881 EVT_UPDATE_UI(self, wxID_UNDO, self.OnUpdateMenu)
882 EVT_UPDATE_UI(self, wxID_REDO, self.OnUpdateMenu)
883 EVT_UPDATE_UI(self, wxID_CUT, self.OnUpdateMenu)
884 EVT_UPDATE_UI(self, wxID_COPY, self.OnUpdateMenu)
885 EVT_UPDATE_UI(self, wxID_PASTE, self.OnUpdateMenu)
886 EVT_UPDATE_UI(self, wxID_CLEAR, self.OnUpdateMenu)
887 EVT_UPDATE_UI(self, ID_AUTOCOMP_SHOW, self.OnUpdateMenu)
888 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_MAGIC, self.OnUpdateMenu)
889 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_SINGLE, self.OnUpdateMenu)
890 EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_DOUBLE, self.OnUpdateMenu)
891 EVT_UPDATE_UI(self, ID_CALLTIPS_SHOW, self.OnUpdateMenu)
892
893 def OnExit(self, event):
894 self.Close(true)
895
896 def OnUndo(self, event):
897 self.shell.Undo()
898
899 def OnRedo(self, event):
900 self.shell.Redo()
901
902 def OnCut(self, event):
903 self.shell.Cut()
904
905 def OnCopy(self, event):
906 self.shell.Copy()
907
908 def OnPaste(self, event):
909 self.shell.Paste()
910
911 def OnClear(self, event):
912 self.shell.Clear()
913
914 def OnSelectAll(self, event):
915 self.shell.SelectAll()
916
917 def OnAbout(self, event):
918 """Display an About PyCrust window."""
919 import sys
920 title = 'About PyCrust'
921 text = 'PyCrust %s\n\n' % VERSION + \
922 'Yet another Python shell, only flakier.\n\n' + \
923 'Half-baked by Patrick K. O\'Brien,\n' + \
924 'the other half is still in the oven.\n\n' + \
925 'Shell Revision: %s\n' % self.shell.revision + \
926 'Interpreter Revision: %s\n\n' % self.shell.interp.revision + \
927 'Python Version: %s\n' % sys.version.split()[0] + \
928 'wxPython Version: %s\n' % wx.__version__ + \
929 'Platform: %s\n' % sys.platform
930 dialog = wxMessageDialog(self, text, title, wxOK | wxICON_INFORMATION)
931 dialog.ShowModal()
932 dialog.Destroy()
933
934 def OnAutoCompleteShow(self, event):
935 self.shell.autoComplete = event.IsChecked()
936
937 def OnAutoCompleteIncludeMagic(self, event):
938 self.shell.autoCompleteIncludeMagic = event.IsChecked()
939
940 def OnAutoCompleteIncludeSingle(self, event):
941 self.shell.autoCompleteIncludeSingle = event.IsChecked()
942
943 def OnAutoCompleteIncludeDouble(self, event):
944 self.shell.autoCompleteIncludeDouble = event.IsChecked()
945
946 def OnCallTipsShow(self, event):
947 self.shell.autoCallTip = event.IsChecked()
948
949 def OnUpdateMenu(self, event):
950 """Update menu items based on current status."""
951 id = event.GetId()
952 if id == wxID_UNDO:
953 event.Enable(self.shell.CanUndo())
954 elif id == wxID_REDO:
955 event.Enable(self.shell.CanRedo())
956 elif id == wxID_CUT:
957 event.Enable(self.shell.CanCut())
958 elif id == wxID_COPY:
959 event.Enable(self.shell.CanCopy())
960 elif id == wxID_PASTE:
961 event.Enable(self.shell.CanPaste())
962 elif id == wxID_CLEAR:
963 event.Enable(self.shell.CanCut())
964 elif id == ID_AUTOCOMP_SHOW:
965 event.Check(self.shell.autoComplete)
966 elif id == ID_AUTOCOMP_INCLUDE_MAGIC:
967 event.Check(self.shell.autoCompleteIncludeMagic)
968 elif id == ID_AUTOCOMP_INCLUDE_SINGLE:
969 event.Check(self.shell.autoCompleteIncludeSingle)
970 elif id == ID_AUTOCOMP_INCLUDE_DOUBLE:
971 event.Check(self.shell.autoCompleteIncludeDouble)
972 elif id == ID_CALLTIPS_SHOW:
973 event.Check(self.shell.autoCallTip)
974
975
976 class ShellFrame(wxFrame, ShellMenu):
977 """Frame containing the PyCrust shell component."""
978
979 name = 'PyCrust Shell Frame'
980 revision = __version__
981
982 def __init__(self, parent=None, id=-1, title='PyShell', \
983 pos=wxDefaultPosition, size=wxDefaultSize, \
984 style=wxDEFAULT_FRAME_STYLE, locals=None, \
985 InterpClass=None, *args, **kwds):
986 """Create a PyCrust ShellFrame instance."""
987 wxFrame.__init__(self, parent, id, title, pos, size, style)
988 intro = 'Welcome To PyCrust %s - The Flakiest Python Shell' % VERSION
989 intro += '\nSponsored by Orbtech.com - Your Source For Python Development Services'
990 self.CreateStatusBar()
991 self.SetStatusText(intro.replace('\n', ', '))
992 if wxPlatform == '__WXMSW__':
993 import os
994 filename = os.path.join(os.path.dirname(__file__), 'PyCrust.ico')
995 icon = wxIcon(filename, wxBITMAP_TYPE_ICO)
996 self.SetIcon(icon)
997 self.shell = Shell(parent=self, id=-1, introText=intro, \
998 locals=locals, InterpClass=InterpClass, \
999 *args, **kwds)
1000 # Override the shell so that status messages go to the status bar.
1001 self.shell.setStatusText = self.SetStatusText
1002 self.createMenus()
1003
1004
1005
1006