1import sys 2import traceback 3import sets 4import keyword 5import time 6from code import InteractiveConsole, softspace 7from StringIO import StringIO 8import objc 9from objc import YES, NO, selector 10from Foundation import * 11from AppKit import * 12from PyObjCTools import AppHelper 13import os 14 15FLT_MAX = 3.40282347e+38 16 17try: 18 sys.ps1 19except AttributeError: 20 sys.ps1 = ">>> " 21try: 22 sys.ps2 23except AttributeError: 24 sys.ps2 = "... " 25 26class PseudoUTF8Output(object): 27 softspace = 0 28 def __init__(self, writemethod): 29 self._write = writemethod 30 31 def write(self, s): 32 if not isinstance(s, unicode): 33 s = s.decode('utf-8', 'replace') 34 self._write(s) 35 36 def writelines(self, lines): 37 for line in lines: 38 self.write(line) 39 40 def flush(self): 41 pass 42 43 def isatty(self): 44 return True 45 46class PseudoUTF8Input(object): 47 softspace = 0 48 def __init__(self, readlinemethod): 49 self._buffer = u'' 50 self._readline = readlinemethod 51 52 def read(self, chars=None): 53 if chars is None: 54 if self._buffer: 55 rval = self._buffer 56 self._buffer = u'' 57 if rval.endswith(u'\r'): 58 rval = rval[:-1]+u'\n' 59 return rval.encode('utf-8') 60 else: 61 return self._readline(u'\x04')[:-1].encode('utf-8') 62 else: 63 while len(self._buffer) < chars: 64 self._buffer += self._readline(u'\x04\r') 65 if self._buffer.endswith('\x04'): 66 self._buffer = self._buffer[:-1] 67 break 68 rval, self._buffer = self._buffer[:chars], self._buffer[chars:] 69 return rval.encode('utf-8').replace('\r','\n') 70 71 def readline(self): 72 if u'\r' not in self._buffer: 73 self._buffer += self._readline(u'\x04\r') 74 if self._buffer.endswith('\x04'): 75 rval = self._buffer[:-1].encode('utf-8') 76 elif self._buffer.endswith('\r'): 77 rval = self._buffer[:-1].encode('utf-8')+'\n' 78 self._buffer = u'' 79 80 return rval 81 82class AsyncInteractiveConsole(InteractiveConsole): 83 lock = False 84 buffer = None 85 86 def __init__(self, *args, **kwargs): 87 InteractiveConsole.__init__(self, *args, **kwargs) 88 self.locals['__interpreter__'] = self 89 90 def asyncinteract(self, write=None, banner=None): 91 if self.lock: 92 raise ValueError, "Can't nest" 93 self.lock = True 94 if write is None: 95 write = self.write 96 cprt = u'Type "help", "copyright", "credits" or "license" for more information.' 97 if banner is None: 98 write(u"Python %s in %s\n%s\n" % ( 99 sys.version, 100 NSBundle.mainBundle().objectForInfoDictionaryKey_('CFBundleName'), 101 cprt, 102 )) 103 else: 104 write(banner + '\n') 105 more = 0 106 _buff = [] 107 try: 108 while True: 109 if more: 110 prompt = sys.ps2 111 else: 112 prompt = sys.ps1 113 write(prompt) 114 # yield the kind of prompt we have 115 yield more 116 # next input function 117 yield _buff.append 118 more = self.push(_buff.pop()) 119 except: 120 self.lock = False 121 raise 122 self.lock = False 123 124 def resetbuffer(self): 125 self.lastbuffer = self.buffer 126 InteractiveConsole.resetbuffer(self) 127 128 def runcode(self, code): 129 try: 130 exec code in self.locals 131 except SystemExit: 132 raise 133 except: 134 self.showtraceback() 135 else: 136 if softspace(sys.stdout, 0): 137 print 138 139 140 def recommendCompletionsFor(self, word): 141 parts = word.split('.') 142 if len(parts) > 1: 143 # has a . so it must be a module or class or something 144 # using eval, which shouldn't normally have side effects 145 # unless there's descriptors/metaclasses doing some nasty 146 # get magic 147 objname = '.'.join(parts[:-1]) 148 try: 149 obj = eval(objname, self.locals) 150 except: 151 return None, 0 152 wordlower = parts[-1].lower() 153 if wordlower == '': 154 # they just punched in a dot, so list all attributes 155 # that don't look private or special 156 prefix = '.'.join(parts[-2:]) 157 check = [ 158 (prefix+_method) 159 for _method 160 in dir(obj) 161 if _method[:1] != '_' and _method.lower().startswith(wordlower) 162 ] 163 else: 164 # they started typing the method name 165 check = filter(lambda s:s.lower().startswith(wordlower), dir(obj)) 166 else: 167 # no dots, must be in the normal namespaces.. no eval necessary 168 check = sets.Set(dir(__builtins__)) 169 check.update(keyword.kwlist) 170 check.update(self.locals) 171 wordlower = parts[-1].lower() 172 check = filter(lambda s:s.lower().startswith(wordlower), check) 173 check.sort() 174 return check, 0 175 176DEBUG_DELEGATE = 0 177PASSTHROUGH = ( 178 'deleteBackward:', 179 'complete:', 180 'moveRight:', 181 'moveLeft:', 182) 183 184class PyInterpreter(NSObject): 185 """ 186 PyInterpreter is a delegate/controller for a NSTextView, 187 turning it into a full featured interactive Python interpreter. 188 """ 189 190 textView = objc.ivar('textView') 191 192 def initWithTextView_(self, textView): 193 self = super(PyInterpreter, self).init() 194 self.textView = textView 195 self.textView.setDelegate_(self) 196 self.awakeFromNib() 197 return self 198 199 def interpreterLocals(self): 200 return self._console.locals 201 202 # 203 # NSApplicationDelegate methods 204 # 205 206 def applicationDidFinishLaunching_(self, aNotification): 207 self.textView.setFont_(self.font()) 208 self.textView.setContinuousSpellCheckingEnabled_(False) 209 self.textView.setRichText_(False) 210 self._executeWithRedirectedIO(self._interp) 211 212 # 213 # NIB loading protocol 214 # 215 216 def awakeFromNib(self): 217 self = super(PyInterpreter, self).init() 218 self._font = NSFont.userFixedPitchFontOfSize_(10) 219 self._stderrColor = NSColor.redColor() 220 self._stdoutColor = NSColor.blueColor() 221 self._codeColor = NSColor.blackColor() 222 self._historyLength = 50 223 self._history = [u''] 224 self._historyView = 0 225 self._characterIndexForInput = 0 226 self._stdin = PseudoUTF8Input(self._nestedRunLoopReaderUntilEOLchars_) 227 #self._stdin = PseudoUTF8Input(self.readStdin) 228 self._stderr = PseudoUTF8Output(self.writeStderr_) 229 self._stdout = PseudoUTF8Output(self.writeStdout_) 230 self._isInteracting = False 231 self._console = AsyncInteractiveConsole() 232 self._interp = self._console.asyncinteract( 233 write=self.writeCode_, 234 ).next 235 self._autoscroll = True 236 self.applicationDidFinishLaunching_(None) 237 238 # 239 # Modal input dialog support 240 # 241 242 def _nestedRunLoopReaderUntilEOLchars_(self, eolchars): 243 """ 244 This makes the baby jesus cry. 245 246 I want co-routines. 247 """ 248 app = NSApplication.sharedApplication() 249 window = self.textView.window() 250 self.setCharacterIndexForInput_(self.lengthOfTextView()) 251 # change the color.. eh 252 self.textView.setTypingAttributes_({ 253 NSFontAttributeName:self.font(), 254 NSForegroundColorAttributeName:self.codeColor(), 255 }) 256 while True: 257 event = app.nextEventMatchingMask_untilDate_inMode_dequeue_( 258 NSAnyEventMask, 259 NSDate.distantFuture(), 260 NSDefaultRunLoopMode, 261 True) 262 if (event.type() == NSKeyDown) and (event.window() == window): 263 eol = event.characters() 264 if eol in eolchars: 265 break 266 app.sendEvent_(event) 267 cl = self.currentLine() 268 if eol == '\r': 269 self.writeCode_('\n') 270 return cl+eol 271 272 # 273 # Interpreter functions 274 # 275 276 def _executeWithRedirectedIO(self, fn, *args, **kwargs): 277 old = sys.stdin, sys.stdout, sys.stderr 278 if self._stdin is not None: 279 sys.stdin = self._stdin 280 sys.stdout, sys.stderr = self._stdout, self._stderr 281 try: 282 rval = fn(*args, **kwargs) 283 finally: 284 sys.stdin, sys.stdout, sys.stderr = old 285 self.setCharacterIndexForInput_(self.lengthOfTextView()) 286 return rval 287 288 def executeLine_(self, line): 289 self.addHistoryLine_(line) 290 self._executeWithRedirectedIO(self._executeLine_, line) 291 self._history = filter(None, self._history) 292 self._history.append(u'') 293 self._historyView = len(self._history) - 1 294 295 def _executeLine_(self, line): 296 self._interp()(line) 297 self._more = self._interp() 298 299 def executeInteractiveLine_(self, line): 300 self.setIsInteracting(True) 301 try: 302 self.executeLine_(line) 303 finally: 304 self.setIsInteracting(False) 305 306 def replaceLineWithCode_(self, s): 307 idx = self.characterIndexForInput() 308 ts = self.textView.textStorage() 309 ts.replaceCharactersInRange_withAttributedString_( 310 (idx, len(ts.mutableString())-idx), self.codeString_(s)) 311 312 # 313 # History functions 314 # 315 316 def historyLength(self): 317 return self._historyLength 318 319 def setHistoryLength_(self, length): 320 self._historyLength = length 321 322 def addHistoryLine_(self, line): 323 line = line.rstrip('\n') 324 if self._history[-1] == line: 325 return False 326 if not line: 327 return False 328 self._history.append(line) 329 if len(self._history) > self.historyLength(): 330 self._history.pop(0) 331 return True 332 333 def historyDown_(self, sender): 334 if self._historyView == (len(self._history) - 1): 335 return 336 self._history[self._historyView] = self.currentLine() 337 self._historyView += 1 338 self.replaceLineWithCode_(self._history[self._historyView]) 339 self.moveToEndOfLine_(self) 340 341 def historyUp_(self, sender): 342 if self._historyView == 0: 343 return 344 self._history[self._historyView] = self.currentLine() 345 self._historyView -= 1 346 self.replaceLineWithCode_(self._history[self._historyView]) 347 self.moveToEndOfLine_(self) 348 349 # 350 # Convenience methods to create/write decorated text 351 # 352 353 def _formatString_forOutput_(self, s, name): 354 return NSAttributedString.alloc().initWithString_attributes_( 355 s, 356 { 357 NSFontAttributeName:self.font(), 358 NSForegroundColorAttributeName:getattr(self, name+'Color')(), 359 }, 360 ) 361 362 def _writeString_forOutput_(self, s, name): 363 self.textView.textStorage().appendAttributedString_(getattr(self, name+'String_')(s)) 364 365 window = self.textView.window() 366 app = NSApplication.sharedApplication() 367 st = time.time() 368 now = time.time 369 370 if self._autoscroll: 371 self.textView.scrollRangeToVisible_((self.lengthOfTextView(), 0)) 372 373 while app.isRunning() and now() - st < 0.01: 374 event = app.nextEventMatchingMask_untilDate_inMode_dequeue_( 375 NSAnyEventMask, 376 NSDate.dateWithTimeIntervalSinceNow_(0.01), 377 NSDefaultRunLoopMode, 378 True) 379 380 if event is None: 381 continue 382 383 if (event.type() == NSKeyDown) and (event.window() == window): 384 chr = event.charactersIgnoringModifiers() 385 if chr == 'c' and (event.modifierFlags() & NSControlKeyMask): 386 raise KeyboardInterrupt 387 388 app.sendEvent_(event) 389 390 391 codeString_ = lambda self, s: self._formatString_forOutput_(s, 'code') 392 stderrString_ = lambda self, s: self._formatString_forOutput_(s, 'stderr') 393 stdoutString_ = lambda self, s: self._formatString_forOutput_(s, 'stdout') 394 writeCode_ = lambda self, s: self._writeString_forOutput_(s, 'code') 395 writeStderr_ = lambda self, s: self._writeString_forOutput_(s, 'stderr') 396 writeStdout_ = lambda self, s: self._writeString_forOutput_(s, 'stdout') 397 398 # 399 # Accessors 400 # 401 402 def more(self): 403 return self._more 404 405 def font(self): 406 return self._font 407 408 def setFont_(self, font): 409 self._font = font 410 411 def stderrColor(self): 412 return self._stderrColor 413 414 def setStderrColor_(self, color): 415 self._stderrColor = color 416 417 def stdoutColor(self): 418 return self._stdoutColor 419 420 def setStdoutColor_(self, color): 421 self._stdoutColor = color 422 423 def codeColor(self): 424 return self._codeColor 425 426 def setStdoutColor_(self, color): 427 self._codeColor = color 428 429 def isInteracting(self): 430 return self._isInteracting 431 432 def setIsInteracting(self, v): 433 self._isInteracting = v 434 435 def isAutoScroll(self): 436 return self._autoScroll 437 438 def setAutoScroll(self, v): 439 self._autoScroll = v 440 441 442 # 443 # Convenience methods for manipulating the NSTextView 444 # 445 446 def currentLine(self): 447 return self.textView.textStorage().mutableString()[self.characterIndexForInput():] 448 449 def moveAndScrollToIndex_(self, idx): 450 self.textView.scrollRangeToVisible_((idx, 0)) 451 self.textView.setSelectedRange_((idx, 0)) 452 453 def characterIndexForInput(self): 454 return self._characterIndexForInput 455 456 def lengthOfTextView(self): 457 return len(self.textView.textStorage().mutableString()) 458 459 def setCharacterIndexForInput_(self, idx): 460 self._characterIndexForInput = idx 461 self.moveAndScrollToIndex_(idx) 462 463 # 464 # NSTextViewDelegate methods 465 # 466 467 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, aTextView, completions, (begin, length), index): 468 txt = self.textView.textStorage().mutableString() 469 end = begin+length 470 while (begin>0) and (txt[begin].isalnum() or txt[begin] in '._'): 471 begin -= 1 472 while not txt[begin].isalnum(): 473 begin += 1 474 return self._console.recommendCompletionsFor(txt[begin:end]) 475 476 def textView_shouldChangeTextInRange_replacementString_(self, aTextView, aRange, newString): 477 begin, length = aRange 478 lastLocation = self.characterIndexForInput() 479 if begin < lastLocation: 480 # no editing anywhere but the interactive line 481 return NO 482 newString = newString.replace('\r', '\n') 483 if '\n' in newString: 484 if begin != lastLocation: 485 # no pasting multiline unless you're at the end 486 # of the interactive line 487 return NO 488 # multiline paste support 489 #self.clearLine() 490 newString = self.currentLine() + newString 491 for s in newString.strip().split('\n'): 492 self.writeCode_(s+'\n') 493 self.executeLine_(s) 494 return NO 495 return YES 496 497 def textView_willChangeSelectionFromCharacterRange_toCharacterRange_(self, aTextView, fromRange, toRange): 498 return toRange 499 begin, length = toRange 500 if length == 0 and begin < self.characterIndexForInput(): 501 # no cursor movement off the interactive line 502 return fromRange 503 return toRange 504 505 def textView_doCommandBySelector_(self, aTextView, aSelector): 506 # deleteForward: is ctrl-d 507 if self.isInteracting(): 508 if aSelector == 'insertNewline:': 509 self.writeCode_('\n') 510 return NO 511 responder = getattr(self, aSelector.replace(':','_'), None) 512 if responder is not None: 513 responder(aTextView) 514 return YES 515 else: 516 if DEBUG_DELEGATE and aSelector not in PASSTHROUGH: 517 print aSelector 518 return NO 519 520 # 521 # doCommandBySelector "posers" on the textView 522 # 523 524 def insertTabIgnoringFieldEditor_(self, sender): 525 # this isn't terribly necessary, b/c F5 and opt-esc do completion 526 # but why not 527 sender.complete_(self) 528 529 def moveToBeginningOfLine_(self, sender): 530 self.moveAndScrollToIndex_(self.characterIndexForInput()) 531 532 def moveToEndOfLine_(self, sender): 533 self.moveAndScrollToIndex_(self.lengthOfTextView()) 534 535 def moveToBeginningOfLineAndModifySelection_(self, sender): 536 begin, length = self.textView.selectedRange() 537 pos = self.characterIndexForInput() 538 if begin+length > pos: 539 self.textView.setSelectedRange_((pos, begin+length-pos)) 540 else: 541 self.moveToBeginningOfLine_(sender) 542 543 def moveToEndOfLineAndModifySelection_(self, sender): 544 begin, length = self.textView.selectedRange() 545 pos = max(self.characterIndexForInput(), begin) 546 self.textView.setSelectedRange_((pos, self.lengthOfTextView())) 547 548 def insertNewline_(self, sender): 549 line = self.currentLine() 550 self.writeCode_('\n') 551 self.executeInteractiveLine_(line) 552 553 moveToBeginningOfParagraph_ = moveToBeginningOfLine_ 554 moveToEndOfParagraph_ = moveToEndOfLine_ 555 insertNewlineIgnoringFieldEditor_ = insertNewline_ 556 moveDown_ = historyDown_ 557 moveUp_ = historyUp_ 558 559class WebKitInterpreter(NSView): 560 561 arguments = objc.ivar('arguments') 562 pyInterpreter = objc.ivar('pyInterpreter') 563 scrollView = objc.ivar('scrollView') 564 textView = objc.ivar('textView') 565 566 def container(self): 567 return self.arguments.get(u'WebPluginContainer') 568 569 def pluginViewWithArguments_(cls, arguments): 570 self = super(WebKitInterpreter, cls).alloc().initWithFrame_(NSZeroRect) 571 NSLog('pluginViewWithArguments:') 572 NSLog(arguments) 573 self.arguments = arguments 574 return self 575 pluginViewWithArguments_ = classmethod(pluginViewWithArguments_) 576 577 def pluginStart(self): 578 NSLog('pluginStart') 579 try: 580 self.doPluginStart() 581 except: 582 import traceback 583 traceback.print_exc() 584 585 def doPluginStart(self): 586 dct = self.arguments[u'WebPluginAttributes'] 587 w, h = [float(dct.get(k, 0)) for k in ('width', 'height')] 588 589 self.setFrame_(((0.0, 0.0), (w, h))) 590 scrollView = NSScrollView.alloc().initWithFrame_(self.frame()) 591 scrollView.setHasVerticalScroller_(True) 592 scrollView.setHasHorizontalScroller_(False) 593 scrollView.setAutoresizingMask_( 594 NSViewWidthSizable | NSViewHeightSizable) 595 contentSize = scrollView.contentSize() 596 textView = NSTextView.alloc().initWithFrame_( 597 ((0, 0), scrollView.contentSize())) 598 textView.setMinSize_( 599 (0, contentSize.height)) 600 textView.setMaxSize_( 601 (FLT_MAX, FLT_MAX)) 602 textView.setVerticallyResizable_(True) 603 textView.setHorizontallyResizable_(False) 604 textView.setAutoresizingMask_(NSViewWidthSizable) 605 606 textView.textContainer().setContainerSize_( 607 (contentSize.width, FLT_MAX)) 608 textView.textContainer().setWidthTracksTextView_(True) 609 610 scrollView.setDocumentView_(textView) 611 self.addSubview_(scrollView) 612 613 self.pyInterpreter = PyInterpreter.alloc().initWithTextView_( 614 textView) 615 616 self.pyInterpreter.interpreterLocals()[u'container'] = self.container() 617 618 def objectForWebScript(self): 619 return self 620 621NSLog('loaded WebKitInterpreter') 622 623objc.removeAutoreleasePool() 624