# Copyright (c) 2007 The RubyCocoa Project.
# Copyright (c) 2006 Tim Burks, Neon Design Technology, Inc.
#
# Find more information about this file online at:
# http://www.rubycocoa.com/mastering-cocoa-with-ruby
require 'irb'
require 'osx/cocoa'
class ConsoleWindowController < OSX::NSObject
attr_accessor :window, :textview, :console
def initWithFrame(frame)
init
styleMask = OSX::NSTitledWindowMask + OSX::NSClosableWindowMask +
OSX::NSMiniaturizableWindowMask + OSX::NSResizableWindowMask
@window = OSX::NSWindow.alloc.initWithContentRect_styleMask_backing_defer(
frame, styleMask, OSX::NSBackingStoreBuffered, false)
@textview = OSX::NSTextView.alloc.initWithFrame(frame)
@console = RubyConsole.alloc.initWithTextView @textview
with @window do |w|
w.contentView = scrollableView(@textview)
w.title = "RubyCocoa Console"
w.delegate = self
w.center
w.makeKeyAndOrderFront(self)
end
self
end
def run
@console.performSelector_withObject_afterDelay("run:", self, 0)
end
def windowWillClose(notification)
OSX::NSApplication.sharedApplication.terminate(self)
end
def windowShouldClose(notification)
@alert = OSX::NSAlert.alloc.init
with @alert do |a|
a.messageText = "Do you really want to close this console?\n"
+ "Your application will exit."
a.alertStyle = OSX::NSCriticalAlertStyle
a.addButtonWithTitle("OK")
a.addButtonWithTitle("Cancel")
a.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo(
window, self, "alertDidEnd:returnCode:contextInfo:", nil)
end
false
end
def alertDidEnd_returnCode_contextInfo(alert, code, contextInfo)
window.close if (code == OSX::NSAlertFirstButtonReturn)
end
end
def with(x)
yield x if block_given?; x
end if not defined? with
def scrollableView(content)
scrollview = OSX::NSScrollView.alloc.initWithFrame(content.frame)
clipview = OSX::NSClipView.alloc.initWithFrame(scrollview.frame)
scrollview.contentView = clipview
scrollview.documentView = clipview.documentView = content
content.frame = clipview.frame
scrollview.hasVerticalScroller = scrollview.hasHorizontalScroller =
scrollview.autohidesScrollers = true
resizingMask = OSX::NSViewWidthSizable + OSX::NSViewHeightSizable
content.autoresizingMask = clipview.autoresizingMask =
scrollview.autoresizingMask = resizingMask
scrollview
end
class RubyCocoaInputMethod < IRB::StdioInputMethod
def initialize(console)
IRB.init_config(nil)
super() # superclass method has no arguments
@console = console
@history_index = 1
@continued_from_line = nil
end
def gets
m = @prompt.match(/(\d+)[>*]/)
level = m ? m[1].to_i : 0
if level > 0
@continued_from_line ||= @line_no
elsif @continued_from_line
mergeLastNLines(@line_no - @continued_from_line + 1)
@continued_from_line = nil
end
@console.write @prompt+" "*level
string = @console.readLine
@line_no += 1
@history_index = @line_no + 1
@line[@line_no] = string
string
end
def mergeLastNLines(i)
return unless i > 1
range = -i..-1
@line[range] = @line[range].map {|l| l.chomp}.join("\n")
@line_no -= (i-1)
@history_index -= (i-1)
end
def prevCmd
return "" if @line_no == 0
@history_index -= 1 unless @history_index <= 1
@line[@history_index]
end
def nextCmd
return "" if (@line_no == 0) or (@history_index >= @line_no)
@history_index += 1
@line[@history_index]
end
end
# this is an output handler for IRB
# and a delegate and controller for an NSTextView
class RubyConsole < OSX::NSObject
attr_accessor :textview, :inputMethod
def initWithTextView(textview)
init
@textview = textview
@textview.delegate = self
@textview.richText = false
@textview.continuousSpellCheckingEnabled = false
@textview.font = @font = OSX::NSFont.fontWithName_size('Monaco', 18.0)
@inputMethod = RubyCocoaInputMethod.new(self)
@context = Kernel::binding
@startOfInput = 0
self
end
def run(sender = nil)
@textview.window.makeKeyAndOrderFront(self)
IRB.startInConsole(self)
OSX::NSApplication.sharedApplication.terminate(self)
end
def attString(string)
OSX::NSAttributedString.alloc.initWithString_attributes(
string, { OSX::NSFontAttributeName => @font })
end
def write(object)
string = object.to_s
@textview.textStorage.insertAttributedString_atIndex(
attString(string), @startOfInput)
@startOfInput += string.length
@textview.scrollRangeToVisible([lengthOfTextView, 0])
handleEvents if OSX::NSApplication.sharedApplication.isRunning
end
def moveAndScrollToIndex(index)
range = OSX::NSRange.new(index, 0)
@textview.scrollRangeToVisible(range)
@textview.setSelectedRange(range)
end
def lengthOfTextView
@textview.textStorage.mutableString.length
end
def currentLine
text = @textview.textStorage.mutableString
text.substringWithRange(
OSX::NSRange.new(@startOfInput, text.length - @startOfInput)).to_s
end
def readLine
app = OSX::NSApplication.sharedApplication
@startOfInput = lengthOfTextView
loop do
event = app.nextEventMatchingMask_untilDate_inMode_dequeue(
OSX::NSAnyEventMask,
OSX::NSDate.distantFuture(),
OSX::NSDefaultRunLoopMode,
true)
if (event.oc_type == OSX::NSKeyDown) and
event.window and
(event.window.isEqual? @textview.window)
break if event.characters.to_s == "\r"
if (event.modifierFlags & OSX::NSControlKeyMask) != 0
case event.keyCode
when 0
moveAndScrollToIndex(@startOfInput) # control-a
when 14
moveAndScrollToIndex(lengthOfTextView) # control-e
end
end
end
app.sendEvent(event)
end
lineToReturn = currentLine
@startOfInput = lengthOfTextView
write("\n")
return lineToReturn + "\n"
end
def handleEvents
app = OSX::NSApplication.sharedApplication
event = app.nextEventMatchingMask_untilDate_inMode_dequeue(
OSX::NSAnyEventMask,
OSX::NSDate.dateWithTimeIntervalSinceNow(0.01),
OSX::NSDefaultRunLoopMode,
true)
if event
if (event.oc_type == OSX::NSKeyDown) and
event.window and
(event.window.isEqualTo @textview.window) and
(event.charactersIgnoringModifiers.to_s == 'c') and
(event.modifierFlags & OSX::NSControlKeyMask)
raise IRB::Abort, "abort, then interrupt!!" # that's what IRB says...
else
app.sendEvent(event)
end
end
end
def replaceLineWithHistory(s)
range = OSX::NSRange.new(@startOfInput, lengthOfTextView - @startOfInput)
@textview.textStorage.replaceCharactersInRange_withAttributedString(
range, attString(s.chomp))
@textview.scrollRangeToVisible([lengthOfTextView, 0])
true
end
# delegate methods
def textView_shouldChangeTextInRange_replacementString(
textview, range, replacement)
return false if range.location < @startOfInput
replacement = replacement.to_s.gsub("\r","\n")
if replacement.length > 0 and replacement[-1].chr == "\n"
@textview.textStorage.appendAttributedString(
attString(replacement)
) if currentLine != ""
@startOfInput = lengthOfTextView
false # don't insert replacement text because we've already inserted it
else
true # caller should insert replacement text
end
end
def textView_willChangeSelectionFromCharacterRange_toCharacterRange(
textview, oldRange, newRange)
return oldRange if (newRange.length == 0) and
(newRange.location < @startOfInput)
newRange
end
def textView_doCommandBySelector(textview, selector)
case selector
when "moveUp:"
replaceLineWithHistory(@inputMethod.prevCmd)
when "moveDown:"
replaceLineWithHistory(@inputMethod.nextCmd)
else
false
end
end
end
module IRB
def IRB.startInConsole(console)
IRB.setup(nil)
@CONF[:PROMPT_MODE] = :SIMPLE
@CONF[:VERBOSE] = false
@CONF[:ECHO] = true
irb = Irb.new(nil, console.inputMethod)
@CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
@CONF[:MAIN_CONTEXT] = irb.context
trap("SIGINT") do
irb.signal_handle
end
old_stdout, old_stderr = $stdout, $stderr
$stdout = $stderr = console
catch(:IRB_EXIT) do
loop do
begin
irb.eval_input
rescue Exception
puts "Error: #{$!}"
end
end
end
$stdout, $stderr = old_stdout, old_stderr
end
class Context
def prompting?
true
end
end
end
class ApplicationDelegate < OSX::NSObject
def applicationDidFinishLaunching(sender)
$consoleWindowController = ConsoleWindowController.alloc.
initWithFrame([50,50,600,300])
$consoleWindowController.run
end
end
# set up the application delegate
$delegate = ApplicationDelegate.alloc.init
OSX::NSApplication.sharedApplication.setDelegate($delegate)