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