import sys import traceback import sets import keyword import time from code import InteractiveConsole, softspace from StringIO import StringIO import objc from objc import YES, NO, selector from Foundation import * from AppKit import * from PyObjCTools import AppHelper import os FLT_MAX = 3.40282347e+38 try: sys.ps1 except AttributeError: sys.ps1 = ">>> " try: sys.ps2 except AttributeError: sys.ps2 = "... " class PseudoUTF8Output(object): softspace = 0 def __init__(self, writemethod): self._write = writemethod def write(self, s): if not isinstance(s, unicode): s = s.decode('utf-8', 'replace') self._write(s) def writelines(self, lines): for line in lines: self.write(line) def flush(self): pass def isatty(self): return True class PseudoUTF8Input(object): softspace = 0 def __init__(self, readlinemethod): self._buffer = u'' self._readline = readlinemethod def read(self, chars=None): if chars is None: if self._buffer: rval = self._buffer self._buffer = u'' if rval.endswith(u'\r'): rval = rval[:-1]+u'\n' return rval.encode('utf-8') else: return self._readline(u'\x04')[:-1].encode('utf-8') else: while len(self._buffer) < chars: self._buffer += self._readline(u'\x04\r') if self._buffer.endswith('\x04'): self._buffer = self._buffer[:-1] break rval, self._buffer = self._buffer[:chars], self._buffer[chars:] return rval.encode('utf-8').replace('\r','\n') def readline(self): if u'\r' not in self._buffer: self._buffer += self._readline(u'\x04\r') if self._buffer.endswith('\x04'): rval = self._buffer[:-1].encode('utf-8') elif self._buffer.endswith('\r'): rval = self._buffer[:-1].encode('utf-8')+'\n' self._buffer = u'' return rval class AsyncInteractiveConsole(InteractiveConsole): lock = False buffer = None def __init__(self, *args, **kwargs): InteractiveConsole.__init__(self, *args, **kwargs) self.locals['__interpreter__'] = self def asyncinteract(self, write=None, banner=None): if self.lock: raise ValueError, "Can't nest" self.lock = True if write is None: write = self.write cprt = u'Type "help", "copyright", "credits" or "license" for more information.' if banner is None: write(u"Python %s in %s\n%s\n" % ( sys.version, NSBundle.mainBundle().objectForInfoDictionaryKey_('CFBundleName'), cprt, )) else: write(banner + '\n') more = 0 _buff = [] try: while True: if more: prompt = sys.ps2 else: prompt = sys.ps1 write(prompt) # yield the kind of prompt we have yield more # next input function yield _buff.append more = self.push(_buff.pop()) except: self.lock = False raise self.lock = False def resetbuffer(self): self.lastbuffer = self.buffer InteractiveConsole.resetbuffer(self) def runcode(self, code): try: exec code in self.locals except SystemExit: raise except: self.showtraceback() else: if softspace(sys.stdout, 0): print def recommendCompletionsFor(self, word): parts = word.split('.') if len(parts) > 1: # has a . so it must be a module or class or something # using eval, which shouldn't normally have side effects # unless there's descriptors/metaclasses doing some nasty # get magic objname = '.'.join(parts[:-1]) try: obj = eval(objname, self.locals) except: return None, 0 wordlower = parts[-1].lower() if wordlower == '': # they just punched in a dot, so list all attributes # that don't look private or special prefix = '.'.join(parts[-2:]) check = [ (prefix+_method) for _method in dir(obj) if _method[:1] != '_' and _method.lower().startswith(wordlower) ] else: # they started typing the method name check = filter(lambda s:s.lower().startswith(wordlower), dir(obj)) else: # no dots, must be in the normal namespaces.. no eval necessary check = sets.Set(dir(__builtins__)) check.update(keyword.kwlist) check.update(self.locals) wordlower = parts[-1].lower() check = filter(lambda s:s.lower().startswith(wordlower), check) check.sort() return check, 0 DEBUG_DELEGATE = 0 PASSTHROUGH = ( 'deleteBackward:', 'complete:', 'moveRight:', 'moveLeft:', ) class PyInterpreter(NSObject): """ PyInterpreter is a delegate/controller for a NSTextView, turning it into a full featured interactive Python interpreter. """ textView = objc.ivar('textView') def initWithTextView_(self, textView): self = super(PyInterpreter, self).init() self.textView = textView self.textView.setDelegate_(self) self.awakeFromNib() return self def interpreterLocals(self): return self._console.locals # # NSApplicationDelegate methods # def applicationDidFinishLaunching_(self, aNotification): self.textView.setFont_(self.font()) self.textView.setContinuousSpellCheckingEnabled_(False) self.textView.setRichText_(False) self._executeWithRedirectedIO(self._interp) # # NIB loading protocol # def awakeFromNib(self): self = super(PyInterpreter, self).init() self._font = NSFont.userFixedPitchFontOfSize_(10) self._stderrColor = NSColor.redColor() self._stdoutColor = NSColor.blueColor() self._codeColor = NSColor.blackColor() self._historyLength = 50 self._history = [u''] self._historyView = 0 self._characterIndexForInput = 0 self._stdin = PseudoUTF8Input(self._nestedRunLoopReaderUntilEOLchars_) #self._stdin = PseudoUTF8Input(self.readStdin) self._stderr = PseudoUTF8Output(self.writeStderr_) self._stdout = PseudoUTF8Output(self.writeStdout_) self._isInteracting = False self._console = AsyncInteractiveConsole() self._interp = self._console.asyncinteract( write=self.writeCode_, ).next self._autoscroll = True self.applicationDidFinishLaunching_(None) # # Modal input dialog support # def _nestedRunLoopReaderUntilEOLchars_(self, eolchars): """ This makes the baby jesus cry. I want co-routines. """ app = NSApplication.sharedApplication() window = self.textView.window() self.setCharacterIndexForInput_(self.lengthOfTextView()) # change the color.. eh self.textView.setTypingAttributes_({ NSFontAttributeName:self.font(), NSForegroundColorAttributeName:self.codeColor(), }) while True: event = app.nextEventMatchingMask_untilDate_inMode_dequeue_( NSAnyEventMask, NSDate.distantFuture(), NSDefaultRunLoopMode, True) if (event.type() == NSKeyDown) and (event.window() == window): eol = event.characters() if eol in eolchars: break app.sendEvent_(event) cl = self.currentLine() if eol == '\r': self.writeCode_('\n') return cl+eol # # Interpreter functions # def _executeWithRedirectedIO(self, fn, *args, **kwargs): old = sys.stdin, sys.stdout, sys.stderr if self._stdin is not None: sys.stdin = self._stdin sys.stdout, sys.stderr = self._stdout, self._stderr try: rval = fn(*args, **kwargs) finally: sys.stdin, sys.stdout, sys.stderr = old self.setCharacterIndexForInput_(self.lengthOfTextView()) return rval def executeLine_(self, line): self.addHistoryLine_(line) self._executeWithRedirectedIO(self._executeLine_, line) self._history = filter(None, self._history) self._history.append(u'') self._historyView = len(self._history) - 1 def _executeLine_(self, line): self._interp()(line) self._more = self._interp() def executeInteractiveLine_(self, line): self.setIsInteracting(True) try: self.executeLine_(line) finally: self.setIsInteracting(False) def replaceLineWithCode_(self, s): idx = self.characterIndexForInput() ts = self.textView.textStorage() ts.replaceCharactersInRange_withAttributedString_( (idx, len(ts.mutableString())-idx), self.codeString_(s)) # # History functions # def historyLength(self): return self._historyLength def setHistoryLength_(self, length): self._historyLength = length def addHistoryLine_(self, line): line = line.rstrip('\n') if self._history[-1] == line: return False if not line: return False self._history.append(line) if len(self._history) > self.historyLength(): self._history.pop(0) return True def historyDown_(self, sender): if self._historyView == (len(self._history) - 1): return self._history[self._historyView] = self.currentLine() self._historyView += 1 self.replaceLineWithCode_(self._history[self._historyView]) self.moveToEndOfLine_(self) def historyUp_(self, sender): if self._historyView == 0: return self._history[self._historyView] = self.currentLine() self._historyView -= 1 self.replaceLineWithCode_(self._history[self._historyView]) self.moveToEndOfLine_(self) # # Convenience methods to create/write decorated text # def _formatString_forOutput_(self, s, name): return NSAttributedString.alloc().initWithString_attributes_( s, { NSFontAttributeName:self.font(), NSForegroundColorAttributeName:getattr(self, name+'Color')(), }, ) def _writeString_forOutput_(self, s, name): self.textView.textStorage().appendAttributedString_(getattr(self, name+'String_')(s)) window = self.textView.window() app = NSApplication.sharedApplication() st = time.time() now = time.time if self._autoscroll: self.textView.scrollRangeToVisible_((self.lengthOfTextView(), 0)) while app.isRunning() and now() - st < 0.01: event = app.nextEventMatchingMask_untilDate_inMode_dequeue_( NSAnyEventMask, NSDate.dateWithTimeIntervalSinceNow_(0.01), NSDefaultRunLoopMode, True) if event is None: continue if (event.type() == NSKeyDown) and (event.window() == window): chr = event.charactersIgnoringModifiers() if chr == 'c' and (event.modifierFlags() & NSControlKeyMask): raise KeyboardInterrupt app.sendEvent_(event) codeString_ = lambda self, s: self._formatString_forOutput_(s, 'code') stderrString_ = lambda self, s: self._formatString_forOutput_(s, 'stderr') stdoutString_ = lambda self, s: self._formatString_forOutput_(s, 'stdout') writeCode_ = lambda self, s: self._writeString_forOutput_(s, 'code') writeStderr_ = lambda self, s: self._writeString_forOutput_(s, 'stderr') writeStdout_ = lambda self, s: self._writeString_forOutput_(s, 'stdout') # # Accessors # def more(self): return self._more def font(self): return self._font def setFont_(self, font): self._font = font def stderrColor(self): return self._stderrColor def setStderrColor_(self, color): self._stderrColor = color def stdoutColor(self): return self._stdoutColor def setStdoutColor_(self, color): self._stdoutColor = color def codeColor(self): return self._codeColor def setStdoutColor_(self, color): self._codeColor = color def isInteracting(self): return self._isInteracting def setIsInteracting(self, v): self._isInteracting = v def isAutoScroll(self): return self._autoScroll def setAutoScroll(self, v): self._autoScroll = v # # Convenience methods for manipulating the NSTextView # def currentLine(self): return self.textView.textStorage().mutableString()[self.characterIndexForInput():] def moveAndScrollToIndex_(self, idx): self.textView.scrollRangeToVisible_((idx, 0)) self.textView.setSelectedRange_((idx, 0)) def characterIndexForInput(self): return self._characterIndexForInput def lengthOfTextView(self): return len(self.textView.textStorage().mutableString()) def setCharacterIndexForInput_(self, idx): self._characterIndexForInput = idx self.moveAndScrollToIndex_(idx) # # NSTextViewDelegate methods # def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, aTextView, completions, (begin, length), index): txt = self.textView.textStorage().mutableString() end = begin+length while (begin>0) and (txt[begin].isalnum() or txt[begin] in '._'): begin -= 1 while not txt[begin].isalnum(): begin += 1 return self._console.recommendCompletionsFor(txt[begin:end]) def textView_shouldChangeTextInRange_replacementString_(self, aTextView, aRange, newString): begin, length = aRange lastLocation = self.characterIndexForInput() if begin < lastLocation: # no editing anywhere but the interactive line return NO newString = newString.replace('\r', '\n') if '\n' in newString: if begin != lastLocation: # no pasting multiline unless you're at the end # of the interactive line return NO # multiline paste support #self.clearLine() newString = self.currentLine() + newString for s in newString.strip().split('\n'): self.writeCode_(s+'\n') self.executeLine_(s) return NO return YES def textView_willChangeSelectionFromCharacterRange_toCharacterRange_(self, aTextView, fromRange, toRange): return toRange begin, length = toRange if length == 0 and begin < self.characterIndexForInput(): # no cursor movement off the interactive line return fromRange return toRange def textView_doCommandBySelector_(self, aTextView, aSelector): # deleteForward: is ctrl-d if self.isInteracting(): if aSelector == 'insertNewline:': self.writeCode_('\n') return NO responder = getattr(self, aSelector.replace(':','_'), None) if responder is not None: responder(aTextView) return YES else: if DEBUG_DELEGATE and aSelector not in PASSTHROUGH: print aSelector return NO # # doCommandBySelector "posers" on the textView # def insertTabIgnoringFieldEditor_(self, sender): # this isn't terribly necessary, b/c F5 and opt-esc do completion # but why not sender.complete_(self) def moveToBeginningOfLine_(self, sender): self.moveAndScrollToIndex_(self.characterIndexForInput()) def moveToEndOfLine_(self, sender): self.moveAndScrollToIndex_(self.lengthOfTextView()) def moveToBeginningOfLineAndModifySelection_(self, sender): begin, length = self.textView.selectedRange() pos = self.characterIndexForInput() if begin+length > pos: self.textView.setSelectedRange_((pos, begin+length-pos)) else: self.moveToBeginningOfLine_(sender) def moveToEndOfLineAndModifySelection_(self, sender): begin, length = self.textView.selectedRange() pos = max(self.characterIndexForInput(), begin) self.textView.setSelectedRange_((pos, self.lengthOfTextView())) def insertNewline_(self, sender): line = self.currentLine() self.writeCode_('\n') self.executeInteractiveLine_(line) moveToBeginningOfParagraph_ = moveToBeginningOfLine_ moveToEndOfParagraph_ = moveToEndOfLine_ insertNewlineIgnoringFieldEditor_ = insertNewline_ moveDown_ = historyDown_ moveUp_ = historyUp_ class WebKitInterpreter(NSView): arguments = objc.ivar('arguments') pyInterpreter = objc.ivar('pyInterpreter') scrollView = objc.ivar('scrollView') textView = objc.ivar('textView') def container(self): return self.arguments.get(u'WebPluginContainer') def pluginViewWithArguments_(cls, arguments): self = super(WebKitInterpreter, cls).alloc().initWithFrame_(NSZeroRect) NSLog('pluginViewWithArguments:') NSLog(arguments) self.arguments = arguments return self pluginViewWithArguments_ = classmethod(pluginViewWithArguments_) def pluginStart(self): NSLog('pluginStart') try: self.doPluginStart() except: import traceback traceback.print_exc() def doPluginStart(self): dct = self.arguments[u'WebPluginAttributes'] w, h = [float(dct.get(k, 0)) for k in ('width', 'height')] self.setFrame_(((0.0, 0.0), (w, h))) scrollView = NSScrollView.alloc().initWithFrame_(self.frame()) scrollView.setHasVerticalScroller_(True) scrollView.setHasHorizontalScroller_(False) scrollView.setAutoresizingMask_( NSViewWidthSizable | NSViewHeightSizable) contentSize = scrollView.contentSize() textView = NSTextView.alloc().initWithFrame_( ((0, 0), scrollView.contentSize())) textView.setMinSize_( (0, contentSize.height)) textView.setMaxSize_( (FLT_MAX, FLT_MAX)) textView.setVerticallyResizable_(True) textView.setHorizontallyResizable_(False) textView.setAutoresizingMask_(NSViewWidthSizable) textView.textContainer().setContainerSize_( (contentSize.width, FLT_MAX)) textView.textContainer().setWidthTracksTextView_(True) scrollView.setDocumentView_(textView) self.addSubview_(scrollView) self.pyInterpreter = PyInterpreter.alloc().initWithTextView_( textView) self.pyInterpreter.interpreterLocals()[u'container'] = self.container() def objectForWebScript(self): return self NSLog('loaded WebKitInterpreter') objc.removeAutoreleasePool()