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