[macruby-changes] [2220] MacRuby/branches/experimental/sample-macruby/HotCocoa
source_changes at macosforge.org
source_changes at macosforge.org
Wed Aug 5 04:42:46 PDT 2009
Revision: 2220
http://trac.macosforge.org/projects/ruby/changeset/2220
Author: vincent.isambart at gmail.com
Date: 2009-08-05 04:42:46 -0700 (Wed, 05 Aug 2009)
Log Message:
-----------
Imported HotConsole
Modified Paths:
--------------
MacRuby/branches/experimental/sample-macruby/HotCocoa/README
Added Paths:
-----------
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/README
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/Rakefile
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/config/
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/config/build.yml
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/application.rb
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/eval_thread.rb
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/helpers.rb
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/menu.rb
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/resources/
MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/resources/HotConsole.icns
Modified: MacRuby/branches/experimental/sample-macruby/HotCocoa/README
===================================================================
--- MacRuby/branches/experimental/sample-macruby/HotCocoa/README 2009-08-05 09:11:23 UTC (rev 2219)
+++ MacRuby/branches/experimental/sample-macruby/HotCocoa/README 2009-08-05 11:42:46 UTC (rev 2220)
@@ -43,6 +43,8 @@
download_and_progress_indicator: demo of downloading data, progress indicator
and scroll view containing a text view.
+hotconsole: an IRB-like console using WebKit.
+
Building the examples:
$ cd <example-directory>
Added: MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/README
===================================================================
--- MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/README (rev 0)
+++ MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/README 2009-08-05 11:42:46 UTC (rev 2220)
@@ -0,0 +1,9 @@
+HotConsole is an graphical IRB-like console written using HotCocoa.
+It does not use the IRB code at all
+Its advantages compared to the command line macirb:
+- you can easily enter non-ASCII characters
+- multiple windows, with code running in multiple threads
+
+A few shortcuts:
+- Alt+Up/Down to navigate in the history
+- Alt+Enter to type text on multiple lines
Added: MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/Rakefile
===================================================================
--- MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/Rakefile (rev 0)
+++ MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/Rakefile 2009-08-05 11:42:46 UTC (rev 2220)
@@ -0,0 +1,4 @@
+require 'hotcocoa/application_builder'
+require 'hotcocoa/standard_rake_tasks'
+
+task :default => [:run]
\ No newline at end of file
Added: MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/config/build.yml
===================================================================
--- MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/config/build.yml (rev 0)
+++ MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/config/build.yml 2009-08-05 11:42:46 UTC (rev 2220)
@@ -0,0 +1,8 @@
+name: HotConsole
+load: lib/application.rb
+version: "1.0"
+icon: resources/HotConsole.icns
+resources:
+ - resources/**/*.*
+sources:
+ - lib/**/*.rb
Added: MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/application.rb
===================================================================
--- MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/application.rb (rev 0)
+++ MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/application.rb 2009-08-05 11:42:46 UTC (rev 2220)
@@ -0,0 +1,322 @@
+require 'lib/eval_thread' # for EvalThread and standard output redirection
+require 'hotcocoa'
+framework 'WebKit'
+include HotCocoa
+
+# TODO:
+# - stdin (should not be very hard but it's used only very rarely so very low priority)
+# - do not perform_action if the code typed is not finished (needs a basic lexer)
+# - maybe always displays puts/writes before the prompt?
+$terminals = []
+
+class Terminal
+ def base_html
+ return <<-HTML
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+ <html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <style type="text/css"><!--
+ body, body * {
+ font-family: Monaco;
+ white-space: pre-wrap; /* in normal mode, WebKit sometimes adds nbsp when pressing space */
+ }
+ --></style>
+ </head>
+ <body></body>
+ </html>
+ HTML
+ end
+
+ def alertDidEnd alert, returnCode: return_code, contextInfo: context_info
+ return if return_code == NSAlertSecondButtonReturn # do nothing if the use presses cancel
+
+ # in all cases, first ask the thread to end nicely if possible
+ @eval_thread.end_thread
+
+ @window.close
+
+ @eval_thread.kill_running_threads if return_code == NSAlertFirstButtonReturn # kill the running code if asked
+ end
+
+ def should_close?
+ # we can always close directly is nothing is running
+ return true if command_line and not @eval_thread.children_threads_running?
+
+ alert = NSAlert.alloc.init
+ alert.messageText = "Some code is still running in this console.\nDo you really want to close it?"
+ alert.alertStyle = NSCriticalAlertStyle
+ alert.addButtonWithTitle("Close and kill")
+ alert.addButtonWithTitle("Cancel")
+ alert.addButtonWithTitle("Close and let run")
+ alert.beginSheetModalForWindow @window, modalDelegate: self, didEndSelector: "alertDidEnd:returnCode:contextInfo:", contextInfo: nil
+ false
+ end
+
+ def start
+ @line_num = 1
+ @history = [ ]
+ @pos_in_history = 0
+
+ @eval_thread = EvalThread.new(self)
+
+ frame = [300, 300, 600, 400]
+ w = NSApp.mainWindow
+ if w
+ frame[0] = w.frame.origin.x + 20
+ frame[1] = w.frame.origin.y - 20
+ end
+ @window = window(:frame => frame, :title => "HotConsole") do |win|
+ win.should_close? { self.should_close? }
+ win.will_close {
+ $terminals.delete(self)
+ @window_closed = true
+ }
+ win.did_become_main {
+ # we want order in $terminals to be
+ # last used terminal to most recent used
+ $terminals.delete(self)
+ $terminals << self
+ }
+ win.contentView.margin = 0
+ @web_view = web_view(:layout => {:expand => [:width, :height]})
+ @web_view.editingDelegate = self # for webView:doCommandBySelector:
+ @web_view.frameLoadDelegate = self # for webView:didFinishLoadForFrame:
+ @web_view.mainFrame.loadHTMLString base_html, baseURL: nil
+ win << @web_view
+ end
+ class << @window
+ attr_accessor :terminal
+ end
+ @window.terminal = self
+ @window_closed = false
+ $terminals << self
+ end
+
+ def display_intro_message
+ write <<-END_MESSAGE
+Welcome to HotConsole!
+Shortcuts:
+- Alt+Up/Down to navigate in the history
+- Alt+Enter to type text on multiple lines
+- Command+K to clean up the terminal
+ END_MESSAGE
+ end
+
+ def empty_body
+ document.body.innerHTML = ''
+ end
+
+ def clear
+ if command_line
+ @next_prompt_command = command_line.innerText
+ empty_body
+ write_prompt
+ else
+ @next_prompt_command = nil
+ empty_body
+ end
+ end
+
+ def webView view, didFinishLoadForFrame: frame
+ # we must be sure the body is really empty because of the preservation of white spaces
+ # we can easily have a carriage return left in the HTML
+ empty_body
+ display_intro_message
+ write_prompt
+ end
+
+ # return the HTML document of the main frame
+ def document
+ @web_view.mainFrame.DOMDocument
+ end
+
+ # returns the DOM node for the command line
+ # (or nil if there is no command line currently)
+ def command_line
+ document.getElementById('command_line')
+ end
+
+ # move the carret of the command line to its first character
+ def command_line_carret_to_beginning
+ cl = command_line
+ range = document.createRange
+ range.setStart cl, offset: 0
+ range.setEnd cl, offset: 0
+ @web_view.setSelectedDOMRange range, affinity: NSSelectionAffinityUpstream
+ end
+
+ def display_history
+ command_line.innerText = @history[@pos_in_history] || ''
+ # if we do not move the caret to the beginning,
+ # we lose the focus if the command line is emptied
+ command_line_carret_to_beginning
+ end
+
+ # callback called when a command by selector is run on the WebView
+ def webView webView, doCommandBySelector: command
+ command = command.to_s
+ if command == 'insertNewline:' # Return
+ perform_action
+ elsif command == 'moveBackward:' # Alt+Up Arrow
+ if @pos_in_history > 0
+ @pos_in_history -= 1
+ display_history
+ end
+ elsif command == 'moveForward:' # Alt+Down Arrow
+ if @pos_in_history < @history.length
+ @pos_in_history += 1
+ display_history
+ end
+ # moveToBeginningOfParagraph and moveToEndOfParagraph are also sent by Alt+Up/Down
+ # but we must ignore them because they move the cursor
+ elsif command != 'moveToBeginningOfParagraph:' and command != 'moveToEndOfParagraph:'
+ return false
+ end
+ true
+ end
+
+ # simply writes (appends) a DOM element to the WebView
+ def write_element(element)
+ document.body.appendChild(element)
+ end
+
+ def write(obj)
+ if obj.respond_to?(:to_str)
+ text = obj
+ else
+ text = obj.to_s
+ end
+
+ if @window_closed
+ # if the window was closed while code that printed text was still running,
+ # the text is displayed on the most recently used terminal if there is one,
+ # or the standard error output if no terminal was found
+ target = $terminals.last || STDERR
+ target.write(text)
+ else
+ # if the window is still opened, just put the text
+ # in a DOM span element and writes it on the WebView
+ span = document.createElement('span')
+ span.innerText = text
+ write_element(span)
+ end
+ end
+
+ # puts is just a write of the text followed by a carriage return and that returns nil
+ def puts(obj)
+ if obj.respond_to?(:to_ary)
+ obj.each { |elem| puts elem }
+ else
+ # we do not call just write because of encoding/string problems
+ # and because Ruby itself does it in two calls to write
+ write obj
+ write "\n"
+ end
+ end
+
+ def scroll_to_bottom
+ body = document.body
+ body.scrollTop = body.scrollHeight
+ @web_view.setNeedsDisplay true
+ end
+
+ def end_edition
+ if command_line
+ command_line.setAttribute('contentEditable', value: nil)
+ command_line.setAttribute('id', value: nil)
+ end
+ end
+
+ def write_prompt
+ end_edition
+
+ table = document.createElement('table')
+ row = table.insertRow(0)
+ prompt = row.insertCell(-1)
+ prompt.setAttribute('style', value: 'vertical-align: top;')
+ prompt.innerText = '>>'
+ typed_text = row.insertCell(-1)
+ typed_text.setAttribute('contentEditable', value: 'true')
+ typed_text.setAttribute('id', value: 'command_line')
+ typed_text.setAttribute('style', value: 'width: 100%;')
+ if @next_prompt_command
+ typed_text.innerText = @next_prompt_command
+ @next_prompt_command = nil
+ end
+ write_element(table)
+
+ command_line.focus
+ scroll_to_bottom
+ end
+
+ # executes the code written on the prompt when the user validates it with return
+ def perform_action
+ current_line_number = @line_num
+ command = command_line.innerText
+ @line_num += command.count("\n")+1
+ if command.strip.empty?
+ write_prompt
+ return
+ end
+
+ @history.push(command)
+ @pos_in_history = @history.length
+
+ # the code is sent to an other thread that will do the evaluation
+ @eval_thread.send_command(current_line_number, command)
+ # the user must not be able to modify the prompt until the command ends
+ # (when back_from_eval is called)
+ end_edition
+ end
+
+ # back_from_eval is called when the evaluation thread has finished its evaluation of the given code
+ # text is either the representation of the value returned by the code executed, or the backtrace of an exception
+ def back_from_eval(text)
+ # if the window was closed while code was still executing,
+ # we can just ignore the call because there is no need
+ # to print the result and a new prompt
+ return if @window_closed
+ write text
+ write_prompt
+ end
+end
+
+class Application
+ def start
+ application :name => "HotConsole" do |app|
+ app.delegate = self
+ start_terminal
+ end
+ end
+
+ def on_new(sender)
+ start_terminal
+ end
+
+ def on_close(sender)
+ w = NSApp.mainWindow
+ if w
+ w.performClose self
+ else
+ NSBeep()
+ end
+ end
+
+ def on_clear(sender)
+ w = NSApp.mainWindow
+ if w and w.respond_to?(:terminal)
+ w.terminal.clear
+ else
+ NSBeep()
+ end
+ end
+
+ private
+
+ def start_terminal
+ Terminal.new.start
+ end
+end
+
+Application.new.start
Added: MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/eval_thread.rb
===================================================================
--- MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/eval_thread.rb (rev 0)
+++ MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/eval_thread.rb 2009-08-05 11:42:46 UTC (rev 2220)
@@ -0,0 +1,162 @@
+require 'thread' # for Queue
+require 'lib/helpers' # for Object#send_on_main_thread
+
+# EvalThread is the class that does all the code evaluation.
+# The code is evaluated in a new thread mainly for 2 reasons:
+# - so the application is not frozen if a commands takes too much time
+# - to be able to find where to write the text for the standard output (using thread variables)
+class EvalThread
+ # Writer is the class to manage the standard output.
+ # The difficult part is to find the good target to print to,
+ # especially if the user starts new threads
+ # warning: It won't work if the current thread is a NSThread or pure C thread,
+ # but anyway for the moment NSThreads do no seem to work well in MacRuby
+ class Writer
+ # sets the target where to write when something is written on the standard output
+ def self.set_stdout_target_for_thread(target)
+ Thread.current[:_irb_stdout_target] = target
+ end
+
+ # a standard output object has only one mandatory method: write.
+ # it generally returns the number of characters written
+ def write(obj)
+ if obj.respond_to?(:to_str)
+ str = obj
+ else
+ str = obj.to_s
+ end
+ find_target_and_call :write, str
+ str.length
+ end
+
+ # if puts is not there, Ruby will automatically use the write
+ # method when calling Kernel#puts, but defining it has 2 advantages:
+ # - if puts is not defined, you cannot of course use $stdout.puts directly
+ # - when Ruby emulates puts, it calls write twice
+ # (once for the string and once for the carriage return)
+ # but here we send the calls to another thread so it's nice
+ # to be able to save up one (slow) interthread call
+ def puts(*args)
+ find_target_and_call :puts, args
+ nil
+ end
+
+ private
+
+ # the core of write/puts: tries to find the target where to
+ # write the text and calls the indicated function on it.
+ # it returns the number of characters in the given string
+ def find_target_and_call(function_name, obj)
+ current_thread = Thread.current
+ target = current_thread[:_irb_stdout_target]
+
+ # first, if we have a target in the thread, use it
+ return target.send_on_main_thread(function_name, obj) if target
+
+ # if we do not have any target, search for a target in every thread in the ThreadGroup
+ if group = current_thread.group
+ group.list.each do |thread|
+ return target.send_on_main_thread(function_name, obj) if target = thread[:_irb_stdout_target]
+ end
+ end
+
+ # if we still do not have any target, try to get the most recently used and opened terminal
+ target = $terminals.last
+ return target.send_on_main_thread(function_name, obj) if target
+
+ # if we do not find any target, just write it on STDERR
+ STDERR.send(function_name, obj)
+ end
+ end
+ # replace Ruby's standard output
+ $stdout = Writer.new
+
+ # sends a command to evaluate.
+ # the line_number is not computed internally because the empty lines
+ # are not sent to the eval thread but still increase the line number
+ def send_command(line_number, command)
+ @queue_for_commands.push([line_number, command])
+ end
+
+ # asks the thread to end.
+ # this operation is not immediate because if an operation is
+ # still working in the thread, we want to let it finish
+ def end_thread
+ @queue_for_commands.push([nil, :END])
+ end
+
+ # kill the evaluation thread and its children
+ def kill_running_threads
+ @thread_group.list.each do |thread|
+ thread.kill if thread != @thread
+ end
+ @thread.kill if @thread.alive? # kill the main thread last
+ end
+
+ def children_threads_running?
+ if @thread.alive?
+ @thread_group.list.count > 1
+ else
+ @thread_group.list.count > 0
+ end
+ end
+
+ # starts a new EvalThread in a separate thread
+ # the target is the terminal where the standard output and prompt are written.
+ # target must have 3 methods: write, puts and back_from_eval, all taking a string argument
+ def initialize(target)
+ @target = target
+ @queue_for_commands = Queue.new
+ @binding = TOPLEVEL_BINDING.dup
+ @underscore_assigner = eval("_ = nil; proc { |val| _ = val }", @binding)
+ @thread = Thread.new { run }
+ end
+
+ private
+
+ # tells the target eval has finished and it can show a new prompt
+ def back_from_eval(text)
+ @target.send_on_main_thread :back_from_eval, text
+ end
+
+ def run
+ # create a new ThreadGroup and sets it as the group for the current thread.
+ # the ThreadGroup allows us to find the parent thread when the standard output is used
+ # from a thread created by the user and not by us as new threads are automatically added
+ # to the ThreadGroup of their parent thread
+ @thread_group = ThreadGroup.new
+ @thread_group.add(Thread.current)
+
+ # when some code in the thread uses the standard output, we want the text
+ # to be printed to the current target (the current terminal)
+ Writer.set_stdout_target_for_thread(@target)
+
+ # evaluation loop
+ loop do
+ # waits for the next command to evaluate
+ line_num, command = @queue_for_commands.pop
+ break if command == :END
+
+ # eval_file and eval_line are used to clean up the backtrace of catched exceptions
+ eval_file = __FILE__
+ eval_line = -1
+ begin
+ # eval_line must have exactly the line number where the eval call occurs
+ eval_line = __LINE__; value = eval(command, @binding, 'hotconsole', line_num)
+ @underscore_assigner.call(value)
+ back_from_eval "=> #{value.inspect}\n"
+ rescue Exception => e
+ backtrace = e.backtrace
+ # we try to remove the backtrace of the call to eval itself
+ # because it only confuses the user
+ i = backtrace.index { |l| l.index("#{eval_file}:#{eval_line}") }
+ if i == 0
+ backtrace = []
+ elsif i
+ backtrace = backtrace[0..i-1]
+ end
+ back_from_eval "#{e.class.name}: #{e.message}\n" + (backtrace.empty? ? '' : "#{backtrace.join("\n")}\n")
+ end
+ end
+ end
+end
Added: MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/helpers.rb
===================================================================
--- MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/helpers.rb (rev 0)
+++ MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/helpers.rb 2009-08-05 11:42:46 UTC (rev 2220)
@@ -0,0 +1,9 @@
+class Object
+ # calls a method on the object on the main thread
+ def send_on_main_thread(function_name, parameter = nil, asynchronous = true)
+ function_name = function_name.to_s
+ # if the target method has a parameter, we have to be sure the method name ends with a ':'
+ function_name << ':' if parameter and not /:$/.match(function_name)
+ performSelectorOnMainThread function_name, withObject: parameter, waitUntilDone: (not asynchronous)
+ end
+end
Added: MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/menu.rb
===================================================================
--- MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/menu.rb (rev 0)
+++ MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/lib/menu.rb 2009-08-05 11:42:46 UTC (rev 2220)
@@ -0,0 +1,46 @@
+module HotCocoa
+ def application_menu
+ menu do |main|
+ main.submenu :apple do |apple|
+ apple.item :about, :title => "About #{NSApp.name}"
+ apple.separator
+ apple.item :preferences, :key => ","
+ apple.separator
+ apple.submenu :services
+ apple.separator
+ apple.item :hide, :title => "Hide #{NSApp.name}", :key => "h"
+ apple.item :hide_others, :title => "Hide Others", :key => "h", :modifiers => [:command, :alt]
+ apple.item :show_all, :title => "Show All"
+ apple.separator
+ apple.item :quit, :title => "Quit #{NSApp.name}", :key => "q"
+ end
+ main.submenu :file do |file|
+ file.item :new, :key => "n"
+ file.item :close, :key => "w", :modifiers => [:command]
+ end
+ main.submenu :edit do |edit|
+ edit.item :undo, :key => "z", :modifiers => [:command], :action => "undo:"
+ edit.item :redo, :key => "z", :modifiers => [:command, :shift], :action => "redo:"
+ edit.separator
+ edit.item :cut, :key => "x", :action => "cut:"
+ edit.item :copy, :key => "c", :action => "copy:"
+ edit.item :paste, :key => "v", :action => "paste:"
+ end
+ main.submenu :view do |view|
+ view.item :bigger, :key => "+", :modifiers => [:command], :action => "makeTextLarger:"
+ view.item :smaller, :key => "-", :modifiers => [:command], :action => "makeTextSmaller:"
+ view.separator
+ view.item :clear, :key => "k", :modifiers => [:command]
+ end
+ main.submenu :window do |win|
+ win.item :minimize, :key => "m"
+ win.item :zoom
+ win.separator
+ win.item :bring_all_to_front, :title => "Bring All to Front", :key => "o"
+ end
+ main.submenu :help do |help|
+ help.item :help, :title => "#{NSApp.name} Help"
+ end
+ end
+ end
+end
Added: MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/resources/HotConsole.icns
===================================================================
(Binary files differ)
Property changes on: MacRuby/branches/experimental/sample-macruby/HotCocoa/hotconsole/resources/HotConsole.icns
___________________________________________________________________
Added: svn:executable
+ *
Added: svn:mime-type
+ application/octet-stream
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/macruby-changes/attachments/20090805/cfcae315/attachment-0001.html>
More information about the macruby-changes
mailing list