#---------------------------------------------------------------------- # Name: sliceshell.py # Author: David N. Mashburn, Patrick K. O'Brien # Tags: phoenix-port #---------------------------------------------------------------------- """Slices 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. Slices is a version of shell modified by David Mashburn.""" __author__ = "David N. Mashburn / " __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 editor from . import editwindow from . import document from . import frame from .pseudo import PseudoFileIn from .pseudo import PseudoFileOut from .pseudo import PseudoFileErr from .version import VERSION from .magic import magic from .parse import testForContinuations 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_HOME, wx.WXK_END, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN) GROUPING_SELECTING=0 IO_SELECTING = 1 GROUPING_START = 2 GROUPING_START_FOLDED = 3 GROUPING_MIDDLE = 4 GROUPING_END = 5 INPUT_START = 6 INPUT_START_FOLDED = 7 INPUT_MIDDLE = 8 INPUT_END = 9 OUTPUT_START = 10 OUTPUT_START_FOLDED = 11 OUTPUT_MIDDLE = 12 OUTPUT_END = 13 OUTPUT_BG = 14 READLINE_BG = 15 INPUT_READLINE = 16 # Could add C integration right into the markers... # Non-editable file marker for auto-loaded files... # Weave VariableInput = 15 # Weave C code = 16 # C code = 17 (only for use with Pyrex) # Pyrex / Cython code = 18 GROUPING_MASK = ( 1<Settings->Shell Mode" (or try PyCrust). In Shell Mode, two returns in a row executes the command, and Ctrl-Return and Shift-Return always print newlines. Saving and opening "sessions" is now supported! This is a little different to other shells where the history is saved. With PySlices, the whole document is saved in a simple text format! To disable this Tutorial on startup, uncheck it in the menu at: "Options->Startup->Show PySlices tutorial" PySlices may not be the best thing since sliced bread, but I hope it makes using Python a little bit sweeter! """ class SlicesShellFrame(frame.Frame, frame.ShellFrameMixin): """Frame containing the sliceshell component.""" name = 'SlicesShell Frame' def __init__(self, parent=None, id=-1, title='PySlicesShell', pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, locals=None, InterpClass=None, config=None, dataDir=None, filename=None, *args, **kwds): """Create SlicesShellFrame instance.""" frame.Frame.__init__(self, parent, id, title, pos, size, style,shellName='PySlices') frame.ShellFrameMixin.__init__(self, config, dataDir) if size == wx.DefaultSize: self.SetSize((750, 525)) intro = 'PySlices %s - The Flakiest Python Shell... Cut Up!' % VERSION self.SetStatusText(intro.replace('\n', ', ')) self.sliceshell = SlicesShell(parent=self, id=-1, introText=intro, locals=locals, InterpClass=InterpClass, startupScript=self.startupScript, execStartupScript=self.execStartupScript, showPySlicesTutorial=self.showPySlicesTutorial, enableShellMode=self.enableShellMode, hideFoldingMargin=self.hideFoldingMargin, *args, **kwds) self.buffer = self.sliceshell.buffer # Override the shell so that status messages go to the status bar. self.sliceshell.setStatusText = self.SetStatusText self.sliceshell.SetFocus() self.LoadSettings() self.currentDirectory = os.path.expanduser('~') if filename!=None: self.bufferOpen(filename) self.Bind(wx.EVT_IDLE, self.OnIdle) def OnClose(self, event): """Event handler for closing.""" self.bufferClose() # This isn't working the way I want, but I'll leave it for now. #if self.sliceshell.waiting: # if event.CanVeto(): # event.Veto(True) #else: # # TODO: Add check for saving # self.SaveSettings() # self.sliceshell.destroy() # self.Destroy() def OnAbout(self, event): """Display an About window.""" title = 'About PySliceShell' text = 'PySliceShell %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.sliceshell.LoadSettings(self.config) def SaveSettings(self, force=False): if self.config is not None: frame.ShellFrameMixin.SaveSettings(self,force) if self.autoSaveSettings or force: frame.Frame.SaveSettings(self, self.config) self.sliceshell.SaveSettings(self.config) def DoSaveSettings(self): if self.config is not None: self.SaveSettings(force=True) self.config.Flush() def OnEnableShellMode(self,event): """Change between Slices Mode and Shell Mode""" frame.Frame.OnEnableShellMode(self,event) self.sliceshell.ToggleShellMode(self.enableShellMode) def OnHideFoldingMargin(self,event): """Change between Slices Mode and Shell Mode""" frame.Frame.OnHideFoldingMargin(self,event) self.sliceshell.ToggleFoldingMargin(self.hideFoldingMargin) # Copied Straight from crustslices.py (update both with any changes...) # Stolen Straight from editor.EditorFrame # Modified a little... :) # || # \/ def OnIdle(self, event): """Event handler for idle time.""" self._updateTitle() event.Skip() def _updateTitle(self): """Show current title information.""" title = self.GetTitle() if self.bufferHasChanged(): if title.startswith('* '): pass else: self.SetTitle('* ' + title) else: if title.startswith('* '): self.SetTitle(title[2:]) def hasBuffer(self): """Return True if there is a current buffer.""" if self.buffer: return True else: return False def bufferClose(self): """Close buffer.""" if self.buffer.hasChanged(): cancel = self.bufferSuggestSave() if cancel: #event.Veto() return cancel self.SaveSettings() self.sliceshell.destroy() self.bufferDestroy() self.Destroy() return False def bufferCreate(self, filename=None): """Create new buffer.""" self.bufferDestroy() buffer = Buffer() self.panel = panel = wx.Panel(parent=self, id=-1) panel.Bind (wx.EVT_ERASE_BACKGROUND, lambda x: x) editor = Editor(parent=panel) panel.editor = editor sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(editor.window, 1, wx.EXPAND) panel.SetSizer(sizer) panel.SetAutoLayout(True) sizer.Layout() buffer.addEditor(editor) buffer.open(filename) self.setEditor(editor) self.editor.setFocus() self.SendSizeEvent() def bufferDestroy(self): """Destroy the current buffer.""" if self.buffer: self.editor = None self.buffer = None def bufferHasChanged(self): """Return True if buffer has changed since last save.""" if self.buffer: return self.buffer.hasChanged() else: return False def bufferNew(self): """Create new buffer.""" cancel = self.bufferSuggestSave() if cancel: return cancel #self.bufferCreate() self.clear() self.SetTitle( 'PySlices') self.sliceshell.NeedsCheckForSave=False self.sliceshell.SetSavePoint() self.buffer.doc = document.Document() self.buffer.name = 'This shell' self.buffer.modulename = self.buffer.doc.filebase cancel = False return cancel def bufferOpen(self,file=None): """Open file in buffer.""" if self.bufferHasChanged(): cancel = self.bufferSuggestSave() if cancel: return cancel if file==None: file=wx.FileSelector('Open a PySlices File', wildcard='*.pyslices', default_path=self.currentDirectory) if file!=None and file!=u'': fid=open(file,'r') self.sliceshell.LoadPySlicesFile(fid) fid.close() self.currentDirectory = os.path.split(file)[0] self.SetTitle( os.path.split(file)[1] + ' - PySlices') self.sliceshell.NeedsCheckForSave=False self.sliceshell.SetSavePoint() self.buffer.doc = document.Document(file) self.buffer.name = self.buffer.doc.filename self.buffer.modulename = self.buffer.doc.filebase self.sliceshell.ScrollToLine(0) return ## def bufferPrint(self): ## """Print buffer.""" ## pass ## def bufferRevert(self): ## """Revert buffer to version of file on disk.""" ## pass # was self.buffer.save(self): # """Save buffer.""" def simpleSave(self,confirmed=False): filepath = self.buffer.doc.filepath self.buffer.confirmed = confirmed if not filepath: return # XXX Get filename if not os.path.exists(filepath): self.buffer.confirmed = True if not self.buffer.confirmed: self.buffer.confirmed = self.buffer.overwriteConfirm(filepath) if self.buffer.confirmed: try: fid = open(filepath, 'wb') self.sliceshell.SavePySlicesFile(fid) finally: if fid: fid.close() self.sliceshell.SetSavePoint() self.SetTitle( os.path.split(filepath)[1] + ' - PySlices') self.sliceshell.NeedsCheckForSave=False def bufferSave(self): """Save buffer to its file.""" if self.buffer.doc.filepath: # self.buffer.save() self.simpleSave(confirmed=True) cancel = False else: cancel = self.bufferSaveAs() return cancel def bufferSaveAs(self): """Save buffer to a new filename.""" if self.bufferHasChanged() and self.buffer.doc.filepath: cancel = self.bufferSuggestSave() if cancel: return cancel filedir = '' if self.buffer and self.buffer.doc.filedir: filedir = self.buffer.doc.filedir result = editor.saveSingle(title='Save PySlices File',directory=filedir, wildcard='PySlices Files (*.pyslices)|*.pyslices') if result.path not in ['',None]: if result.path[-9:]!=".pyslices": result.path+=".pyslices" self.buffer.doc = document.Document(result.path) self.buffer.name = self.buffer.doc.filename self.buffer.modulename = self.buffer.doc.filebase self.simpleSave(confirmed=True) # allow overwrite cancel = False else: cancel = True return cancel def bufferSaveACopy(self): """Save buffer to a new filename.""" filedir = '' if self.buffer and self.buffer.doc.filedir: filedir = self.buffer.doc.filedir result = editor.saveSingle(title='Save a Copy of PySlices File',directory=filedir, wildcard='PySlices Files (*.pyslices)|*.pyslices') if result.path not in ['',None]: if result.path[-9:]!=".pyslices": result.path+=".pyslices" # if not os.path.exists(result.path): try: # Allow overwrite... fid = open(result.path, 'wb') self.sliceshell.SavePySlicesFile(fid) finally: if fid: fid.close() cancel = False else: cancel = True return cancel def bufferSuggestSave(self): """Suggest saving changes. Return True if user selected Cancel.""" result = editor.messageDialog(parent=None, message='%s has changed.\n' 'Would you like to save it first' '?' % self.buffer.name, title='Save current file?', style=wx.YES_NO | wx.CANCEL | wx.NO_DEFAULT | wx.CENTRE | wx.ICON_QUESTION ) if result.positive: cancel = self.bufferSave() else: cancel = result.text == 'Cancel' return cancel def updateNamespace(self): """Update the buffer namespace for autocompletion and calltips.""" if self.buffer.updateNamespace(): self.SetStatusText('Namespace updated') else: self.SetStatusText('Error executing, unable to update namespace') # TODO : Update the help text HELP_TEXT = """\ * Key bindings: Home Go to the beginning of the line. End Go to the end of the line. Shift+Home Select to the beginning of the line. Shift+End Select to the end of the line. Ctrl-Home Jump to the beginning of the slice; If already there, jump to beginning of previous slice Ctrl-End Jump to the end of the slice; If already there, jump to end of next slice Ctrl-PageUp Jump to the beginning of the shell Ctrl-PageDown Jump to the end of the shell 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+] Increase font size. Ctrl+[ Decrease font size. Ctrl+= Default font size. Ctrl-Space Show Auto Completion. Ctrl-Shift-Space Show Call Tip. Ctrl-Shift-H Complete Text from History. Ctrl+F Search Ctrl+G Search next F12 on/off "free-edit" mode For testing only -- This does not preserve markers! In "Slices Mode": Return Insert new line Enter (Numpad) Run command in slice Ctrl+Return "" Shift+Return "" In "Shell Mode": Return or Enter Insert a new line Ctrl+Return "" Shift+Return "" 2 Returns in a row Run command in slice """ class SlicesShellFacade: """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 = 'SlicesShell Interface' def __init__(self, other): """Create a SlicesShellFacade instance.""" d = self.__dict__ d['other'] = other d['helpText'] = HELP_TEXT def help(self): """Display some useful information about how to use the slices shell.""" self.write(self.helpText,type='Output') 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 DISPLAY_TEXT=""" Author: %r Py Version: %s Python Version: %s wxPython Version: %s wxPython PlatformInfo: %s Platform: %s""" class SlicesShell(editwindow.EditWindow): """Notebook Shell based on StyledTextCtrl.""" name = 'SlicesShell' 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, showPySlicesTutorial=True,enableShellMode=False, hideFoldingMargin=False, *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() self.id = self.GetId() self.buffer.addEditor(self) self.buffer.name='This shell' self.NeedsCheckForSave=False # Find out for which keycodes the interpreter will autocomplete. self.autoCompleteKeys = self.interp.getAutoCompleteKeys() # Keep track of the last non-continuation prompt positions. # Removed all references to these... solved a lot of odd bugs... # self.promptPosStart = 0 # self.promptPosEnd = 0 # Keep track of multi-line commands. self.more = False # Use Margins to track input / output / slice number self.margins = True # For use with forced updates during long-running scripts self.lastUpdate=None if self.margins: # margin 1 is already defined for the line numbers # may eventually change it back to 0 like it ought to be... self.SetMarginType(2, stc.STC_MARGIN_SYMBOL) self.SetMarginType(3, stc.STC_MARGIN_SYMBOL) self.SetMarginType(4, stc.STC_MARGIN_SYMBOL) self.SetMarginWidth(2, 22) self.SetMarginWidth(3, 22) self.SetMarginWidth(4, 12) self.SetMarginSensitive(2,True) self.SetMarginSensitive(3,True) self.SetMarginSensitive(4,True) self.SetProperty("fold", "1") # tabs are bad, use spaces self.SetProperty("tab.timmy.whinge.level", "4") self.SetMargins(0,0) self.SetMarginMask(2, GROUPING_MASK | 1<