8 from code import InteractiveConsole, softspace
9 from StringIO import StringIO
10 from objc import YES, NO, selector
11 from Foundation import *
13 from PyObjCTools import NibClassBuilder, AppHelper
15 from EmbeddedInterpreterPlugIn import InterpreterKeyController
17 NibClassBuilder.extractClasses("PyInterpreter.nib")
21 except AttributeError:
25 except AttributeError:
28 class PseudoUTF8Output(object):
32 def __init__(self, writemethod):
33 self._write = writemethod
36 if not isinstance(s, unicode):
37 s = s.decode('utf-8', 'replace')
40 def writelines(self, lines):
50 class PseudoUTF8Input(object):
54 def __init__(self, readlinemethod):
56 self._readline = readlinemethod
59 raise IOError(errno.EBADF, posix.strerror(errno.EBADF))
61 def read(self, chars=None):
66 if rval.endswith(u'\r'):
67 rval = rval[:-1]+u'\n'
68 return rval.encode('utf-8')
70 return self._readline(u'\x04')[:-1].encode('utf-8')
72 while len(self._buffer) < chars:
73 self._buffer += self._readline(u'\x04\r')
74 if self._buffer.endswith('\x04'):
75 self._buffer = self._buffer[:-1]
77 rval, self._buffer = self._buffer[:chars], self._buffer[chars:]
78 return rval.encode('utf-8').replace('\r','\n')
81 if u'\r' not in self._buffer:
82 self._buffer += self._readline(u'\x04\r')
83 if self._buffer.endswith('\x04'):
84 rval = self._buffer[:-1].encode('utf-8')
85 elif self._buffer.endswith('\r'):
86 rval = self._buffer[:-1].encode('utf-8')+'\n'
94 class AsyncInteractiveConsole(InteractiveConsole):
98 def __init__(self, *args, **kwargs):
99 InteractiveConsole.__init__(self, *args, **kwargs)
100 self.locals['__interpreter__'] = self
102 def asyncinteract(self, write=None, banner=None):
104 raise ValueError, "Can't nest"
108 cprt = u'Type "help", "copyright", "credits" or "license" for more information.'
110 write(u"Python %s in %s\n%s\n" % (
112 NSBundle.mainBundle().objectForInfoDictionaryKey_('CFBundleName'),
126 # yield the kind of prompt we have
128 # next input function
130 more = self.push(_buff.pop())
136 def resetbuffer(self):
137 self.lastbuffer = self.buffer
138 InteractiveConsole.resetbuffer(self)
140 def runcode(self, code):
142 InterpreterKeyController.setEnabled_(True);
143 exec code in self.locals
145 InterpreterKeyController.setEnabled_(False);
148 InterpreterKeyController.setEnabled_(False);
151 InterpreterKeyController.setEnabled_(False);
152 if softspace(sys.stdout, 0):
156 def recommendCompletionsFor(self, word):
157 parts = word.split('.')
159 # has a . so it must be a module or class or something
160 # using eval, which shouldn't normally have side effects
161 # unless there's descriptors/metaclasses doing some nasty
163 objname = '.'.join(parts[:-1])
165 obj = eval(objname, self.locals)
168 wordlower = parts[-1].lower()
170 # they just punched in a dot, so list all attributes
171 # that don't look private or special
172 prefix = '.'.join(parts[-2:])
177 if _method[:1] != '_' and _method.lower().startswith(wordlower)
180 # they started typing the method name
181 check = filter(lambda s:s.lower().startswith(wordlower), dir(obj))
183 # no dots, must be in the normal namespaces.. no eval necessary
184 check = sets.Set(dir(__builtins__))
185 check.update(keyword.kwlist)
186 check.update(self.locals)
187 wordlower = parts[-1].lower()
188 check = filter(lambda s:s.lower().startswith(wordlower), check)
200 class PyInterpreter(NibClassBuilder.AutoBaseClass):
202 PyInterpreter is a delegate/controller for a NSTextView,
203 turning it into a full featured interactive Python interpreter.
207 # Outlets - for documentation only
211 (NSTextView, 'textView', 'The interpreter'),
215 # NIB loading protocol
218 def awakeFromNib(self):
219 self = super(PyInterpreter, self).init()
220 self._font = NSFont.userFixedPitchFontOfSize_(10)
221 self._stderrColor = NSColor.redColor()
222 self._stdoutColor = NSColor.blueColor()
223 self._codeColor = NSColor.blackColor()
224 self._historyLength = 50
225 self._history = [u'']
226 self._historyView = 0
227 self._characterIndexForInput = 0
228 self._stdin = PseudoUTF8Input(self._nestedRunLoopReaderUntilEOLchars_)
229 #self._stdin = PseudoUTF8Input(self.readStdin)
230 self._stderr = PseudoUTF8Output(self.writeStderr_)
231 self._stdout = PseudoUTF8Output(self.writeStdout_)
232 self._isInteracting = False
233 self._console = AsyncInteractiveConsole()
234 self._interp = self._console.asyncinteract(
235 write=self.writeCode_,
237 self._autoscroll = True
238 self.textView.setFont_(self.font())
239 self.textView.setContinuousSpellCheckingEnabled_(False)
240 self.textView.setRichText_(False)
241 self._executeWithRedirectedIO(self._interp)
244 # Modal input dialog support
247 def _nestedRunLoopReaderUntilEOLchars_(self, eolchars):
249 This makes the baby jesus cry.
253 app = NSApplication.sharedApplication()
254 window = self.textView.window()
255 self.setCharacterIndexForInput_(self.lengthOfTextView())
256 # change the color.. eh
257 self.textView.setTypingAttributes_({
258 NSFontAttributeName:self.font(),
259 NSForegroundColorAttributeName:self.codeColor(),
262 event = app.nextEventMatchingMask_untilDate_inMode_dequeue_(
264 NSDate.distantFuture(),
265 NSDefaultRunLoopMode,
267 if (event.type() == NSKeyDown) and (event.window() == window):
268 eol = event.characters()
271 app.sendEvent_(event)
272 cl = self.currentLine()
274 self.writeCode_('\n')
278 # Interpreter functions
281 def _executeWithRedirectedIO(self, fn, *args, **kwargs):
282 old = sys.stdin, sys.stdout, sys.stderr
283 if self._stdin is not None:
284 sys.stdin = self._stdin
285 sys.stdout, sys.stderr = self._stdout, self._stderr
287 rval = fn(*args, **kwargs)
289 sys.stdin, sys.stdout, sys.stderr = old
290 self.setCharacterIndexForInput_(self.lengthOfTextView())
293 def executeLine_(self, line):
294 self.addHistoryLine_(line)
295 self._executeWithRedirectedIO(self._executeLine_, line)
296 self._history = filter(None, self._history)
297 self._history.append(u'')
298 self._historyView = len(self._history) - 1
300 def _executeLine_(self, line):
302 self._more = self._interp()
304 def executeInteractiveLine_(self, line):
305 self.setIsInteracting(True)
307 self.executeLine_(line)
309 self.setIsInteracting(False)
311 def replaceLineWithCode_(self, s):
312 idx = self.characterIndexForInput()
313 ts = self.textView.textStorage()
314 ts.replaceCharactersInRange_withAttributedString_(
315 (idx, len(ts.mutableString())-idx), self.codeString_(s))
321 def historyLength(self):
322 return self._historyLength
324 def setHistoryLength_(self, length):
325 self._historyLength = length
327 def addHistoryLine_(self, line):
328 line = line.rstrip('\n')
329 if self._history[-1] == line:
333 self._history.append(line)
334 if len(self._history) > self.historyLength():
338 def historyDown_(self, sender):
339 if self._historyView == (len(self._history) - 1):
341 self._history[self._historyView] = self.currentLine()
342 self._historyView += 1
343 self.replaceLineWithCode_(self._history[self._historyView])
344 self.moveToEndOfLine_(self)
346 def historyUp_(self, sender):
347 if self._historyView == 0:
349 self._history[self._historyView] = self.currentLine()
350 self._historyView -= 1
351 self.replaceLineWithCode_(self._history[self._historyView])
352 self.moveToEndOfLine_(self)
355 # Convenience methods to create/write decorated text
358 def _formatString_forOutput_(self, s, name):
359 return NSAttributedString.alloc().initWithString_attributes_(
362 NSFontAttributeName:self.font(),
363 NSForegroundColorAttributeName:getattr(self, name+'Color')(),
367 def _writeString_forOutput_(self, s, name):
368 self.textView.textStorage().appendAttributedString_(getattr(self, name+'String_')(s))
370 window = self.textView.window()
371 app = NSApplication.sharedApplication()
376 self.textView.scrollRangeToVisible_((self.lengthOfTextView(), 0))
378 while app.isRunning() and now() - st < 0.01:
379 event = app.nextEventMatchingMask_untilDate_inMode_dequeue_(
381 NSDate.dateWithTimeIntervalSinceNow_(0.01),
382 NSDefaultRunLoopMode,
388 if (event.type() == NSKeyDown) and (event.window() == window):
389 chr = event.charactersIgnoringModifiers()
390 if chr == 'c' and (event.modifierFlags() & NSControlKeyMask):
391 raise KeyboardInterrupt
393 app.sendEvent_(event)
396 codeString_ = lambda self, s: self._formatString_forOutput_(s, 'code')
397 stderrString_ = lambda self, s: self._formatString_forOutput_(s, 'stderr')
398 stdoutString_ = lambda self, s: self._formatString_forOutput_(s, 'stdout')
399 writeCode_ = lambda self, s: self._writeString_forOutput_(s, 'code')
400 writeStderr_ = lambda self, s: self._writeString_forOutput_(s, 'stderr')
401 writeStdout_ = lambda self, s: self._writeString_forOutput_(s, 'stdout')
413 def setFont_(self, font):
416 def stderrColor(self):
417 return self._stderrColor
419 def setStderrColor_(self, color):
420 self._stderrColor = color
422 def stdoutColor(self):
423 return self._stdoutColor
425 def setStdoutColor_(self, color):
426 self._stdoutColor = color
429 return self._codeColor
431 def setStdoutColor_(self, color):
432 self._codeColor = color
434 def isInteracting(self):
435 return self._isInteracting
437 def setIsInteracting(self, v):
438 self._isInteracting = v
440 def isAutoScroll(self):
441 return self._autoScroll
443 def setAutoScroll(self, v):
448 # Convenience methods for manipulating the NSTextView
451 def currentLine(self):
452 return self.textView.textStorage().mutableString()[self.characterIndexForInput():]
454 def moveAndScrollToIndex_(self, idx):
455 self.textView.scrollRangeToVisible_((idx, 0))
456 self.textView.setSelectedRange_((idx, 0))
458 def characterIndexForInput(self):
459 return self._characterIndexForInput
461 def lengthOfTextView(self):
462 return len(self.textView.textStorage().mutableString())
464 def setCharacterIndexForInput_(self, idx):
465 self._characterIndexForInput = idx
466 self.moveAndScrollToIndex_(idx)
469 # NSTextViewDelegate methods
472 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, aTextView, completions, (begin, length), index):
473 txt = self.textView.textStorage().mutableString()
475 while (begin>0) and (txt[begin].isalnum() or txt[begin] in '._'):
477 while begin < end and not txt[begin].isalnum():
479 return self._console.recommendCompletionsFor(txt[begin:end])
481 def textView_shouldChangeTextInRange_replacementString_(self, aTextView, aRange, newString):
482 begin, length = aRange
483 lastLocation = self.characterIndexForInput()
484 if begin < lastLocation:
485 # no editing anywhere but the interactive line
487 newString = newString.replace('\r', '\n')
488 if '\n' in newString:
489 if begin != lastLocation:
490 # no pasting multiline unless you're at the end
491 # of the interactive line
493 # multiline paste support
495 newString = self.currentLine() + newString
496 for s in newString.strip().split('\n'):
497 self.writeCode_(s+'\n')
502 def textView_willChangeSelectionFromCharacterRange_toCharacterRange_(self, aTextView, fromRange, toRange):
504 begin, length = toRange
505 if length == 0 and begin < self.characterIndexForInput():
506 # no cursor movement off the interactive line
510 def textView_doCommandBySelector_(self, aTextView, aSelector):
511 # deleteForward: is ctrl-d
512 if self.isInteracting():
513 if aSelector == 'insertNewline:':
514 self.writeCode_('\n')
516 responder = getattr(self, aSelector.replace(':','_'), None)
517 if responder is not None:
521 if DEBUG_DELEGATE and aSelector not in PASSTHROUGH:
526 # doCommandBySelector "posers" on the textView
529 def insertTabIgnoringFieldEditor_(self, sender):
530 # this isn't terribly necessary, b/c F5 and opt-esc do completion
532 sender.complete_(self)
534 def insertTab_(self, sender):
535 # this isn't terribly necessary, b/c F5 and opt-esc do completion
537 sender.complete_(self)
539 def moveToBeginningOfLine_(self, sender):
540 self.moveAndScrollToIndex_(self.characterIndexForInput())
542 def moveToEndOfLine_(self, sender):
543 self.moveAndScrollToIndex_(self.lengthOfTextView())
545 def moveToBeginningOfLineAndModifySelection_(self, sender):
546 begin, length = self.textView.selectedRange()
547 pos = self.characterIndexForInput()
548 if begin+length > pos:
549 self.textView.setSelectedRange_((pos, begin+length-pos))
551 self.moveToBeginningOfLine_(sender)
553 def moveToEndOfLineAndModifySelection_(self, sender):
554 begin, length = self.textView.selectedRange()
555 pos = max(self.characterIndexForInput(), begin)
556 self.textView.setSelectedRange_((pos, self.lengthOfTextView()))
558 def insertNewline_(self, sender):
559 line = self.currentLine()
560 self.writeCode_('\n')
561 self.executeInteractiveLine_(line)
563 moveToBeginningOfParagraph_ = moveToBeginningOfLine_
564 moveToEndOfParagraph_ = moveToEndOfLine_
565 insertNewlineIgnoringFieldEditor_ = insertNewline_
566 moveDown_ = historyDown_
570 if __name__ == '__main__':
571 AppHelper.runEventLoop()