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