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