'helv' : 'Lucida Console',
'lucida' : 'Lucida Console',
'other' : 'Comic Sans MS',
- 'size' : 8,
- 'lnsize' : 7,
+ 'size' : 10,
+ 'lnsize' : 9,
'backcol': '#FFFFFF',
}
+ # Versions of wxPython prior to 2.3.2 had a sizing bug on Win platform.
+ # The font was 2 points too large. So we need to reduce the font size.
+ if ((wxMAJOR_VERSION, wxMINOR_VERSION) == (2, 3) and wxRELEASE_NUMBER < 2) \
+ or (wxMAJOR_VERSION <= 2 and wxMINOR_VERSION <= 2):
+ faces['size'] -= 2
+ faces['lnsize'] -= 2
else: # GTK
faces = { 'times' : 'Times',
'mono' : 'Courier',
}
+class ShellFacade:
+ """Simplified interface to all shell-related functionality.
+
+ This is a semi-transparent facade, in that all attributes of other are
+ still accessible, even though only some are visible to the user."""
+
+ name = 'PyCrust Shell Interface'
+ revision = __version__
+
+ def __init__(self, other):
+ """Create a ShellFacade instance."""
+ methods = ['ask',
+ 'clear',
+ 'pause',
+ 'prompt',
+ 'quit',
+ 'redirectStderr',
+ 'redirectStdin',
+ 'redirectStdout',
+ 'run',
+ 'runfile',
+ ]
+ for method in methods:
+ self.__dict__[method] = getattr(other, method)
+ d = self.__dict__
+ d['other'] = other
+ d['help'] = 'There is no help available, yet.'
+
+
+ def __getattr__(self, name):
+ if hasattr(self.other, name):
+ return getattr(self.other, name)
+ else:
+ raise AttributeError, name
+
+ def __setattr__(self, name, value):
+ if self.__dict__.has_key(name):
+ self.__dict__[name] = value
+ elif hasattr(self.other, name):
+ return setattr(self.other, name, value)
+ else:
+ raise AttributeError, name
+
+ def _getAttributeNames(self):
+ """Return list of magic attributes to extend introspection."""
+ list = ['autoCallTip',
+ 'autoComplete',
+ 'autoCompleteCaseInsensitive',
+ 'autoCompleteIncludeDouble',
+ 'autoCompleteIncludeMagic',
+ 'autoCompleteIncludeSingle',
+ ]
+ list.sort()
+ return list
+
+
class Shell(wxStyledTextCtrl):
"""PyCrust Shell based on wxStyledTextCtrl."""
-
+
name = 'PyCrust Shell'
revision = __version__
-
+
def __init__(self, parent, id=-1, pos=wxDefaultPosition, \
size=wxDefaultSize, style=wxCLIP_CHILDREN, introText='', \
locals=None, InterpClass=None, *args, **kwds):
else:
Interpreter = InterpClass
# Create default locals so we have something interesting.
- shellLocals = {'__name__': 'PyShell',
+ shellLocals = {'__name__': 'PyShell',
'__doc__': 'PyShell, The PyCrust Python Shell.',
'__version__': VERSION,
}
*args, **kwds)
# Keep track of the most recent prompt starting and ending positions.
self.promptPos = [0, 0]
+ # Keep track of the most recent non-continuation prompt.
+ self.prompt1Pos = [0, 0]
# Keep track of multi-line commands.
self.more = 0
# Create the command history. Commands are added into the front of
- # the list (ie. at index 0) as they are entered. self.historyPos 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.historyPos == -1 means you're on
- # the current command, not in the history. self.tempCommand is
- # storage space for whatever was on the last line when you first hit
- # "Retrieve-Previous", so that the final "Retrieve-Next" will restore
- # whatever was originally there. self.lastCommandRecalled remembers
- # the index of the last command to be recalled from the history, so
- # you can repeat a group of commands by going up-up-up-enter to find
- # the first one in the group then down-enter-down-enter to recall each
- # subsequent command. Also useful for multiline commands, in lieu of
- # a proper implementation of those.
+ # 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.historyPos = -1
- self.tempCommand = ''
- self.lastCommandRecalled = -1
+ self.historyIndex = -1
# Assign handlers for keyboard events.
EVT_KEY_DOWN(self, self.OnKeyDown)
EVT_CHAR(self, self.OnChar)
def destroy(self):
del self.interp
-
+
def config(self):
"""Configure shell based on user preferences."""
self.SetMarginType(1, wxSTC_MARGIN_NUMBER)
self.SetMarginWidth(1, 40)
-
+
self.SetLexer(wxSTC_LEX_PYTHON)
self.SetKeyWords(0, ' '.join(keyword.kwlist))
self.write(self.interp.introText)
except AttributeError:
pass
-
+
def setBuiltinKeywords(self):
"""Create pseudo keywords as part of builtins.
-
- This is a rather clever hack that sets "close", "exit" and "quit"
+
+ This is a rather clever hack that sets "close", "exit" and "quit"
to a PseudoKeyword object so that we can make them do what we want.
In this case what we want is to call our self.quit() method.
The user can type "close", "exit" or "quit" without the final parens.
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 prompt to make sure they want to quit.
+
+ # 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."""
- self.interp.locals['shell'] = 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):
(`startupText`, `startupScript`))
else:
self.push('')
-
+
def setStyles(self, faces):
"""Configure font size, typeface and color for lexer."""
-
+
# Default style
self.StyleSetSpec(wxSTC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d" % faces)
self.StyleSetSpec(wxSTC_P_COMMENTBLOCK, "fore:#7F7F7F")
self.StyleSetSpec(wxSTC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eolfilled" % faces)
+ def OnChar(self, event):
+ """Keypress event handler.
+
+ Prevents modification of previously submitted commands/responses."""
+ key = event.KeyCode()
+ currpos = self.GetCurrentPos()
+ if currpos < self.prompt1Pos[1]:
+ wxBell()
+ return
+ stoppos = self.promptPos[1]
+ if key == ord('.'):
+ # The dot or period key activates auto completion.
+ # Get the command between the prompt and the cursor.
+ # Add a dot to the end of the command.
+ command = self.GetTextRange(stoppos, currpos) + '.'
+ self.write('.')
+ 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.
+ command = self.GetTextRange(stoppos, currpos) + '('
+ self.write('(')
+ if self.autoCallTip: self.autoCallTipShow(command)
+ else:
+ # Allow the normal event handling to take place.
+ event.Skip()
+
def OnKeyDown(self, event):
"""Key down event handler.
-
- The main goal here is to not allow modifications to previous
- lines of text."""
+
+ Prevents modification of previously submitted commands/responses."""
key = event.KeyCode()
currpos = self.GetCurrentPos()
stoppos = self.promptPos[1]
# If the auto-complete window is up let it do its thing.
if self.AutoCompActive():
event.Skip()
- # Control+UpArrow steps up through the history.
- elif key == WXK_UP and event.ControlDown() \
- and self.historyPos < len(self.history) - 1:
- # Move to the end of the buffer.
- endpos = self.GetTextLength()
- self.SetCurrentPos(endpos)
- # The first Control+Up stores the current command;
- # Control+Down brings it back.
- if self.historyPos == -1:
- self.tempCommand = self.getCommand()
- # Now replace the current line with the next one from the history.
- self.historyPos = self.historyPos + 1
- self.SetSelection(stoppos, endpos)
- self.ReplaceSelection(self.history[self.historyPos])
- # Control+DownArrow steps down through the history.
- elif key == WXK_DOWN and event.ControlDown():
- # Move to the end of the buffer.
- endpos = self.GetTextLength()
- self.SetCurrentPos(endpos)
- # Are we at the bottom end of the history?
- if self.historyPos == -1:
- # Do we have a lastCommandRecalled stored?
- if self.lastCommandRecalled >= 0:
- # Replace the current line with the command after the
- # last-recalled command (you'd think there should be a +1
- # here but there isn't because the history was shuffled up
- # by 1 after the previous command was recalled).
- self.SetSelection(stoppos, endpos)
- self.ReplaceSelection(self.history[self.lastCommandRecalled])
- # We've now warped into middle of the history.
- self.historyPos = self.lastCommandRecalled
- self.lastCommandRecalled = -1
- else:
- # Fetch either the previous line from the history, or the saved
- # command if we're back at the start.
- self.historyPos = self.historyPos - 1
- if self.historyPos == -1:
- newText = self.tempCommand
- else:
- newText = self.history[self.historyPos]
- # Replace the current line with the new text.
- self.SetSelection(stoppos, endpos)
- self.ReplaceSelection(newText)
- # F8 on the last line does a search up the history for the text in
- # front of the cursor.
- elif key == WXK_F8 and self.GetCurrentLine() == self.GetLineCount()-1:
- tempCommand = self.getCommand()
- # The first F8 saves the current command, just like Control+Up.
- if self.historyPos == -1:
- self.tempCommand = tempCommand
- # The text up to the cursor is what we search for.
- searchText = tempCommand
- numCharsAfterCursor = self.GetTextLength() - self.GetCurrentPos()
- if numCharsAfterCursor > 0:
- searchText = searchText[:-numCharsAfterCursor]
- # Search upwards from the current history position and loop back
- # to the beginning if we don't find anything.
- for i in range(self.historyPos+1, len(self.history)) + \
- range(self.historyPos):
- command = self.history[i]
- if command[:len(searchText)] == searchText:
- # Replace the current line with the one we've found.
- endpos = self.GetTextLength()
- self.SetSelection(stoppos, endpos)
- self.ReplaceSelection(command)
- # Put the cursor back at the end of the search text.
- pos = self.GetTextLength() - len(command) + len(searchText)
- self.SetCurrentPos(pos)
- self.SetAnchor(pos)
- # We've now warped into middle of the history.
- self.historyPos = i
- self.lastCommandRecalled = -1
- break
+ # Retrieve the previous command from the history buffer.
+ elif (event.ControlDown() and key == WXK_UP) \
+ or (event.AltDown() and key in (ord('P'), ord('p'))):
+ self.OnHistoryRetrieve(step=+1)
+ # Retrieve the next command from the history buffer.
+ elif (event.ControlDown() and key == WXK_DOWN) \
+ or (event.AltDown() and key in (ord('N'), ord('n'))):
+ self.OnHistoryRetrieve(step=-1)
+ # Search up the history for the text in front of the cursor.
+ elif key == WXK_F8:
+ self.OnHistorySearch()
# Return is used to submit a command to the interpreter.
elif key == WXK_RETURN:
if self.CallTipActive: self.CallTipCancel()
# Home needs to be aware of the prompt.
elif key == WXK_HOME:
if currpos >= stoppos:
- self.SetCurrentPos(stoppos)
- self.SetAnchor(stoppos)
+ if event.ShiftDown():
+ # Select text from current position to end of prompt.
+ self.SetSelection(self.GetCurrentPos(), stoppos)
+ else:
+ self.SetCurrentPos(stoppos)
+ self.SetAnchor(stoppos)
else:
event.Skip()
# Basic navigation keys should work anywhere.
event.Skip()
# Don't backspace over the latest prompt.
elif key == WXK_BACK:
- if currpos > stoppos:
+ if currpos > self.prompt1Pos[1]:
event.Skip()
# Only allow these keys after the latest prompt.
elif key in (WXK_TAB, WXK_DELETE):
- if currpos >= stoppos:
+ if currpos >= self.prompt1Pos[1]:
event.Skip()
# Don't toggle between insert mode and overwrite mode.
elif key == WXK_INSERT:
else:
event.Skip()
- def OnChar(self, event):
- """Keypress event handler.
-
- The main goal here is to not allow modifications to previous
- lines of text."""
- key = event.KeyCode()
- currpos = self.GetCurrentPos()
- stoppos = self.promptPos[1]
- if currpos >= stoppos:
- if key == 46:
- # "." The dot or period key activates auto completion.
- # Get the command between the prompt and the cursor.
- # Add a dot to the end of the command.
- command = self.GetTextRange(stoppos, currpos) + '.'
- self.write('.')
- if self.autoComplete: self.autoCompleteShow(command)
- elif key == 40:
- # "(" 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.
- command = self.GetTextRange(stoppos, currpos) + '('
- self.write('(')
- if self.autoCallTip: self.autoCallTipShow(command)
- else:
- # Allow the normal event handling to take place.
- event.Skip()
+ def OnHistoryRetrieve(self, step):
+ """Retrieve the previous/next command from the history buffer."""
+ startpos = self.GetCurrentPos()
+ if startpos < self.prompt1Pos[1]:
+ wxBell()
+ return
+ newindex = self.historyIndex + step
+ if not (-1 <= newindex < len(self.history)):
+ wxBell()
+ return
+ self.historyIndex = newindex
+ if newindex == -1:
+ self.ReplaceSelection('')
else:
- pass
+ self.ReplaceSelection('')
+ command = self.history[self.historyIndex]
+ command = command.replace('\n', os.linesep + sys.ps2)
+ self.ReplaceSelection(command)
+ endpos = self.GetCurrentPos()
+ self.SetSelection(endpos, startpos)
+
+ def OnHistorySearch(self):
+ """Search up the history buffer for the text in front of the cursor."""
+ startpos = self.GetCurrentPos()
+ if startpos < self.prompt1Pos[1]:
+ wxBell()
+ return
+ # The text up to the cursor is what we search for.
+ numCharsAfterCursor = self.GetTextLength() - startpos
+ searchText = self.getCommand(rstrip=0)
+ 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've 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 most likely be replaced by the enclosing app
# to do something more interesting, like write to a status bar.
print text
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.
-
- # Grab information about the current line.
+
thepos = self.GetCurrentPos()
- theline = self.GetCurrentLine()
- command = self.getCommand()
- # Go to the very bottom of the text.
endpos = self.GetTextLength()
- self.SetCurrentPos(endpos)
- endline = self.GetCurrentLine()
- # If they hit RETURN on the last line, execute the command.
- if theline == endline:
+ # If they hit RETURN at the very bottom, execute the command.
+ if thepos == endpos:
+ self.interp.more = 0
+ if self.getCommand():
+ command = self.GetTextRange(self.prompt1Pos[1], endpos)
+ else:
+ command = self.GetTextRange(self.prompt1Pos[1], \
+ self.promptPos[1])
+ command = command.replace(os.linesep + sys.ps2, '\n')
self.push(command)
- # Otherwise, replace the last line with the new line.
- else:
+ # Otherwise, replace the last command with the new command.
+ elif thepos < self.prompt1Pos[0]:
+ theline = self.GetCurrentLine()
+ command = self.getCommand()
# If the new line contains a command (even an invalid one).
if command:
- startpos = self.PositionFromLine(endline)
+ self.SetCurrentPos(endpos)
+ startpos = self.prompt1Pos[1]
self.SetSelection(startpos, endpos)
self.ReplaceSelection('')
- self.prompt()
self.write(command)
+ self.more = 0
# Otherwise, put the cursor back where we started.
else:
self.SetCurrentPos(thepos)
self.SetAnchor(thepos)
- def getCommand(self, text=None):
+ def getCommand(self, text=None, rstrip=1):
"""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]
ps1size = len(ps1)
ps2 = str(sys.ps2)
ps2size = len(ps2)
- text = text.rstrip()
+ if rstrip:
+ text = text.rstrip()
# Strip the prompt off the front of text leaving just the command.
if text[:ps1size] == ps1:
command = text[ps1size:]
else:
command = ''
return command
-
+
def push(self, command):
"""Send command to the interpreter for execution."""
- self.addHistory(command)
self.write(os.linesep)
self.more = self.interp.push(command)
+ if not self.more:
+ self.addHistory(command.rstrip())
self.prompt()
# Keep the undo feature from undoing previous responses. The only
# thing that can be undone is stuff typed after the prompt, before
def addHistory(self, command):
"""Add command to the command history."""
- # Store the last-recalled command; see the main comment for
- # self.lastCommandRecalled.
- if command != '':
- self.lastCommandRecalled = self.historyPos
# Reset the history position.
- self.historyPos = -1
+ self.historyIndex = -1
# Insert this command into the history, unless it's a blank
# line or the same as the last command.
if command != '' \
def prompt(self):
"""Display appropriate prompt for the context, either ps1 or ps2.
-
+
If this is a continuation line, autoindent as necessary."""
if self.more:
prompt = str(sys.ps2)
pos = self.GetCurLine()[1]
if pos > 0: self.write(os.linesep)
self.promptPos[0] = self.GetCurrentPos()
+ if not self.more: self.prompt1Pos[0] = self.GetCurrentPos()
self.write(prompt)
self.promptPos[1] = self.GetCurrentPos()
+ if not self.more: self.prompt1Pos[1] = self.GetCurrentPos()
# XXX Add some autoindent magic here if more.
if self.more:
- self.write('\t') # Temporary hack indentation.
+ self.write(' '*4) # Temporary hack indentation.
self.EnsureCaretVisible()
self.ScrollToColumn(0)
def ask(self, prompt='Please enter your response:'):
"""Get response from the user."""
return raw_input(prompt=prompt)
-
+
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=1, verbose=1):
"""Execute command within the shell 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.run(command, prompt=0, verbose=1)
finally:
file.close()
-
+
def autoCompleteShow(self, command):
"""Display auto-completion popup list."""
list = self.interp.getAutoCompleteList(command, \
def writeOut(self, text):
"""Replacement for stdout."""
self.write(text)
-
+
def writeErr(self, text):
"""Replacement for stderr."""
self.write(text)
-
+
def redirectStdin(self, redirect=1):
"""If redirect is true then sys.stdin will come from the shell."""
if redirect:
def CanCut(self):
"""Return true if text is selected and can be cut."""
return self.GetSelectionStart() != self.GetSelectionEnd()
-
+
def CanCopy(self):
"""Return true if text is selected and can be copied."""
return self.GetSelectionStart() != self.GetSelectionEnd()
class ShellMenu:
"""Mixin class to add standard menu items."""
-
+
def createMenus(self):
m = self.fileMenu = wxMenu()
m.AppendSeparator()
event.Check(self.shell.autoCompleteIncludeDouble)
elif id == ID_CALLTIPS_SHOW:
event.Check(self.shell.autoCallTip)
-
+
class ShellFrame(wxFrame, ShellMenu):
"""Frame containing the PyCrust shell component."""
-
+
name = 'PyCrust Shell Frame'
revision = __version__
-
+
def __init__(self, parent=None, id=-1, title='PyShell', \
pos=wxDefaultPosition, size=wxDefaultSize, \
style=wxDEFAULT_FRAME_STYLE, locals=None, \
self.createMenus()
+