#! /usr/local/python XXX This file needs some work for Python 0.9.6!!! # 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: # # Although this supports multiple windows, the whole application # is deaf and dumb when a command is running in one window. # # Everything written to stdout or stderr is saved on a file which # is inserted in the window at the next input request. # # On UNIX (using X11), interrupts typed in the window will not be # seen until the next input request. (On the Mac, interrupts work.) # # Direct input from stdin should not be attempted. import sys import builtin import stdwin from stdwinevents import * import rand import mainloop from util import readfile # 0.9.1 try: import mac os = mac except NameError: import posix os = posix # Filename used to capture output from commands; change to suit your taste # OUTFILE = '@python.stdout.tmp' # 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 # stdwin.setdefwinsize(stdwin.textwidth('in')*40, stdwin.lineheight() * 24) win = stdwin.open('Python interpreter ready') win.editor = win.textcreate((0,0), win.getwinsize()) win.outfile = OUTFILE + `rand.rand()` win.globals = {} # Dictionary for user's global variables 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 associated with 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. # Other events are directly sent to the editor. # # Exception: WE_COMMAND/WC_RETURN causes the current selection # (if not empty) or current line (if empty) to be sent to the # interpreter. (In the future, there should be a way to insert # newlines in the text; or perhaps Enter or Meta-RETURN should be # used to trigger execution, like in MPW, though personally I prefer # using a plain Return to trigger execution, as this is what I want # in the majority of cases.) # # Also, WE_COMMAND/WC_CANCEL cancels any command in progress. # def pdispatch(event): type, win, detail = event if type == WE_CLOSE: do_close(win) 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 InputAvailable, (EOFError, None) else: win.command = '' settitle(win) elif type == WE_MENU: mp, item = detail mp.callback[item](win) else: void = win.editor.event(event) if win.editor: # 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 - dml # 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(readfile(filename)) # 0.9.1 # win.editor.replace(open(filename, 'r').read()) # 0.9.2 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?? try: os.unlink(win.outfile) except os.error: pass mainloop.unregister(win) # 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': a = a-1 # Find beginning of line. 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 newline. 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, (None, 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: sys.stdout is temporarily redirected to a file, # so we can intercept the command's output and insert it # in the editor window; the built-in function raw_input # and input() are replaced by out versions; # and a second, undocumented 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_input = builtin.input save_raw_input = builtin.raw_input save_stdout = sys.stdout save_stderr = sys.stderr iwin = Input().init(win) try: builtin.input = iwin.input builtin.raw_input = iwin.raw_input sys.stdout = sys.stderr = open(win.outfile, 'w') win.busy = 1 try: exec(command, win.globals) except KeyboardInterrupt: pass # Don't give an error. except: msg = sys.exc_type 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 builtin.raw_input = save_raw_input builtin.input = save_input settitle(win) getoutput(win) # Read any output the command may have produced back from the file # and show it. Optionally insert it after the focus, like MPW does, # or always append at the end. # def getoutput(win): filename = win.outfile try: fp = open(filename, 'r') except: stdwin.message('Can\'t read output from ' + filename) return #out = fp.read() # Not in Python 0.9.1 out = fp.read(10000) # For Python 0.9.1 del fp # Close it if out or win.insertOutput: replace(win, out) # Implementation of input() and raw_input(). # This uses a class only because we must support calls # with and without arguments; this can't be done normally in Python, # but the extra, implicit argument for instance methods does the trick. # class Input: # def init(self, win): self.win = win return self # def input(args): # Hack around call with or without argument: if type(args) == type(()): self, prompt = args else: self, prompt = args, '' # return eval(self.raw_input(prompt), self.win.globals) # def raw_input(args): # Hack around call with or without argument: if type(args) == type(()): self, prompt = args else: self, prompt = args, '' # print prompt # Need to terminate with newline. sys.stdout.close() sys.stdout = sys.stderr = None getoutput(self.win) sys.stdout = sys.stderr = open(self.win.outfile, 'w') save_title = self.win.gettitle() n = len(inputwindows) title = n*'(' + 'Requesting input...' + ')'*n self.win.settitle(title) inputwindows.insert(0, self.win) try: mainloop.mainloop() except InputAvailable, (exc, val): # See do_exec above. if exc: raise exc, val if val[-1:] == '\n': val = val[:-1] return val finally: del inputwindows[0] self.win.settitle(save_title) # If we don't catch InputAvailable, something's wrong... raise EOFError # # 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()