changes by Barry, e.g. font lock & email addresses
[python/dscho.git] / Demo / stdwin / python.py
blob29b0495fd7cc395e065b19177abbcdcdf8433d12
1 #! /usr/local/bin/python
3 # A STDWIN-based front end for the Python interpreter.
5 # This is useful if you want to avoid console I/O and instead
6 # use text windows to issue commands to the interpreter.
8 # It supports multiple interpreter windows, each with its own context.
10 # BUGS AND CAVEATS:
12 # This was written long ago as a demonstration, and slightly hacked to
13 # keep it up-to-date, but never as an industry-strength alternative
14 # interface to Python. It should be rewritten using more classes, and
15 # merged with something like wdb.
17 # Although this supports multiple windows, the whole application
18 # is deaf and dumb when a command is running in one window.
20 # Interrupt is (ab)used to signal EOF on input requests.
22 # On UNIX (using X11), interrupts typed in the window will not be
23 # seen until the next input or output operation. When you are stuck
24 # in an infinite loop, try typing ^C in the shell window where you
25 # started this interpreter. (On the Mac, interrupts work normally.)
28 import sys
29 import stdwin
30 from stdwinevents import *
31 import rand
32 import mainloop
33 import os
36 # Stack of windows waiting for [raw_]input().
37 # Element [0] is the top.
38 # If there are multiple windows waiting for input, only the
39 # one on top of the stack can accept input, because the way
40 # raw_input() is implemented (using recursive mainloop() calls).
42 inputwindows = []
45 # Exception raised when input is available
47 InputAvailable = 'input available for raw_input (not an error)'
50 # Main program -- create the window and call the mainloop
52 def main():
53 # Hack so 'import python' won't load another copy
54 # of this if we were loaded though 'python python.py'.
55 # (Should really look at sys.argv[0]...)
56 if 'inputwindows' in dir(sys.modules['__main__']) and \
57 sys.modules['__main__'].inputwindows is inputwindows:
58 sys.modules['python'] = sys.modules['__main__']
60 win = makewindow()
61 mainloop.mainloop()
64 # Create a new window
66 def makewindow():
67 # stdwin.setdefscrollbars(0, 1) # Not in Python 0.9.1
68 # stdwin.setfont('monaco') # Not on UNIX! and not Python 0.9.1
69 # width, height = stdwin.textwidth('in')*40, stdwin.lineheight()*24
70 # stdwin.setdefwinsize(width, height)
71 win = stdwin.open('Python interpreter ready')
72 win.editor = win.textcreate((0,0), win.getwinsize())
73 win.globals = {} # Dictionary for user's globals
74 win.command = '' # Partially read command
75 win.busy = 0 # Ready to accept a command
76 win.auto = 1 # [CR] executes command
77 win.insertOutput = 1 # Insert output at focus
78 win.insertError = 1 # Insert error output at focus
79 win.setwincursor('ibeam')
80 win.filename = '' # Empty if no file for this window
81 makefilemenu(win)
82 makeeditmenu(win)
83 win.dispatch = pdispatch # Event dispatch function
84 mainloop.register(win)
85 return win
88 # Make a 'File' menu
90 def makefilemenu(win):
91 win.filemenu = mp = win.menucreate('File')
92 mp.callback = []
93 additem(mp, 'New', 'N', do_new)
94 additem(mp, 'Open...', 'O', do_open)
95 additem(mp, '', '', None)
96 additem(mp, 'Close', 'W', do_close)
97 additem(mp, 'Save', 'S', do_save)
98 additem(mp, 'Save as...', '', do_saveas)
99 additem(mp, '', '', None)
100 additem(mp, 'Quit', 'Q', do_quit)
103 # Make an 'Edit' menu
105 def makeeditmenu(win):
106 win.editmenu = mp = win.menucreate('Edit')
107 mp.callback = []
108 additem(mp, 'Cut', 'X', do_cut)
109 additem(mp, 'Copy', 'C', do_copy)
110 additem(mp, 'Paste', 'V', do_paste)
111 additem(mp, 'Clear', '', do_clear)
112 additem(mp, '', '', None)
113 win.iauto = len(mp.callback)
114 additem(mp, 'Autoexecute', '', do_auto)
115 mp.check(win.iauto, win.auto)
116 win.insertOutputNum = len(mp.callback)
117 additem(mp, 'Insert Output', '', do_insertOutputOption)
118 win.insertErrorNum = len(mp.callback)
119 additem(mp, 'Insert Error', '', do_insertErrorOption)
120 additem(mp, 'Exec', '\r', do_exec)
123 # Helper to add a menu item and callback function
125 def additem(mp, text, shortcut, handler):
126 if shortcut:
127 mp.additem(text, shortcut)
128 else:
129 mp.additem(text)
130 mp.callback.append(handler)
133 # Dispatch a single event to the interpreter.
134 # Resize events cause a resize of the editor.
135 # Some events are treated specially.
136 # Most other events are passed directly to the editor.
138 def pdispatch(event):
139 type, win, detail = event
140 if not win:
141 win = stdwin.getactive()
142 if not win: return
143 if type == WE_CLOSE:
144 do_close(win)
145 return
146 elif type == WE_SIZE:
147 win.editor.move((0, 0), win.getwinsize())
148 elif type == WE_COMMAND and detail == WC_RETURN:
149 if win.auto:
150 do_exec(win)
151 else:
152 void = win.editor.event(event)
153 elif type == WE_COMMAND and detail == WC_CANCEL:
154 if win.busy:
155 raise KeyboardInterrupt
156 else:
157 win.command = ''
158 settitle(win)
159 elif type == WE_MENU:
160 mp, item = detail
161 mp.callback[item](win)
162 else:
163 void = win.editor.event(event)
164 if win in mainloop.windows:
165 # May have been deleted by close...
166 win.setdocsize(0, win.editor.getrect()[1][1])
167 if type in (WE_CHAR, WE_COMMAND):
168 win.editor.setfocus(win.editor.getfocus())
171 # Helper to set the title of the window
173 def settitle(win):
174 if win.filename == '':
175 win.settitle('Python interpreter ready')
176 else:
177 win.settitle(win.filename)
180 # Helper to replace the text of the focus
182 def replace(win, text):
183 win.editor.replace(text)
184 # Resize the window to display the text
185 win.setdocsize(0, win.editor.getrect()[1][1]) # update the size before
186 win.editor.setfocus(win.editor.getfocus()) # move focus to the change
189 # File menu handlers
191 def do_new(win):
192 win = makewindow()
194 def do_open(win):
195 try:
196 filename = stdwin.askfile('Open file', '', 0)
197 win = makewindow()
198 win.filename = filename
199 win.editor.replace(open(filename, 'r').read())
200 win.editor.setfocus(0, 0)
201 win.settitle(win.filename)
203 except KeyboardInterrupt:
204 pass # Don't give an error on cancel
206 def do_save(win):
207 try:
208 if win.filename == '':
209 win.filename = stdwin.askfile('Open file', '', 1)
210 f = open(win.filename, 'w')
211 f.write(win.editor.gettext())
213 except KeyboardInterrupt:
214 pass # Don't give an error on cancel
216 def do_saveas(win):
217 currentFilename = win.filename
218 win.filename = ''
219 do_save(win) # Use do_save with empty filename
220 if win.filename == '': # Restore the name if do_save did not set it
221 win.filename = currentFilename
223 def do_close(win):
224 if win.busy:
225 stdwin.message('Can\'t close busy window')
226 return # need to fail if quitting??
227 win.editor = None # Break circular reference
228 #del win.editmenu # What about the filemenu??
229 mainloop.unregister(win)
230 win.close()
232 def do_quit(win):
233 # Call win.dispatch instead of do_close because there
234 # may be 'alien' windows in the list.
235 for win in mainloop.windows[:]:
236 mainloop.dispatch((WE_CLOSE, win, None))
237 # need to catch failed close
240 # Edit menu handlers
242 def do_cut(win):
243 text = win.editor.getfocustext()
244 if not text:
245 stdwin.fleep()
246 return
247 stdwin.setcutbuffer(0, text)
248 replace(win, '')
250 def do_copy(win):
251 text = win.editor.getfocustext()
252 if not text:
253 stdwin.fleep()
254 return
255 stdwin.setcutbuffer(0, text)
257 def do_paste(win):
258 text = stdwin.getcutbuffer(0)
259 if not text:
260 stdwin.fleep()
261 return
262 replace(win, text)
264 def do_clear(win):
265 replace(win, '')
268 # These would be better in a preferences dialog:
270 def do_auto(win):
271 win.auto = (not win.auto)
272 win.editmenu.check(win.iauto, win.auto)
274 def do_insertOutputOption(win):
275 win.insertOutput = (not win.insertOutput)
276 title = ['Append Output', 'Insert Output'][win.insertOutput]
277 win.editmenu.setitem(win.insertOutputNum, title)
279 def do_insertErrorOption(win):
280 win.insertError = (not win.insertError)
281 title = ['Error Dialog', 'Insert Error'][win.insertError]
282 win.editmenu.setitem(win.insertErrorNum, title)
285 # Extract a command from the editor and execute it, or pass input to
286 # an interpreter waiting for it.
287 # Incomplete commands are merely placed in the window's command buffer.
288 # All exceptions occurring during the execution are caught and reported.
289 # (Tracebacks are currently not possible, as the interpreter does not
290 # save the traceback pointer until it reaches its outermost level.)
292 def do_exec(win):
293 if win.busy:
294 if win not in inputwindows:
295 stdwin.message('Can\'t run recursive commands')
296 return
297 if win <> inputwindows[0]:
298 stdwin.message('Please complete recursive input first')
299 return
301 # Set text to the string to execute.
302 a, b = win.editor.getfocus()
303 alltext = win.editor.gettext()
304 n = len(alltext)
305 if a == b:
306 # There is no selected text, just an insert point;
307 # so execute the current line.
308 while 0 < a and alltext[a-1] <> '\n': # Find beginning of line
309 a = a-1
310 while b < n and alltext[b] <> '\n': # Find end of line after b
311 b = b+1
312 text = alltext[a:b] + '\n'
313 else:
314 # Execute exactly the selected text.
315 text = win.editor.getfocustext()
316 if text[-1:] <> '\n': # Make sure text ends with \n
317 text = text + '\n'
318 while b < n and alltext[b] <> '\n': # Find end of line after b
319 b = b+1
321 # Set the focus to expect the output, since there is always something.
322 # Output will be inserted at end of line after current focus,
323 # or appended to the end of the text.
324 b = [n, b][win.insertOutput]
325 win.editor.setfocus(b, b)
327 # Make sure there is a preceeding newline.
328 if alltext[b-1:b] <> '\n':
329 win.editor.replace('\n')
332 if win.busy:
333 # Send it to raw_input() below
334 raise InputAvailable, text
336 # Like the real Python interpreter, we want to execute
337 # single-line commands immediately, but save multi-line
338 # commands until they are terminated by a blank line.
339 # Unlike the real Python interpreter, we don't do any syntax
340 # checking while saving up parts of a multi-line command.
342 # The current heuristic to determine whether a command is
343 # the first line of a multi-line command simply checks whether
344 # the command ends in a colon (followed by a newline).
345 # This is not very robust (comments and continuations will
346 # confuse it), but it is usable, and simple to implement.
347 # (It even has the advantage that single-line loops etc.
348 # don't need te be terminated by a blank line.)
350 if win.command:
351 # Already continuing
352 win.command = win.command + text
353 if win.command[-2:] <> '\n\n':
354 win.settitle('Unfinished command...')
355 return # Need more...
356 else:
357 # New command
358 win.command = text
359 if text[-2:] == ':\n':
360 win.settitle('Unfinished command...')
361 return
362 command = win.command
363 win.command = ''
364 win.settitle('Executing command...')
366 # Some hacks:
367 # - The standard files are replaced by an IOWindow instance.
368 # - A 2nd argument to exec() is used to specify the directory
369 # holding the user's global variables. (If this wasn't done,
370 # the exec would be executed in the current local environment,
371 # and the user's assignments to globals would be lost...)
373 save_stdin = sys.stdin
374 save_stdout = sys.stdout
375 save_stderr = sys.stderr
376 try:
377 sys.stdin = sys.stdout = sys.stderr = IOWindow(win)
378 win.busy = 1
379 try:
380 exec(command, win.globals)
381 except KeyboardInterrupt:
382 print '[Interrupt]'
383 except:
384 if type(sys.exc_type) == type(''):
385 msg = sys.exc_type
386 else: msg = sys.exc_type.__name__
387 if sys.exc_value <> None:
388 msg = msg + ': ' + `sys.exc_value`
389 if win.insertError:
390 stdwin.fleep()
391 replace(win, msg + '\n')
392 else:
393 win.settitle('Unhandled exception')
394 stdwin.message(msg)
395 finally:
396 # Restore redirected I/O in *all* cases
397 win.busy = 0
398 sys.stderr = save_stderr
399 sys.stdout = save_stdout
400 sys.stdin = save_stdin
401 settitle(win)
404 # Class emulating file I/O from/to a window
406 class IOWindow:
408 def __init__(self, win):
409 self.win = win
411 def readline(self, *unused_args):
412 n = len(inputwindows)
413 save_title = self.win.gettitle()
414 title = n*'(' + 'Requesting input...' + ')'*n
415 self.win.settitle(title)
416 inputwindows.insert(0, self.win)
417 try:
418 try:
419 mainloop.mainloop()
420 finally:
421 del inputwindows[0]
422 self.win.settitle(save_title)
423 except InputAvailable, val: # See do_exec above
424 return val
425 except KeyboardInterrupt:
426 raise EOFError # Until we have a "send EOF" key
427 # If we didn't catch InputAvailable, something's wrong...
428 raise EOFError
430 def write(self, text):
431 mainloop.check()
432 replace(self.win, text)
433 mainloop.check()
436 # Currently unused function to test a command's syntax without executing it
438 def testsyntax(s):
439 import string
440 lines = string.splitfields(s, '\n')
441 for i in range(len(lines)): lines[i] = '\t' + lines[i]
442 lines.insert(0, 'if 0:')
443 lines.append('')
444 exec(string.joinfields(lines, '\n'))
447 # Call the main program
449 main()