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