blob: c43fd48c419d7c5d886816d511f1ec5637c7c063 [file] [log] [blame]
Guido van Rossum9cf8f331992-03-30 10:54:51 +00001#! /usr/local/python
2
Guido van Rossum9cf8f331992-03-30 10:54:51 +00003# A STDWIN-based front end for the Python interpreter.
4#
5# This is useful if you want to avoid console I/O and instead
6# use text windows to issue commands to the interpreter.
7#
8# It supports multiple interpreter windows, each with its own context.
9#
10# BUGS AND CAVEATS:
11#
12# Although this supports multiple windows, the whole application
13# is deaf and dumb when a command is running in one window.
14#
15# Everything written to stdout or stderr is saved on a file which
16# is inserted in the window at the next input request.
17#
18# On UNIX (using X11), interrupts typed in the window will not be
19# seen until the next input request. (On the Mac, interrupts work.)
20#
21# Direct input from stdin should not be attempted.
22
23
24import sys
25import builtin
26import stdwin
27from stdwinevents import *
28import rand
29import mainloop
Guido van Rossum4ea570d1992-03-30 11:01:26 +000030import os
Guido van Rossum9cf8f331992-03-30 10:54:51 +000031
32
33# Filename used to capture output from commands; change to suit your taste
34#
35OUTFILE = '@python.stdout.tmp'
36
37
38# Stack of windows waiting for [raw_]input().
39# Element [0] is the top.
40# If there are multiple windows waiting for input, only the
41# one on top of the stack can accept input, because the way
42# raw_input() is implemented (using recursive mainloop() calls).
43#
44inputwindows = []
45
46
47# Exception raised when input is available.
48#
49InputAvailable = 'input available for raw_input (not an error)'
50
51
52# Main program. Create the window and call the mainloop.
53#
54def main():
55 # Hack so 'import python' won't load another copy
56 # of this if we were loaded though 'python python.py'.
57 # (Should really look at sys.argv[0]...)
58 if 'inputwindows' in dir(sys.modules['__main__']) and \
59 sys.modules['__main__'].inputwindows is inputwindows:
60 sys.modules['python'] = sys.modules['__main__']
61 #
62 win = makewindow()
63 mainloop.mainloop()
64
65
66# Create a new window.
67#
68def makewindow():
69 # stdwin.setdefscrollbars(0, 1) # Not in Python 0.9.1
70 # stdwin.setfont('monaco') # Not on UNIX! and not Python 0.9.1
71 # stdwin.setdefwinsize(stdwin.textwidth('in')*40, stdwin.lineheight() * 24)
72 win = stdwin.open('Python interpreter ready')
73 win.editor = win.textcreate((0,0), win.getwinsize())
74 win.outfile = OUTFILE + `rand.rand()`
75 win.globals = {} # Dictionary for user's global variables
76 win.command = '' # Partially read command
77 win.busy = 0 # Ready to accept a command
78 win.auto = 1 # [CR] executes command
79 win.insertOutput = 1 # Insert output at focus.
80 win.insertError = 1 # Insert error output at focus.
81 win.setwincursor('ibeam')
82 win.filename = '' # Empty if no file associated with this window
83 makefilemenu(win)
84 makeeditmenu(win)
85 win.dispatch = pdispatch # Event dispatch function
86 mainloop.register(win)
87 return win
88
89
90# Make a 'File' menu
91#
92def makefilemenu(win):
93 win.filemenu = mp = win.menucreate('File')
94 mp.callback = []
95 additem(mp, 'New', 'N', do_new)
96 additem(mp, 'Open...', 'O', do_open)
97 additem(mp, '', '', None)
98 additem(mp, 'Close', 'W', do_close)
99 additem(mp, 'Save', 'S', do_save)
100 additem(mp, 'Save as...', '', do_saveas)
101 additem(mp, '', '', None)
102 additem(mp, 'Quit', 'Q', do_quit)
103
104
105# Make an 'Edit' menu
106#
107def makeeditmenu(win):
108 win.editmenu = mp = win.menucreate('Edit')
109 mp.callback = []
110 additem(mp, 'Cut', 'X', do_cut)
111 additem(mp, 'Copy', 'C', do_copy)
112 additem(mp, 'Paste', 'V', do_paste)
113 additem(mp, 'Clear', '', do_clear)
114 additem(mp, '', '', None)
115 win.iauto = len(mp.callback)
116 additem(mp, 'Autoexecute', '', do_auto)
117 mp.check(win.iauto, win.auto)
118 win.insertOutputNum = len(mp.callback)
119 additem(mp, 'Insert Output', '', do_insertOutputOption)
120 win.insertErrorNum = len(mp.callback)
121 additem(mp, 'Insert Error', '', do_insertErrorOption)
122 additem(mp, 'Exec', '\r', do_exec)
123
124
125# Helper to add a menu item and callback function
126#
127def additem(mp, text, shortcut, handler):
128 if shortcut:
129 mp.additem(text, shortcut)
130 else:
131 mp.additem(text)
132 mp.callback.append(handler)
133
134
135# Dispatch a single event to the interpreter.
136# Resize events cause a resize of the editor.
137# Other events are directly sent to the editor.
138#
139# Exception: WE_COMMAND/WC_RETURN causes the current selection
140# (if not empty) or current line (if empty) to be sent to the
141# interpreter. (In the future, there should be a way to insert
142# newlines in the text; or perhaps Enter or Meta-RETURN should be
143# used to trigger execution, like in MPW, though personally I prefer
144# using a plain Return to trigger execution, as this is what I want
145# in the majority of cases.)
146#
147# Also, WE_COMMAND/WC_CANCEL cancels any command in progress.
148#
149def pdispatch(event):
150 type, win, detail = event
151 if type == WE_CLOSE:
152 do_close(win)
153 elif type == WE_SIZE:
154 win.editor.move((0, 0), win.getwinsize())
155 elif type == WE_COMMAND and detail == WC_RETURN:
156 if win.auto:
157 do_exec(win)
158 else:
159 void = win.editor.event(event)
160 elif type == WE_COMMAND and detail == WC_CANCEL:
161 if win.busy:
162 raise InputAvailable, (EOFError, None)
163 else:
164 win.command = ''
165 settitle(win)
166 elif type == WE_MENU:
167 mp, item = detail
168 mp.callback[item](win)
169 else:
170 void = win.editor.event(event)
171 if win.editor:
172 # May have been deleted by close...
173 win.setdocsize(0, win.editor.getrect()[1][1])
174 if type in (WE_CHAR, WE_COMMAND):
175 win.editor.setfocus(win.editor.getfocus())
176
177
178# Helper to set the title of the window.
179#
180def settitle(win):
181 if win.filename == '':
182 win.settitle('Python interpreter ready')
183 else:
184 win.settitle(win.filename)
185
186
187# Helper to replace the text of the focus.
188#
189def replace(win, text):
190 win.editor.replace(text)
191 # Resize the window to display the text
192 win.setdocsize(0, win.editor.getrect()[1][1]) # update the size before..
193 win.editor.setfocus(win.editor.getfocus()) # move focus to the change - dml
194
195
196# File menu handlers
197#
198def do_new(win):
199 win = makewindow()
200#
201def do_open(win):
202 try:
203 filename = stdwin.askfile('Open file', '', 0)
204 win = makewindow()
205 win.filename = filename
Guido van Rossum4ea570d1992-03-30 11:01:26 +0000206 win.editor.replace(open(filename, 'r').read())
Guido van Rossum9cf8f331992-03-30 10:54:51 +0000207 win.editor.setfocus(0, 0)
208 win.settitle(win.filename)
209 #
210 except KeyboardInterrupt:
211 pass # Don't give an error on cancel.
212#
213def do_save(win):
214 try:
215 if win.filename == '':
216 win.filename = stdwin.askfile('Open file', '', 1)
217 f = open(win.filename, 'w')
218 f.write(win.editor.gettext())
219 #
220 except KeyboardInterrupt:
221 pass # Don't give an error on cancel.
222
223def do_saveas(win):
224 currentFilename = win.filename
225 win.filename = ''
226 do_save(win) # Use do_save with empty filename
227 if win.filename == '': # Restore the name if do_save did not set it.
228 win.filename = currentFilename
229#
230def do_close(win):
231 if win.busy:
232 stdwin.message('Can\'t close busy window')
233 return # need to fail if quitting??
234 win.editor = None # Break circular reference
235 #del win.editmenu # What about the filemenu??
236 try:
237 os.unlink(win.outfile)
238 except os.error:
239 pass
240 mainloop.unregister(win)
241#
242def do_quit(win):
243 # Call win.dispatch instead of do_close because there
244 # may be 'alien' windows in the list.
245 for win in mainloop.windows:
246 mainloop.dispatch(WE_CLOSE, win, None) # need to catch failed close
247
248
249# Edit menu handlers
250#
251def do_cut(win):
252 text = win.editor.getfocustext()
253 if not text:
254 stdwin.fleep()
255 return
256 stdwin.setcutbuffer(0, text)
257 replace(win, '')
258#
259def do_copy(win):
260 text = win.editor.getfocustext()
261 if not text:
262 stdwin.fleep()
263 return
264 stdwin.setcutbuffer(0, text)
265#
266def do_paste(win):
267 text = stdwin.getcutbuffer(0)
268 if not text:
269 stdwin.fleep()
270 return
271 replace(win, text)
272#
273def do_clear(win):
274 replace(win, '')
275
276#
277# These would be better in a preferences dialog:
278def do_auto(win):
279 win.auto = (not win.auto)
280 win.editmenu.check(win.iauto, win.auto)
281#
282def do_insertOutputOption(win):
283 win.insertOutput = (not win.insertOutput)
284 title = ['Append Output', 'Insert Output'][win.insertOutput]
285 win.editmenu.setitem(win.insertOutputNum, title)
286#
287def do_insertErrorOption(win):
288 win.insertError = (not win.insertError)
289 title = ['Error Dialog', 'Insert Error'][win.insertError]
290 win.editmenu.setitem(win.insertErrorNum, title)
291
292
293# Extract a command from the editor and execute it, or pass input to
294# an interpreter waiting for it.
295# Incomplete commands are merely placed in the window's command buffer.
296# All exceptions occurring during the execution are caught and reported.
297# (Tracebacks are currently not possible, as the interpreter does not
298# save the traceback pointer until it reaches its outermost level.)
299#
300def do_exec(win):
301 if win.busy:
302 if win not in inputwindows:
303 stdwin.message('Can\'t run recursive commands')
304 return
305 if win <> inputwindows[0]:
306 stdwin.message( \
307 'Please complete recursive input first')
308 return
309 #
310 # Set text to the string to execute.
311 a, b = win.editor.getfocus()
312 alltext = win.editor.gettext()
313 n = len(alltext)
314 if a == b:
315 # There is no selected text, just an insert point;
316 # so execute the current line.
317 while 0 < a and alltext[a-1] <> '\n': a = a-1 # Find beginning of line.
318 while b < n and alltext[b] <> '\n': # Find end of line after b.
319 b = b+1
320 text = alltext[a:b] + '\n'
321 else:
322 # Execute exactly the selected text.
323 text = win.editor.getfocustext()
324 if text[-1:] <> '\n': # Make sure text ends with newline.
325 text = text + '\n'
326 while b < n and alltext[b] <> '\n': # Find end of line after b.
327 b = b+1
328 #
329 # Set the focus to expect the output, since there is always something.
330 # Output will be inserted at end of line after current focus,
331 # or appended to the end of the text.
332 b = [n, b][win.insertOutput]
333 win.editor.setfocus(b, b)
334 #
335 # Make sure there is a preceeding newline.
336 if alltext[b-1:b] <> '\n':
337 win.editor.replace('\n')
338 #
339 #
340 if win.busy:
341 # Send it to raw_input() below
342 raise InputAvailable, (None, text)
343 #
344 # Like the real Python interpreter, we want to execute
345 # single-line commands immediately, but save multi-line
346 # commands until they are terminated by a blank line.
347 # Unlike the real Python interpreter, we don't do any syntax
348 # checking while saving up parts of a multi-line command.
349 #
350 # The current heuristic to determine whether a command is
351 # the first line of a multi-line command simply checks whether
352 # the command ends in a colon (followed by a newline).
353 # This is not very robust (comments and continuations will
354 # confuse it), but it is usable, and simple to implement.
355 # (It even has the advantage that single-line loops etc.
356 # don't need te be terminated by a blank line.)
357 #
358 if win.command:
359 # Already continuing
360 win.command = win.command + text
361 if win.command[-2:] <> '\n\n':
362 win.settitle('Unfinished command...')
363 return # Need more...
364 else:
365 # New command
366 win.command = text
367 if text[-2:] == ':\n':
368 win.settitle('Unfinished command...')
369 return
370 command = win.command
371 win.command = ''
372 win.settitle('Executing command...')
373 #
374 # Some hacks: sys.stdout is temporarily redirected to a file,
375 # so we can intercept the command's output and insert it
376 # in the editor window; the built-in function raw_input
377 # and input() are replaced by out versions;
378 # and a second, undocumented argument
379 # to exec() is used to specify the directory holding the
380 # user's global variables. (If this wasn't done, the
381 # exec would be executed in the current local environment,
382 # and the user's assignments to globals would be lost...)
383 #
384 save_input = builtin.input
385 save_raw_input = builtin.raw_input
386 save_stdout = sys.stdout
387 save_stderr = sys.stderr
388 iwin = Input().init(win)
389 try:
390 builtin.input = iwin.input
391 builtin.raw_input = iwin.raw_input
392 sys.stdout = sys.stderr = open(win.outfile, 'w')
393 win.busy = 1
394 try:
395 exec(command, win.globals)
396 except KeyboardInterrupt:
397 pass # Don't give an error.
398 except:
399 msg = sys.exc_type
400 if sys.exc_value <> None:
401 msg = msg + ': ' + `sys.exc_value`
402 if win.insertError:
403 stdwin.fleep()
404 replace(win, msg + '\n')
405 else:
406 win.settitle('Unhandled exception')
407 stdwin.message(msg)
408 finally:
409 # Restore redirected I/O in *all* cases
410 win.busy = 0
411 sys.stderr = save_stderr
412 sys.stdout = save_stdout
413 builtin.raw_input = save_raw_input
414 builtin.input = save_input
415 settitle(win)
416 getoutput(win)
417
418
419# Read any output the command may have produced back from the file
420# and show it. Optionally insert it after the focus, like MPW does,
421# or always append at the end.
422#
423def getoutput(win):
424 filename = win.outfile
425 try:
426 fp = open(filename, 'r')
427 except:
428 stdwin.message('Can\'t read output from ' + filename)
429 return
430 #out = fp.read() # Not in Python 0.9.1
431 out = fp.read(10000) # For Python 0.9.1
432 del fp # Close it
433 if out or win.insertOutput:
434 replace(win, out)
435
436
437# Implementation of input() and raw_input().
438# This uses a class only because we must support calls
439# with and without arguments; this can't be done normally in Python,
440# but the extra, implicit argument for instance methods does the trick.
441#
442class Input:
443 #
444 def init(self, win):
445 self.win = win
446 return self
447 #
448 def input(args):
449 # Hack around call with or without argument:
450 if type(args) == type(()):
451 self, prompt = args
452 else:
453 self, prompt = args, ''
454 #
455 return eval(self.raw_input(prompt), self.win.globals)
456 #
457 def raw_input(args):
458 # Hack around call with or without argument:
459 if type(args) == type(()):
460 self, prompt = args
461 else:
462 self, prompt = args, ''
463 #
464 print prompt # Need to terminate with newline.
465 sys.stdout.close()
466 sys.stdout = sys.stderr = None
467 getoutput(self.win)
468 sys.stdout = sys.stderr = open(self.win.outfile, 'w')
469 save_title = self.win.gettitle()
470 n = len(inputwindows)
471 title = n*'(' + 'Requesting input...' + ')'*n
472 self.win.settitle(title)
473 inputwindows.insert(0, self.win)
474 try:
Guido van Rossumcb4b2951992-05-15 15:40:30 +0000475 try:
476 mainloop.mainloop()
477 except InputAvailable, (exc, val): # See do_exec above.
478 if exc:
479 raise exc, val
480 if val[-1:] == '\n':
481 val = val[:-1]
482 return val
Guido van Rossum9cf8f331992-03-30 10:54:51 +0000483 finally:
484 del inputwindows[0]
485 self.win.settitle(save_title)
486 # If we don't catch InputAvailable, something's wrong...
487 raise EOFError
488 #
489
490
491# Currently unused function to test a command's syntax without executing it
492#
493def testsyntax(s):
494 import string
495 lines = string.splitfields(s, '\n')
496 for i in range(len(lines)): lines[i] = '\t' + lines[i]
497 lines.insert(0, 'if 0:')
498 lines.append('')
499 exec(string.joinfields(lines, '\n'))
500
501
502# Call the main program.
503#
504main()