Guido van Rossum | 9cf8f33 | 1992-03-30 10:54:51 +0000 | [diff] [blame^] | 1 | #! /usr/local/python |
| 2 | |
| 3 | XXX 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 | |
| 26 | import sys |
| 27 | import builtin |
| 28 | import stdwin |
| 29 | from stdwinevents import * |
| 30 | import rand |
| 31 | import mainloop |
| 32 | |
| 33 | from util import readfile # 0.9.1 |
| 34 | |
| 35 | try: |
| 36 | import mac |
| 37 | os = mac |
| 38 | except NameError: |
| 39 | import posix |
| 40 | os = posix |
| 41 | |
| 42 | |
| 43 | # Filename used to capture output from commands; change to suit your taste |
| 44 | # |
| 45 | OUTFILE = '@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 | # |
| 54 | inputwindows = [] |
| 55 | |
| 56 | |
| 57 | # Exception raised when input is available. |
| 58 | # |
| 59 | InputAvailable = 'input available for raw_input (not an error)' |
| 60 | |
| 61 | |
| 62 | # Main program. Create the window and call the mainloop. |
| 63 | # |
| 64 | def 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 | # |
| 78 | def 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 | # |
| 102 | def 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 | # |
| 117 | def 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 | # |
| 137 | def 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 | # |
| 159 | def 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 | # |
| 190 | def 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 | # |
| 199 | def 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 | # |
| 208 | def do_new(win): |
| 209 | win = makewindow() |
| 210 | # |
| 211 | def 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 | # |
| 224 | def 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 | |
| 234 | def 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 | # |
| 241 | def 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 | # |
| 253 | def 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 | # |
| 262 | def 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 | # |
| 270 | def do_copy(win): |
| 271 | text = win.editor.getfocustext() |
| 272 | if not text: |
| 273 | stdwin.fleep() |
| 274 | return |
| 275 | stdwin.setcutbuffer(0, text) |
| 276 | # |
| 277 | def do_paste(win): |
| 278 | text = stdwin.getcutbuffer(0) |
| 279 | if not text: |
| 280 | stdwin.fleep() |
| 281 | return |
| 282 | replace(win, text) |
| 283 | # |
| 284 | def do_clear(win): |
| 285 | replace(win, '') |
| 286 | |
| 287 | # |
| 288 | # These would be better in a preferences dialog: |
| 289 | def do_auto(win): |
| 290 | win.auto = (not win.auto) |
| 291 | win.editmenu.check(win.iauto, win.auto) |
| 292 | # |
| 293 | def 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 | # |
| 298 | def 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 | # |
| 311 | def 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 | # |
| 434 | def 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 | # |
| 453 | class 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 | # |
| 503 | def 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 | # |
| 514 | main() |