1. Build your own bionics

Do you remember the Six Million Dollar Man? This TV show from the 70’s inspired a generation of young boys to run and fight in slow motion. Each week the opening sequence replayed Steve Austin’s near-fatal plane crash and bionic reconstruction. “We have the technology”, the unseen voice would promise, to give one person a way to do things far beyond normal human abilities.

Ten years later, a nonfictional Steve put seven million dollars into a company that promised a new way of working with computers. That way evolved into Cocoa, the object-oriented framework that we have been learning to unlock with Ruby.

In past articles, I’ve shown you how Ruby can seamlessly interact with Objective-C, Cocoa’s native language. This has let us extend Ruby with Objective-C, make a toy game with Ruby and Cocoa, rewrite existing Cocoa applications in Ruby, use Cocoa features in standalone Ruby scripts, and integrate third-party Objective-C components with Ruby.

Now we’re going to take our Ruby and Cocoa capabilities to a new level.

This article will show you how to modify a RubyCocoa application to make it easier to write Cocoa programs in Ruby with your favorite text editor. Then you’ll see how to construct windows and views directly from Ruby. That will take us to a single-file, drop-in interactive Ruby console that runs in a Cocoa text view. We’ll play with our new console to get a feeling for the power that it gives us, and then we’ll round out our toolkit with a DSL for making menus with Ruby.

Make sure that you have Xcode, Ruby, and RubyCocoa installed. I won’t spend time here explaining RubyCocoa, so if something seems missing, please go back and read my introductory article.

Ready?

Start Xcode. Select the File->New Project… menu item to create a new project. In the dialog that appears, select the Cocoa-Ruby Application project type (it’s in the group labeled Application). Press the Next button.

Name your application something that you like; you’re going to use it a lot. I called mine “rubyapp”. When you are happy with the project name and directory shown, press the Finish button. You should see a window like the one below.

This is the basic framework for a RubyCocoa application. There’s not much here: it’s mainly just two source files and a nib (Interface Builder) file.

Let’s first look at the Objective-C source file.

main.m [Objective-C]
#import <RubyCocoa/RBRuntime.h>

int main(int argc, const char *argv[])
{
    return RBApplicationMain("rb_main.rb", argc, argv);
}

It’s not that different from one you’d see in a typical Cocoa application. It doesn’t do much; all the interesting work is done in classes that we write and add to our application. Our main just launches everything with a call to a function called RBApplicationMain. Typically, a Cocoa application calls a different function, NSApplicationMain, which launches the Cocoa runtime environment. In a RubyCocoa application, RBApplicationMain first starts the Ruby runtime environment. Then it loads and runs the Ruby file whose name is passed as its first argument. In this case, it’s rb_main.rb. Let’s look at that file next:

rb_main.rb [ruby]
require 'osx/cocoa'

def rb_main_init
  path = OSX::NSBundle.mainBundle.resourcePath.fileSystemRepresentation
  rbfiles = Dir.entries(path).select {|x| /\.rb\z/ =~ x}
  rbfiles -= [ File.basename(__FILE__) ]
  rbfiles.each do |path|
    require( File.basename(path) )
  end
end

if $0 == __FILE__ then
  rb_main_init
  OSX.NSApplicationMain(0, nil)
end

Look this over for a while. Can you work out what it does? There’s a function definition at the top and a block of code at the bottom that calls it. The function, rb_main_init, uses NSBundle’s mainBundle method to get the main application bundle, then it calls resourcePath to get the path to the bundle’s Resources directory (look inside an application bundle sometime if this doesn’t make sense to you). Then it looks in that directory for all files with the .rb extension. The result goes into a list that’s assigned to the rbfiles variable. Then it removes the file being run (in this case it’s rb_main.rb) and loads each file using Ruby’s require command. After all the Ruby source files have been loaded, the script calls NSApplicationMain to start Cocoa.

Got it?

It’s nice to know what’s going on beneath the surface, especially if you ever want to do anything different. In my case, I had recently begun using TextMate to work with Ruby and had gotten hooked on using Paul Lutus’ automatic code beautifier to help me massage my code into shape.

Since TextMate had become my preferred Ruby editing environment, I decided to build a custom RubyCocoa application that I could use to run Ruby code that I wrote with TextMate outside of Xcode. Here’s how I rewrote rb_main.rb:

rb_main.rb [ruby]
require 'osx/cocoa'

def internal_resource_path
    OSX::NSBundle.mainBundle.resourcePath.fileSystemRepresentation 
end

def external_resource_path
    File.dirname(OSX::NSBundle.mainBundle.bundlePath.fileSystemRepresentation)
end

def require_all_files(path)
    rbfiles = Dir.entries(path).select {|x| /\.rb\z/ =~ x}
    rbfiles -= [ File.basename(__FILE__) ]
    rbfiles.each do |path|
	require( File.basename(path) )
    end
end

if $0 == __FILE__ then
    require_all_files internal_resource_path
    $:.push external_resource_path
    require_all_files external_resource_path
    OSX.NSApplicationMain(0, nil)
end

Now when my application starts, it will load all the Ruby files that it finds in two places: (1) the Resources directory inside the application bundle, and (2) the directory where my application resides. Now this is probably not something that you would want to do in a production Cocoa application. But we’re developers, and we’re looking for ways to write and test our code faster. Now all that I have to do to start writing a new RubyCocoa application is put a copy of this application in a directory containing Ruby source files, and we’ll be up and running right away. When I have something ready to ship or share, I can just move these files inside the application and restore the original rb_main.rb.

After you’ve replaced the contents of your rb_main.rb with what I’ve shown above, there’s one more thing for us to do. Our project contains a file called MainMenu.nib. It contains objects that get loaded when our application starts. Most of them are dummy objects that we would need to customize in Interface Builder. You can continue to use Interface Builder if you want, and some of my prior articles show how Interface Builder can be used with RubyCocoa. But now I’m going to show you another way to do things. To start, we need to open MainMenu.nib and strip out everything that we won’t need.

In your Xcode project window, double-click on the MainMenu.nib item. That will start Interface Builder and you’ll see several windows open, including one like the one below.

There’s an icon labeled “Window” in the bottom left. It corresponds to the blank window that you may see somewhere else on your screen (if you don’t see it, double-click on the icon and it will appear). Delete this. We won’t need it. You should also see a small horizontal window containing an application menu. When you click on its elements, you’ll see them expand to show you each submenu. It should look like this:

If you were using this to specify your application’s main menu, you’d have to manually change all those references to “NewApplication” to the name of your application and carefully review each submenu, one-by-one. There’s a special window called the Inspector that you can use to check each submenu item to see the action that is assigned to it.

But we’re not going to need all this. So one-by-one, select the File, Edit, Window, and Help menus and press the delete key to delete each one. That will leave only the NewApplication menu item. Select it, and then one-by-one, delete each menu item except the last one, the one titled “Quit NewApplication”. Now manually change “NewApplication” in the menu title and quit item to the name you chose for your application (Mine is called “RubyApp”). When you’ve finished, this is all that will be left of your menu:

Now you can save your nib file and exit Interface Builder. We won’t need it again.

Back in Xcode, find the Project menu and select Project -> Set Active Build Configuration -> Release (in older versions of Xcode this was called “Deployment”). Then build and run your application. There won’t be much to see; the top-level menu has a single element with one item in its submenu (our quit item). You may notice that the name of your top-level menu item is slightly different than the one in your nib file. Xcode puts the name of your project in a file called Info.plist that it puts inside your application’s bundle. Cocoa uses that name to find the application’s executable file and also automatically sets the name of the top level menu. (I don’t know why it doesn’t also automatically replace “NewApplication” in the rest of our application menus; has anyone ever asked someone at Apple about this?)

Back in the Finder, open the directory containing your Xcode project. Then open the “build” folder that you’ll see there. Inside it, you should see a “Release” folder. Open that, and you’ll find your application. Mine is called rubyapp.app. Make a new folder on your desktop and call it whatever you want. Copy your application there. For the rest of this article we’re going to build our application by adding Ruby source files to this directory. From now on, whenever I refer to “your project directory”, this is the one that I mean.

Now you are finished programming in Xcode. You can close your Xcode project, but leave Xcode running. Its Help menu leads to some great searchable Cocoa documentation that we’ll want to use as we proceed.

Did you find an error? Is something missing? Post your comment or suggestion below!

Comments (11) post
  1. Klaus Oberpichler (klaus.oberpichler@plus.cablesurf.de) Mon Aug 07 12:38:51 +0000 2006

    Hello Tim, I am trying to use textmate in combination with ruby using your version of rb_main.rb. Whenever start the application from textmate I get the following result from rubymate: “2006-08-07 21:32:17.939 ruby1674 No Info.plist file in application bundle or no NSPrincipalClass in the Info.plist file, exiting” I figured out that the functions “internal_resource_path” and “xternal_resource_path” return “usr/local”. Starting the application from xcode with the old “rb_main.rb”-file returns the correct path. To be onest I am a beginner and I do not really comprehend what I am doing wrong

    Best regards Klaus

  2. Tim Burks Mon Aug 07 15:53:51 +0000 2006

    Klaus, it sounds like you’re trying to run your RubyCocoa script directly from TextMate (maybe with command-R). That unfortunately won’t work because of some expectations that Mac OS X has about application structure. Instead, I recommend that you start your “rubyapp” application directly from the Finder.

    If you still want a way to launch your application from TextMate, check the TextMate built-in help. If you look there for information about TextMate customization, I think you’ll find a way there to run shell commands from TextMate. With this, you could associate a key equivalent with a shell command that opens your application. The shell command to run is “open rubyapp”, but you’ll probably also need to qualify “rubyapp” with its full path name. Best of luck!

  3. Dave Baldwin Thu May 31 02:15:15 +0000 2007

    Hi Tim,

    Any reason why you don’t just edit the *.rb files directly in the application bundle’s resource folder? This way you can avoid the changes to rb_main.rb. You can set up the textmate project to give a nice view into this folder and avoid seeing the *.lproj folder if you prefer.

    Great web site and resouce, by the way. Dave.

  4. Tim Burks Thu May 31 08:03:26 +0000 2007

    Dave,

    I agree—someone can do exactly what you described. Also when you this, you can edit code and reload your changes while your application is running using Ruby’s load command. That’s one of many great development features you get with an interactive console.

  5. Artie Tue Jul 17 21:10:40 +0000 2007

    Going through this tutorial, I found myself frustrated by the inability to use my usual NSTextView editing methods (like option-arrow for navigation). I realize that sort of thing has little place on a command-line simulator, but can you give any hints on how to enable the more basic functions like delete and arrow-key movement?

  6. Tim Burks Tue Jul 17 23:14:52 +0000 2007

    Artie,

    I think the place to start is the readLine method in console.rb (see section 3.2). The excerpt below provides some handling for two special keys, and support for others could be added here as well.

      if (event.modifierFlags & OSX::NSControlKeyMask) != 0:
              case event.keyCode
              when 0:  moveAndScrollToIndex(@startOfInput)     # control-a
              when 14: moveAndScrollToIndex(lengthOfTextView)  # control-e
              end
            end
    
  7. Artie Wed Jul 18 10:41:54 +0000 2007

    Tim,

    Thanks for the quick reply. Is there a way to just pass any non-special keypresses directly on to the NSTextView, without having to explicitly specify behavior for each one? It seems like a lot of wasted effort to try and re-implement something simple like “delete.”

    I noticed that the console you use in rubyobjc doesn’t seem to have this limitation—how did you get around it there?

  8. Tim Burks Thu Jul 19 13:59:56 +0000 2007

    That’s interesting; I haven’t been comparing my original rubycocoa console with the one in rubyobjc. If you’d like to do so yourself, the rubyobjc console is here on rubyforge.

  9. Artie Thu Jul 19 15:10:10 +0000 2007

    Yeah, I did a diff/merge on your RubyCocoa console and your RubyObjC console and still had the issue. I suspect it’s related to what’s being declared as a first responder and who gets the fall-through messages if the delegate doesn’t handle something. (This could be entirely wrong—I’m still wrapping my brain around Cocoa’s interface/messaging system.)

    It could also have to do with the fact that I’m hooking it up to a nib (need to have other elements than just the console in my window), whereas you’re generating everything from whole cloth in code.

    I’m going to play around with the various setups some more and will comment again with any relevant findings.

    Thanks again for your great examples and timely replies. :-)

  10. Mark Wed Dec 24 16:12:39 +0000 2008

    I get a 404 when I go to the “home page” of this article: http://www.rubycocoa.com/mastering-cocoa-with-ruby

    It also seems woid’s github page hosting this excercise as a project is no longer there.

  11. Michael Black Wed Dec 24 22:39:32 +0000 2008

    Hi Mark,

    Thanks for reporting the 404 errors. It seems to be an issue with the link provided on MacRubyResources – if you remove the ‘www’ it should fix the problem :)