summaryrefslogtreecommitdiffstats
path: root/Demo/stdwin/python.py
blob: bb853169a58cf60bcd35429fb257c39b21e6254a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
#! /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()