"""Shell is an interactive text control in which a user types in commands to be sent to the interpreter. This particular shell is based on wxPython's wxStyledTextCtrl. Sponsored by Orbtech - Your source for Python programming expertise.""" __author__ = "Patrick K. O'Brien " import wx from wx import stc from wx.lib.six import PY3 import keyword import os import sys import time from .buffer import Buffer from . import dispatcher from . import editwindow from . import frame from .pseudo import PseudoFileIn from .pseudo import PseudoFileOut from .pseudo import PseudoFileErr from .version import VERSION from .magic import magic from .path import ls,cd,pwd,sx sys.ps3 = '<-- ' # Input prompt. USE_MAGIC=True # Force updates from long-running commands after this many seconds PRINT_UPDATE_MAX_TIME=2 NAVKEYS = (wx.WXK_END, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN) class ShellFrame(frame.Frame, frame.ShellFrameMixin): """Frame containing the shell component.""" name = 'Shell Frame' def __init__(self, parent=None, id=-1, title='PyShell', pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, locals=None, InterpClass=None, config=None, dataDir=None, *args, **kwds): """Create ShellFrame instance.""" frame.Frame.__init__(self, parent, id, title, pos, size, style) frame.ShellFrameMixin.__init__(self, config, dataDir) if size == wx.DefaultSize: self.SetSize((750, 525)) intro = 'PyShell %s - The Flakiest Python Shell' % VERSION self.SetStatusText(intro.replace('\n', ', ')) self.shell = Shell(parent=self, id=-1, introText=intro, locals=locals, InterpClass=InterpClass, startupScript=self.startupScript, execStartupScript=self.execStartupScript, *args, **kwds) # Override the shell so that status messages go to the status bar. self.shell.setStatusText = self.SetStatusText self.shell.SetFocus() self.LoadSettings() def OnClose(self, event): """Event handler for closing.""" # This isn't working the way I want, but I'll leave it for now. if self.shell.waiting: if event.CanVeto(): event.Veto(True) else: self.SaveSettings() self.shell.destroy() self.Destroy() def OnAbout(self, event): """Display an About window.""" title = 'About PyShell' text = 'PyShell %s\n\n' % VERSION + \ 'Yet another Python shell, only flakier.\n\n' + \ 'Half-baked by Patrick K. O\'Brien,\n' + \ 'the other half is still in the oven.\n\n' + \ 'Platform: %s\n' % sys.platform + \ 'Python Version: %s\n' % sys.version.split()[0] + \ 'wxPython Version: %s\n' % wx.VERSION_STRING + \ ('\t(%s)\n' % ", ".join(wx.PlatformInfo[1:])) dialog = wx.MessageDialog(self, text, title, wx.OK | wx.ICON_INFORMATION) dialog.ShowModal() dialog.Destroy() def OnHelp(self, event): """Show a help dialog.""" frame.ShellFrameMixin.OnHelp(self, event) def LoadSettings(self): if self.config is not None: frame.ShellFrameMixin.LoadSettings(self) frame.Frame.LoadSettings(self, self.config) self.shell.LoadSettings(self.config) def SaveSettings(self, force=False): if self.config is not None: frame.ShellFrameMixin.SaveSettings(self) if self.autoSaveSettings or force: frame.Frame.SaveSettings(self, self.config) self.shell.SaveSettings(self.config) def DoSaveSettings(self): if self.config is not None: self.SaveSettings(force=True) self.config.Flush() HELP_TEXT = """\ * Key bindings: Home Go to the beginning of the command or line. Shift+Home Select to the beginning of the command or line. Shift+End Select to the end of the line. End Go to the end of the line. Ctrl+C Copy selected text, removing prompts. Ctrl+Shift+C Copy selected text, retaining prompts. Alt+C Copy to the clipboard, including prefixed prompts. Ctrl+X Cut selected text. Ctrl+V Paste from clipboard. Ctrl+Shift+V Paste and run multiple commands from clipboard. Ctrl+Up Arrow Retrieve Previous History item. Alt+P Retrieve Previous History item. Ctrl+Down Arrow Retrieve Next History item. Alt+N Retrieve Next History item. Shift+Up Arrow Insert Previous History item. Shift+Down Arrow Insert Next History item. F8 Command-completion of History item. (Type a few characters of a previous command and press F8.) Ctrl+Enter Insert new line into multiline command. Ctrl+] Increase font size. Ctrl+[ Decrease font size. Ctrl+= Default font size. Ctrl-Space Show Auto Completion. Ctrl-Alt-Space Show Call Tip. Shift+Enter Complete Text from History. Ctrl+F Search F3 Search next Ctrl+H "hide" lines containing selection / "unhide" F12 on/off "free-edit" mode """ class ShellFacade: """Simplified interface to all shell-related functionality. This is a semi-transparent facade, in that all attributes of other are accessible, even though only some are visible to the user.""" name = 'Shell Interface' def __init__(self, other): """Create a ShellFacade instance.""" d = self.__dict__ d['other'] = other d['helpText'] = HELP_TEXT def help(self): """Display some useful information about how to use the shell.""" self.write(self.helpText) def __getattr__(self, name): if hasattr(self.other, name): return getattr(self.other, name) else: raise AttributeError(name) def __setattr__(self, name, value): if name in self.__dict__: self.__dict__[name] = value elif hasattr(self.other, name): setattr(self.other, name, value) else: raise AttributeError(name) def _getAttributeNames(self): """Return list of magic attributes to extend introspection.""" list = [ 'about', 'ask', 'autoCallTip', 'autoComplete', 'autoCompleteAutoHide', 'autoCompleteCaseInsensitive', 'autoCompleteIncludeDouble', 'autoCompleteIncludeMagic', 'autoCompleteIncludeSingle', 'callTipInsert', 'clear', 'pause', 'prompt', 'quit', 'redirectStderr', 'redirectStdin', 'redirectStdout', 'run', 'runfile', 'wrap', 'zoom', ] list.sort() return list #DNM DISPLAY_TEXT=""" Author: %r Py Version: %s Python Version: %s wxPython Version: %s wxPython PlatformInfo: %s Platform: %s""" class Shell(editwindow.EditWindow): """Shell based on StyledTextCtrl.""" name = 'Shell' def __init__(self, parent, id=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.CLIP_CHILDREN, introText='', locals=None, InterpClass=None, startupScript=None, execStartupScript=True, *args, **kwds): """Create Shell instance.""" editwindow.EditWindow.__init__(self, parent, id, pos, size, style) self.wrap() if locals is None: import __main__ locals = __main__.__dict__ # Grab these so they can be restored by self.redirect* methods. self.stdin = sys.stdin self.stdout = sys.stdout self.stderr = sys.stderr # Import a default interpreter class if one isn't provided. if InterpClass == None: from .interpreter import Interpreter else: Interpreter = InterpClass # Create a replacement for stdin. self.reader = PseudoFileIn(self.readline, self.readlines) self.reader.input = '' self.reader.isreading = False # Set up the interpreter. self.interp = Interpreter(locals=locals, rawin=self.raw_input, stdin=self.reader, stdout=PseudoFileOut(self.writeOut), stderr=PseudoFileErr(self.writeErr), *args, **kwds) # Set up the buffer. self.buffer = Buffer() # Find out for which keycodes the interpreter will autocomplete. self.autoCompleteKeys = self.interp.getAutoCompleteKeys() # Keep track of the last non-continuation prompt positions. self.promptPosStart = 0 self.promptPosEnd = 0 # Keep track of multi-line commands. self.more = False # For use with forced updates during long-running scripts self.lastUpdate=None # Create the command history. Commands are added into the # front of the list (ie. at index 0) as they are entered. # self.historyIndex is the current position in the history; it # gets incremented as you retrieve the previous command, # decremented as you retrieve the next, and reset when you hit # Enter. self.historyIndex == -1 means you're on the current # command, not in the history. self.history = [] self.historyIndex = -1 #seb add mode for "free edit" self.noteMode = 0 self.MarkerDefine(0,stc.STC_MARK_ROUNDRECT) # marker for hidden self.searchTxt = "" # Assign handlers for keyboard events. self.Bind(wx.EVT_CHAR, self.OnChar) self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) # Assign handler for the context menu self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI) # Assign handlers for edit events self.Bind(wx.EVT_MENU, lambda evt: self.Cut(), id=wx.ID_CUT) self.Bind(wx.EVT_MENU, lambda evt: self.Copy(), id=wx.ID_COPY) self.Bind(wx.EVT_MENU, lambda evt: self.CopyWithPrompts(), id=frame.ID_COPY_PLUS) self.Bind(wx.EVT_MENU, lambda evt: self.Paste(), id=wx.ID_PASTE) self.Bind(wx.EVT_MENU, lambda evt: self.PasteAndRun(), id=frame.ID_PASTE_PLUS) self.Bind(wx.EVT_MENU, lambda evt: self.SelectAll(), id=wx.ID_SELECTALL) self.Bind(wx.EVT_MENU, lambda evt: self.Clear(), id=wx.ID_CLEAR) self.Bind(wx.EVT_MENU, lambda evt: self.Undo(), id=wx.ID_UNDO) self.Bind(wx.EVT_MENU, lambda evt: self.Redo(), id=wx.ID_REDO) # Assign handler for idle time. self.waiting = False self.Bind(wx.EVT_IDLE, self.OnIdle) # Display the introductory banner information. self.showIntro(introText) # Assign some pseudo keywords to the interpreter's namespace. self.setBuiltinKeywords() # Add 'shell' to the interpreter's local namespace. self.setLocalShell() ## NOTE: See note at bottom of this file... ## #seb: File drag and drop ## self.SetDropTarget( FileDropTarget(self) ) # Do this last so the user has complete control over their # environment. They can override anything they want. if execStartupScript: if startupScript is None: startupScript = os.environ.get('PYTHONSTARTUP') self.execStartupScript(startupScript) else: self.prompt() wx.CallAfter(self.ScrollToLine, 0) def clearHistory(self): self.history = [] self.historyIndex = -1 dispatcher.send(signal="Shell.clearHistory") def destroy(self): del self.interp def setFocus(self): """Set focus to the shell.""" self.SetFocus() def OnIdle(self, event): """Free the CPU to do other things.""" if self.waiting: time.sleep(0.05) event.Skip() def showIntro(self, text=''): """Display introductory text in the shell.""" if text: self.write(text) try: if self.interp.introText: if text and not text.endswith(os.linesep): self.write(os.linesep) self.write(self.interp.introText) except AttributeError: pass def setBuiltinKeywords(self): """Create pseudo keywords as part of builtins. This sets "close", "exit" and "quit" to a helpful string. """ from wx.lib.six.moves import builtins builtins.close = builtins.exit = builtins.quit = \ 'Click on the close button to leave the application.' builtins.cd = cd builtins.ls = ls builtins.pwd = pwd builtins.sx = sx def quit(self): """Quit the application.""" # XXX Good enough for now but later we want to send a close event. # In the close event handler we can make sure they want to # quit. Other applications, like PythonCard, may choose to # hide rather than quit so we should just post the event and # let the surrounding app decide what it wants to do. self.write('Click on the close button to leave the application.') def setLocalShell(self): """Add 'shell' to locals as reference to ShellFacade instance.""" self.interp.locals['shell'] = ShellFacade(other=self) def execStartupScript(self, startupScript): """Execute the user's PYTHONSTARTUP script if they have one.""" if startupScript and os.path.isfile(startupScript): text = 'Startup script executed: ' + startupScript if PY3: self.push('print(%r)' % text) self.push('with open(%r, "r") as f:\n' ' exec(f.read())\n' % (startupScript)) else: self.push('print(%r); execfile(%r)' % (text, startupScript)) self.interp.startupScript = startupScript else: self.push('') def about(self): """Display information about Py.""" #DNM text = DISPLAY_TEXT % \ (__author__, VERSION, sys.version.split()[0], wx.VERSION_STRING, str(wx.PlatformInfo), sys.platform) self.write(text.strip()) def OnChar(self, event): """Keypress event handler. Only receives an event if OnKeyDown calls event.Skip() for the corresponding event.""" if self.noteMode: event.Skip() return # Prevent modification of previously submitted # commands/responses. if not self.CanEdit(): return key = event.GetKeyCode() currpos = self.GetCurrentPos() stoppos = self.promptPosEnd # Return (Enter) needs to be ignored in this handler. if key in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]: pass elif key in self.autoCompleteKeys: # Usually the dot (period) key activates auto completion. # Get the command between the prompt and the cursor. Add # the autocomplete character to the end of the command. if self.AutoCompActive(): self.AutoCompCancel() command = self.GetTextRange(stoppos, currpos) + chr(key) self.write(chr(key)) if self.autoComplete: self.autoCompleteShow(command) elif key == ord('('): # The left paren activates a call tip and cancels an # active auto completion. if self.AutoCompActive(): self.AutoCompCancel() # Get the command between the prompt and the cursor. Add # the '(' to the end of the command. self.ReplaceSelection('') command = self.GetTextRange(stoppos, currpos) + '(' self.write('(') self.autoCallTipShow(command, self.GetCurrentPos() == self.GetTextLength()) else: # Allow the normal event handling to take place. event.Skip() def OnKeyDown(self, event): """Key down event handler.""" key = event.GetKeyCode() # If the auto-complete window is up let it do its thing. if self.AutoCompActive(): event.Skip() return # Prevent modification of previously submitted # commands/responses. controlDown = event.ControlDown() rawControlDown = event.RawControlDown() altDown = event.AltDown() shiftDown = event.ShiftDown() currpos = self.GetCurrentPos() endpos = self.GetTextLength() selecting = self.GetSelectionStart() != self.GetSelectionEnd() if (rawControlDown or controlDown) and shiftDown and key in (ord('F'), ord('f')): li = self.GetCurrentLine() m = self.MarkerGet(li) if m & 1<<0: startP = self.PositionFromLine(li) self.MarkerDelete(li, 0) maxli = self.GetLineCount() li += 1 # li stayed visible as header-line li0 = li while li home: self.SetCurrentPos(home) if not selecting and not shiftDown: self.SetAnchor(home) self.EnsureCaretVisible() else: event.Skip() # Home needs to be aware of the prompt. elif key == wx.WXK_HOME: home = self.promptPosEnd if currpos > home: [line_str,line_len] = self.GetCurLine() pos=self.GetCurrentPos() if line_str[:4] in [sys.ps1,sys.ps2,sys.ps3]: self.SetCurrentPos(pos+4-line_len) #self.SetCurrentPos(home) if not selecting and not shiftDown: self.SetAnchor(pos+4-line_len) self.EnsureCaretVisible() else: event.Skip() else: event.Skip() # # The following handlers modify text, so we need to see if # there is a selection that includes text prior to the prompt. # # Don't modify a selection with text prior to the prompt. elif selecting and key not in NAVKEYS and not self.CanEdit(): pass # Paste from the clipboard. elif ((rawControlDown or controlDown) and not shiftDown and key in (ord('V'), ord('v'))) \ or (shiftDown and not controlDown and key == wx.WXK_INSERT): self.Paste() # manually invoke AutoComplete and Calltips elif (rawControlDown or controlDown) and key == wx.WXK_SPACE: self.OnCallTipAutoCompleteManually(shiftDown) # Paste from the clipboard, run commands. elif (rawControlDown or controlDown) and shiftDown and key in (ord('V'), ord('v')): self.PasteAndRun() # Replace with the previous command from the history buffer. elif ((rawControlDown or controlDown) and not shiftDown and key == wx.WXK_UP) \ or (altDown and key in (ord('P'), ord('p'))): self.OnHistoryReplace(step=+1) # Replace with the next command from the history buffer. elif ((rawControlDown or controlDown) and not shiftDown and key == wx.WXK_DOWN) \ or (altDown and key in (ord('N'), ord('n'))): self.OnHistoryReplace(step=-1) # Insert the previous command from the history buffer. elif ((rawControlDown or controlDown) and shiftDown and key == wx.WXK_UP) and self.CanEdit(): self.OnHistoryInsert(step=+1) # Insert the next command from the history buffer. elif ((rawControlDown or controlDown) and shiftDown and key == wx.WXK_DOWN) and self.CanEdit(): self.OnHistoryInsert(step=-1) # Search up the history for the text in front of the cursor. elif key == wx.WXK_F8: self.OnHistorySearch() # Don't backspace over the latest non-continuation prompt. elif key == wx.WXK_BACK: if selecting and self.CanEdit(): event.Skip() elif currpos > self.promptPosEnd: event.Skip() # Only allow these keys after the latest prompt. elif key in (wx.WXK_TAB, wx.WXK_DELETE): if self.CanEdit(): event.Skip() # Don't toggle between insert mode and overwrite mode. elif key == wx.WXK_INSERT: pass # Don't allow line deletion. elif controlDown and key in (ord('L'), ord('l')): # TODO : Allow line deletion eventually... #event.Skip() pass # Don't allow line transposition. elif controlDown and key in (ord('T'), ord('t')): # TODO : Allow line transposition eventually... # TODO : Will have to adjust markers accordingly and test if allowed... #event.Skip() pass # Basic navigation keys should work anywhere. elif key in NAVKEYS: event.Skip() # Protect the readonly portion of the shell. elif not self.CanEdit(): pass else: event.Skip() def OnShowCompHistory(self): """Show possible autocompletion Words from already typed words.""" #copy from history his = self.history[:] #put together in one string joined = " ".join (his) import re #sort out only "good" words newlist = re.split("[ \.\[\]=}(\)\,0-9\"]", joined) #length > 1 (mix out "trash") thlist = [] for i in newlist: if len (i) > 1: thlist.append (i) #unique (no duplicate words #oneliner from german python forum => unique list unlist = [thlist[i] for i in xrange(len(thlist)) if thlist[i] not in thlist[:i]] #sort lowercase unlist.sort(lambda a, b: cmp(a.lower(), b.lower())) #this is more convenient, isn't it? self.AutoCompSetIgnoreCase(True) #join again together in a string stringlist = " ".join(unlist) #pos von 0 noch ausrechnen #how big is the offset? cpos = self.GetCurrentPos() - 1 while chr (self.GetCharAt (cpos)).isalnum(): cpos -= 1 #the most important part self.AutoCompShow(self.GetCurrentPos() - cpos -1, stringlist) def clearCommand(self): """Delete the current, unexecuted command.""" startpos = self.promptPosEnd endpos = self.GetTextLength() self.SetSelection(startpos, endpos) self.ReplaceSelection('') self.more = False def OnHistoryReplace(self, step): """Replace with the previous/next command from the history buffer.""" self.clearCommand() self.replaceFromHistory(step) def replaceFromHistory(self, step): """Replace selection with command from the history buffer.""" ps2 = str(sys.ps2) self.ReplaceSelection('') newindex = self.historyIndex + step if -1 <= newindex <= len(self.history): self.historyIndex = newindex if 0 <= newindex <= len(self.history)-1: command = self.history[self.historyIndex] command = command.replace('\n', os.linesep + ps2) self.ReplaceSelection(command) def OnHistoryInsert(self, step): """Insert the previous/next command from the history buffer.""" if not self.CanEdit(): return startpos = self.GetCurrentPos() self.replaceFromHistory(step) endpos = self.GetCurrentPos() self.SetSelection(endpos, startpos) def OnHistorySearch(self): """Search up the history buffer for the text in front of the cursor.""" if not self.CanEdit(): return startpos = self.GetCurrentPos() # The text up to the cursor is what we search for. numCharsAfterCursor = self.GetTextLength() - startpos searchText = self.getCommand(rstrip=False) if numCharsAfterCursor > 0: searchText = searchText[:-numCharsAfterCursor] if not searchText: return # Search upwards from the current history position and loop # back to the beginning if we don't find anything. if (self.historyIndex <= -1) \ or (self.historyIndex >= len(self.history)-2): searchOrder = range(len(self.history)) else: searchOrder = range(self.historyIndex+1, len(self.history)) + \ range(self.historyIndex) for i in searchOrder: command = self.history[i] if command[:len(searchText)] == searchText: # Replace the current selection with the one we found. self.ReplaceSelection(command[len(searchText):]) endpos = self.GetCurrentPos() self.SetSelection(endpos, startpos) # We've now warped into middle of the history. self.historyIndex = i break def setStatusText(self, text): """Display status information.""" # This method will likely be replaced by the enclosing app to # do something more interesting, like write to a status bar. print(text) def insertLineBreak(self): """Insert a new line break.""" if self.CanEdit(): self.write(os.linesep) self.more = True self.prompt() def processLine(self): """Process the line of text at which the user hit Enter.""" # The user hit ENTER and we need to decide what to do. They # could be sitting on any line in the shell. thepos = self.GetCurrentPos() startpos = self.promptPosEnd endpos = self.GetTextLength() ps2 = str(sys.ps2) # If they hit RETURN inside the current command, execute the # command. if self.CanEdit(): self.SetCurrentPos(endpos) self.interp.more = False command = self.GetTextRange(startpos, endpos) lines = command.split(os.linesep + ps2) lines = [line.rstrip() for line in lines] command = '\n'.join(lines) if self.reader.isreading: if not command: # Match the behavior of the standard Python shell # when the user hits return without entering a # value. command = '\n' self.reader.input = command self.write(os.linesep) else: self.push(command) wx.CallLater(1, self.EnsureCaretVisible) # Or replace the current command with the other command. else: # If the line contains a command (even an invalid one). if self.getCommand(rstrip=False): command = self.getMultilineCommand() self.clearCommand() self.write(command) # Otherwise, put the cursor back where we started. else: self.SetCurrentPos(thepos) self.SetAnchor(thepos) def getMultilineCommand(self, rstrip=True): """Extract a multi-line command from the editor. The command may not necessarily be valid Python syntax.""" # XXX Need to extract real prompts here. Need to keep track of # the prompt every time a command is issued. ps1 = str(sys.ps1) ps1size = len(ps1) ps2 = str(sys.ps2) ps2size = len(ps2) # This is a total hack job, but it works. text = self.GetCurLine()[0] line = self.GetCurrentLine() while text[:ps2size] == ps2 and line > 0: line -= 1 self.GotoLine(line) text = self.GetCurLine()[0] if text[:ps1size] == ps1: line = self.GetCurrentLine() self.GotoLine(line) startpos = self.GetCurrentPos() + ps1size line += 1 self.GotoLine(line) while self.GetCurLine()[0][:ps2size] == ps2: line += 1 self.GotoLine(line) stoppos = self.GetCurrentPos() command = self.GetTextRange(startpos, stoppos) command = command.replace(os.linesep + ps2, '\n') command = command.rstrip() command = command.replace('\n', os.linesep + ps2) else: command = '' if rstrip: command = command.rstrip() return command def getCommand(self, text=None, rstrip=True): """Extract a command from text which may include a shell prompt. The command may not necessarily be valid Python syntax.""" if not text: text = self.GetCurLine()[0] # Strip the prompt off the front leaving just the command. command = self.lstripPrompt(text) if command == text: command = '' # Real commands have prompts. if rstrip: command = command.rstrip() return command def lstripPrompt(self, text): """Return text without a leading prompt.""" ps1 = str(sys.ps1) ps1size = len(ps1) ps2 = str(sys.ps2) ps2size = len(ps2) # Strip the prompt off the front of text. if text[:ps1size] == ps1: text = text[ps1size:] elif text[:ps2size] == ps2: text = text[ps2size:] return text def push(self, command, silent = False): """Send command to the interpreter for execution.""" if not silent: self.write(os.linesep) #DNM if USE_MAGIC: command=magic(command) busy = wx.BusyCursor() self.waiting = True self.lastUpdate=None self.more = self.interp.push(command) self.lastUpdate=None self.waiting = False del busy if not self.more: self.addHistory(command.rstrip()) if not silent: self.prompt() def addHistory(self, command): """Add command to the command history.""" # Reset the history position. self.historyIndex = -1 # Insert this command into the history, unless it's a blank # line or the same as the last command. if command != '' \ and (len(self.history) == 0 or command != self.history[0]): self.history.insert(0, command) dispatcher.send(signal="Shell.addHistory", command=command) def write(self, text): """Display text in the shell. Replace line endings with OS-specific endings.""" text = self.fixLineEndings(text) self.AddText(text) self.EnsureCaretVisible() if self.waiting: if self.lastUpdate==None: self.lastUpdate=time.time() if time.time()-self.lastUpdate > PRINT_UPDATE_MAX_TIME: self.Update() self.lastUpdate=time.time() def fixLineEndings(self, text): """Return text with line endings replaced by OS-specific endings.""" lines = text.split('\r\n') for l in range(len(lines)): chunks = lines[l].split('\r') for c in range(len(chunks)): chunks[c] = os.linesep.join(chunks[c].split('\n')) lines[l] = os.linesep.join(chunks) text = os.linesep.join(lines) return text def prompt(self): """Display proper prompt for the context: ps1, ps2 or ps3. If this is a continuation line, autoindent as necessary.""" isreading = self.reader.isreading skip = False if isreading: prompt = str(sys.ps3) elif self.more: prompt = str(sys.ps2) else: prompt = str(sys.ps1) pos = self.GetCurLine()[1] if pos > 0: if isreading: skip = True else: self.write(os.linesep) if not self.more: self.promptPosStart = self.GetCurrentPos() if not skip: self.write(prompt) if not self.more: self.promptPosEnd = self.GetCurrentPos() # Keep the undo feature from undoing previous responses. self.EmptyUndoBuffer() if self.more: line_num=self.GetCurrentLine() currentLine=self.GetLine(line_num) previousLine=self.GetLine(line_num-1)[len(prompt):] pstrip=previousLine.strip() lstrip=previousLine.lstrip() # Get the first alnum word: first_word=[] for i in pstrip: if i.isalnum(): first_word.append(i) else: break first_word = ''.join(first_word) if pstrip == '': # because it is all whitespace! indent=previousLine.strip('\n').strip('\r') else: indent=previousLine[:(len(previousLine)-len(lstrip))] if pstrip[-1]==':' and \ first_word in ['if','else','elif','for','while', 'def','class','try','except','finally']: indent+=' '*4 self.write(indent) self.EnsureCaretVisible() self.ScrollToColumn(0) def readline(self): """Replacement for stdin.readline().""" input = '' reader = self.reader reader.isreading = True self.prompt() try: while not reader.input: wx.YieldIfNeeded() input = reader.input finally: reader.input = '' reader.isreading = False input = str(input) # In case of Unicode. return input def readlines(self): """Replacement for stdin.readlines().""" lines = [] while lines[-1:] != ['\n']: lines.append(self.readline()) return lines def raw_input(self, prompt=''): """Return string based on user input.""" if prompt: self.write(prompt) return self.readline() def ask(self, prompt='Please enter your response:'): """Get response from the user using a dialog box.""" dialog = wx.TextEntryDialog(None, prompt, 'Input Dialog (Raw)', '') try: if dialog.ShowModal() == wx.ID_OK: text = dialog.GetValue() return text finally: dialog.Destroy() return '' def pause(self): """Halt execution pending a response from the user.""" self.ask('Press enter to continue:') def clear(self): """Delete all text from the shell.""" self.ClearAll() def run(self, command, prompt=True, verbose=True): """Execute command as if it was typed in directly. >>> shell.run('print("this")') >>> print("this") this >>> """ # Go to the very bottom of the text. endpos = self.GetTextLength() self.SetCurrentPos(endpos) command = command.rstrip() if prompt: self.prompt() if verbose: self.write(command) self.push(command) def runfile(self, filename): """Execute all commands in file as if they were typed into the shell.""" file = open(filename) try: self.prompt() for command in file.readlines(): if command[:6] == 'shell.': # Run shell methods silently. self.run(command, prompt=False, verbose=False) else: self.run(command, prompt=False, verbose=True) finally: file.close() def autoCompleteShow(self, command, offset = 0): """Display auto-completion popup list.""" self.AutoCompSetAutoHide(self.autoCompleteAutoHide) self.AutoCompSetIgnoreCase(self.autoCompleteCaseInsensitive) list = self.interp.getAutoCompleteList(command, includeMagic=self.autoCompleteIncludeMagic, includeSingle=self.autoCompleteIncludeSingle, includeDouble=self.autoCompleteIncludeDouble) if list: options = ' '.join(list) #offset = 0 self.AutoCompShow(offset, options) def autoCallTipShow(self, command, insertcalltip = True, forceCallTip = False): """Display argument spec and docstring in a popup window.""" if self.CallTipActive(): self.CallTipCancel() (name, argspec, tip) = self.interp.getCallTip(command) if tip: dispatcher.send(signal='Shell.calltip', sender=self, calltip=tip) if not self.autoCallTip and not forceCallTip: return startpos = self.GetCurrentPos() if argspec and insertcalltip and self.callTipInsert: self.write(argspec + ')') endpos = self.GetCurrentPos() self.SetSelection(startpos, endpos) if tip: tippos = startpos - (len(name) + 1) fallback = startpos - self.GetColumn(startpos) # In case there isn't enough room, only go back to the # fallback. tippos = max(tippos, fallback) self.CallTipShow(tippos, tip) def OnCallTipAutoCompleteManually (self, shiftDown): """AutoComplete and Calltips manually.""" if self.AutoCompActive(): self.AutoCompCancel() currpos = self.GetCurrentPos() stoppos = self.promptPosEnd cpos = currpos #go back until '.' is found pointavailpos = -1 while cpos >= stoppos: if self.GetCharAt(cpos) == ord ('.'): pointavailpos = cpos break cpos -= 1 #word from non whitespace until '.' if pointavailpos != -1: #look backward for first whitespace char textbehind = self.GetTextRange (pointavailpos + 1, currpos) pointavailpos += 1 if not shiftDown: #call AutoComplete stoppos = self.promptPosEnd textbefore = self.GetTextRange(stoppos, pointavailpos) self.autoCompleteShow(textbefore, len (textbehind)) else: #call CallTips cpos = pointavailpos begpos = -1 while cpos > stoppos: if chr(self.GetCharAt(cpos)).isspace(): begpos = cpos break cpos -= 1 if begpos == -1: begpos = cpos ctips = self.GetTextRange (begpos, currpos) ctindex = ctips.find ('(') if ctindex != -1 and not self.CallTipActive(): #insert calltip, if current pos is '(', otherwise show it only self.autoCallTipShow(ctips[:ctindex + 1], self.GetCharAt(currpos - 1) == ord('(') and \ self.GetCurrentPos() == self.GetTextLength(), True) def writeOut(self, text): """Replacement for stdout.""" self.write(text) def writeErr(self, text): """Replacement for stderr.""" self.write(text) def redirectStdin(self, redirect=True): """If redirect is true then sys.stdin will come from the shell.""" if redirect: sys.stdin = self.reader else: sys.stdin = self.stdin def redirectStdout(self, redirect=True): """If redirect is true then sys.stdout will go to the shell.""" if redirect: sys.stdout = PseudoFileOut(self.writeOut) else: sys.stdout = self.stdout def redirectStderr(self, redirect=True): """If redirect is true then sys.stderr will go to the shell.""" if redirect: sys.stderr = PseudoFileErr(self.writeErr) else: sys.stderr = self.stderr def CanCut(self): """Return true if text is selected and can be cut.""" if self.GetSelectionStart() != self.GetSelectionEnd() \ and self.GetSelectionStart() >= self.promptPosEnd \ and self.GetSelectionEnd() >= self.promptPosEnd: return True else: return False def CanPaste(self): """Return true if a paste should succeed.""" if self.CanEdit() and editwindow.EditWindow.CanPaste(self): return True else: return False def CanEdit(self): """Return true if editing should succeed.""" if self.GetSelectionStart() != self.GetSelectionEnd(): if self.GetSelectionStart() >= self.promptPosEnd \ and self.GetSelectionEnd() >= self.promptPosEnd: return True else: return False else: return self.GetCurrentPos() >= self.promptPosEnd def Cut(self): """Remove selection and place it on the clipboard.""" if self.CanCut() and self.CanCopy(): if self.AutoCompActive(): self.AutoCompCancel() if self.CallTipActive(): self.CallTipCancel() self.Copy() self.ReplaceSelection('') def Copy(self): """Copy selection and place it on the clipboard.""" if self.CanCopy(): ps1 = str(sys.ps1) ps2 = str(sys.ps2) command = self.GetSelectedText() command = command.replace(os.linesep + ps2, os.linesep) command = command.replace(os.linesep + ps1, os.linesep) command = self.lstripPrompt(text=command) data = wx.TextDataObject(command) self._clip(data) def CopyWithPrompts(self): """Copy selection, including prompts, and place it on the clipboard.""" if self.CanCopy(): command = self.GetSelectedText() data = wx.TextDataObject(command) self._clip(data) def CopyWithPromptsPrefixed(self): """Copy selection, including prompts prefixed with four spaces, and place it on the clipboard.""" if self.CanCopy(): command = self.GetSelectedText() spaces = ' ' * 4 command = spaces + command.replace(os.linesep, os.linesep + spaces) data = wx.TextDataObject(command) self._clip(data) def _clip(self, data): if wx.TheClipboard.Open(): wx.TheClipboard.UsePrimarySelection(False) wx.TheClipboard.SetData(data) wx.TheClipboard.Flush() wx.TheClipboard.Close() def Paste(self): """Replace selection with clipboard contents.""" if self.CanPaste() and wx.TheClipboard.Open(): ps2 = str(sys.ps2) if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)): data = wx.TextDataObject() if wx.TheClipboard.GetData(data): self.ReplaceSelection('') command = data.GetText() command = command.rstrip() command = self.fixLineEndings(command) command = self.lstripPrompt(text=command) command = command.replace(os.linesep + ps2, '\n') command = command.replace(os.linesep, '\n') command = command.replace('\n', os.linesep + ps2) self.write(command) wx.TheClipboard.Close() def PasteAndRun(self): """Replace selection with clipboard contents, run commands.""" text = '' if wx.TheClipboard.Open(): if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)): data = wx.TextDataObject() if wx.TheClipboard.GetData(data): text = data.GetText() wx.TheClipboard.Close() if text: self.Execute(text) def Execute(self, text): """Replace selection with text and run commands.""" ps1 = str(sys.ps1) ps2 = str(sys.ps2) endpos = self.GetTextLength() self.SetCurrentPos(endpos) startpos = self.promptPosEnd self.SetSelection(startpos, endpos) self.ReplaceSelection('') text = text.lstrip() text = self.fixLineEndings(text) text = self.lstripPrompt(text) text = text.replace(os.linesep + ps1, '\n') text = text.replace(os.linesep + ps2, '\n') text = text.replace(os.linesep, '\n') lines = text.split('\n') commands = [] command = '' for line in lines: if line.strip() == ps2.strip(): # If we are pasting from something like a # web page that drops the trailing space # from the ps2 prompt of a blank line. line = '' lstrip = line.lstrip() if line.strip() != '' and lstrip == line and \ lstrip[:4] not in ['else','elif'] and \ lstrip[:6] != 'except': # New command. if command: # Add the previous command to the list. commands.append(command) # Start a new command, which may be multiline. command = line else: # Multiline command. Add to the command. command += '\n' command += line commands.append(command) for command in commands: command = command.replace('\n', os.linesep + ps2) self.write(command) self.processLine() def wrap(self, wrap=True): """Sets whether text is word wrapped.""" try: self.SetWrapMode(wrap) except AttributeError: return 'Wrapping is not available in this version.' def zoom(self, points=0): """Set the zoom level. This number of points is added to the size of all fonts. It may be positive to magnify or negative to reduce.""" self.SetZoom(points) def LoadSettings(self, config): self.autoComplete = \ config.ReadBool('Options/AutoComplete', True) self.autoCompleteIncludeMagic = \ config.ReadBool('Options/AutoCompleteIncludeMagic', True) self.autoCompleteIncludeSingle = \ config.ReadBool('Options/AutoCompleteIncludeSingle', True) self.autoCompleteIncludeDouble = \ config.ReadBool('Options/AutoCompleteIncludeDouble', True) self.autoCallTip = config.ReadBool('Options/AutoCallTip', True) self.callTipInsert = config.ReadBool('Options/CallTipInsert', True) self.SetWrapMode(config.ReadBool('View/WrapMode', True)) self.lineNumbers = config.ReadBool('View/ShowLineNumbers', True) self.setDisplayLineNumbers (self.lineNumbers) zoom = config.ReadInt('View/Zoom/Shell', -99) if zoom != -99: self.SetZoom(zoom) def SaveSettings(self, config): config.WriteBool('Options/AutoComplete', self.autoComplete) config.WriteBool('Options/AutoCompleteIncludeMagic', self.autoCompleteIncludeMagic) config.WriteBool('Options/AutoCompleteIncludeSingle', self.autoCompleteIncludeSingle) config.WriteBool('Options/AutoCompleteIncludeDouble', self.autoCompleteIncludeDouble) config.WriteBool('Options/AutoCallTip', self.autoCallTip) config.WriteBool('Options/CallTipInsert', self.callTipInsert) config.WriteBool('View/WrapMode', self.GetWrapMode()) config.WriteBool('View/ShowLineNumbers', self.lineNumbers) config.WriteInt('View/Zoom/Shell', self.GetZoom()) def GetContextMenu(self): """ Create and return a context menu for the shell. This is used instead of the scintilla default menu in order to correctly respect our immutable buffer. """ menu = wx.Menu() menu.Append(wx.ID_UNDO, "Undo") menu.Append(wx.ID_REDO, "Redo") menu.AppendSeparator() menu.Append(wx.ID_CUT, "Cut") menu.Append(wx.ID_COPY, "Copy") menu.Append(frame.ID_COPY_PLUS, "Copy Plus") menu.Append(wx.ID_PASTE, "Paste") menu.Append(frame.ID_PASTE_PLUS, "Paste Plus") menu.Append(wx.ID_CLEAR, "Clear") menu.AppendSeparator() menu.Append(wx.ID_SELECTALL, "Select All") return menu def OnContextMenu(self, evt): menu = self.GetContextMenu() self.PopupMenu(menu) def OnUpdateUI(self, evt): id = evt.Id if id in (wx.ID_CUT, wx.ID_CLEAR): evt.Enable(self.CanCut()) elif id in (wx.ID_COPY, frame.ID_COPY_PLUS): evt.Enable(self.CanCopy()) elif id in (wx.ID_PASTE, frame.ID_PASTE_PLUS): evt.Enable(self.CanPaste()) elif id == wx.ID_UNDO: evt.Enable(self.CanUndo()) elif id == wx.ID_REDO: evt.Enable(self.CanRedo()) ## NOTE: The DnD of file names is disabled until we can figure out how ## best to still allow DnD of text. ## #seb : File drag and drop ## class FileDropTarget(wx.FileDropTarget): ## def __init__(self, obj): ## wx.FileDropTarget.__init__(self) ## self.obj = obj ## def OnDropFiles(self, x, y, filenames): ## if len(filenames) == 1: ## txt = 'r\"%s\"' % filenames[0] ## else: ## txt = '( ' ## for f in filenames: ## txt += 'r\"%s\" , ' % f ## txt += ')' ## self.obj.AppendText(txt) ## pos = self.obj.GetCurrentPos() ## self.obj.SetCurrentPos( pos ) ## self.obj.SetSelection( pos, pos ) ## class TextAndFileDropTarget(wx.DropTarget): ## def __init__(self, shell): ## wx.DropTarget.__init__(self) ## self.shell = shell ## self.compdo = wx.DataObjectComposite() ## self.textdo = wx.TextDataObject() ## self.filedo = wx.FileDataObject() ## self.compdo.Add(self.textdo) ## self.compdo.Add(self.filedo, True) ## self.SetDataObject(self.compdo) ## def OnDrop(self, x, y): ## return True ## def OnData(self, x, y, result): ## self.GetData() ## if self.textdo.GetTextLength() > 1: ## text = self.textdo.GetText() ## # *** Do somethign with the dragged text here... ## self.textdo.SetText('') ## else: ## filenames = str(self.filename.GetFilenames()) ## if len(filenames) == 1: ## txt = 'r\"%s\"' % filenames[0] ## else: ## txt = '( ' ## for f in filenames: ## txt += 'r\"%s\" , ' % f ## txt += ')' ## self.shell.AppendText(txt) ## pos = self.shell.GetCurrentPos() ## self.shell.SetCurrentPos( pos ) ## self.shell.SetSelection( pos, pos ) ## return result