blob: 8a3dfce1ba171fa38b88773f46e0df6c065d8265 [file] [log] [blame]
#! /usr/bin/env python
# A STDWIN-based front end for the Python interpreter.
#
# This is useful if you want to avoid console I/O and instead
# use text windows to issue commands to the interpreter.
#
# It supports multiple interpreter windows, each with its own context.
#
# BUGS AND CAVEATS:
#
# This was written long ago as a demonstration, and slightly hacked to
# keep it up-to-date, but never as an industry-strength alternative
# interface to Python. It should be rewritten using more classes, and
# merged with something like wdb.
#
# Although this supports multiple windows, the whole application
# is deaf and dumb when a command is running in one window.
#
# Interrupt is (ab)used to signal EOF on input requests.
#
# On UNIX (using X11), interrupts typed in the window will not be
# seen until the next input or output operation. When you are stuck
# in an infinite loop, try typing ^C in the shell window where you
# started this interpreter. (On the Mac, interrupts work normally.)
import sys
import stdwin
from stdwinevents import *
import rand
import mainloop
import os
# Stack of windows waiting for [raw_]input().
# Element [0] is the top.
# If there are multiple windows waiting for input, only the
# one on top of the stack can accept input, because the way
# raw_input() is implemented (using recursive mainloop() calls).
#
inputwindows = []
# Exception raised when input is available
#
InputAvailable = 'input available for raw_input (not an error)'
# Main program -- create the window and call the mainloop
#
def main():
# Hack so 'import python' won't load another copy
# of this if we were loaded though 'python python.py'.
# (Should really look at sys.argv[0]...)
if 'inputwindows' in dir(sys.modules['__main__']) and \
sys.modules['__main__'].inputwindows is inputwindows:
sys.modules['python'] = sys.modules['__main__']
#
win = makewindow()
mainloop.mainloop()
# Create a new window
#
def makewindow():
# stdwin.setdefscrollbars(0, 1) # Not in Python 0.9.1
# stdwin.setfont('monaco') # Not on UNIX! and not Python 0.9.1
# width, height = stdwin.textwidth('in')*40, stdwin.lineheight()*24
# stdwin.setdefwinsize(width, height)
win = stdwin.open('Python interpreter ready')
win.editor = win.textcreate((0,0), win.getwinsize())
win.globals = {} # Dictionary for user's globals
win.command = '' # Partially read command
win.busy = 0 # Ready to accept a command
win.auto = 1 # [CR] executes command
win.insertOutput = 1 # Insert output at focus
win.insertError = 1 # Insert error output at focus
win.setwincursor('ibeam')
win.filename = '' # Empty if no file for this window
makefilemenu(win)
makeeditmenu(win)
win.dispatch = pdispatch # Event dispatch function
mainloop.register(win)
return win
# Make a 'File' menu
#
def makefilemenu(win):
win.filemenu = mp = win.menucreate('File')
mp.callback = []
additem(mp, 'New', 'N', do_new)
additem(mp, 'Open...', 'O', do_open)
additem(mp, '', '', None)
additem(mp, 'Close', 'W', do_close)
additem(mp, 'Save', 'S', do_save)
additem(mp, 'Save as...', '', do_saveas)
additem(mp, '', '', None)
additem(mp, 'Quit', 'Q', do_quit)
# Make an 'Edit' menu
#
def makeeditmenu(win):
win.editmenu = mp = win.menucreate('Edit')
mp.callback = []
additem(mp, 'Cut', 'X', do_cut)
additem(mp, 'Copy', 'C', do_copy)
additem(mp, 'Paste', 'V', do_paste)
additem(mp, 'Clear', '', do_clear)
additem(mp, '', '', None)
win.iauto = len(mp.callback)
additem(mp, 'Autoexecute', '', do_auto)
mp.check(win.iauto, win.auto)
win.insertOutputNum = len(mp.callback)
additem(mp, 'Insert Output', '', do_insertOutputOption)
win.insertErrorNum = len(mp.callback)
additem(mp, 'Insert Error', '', do_insertErrorOption)
additem(mp, 'Exec', '\r', do_exec)
# Helper to add a menu item and callback function
#
def additem(mp, text, shortcut, handler):
if shortcut:
mp.additem(text, shortcut)
else:
mp.additem(text)
mp.callback.append(handler)
# Dispatch a single event to the interpreter.
# Resize events cause a resize of the editor.
# Some events are treated specially.
# Most other events are passed directly to the editor.
#
def pdispatch(event):
type, win, detail = event
if not win:
win = stdwin.getactive()
if not win: return
if type == WE_CLOSE:
do_close(win)
return
elif type == WE_SIZE:
win.editor.move((0, 0), win.getwinsize())
elif type == WE_COMMAND and detail == WC_RETURN:
if win.auto:
do_exec(win)
else:
void = win.editor.event(event)
elif type == WE_COMMAND and detail == WC_CANCEL:
if win.busy:
raise KeyboardInterrupt
else:
win.command = ''
settitle(win)
elif type == WE_MENU:
mp, item = detail
mp.callback[item](win)
else:
void = win.editor.event(event)
if win in mainloop.windows:
# May have been deleted by close...
win.setdocsize(0, win.editor.getrect()[1][1])
if type in (WE_CHAR, WE_COMMAND):
win.editor.setfocus(win.editor.getfocus())
# Helper to set the title of the window
#
def settitle(win):
if win.filename == '':
win.settitle('Python interpreter ready')
else:
win.settitle(win.filename)
# Helper to replace the text of the focus
#
def replace(win, text):
win.editor.replace(text)
# Resize the window to display the text
win.setdocsize(0, win.editor.getrect()[1][1]) # update the size before
win.editor.setfocus(win.editor.getfocus()) # move focus to the change
# File menu handlers
#
def do_new(win):
win = makewindow()
#
def do_open(win):
try:
filename = stdwin.askfile('Open file', '', 0)
win = makewindow()
win.filename = filename
win.editor.replace(open(filename, 'r').read())
win.editor.setfocus(0, 0)
win.settitle(win.filename)
#
except KeyboardInterrupt:
pass # Don't give an error on cancel
#
def do_save(win):
try:
if win.filename == '':
win.filename = stdwin.askfile('Open file', '', 1)
f = open(win.filename, 'w')
f.write(win.editor.gettext())
#
except KeyboardInterrupt:
pass # Don't give an error on cancel
def do_saveas(win):
currentFilename = win.filename
win.filename = ''
do_save(win) # Use do_save with empty filename
if win.filename == '': # Restore the name if do_save did not set it
win.filename = currentFilename
#
def do_close(win):
if win.busy:
stdwin.message('Can\'t close busy window')
return # need to fail if quitting??
win.editor = None # Break circular reference
#del win.editmenu # What about the filemenu??
mainloop.unregister(win)
win.close()
#
def do_quit(win):
# Call win.dispatch instead of do_close because there
# may be 'alien' windows in the list.
for win in mainloop.windows[:]:
mainloop.dispatch((WE_CLOSE, win, None))
# need to catch failed close
# Edit menu handlers
#
def do_cut(win):
text = win.editor.getfocustext()
if not text:
stdwin.fleep()
return
stdwin.setcutbuffer(0, text)
replace(win, '')
#
def do_copy(win):
text = win.editor.getfocustext()
if not text:
stdwin.fleep()
return
stdwin.setcutbuffer(0, text)
#
def do_paste(win):
text = stdwin.getcutbuffer(0)
if not text:
stdwin.fleep()
return
replace(win, text)
#
def do_clear(win):
replace(win, '')
# These would be better in a preferences dialog:
#
def do_auto(win):
win.auto = (not win.auto)
win.editmenu.check(win.iauto, win.auto)
#
def do_insertOutputOption(win):
win.insertOutput = (not win.insertOutput)
title = ['Append Output', 'Insert Output'][win.insertOutput]
win.editmenu.setitem(win.insertOutputNum, title)
#
def do_insertErrorOption(win):
win.insertError = (not win.insertError)
title = ['Error Dialog', 'Insert Error'][win.insertError]
win.editmenu.setitem(win.insertErrorNum, title)
# Extract a command from the editor and execute it, or pass input to
# an interpreter waiting for it.
# Incomplete commands are merely placed in the window's command buffer.
# All exceptions occurring during the execution are caught and reported.
# (Tracebacks are currently not possible, as the interpreter does not
# save the traceback pointer until it reaches its outermost level.)
#
def do_exec(win):
if win.busy:
if win not in inputwindows:
stdwin.message('Can\'t run recursive commands')
return
if win <> inputwindows[0]:
stdwin.message('Please complete recursive input first')
return
#
# Set text to the string to execute.
a, b = win.editor.getfocus()
alltext = win.editor.gettext()
n = len(alltext)
if a == b:
# There is no selected text, just an insert point;
# so execute the current line.
while 0 < a and alltext[a-1] <> '\n': # Find beginning of line
a = a-1
while b < n and alltext[b] <> '\n': # Find end of line after b
b = b+1
text = alltext[a:b] + '\n'
else:
# Execute exactly the selected text.
text = win.editor.getfocustext()
if text[-1:] <> '\n': # Make sure text ends with \n
text = text + '\n'
while b < n and alltext[b] <> '\n': # Find end of line after b
b = b+1
#
# Set the focus to expect the output, since there is always something.
# Output will be inserted at end of line after current focus,
# or appended to the end of the text.
b = [n, b][win.insertOutput]
win.editor.setfocus(b, b)
#
# Make sure there is a preceeding newline.
if alltext[b-1:b] <> '\n':
win.editor.replace('\n')
#
#
if win.busy:
# Send it to raw_input() below
raise InputAvailable, text
#
# Like the real Python interpreter, we want to execute
# single-line commands immediately, but save multi-line
# commands until they are terminated by a blank line.
# Unlike the real Python interpreter, we don't do any syntax
# checking while saving up parts of a multi-line command.
#
# The current heuristic to determine whether a command is
# the first line of a multi-line command simply checks whether
# the command ends in a colon (followed by a newline).
# This is not very robust (comments and continuations will
# confuse it), but it is usable, and simple to implement.
# (It even has the advantage that single-line loops etc.
# don't need te be terminated by a blank line.)
#
if win.command:
# Already continuing
win.command = win.command + text
if win.command[-2:] <> '\n\n':
win.settitle('Unfinished command...')
return # Need more...
else:
# New command
win.command = text
if text[-2:] == ':\n':
win.settitle('Unfinished command...')
return
command = win.command
win.command = ''
win.settitle('Executing command...')
#
# Some hacks:
# - The standard files are replaced by an IOWindow instance.
# - A 2nd argument to exec() is used to specify the directory
# holding the user's global variables. (If this wasn't done,
# the exec would be executed in the current local environment,
# and the user's assignments to globals would be lost...)
#
save_stdin = sys.stdin
save_stdout = sys.stdout
save_stderr = sys.stderr
try:
sys.stdin = sys.stdout = sys.stderr = IOWindow(win)
win.busy = 1
try:
exec(command, win.globals)
except KeyboardInterrupt:
print '[Interrupt]'
except:
if type(sys.exc_type) == type(''):
msg = sys.exc_type
else: msg = sys.exc_type.__name__
if sys.exc_value <> None:
msg = msg + ': ' + `sys.exc_value`
if win.insertError:
stdwin.fleep()
replace(win, msg + '\n')
else:
win.settitle('Unhandled exception')
stdwin.message(msg)
finally:
# Restore redirected I/O in *all* cases
win.busy = 0
sys.stderr = save_stderr
sys.stdout = save_stdout
sys.stdin = save_stdin
settitle(win)
# Class emulating file I/O from/to a window
#
class IOWindow:
#
def __init__(self, win):
self.win = win
#
def readline(self, *unused_args):
n = len(inputwindows)
save_title = self.win.gettitle()
title = n*'(' + 'Requesting input...' + ')'*n
self.win.settitle(title)
inputwindows.insert(0, self.win)
try:
try:
mainloop.mainloop()
finally:
del inputwindows[0]
self.win.settitle(save_title)
except InputAvailable, val: # See do_exec above
return val
except KeyboardInterrupt:
raise EOFError # Until we have a "send EOF" key
# If we didn't catch InputAvailable, something's wrong...
raise EOFError
#
def write(self, text):
mainloop.check()
replace(self.win, text)
mainloop.check()
# Currently unused function to test a command's syntax without executing it
#
def testsyntax(s):
import string
lines = string.splitfields(s, '\n')
for i in range(len(lines)): lines[i] = '\t' + lines[i]
lines.insert(0, 'if 0:')
lines.append('')
exec(string.joinfields(lines, '\n'))
# Call the main program
#
main()