Cobalt Edge

 
Filed under

RubyCocoa

 

Speech / Talking Results for RSpactor

After discovering that autospec was taking up a lot of CPU while it was "idle", I looked for alternatives. I found RSpactor, which doesn't take as much CPU, and is better since it's a dedicated window with nice GUI results and so. My only gripe was that I'd gotten used to the spoken results output I'd rigged up for Autospec.

I really prefer the spoken results because it is not visually distracting, and doesn't require me to be paying attention to the area of the screen where they pop up (I use a 30" monitor, plus the 17" laptop monitor, so I'm not always looking at the right spot for the Growl notices, and I don't like the monitor-wide growls). Lucky for me, RSpactor is open source and is up on GitHub.

I thought it was an Objective-C Cocoa app, but as it turns out it's a RubyCocoa app. I'd built RubyCocoa apps before, so was familiar with that, plus of course know Ruby. It wouldn't have mattered either way (I'm fine working in ObjC as well), but this did make things a slight bit faster.

Anyway, did a quick bit of work and got a new preferences panel for Speech added, and then rigged that up to test results, so that I now have my desired spoken results. A slight improvement comes along in that it (optionally) speaks the number of passing/failing/pending tests - just insert a question mark in the string/phrase you want spoken for each and it'll say the number at that spot.

I've sent a pull request to RubyPhunk, but no guarantees it will get added to the main line. In the mean time, if you're interested, grab it from my RSpactor fork on GitHub. Update: RubyPhunk integrated my changes into the main RSpactor code.

Loading mentions Retweet
Filed under  //   ContinuousIntegration   RSpactor   RSpec   Ruby   RubyCocoa   Testing  

Comments [0]

Creating Sparkle Appcast via Rake Tasks

I have a RubyCocoa application that self-updates using Sparkle. To do so, you need to create an "appcast" file which contains the version and download information for your application, as well as creating the zip file that holds your app. Then, you of course have to upload this to the server and location that you have specified in the SUFeedURL key value in your Info.plist file of your app. For general instructions on using Sparkle and setting it up, see their Basic Instructions page.

My Rake tasks do not create the zip file. I may enhance it to do this at some point, but so far I haven't needed to, and have had cases where I need to create it myself for various reasons. What the tasks do is to build an appcast.xml file from a YAML file that contains all the necessary information. Note that the name of my app is "Linker", so you'll see that in various spots. The tasks do rely on a simple directory structure:


  • Your app root directory


    • Rakefile

    • appcast


      • version_info.yml

      • build


        • Your app zip files go here (e.g. Linker_0.8.zip, Linker_0.9.zip, etc.)

        • Rake task will create the linker_appcast.xml file here




So, you have a spot you drop your zip files into, and this same dir is where the Rake tasks create the appcast file. The version_info.yml file is where you put the info needed to generate the appcast. It looks like this:


linker-04:
title: Linker 0.4
filename: Linker_0.4.zip
description: Added Sparkle updating mechanism.

linker-05:
title: Linker 0.5
filename: Linker_0.5.zip
description: Added help (see Help menu). Added bookmarklet support/custom URL protocol handling. See the new help for information on how to use the bookmarklet.

Note that you can put HTML into the "description" field, and my Rake task will deal with and preserve that.

Finally, I have two Rake tasks, one for building the appcast, and the other for uploading it and the latest zip file to the server. These each are simply one liners that call a parallel Ruby method within the Rakefile:


namespace :appcast do
desc "Create/update the appcast file"
task :build do
make_appcast
end

desc "Upload the appcast file to the server"
task :upload do
upload_appcast
end
end

The two methods rely on you defining a couple of variables in your Rakefile, adjust these as desired:


APPCAST_SERVER = 'your_appcast_server.com'
APPCAST_URL = "http://#{APPCAST_SERVER}"
APPCAST_FILENAME = 'linker_appcast.xml'

Here are the methods, first the one that builds the appcast, which you'll need to modify for your app:


def make_appcast
begin
versions = YAML.load_file("appcast/version_info.yml")
rescue Exception => e
raise StandardError, "appcast/version_info.yml could not be loaded: #{e.message}"
end

appcast = File.open("appcast/build/#{APPCAST_FILENAME}", 'w')

xml = Builder::XmlMarkup.new(:target => appcast, :indent => 2)

xml.instruct!
xml.rss('xmlns:atom' => "http://www.w3.org/2005/Atom",
'xmlns:sparkle' => "http://www.andymatuschak.org/xml-namespaces/sparkle",
:version => "2.0") do
xml.channel do
xml.title('BWA Linker')
xml.link(APPCAST_URL)
xml.description('Linker app updates')
xml.language('en')
xml.pubDate(Time.now.rfc822)
xml.lastBuildDate(Time.now.rfc822)
xml.atom(:link, :href => "#{APPCAST_URL}/#{APPCAST_FILENAME}",
:rel => "self", :type => "application/rss+xml")

versions.each do |version|
guid = version.first
items = version[1]
file = "appcast/build/#{items['filename']}"

xml.item do
xml.title(items['title'])
xml.description { xml xml.pubDate(File.mtime(file))
xml.enclosure(:url => "#{APPCAST_URL}/#{items['filename']}",
:length => "#{File.size(file)}", :type => "application/zip")
xml.guid(guid, :isPermaLink => "false")
end
end
end
end
end


Looking through that above, you'll want to modify the title and description at least. Now on to the uploader method:

def upload_appcast
remote_dir = "/var/www/apps/bwa/shared/public/updaters/"

Net::SSH.start( APPCAST_SERVER, 'deploy' ) do |session|
cwd = Dir.pwd
Dir.chdir('appcast/build')

shell = session.shell.sync

begin
out = shell.cd remote_dir
raise "Failed to change to proper remote directory." unless out.status == 0

out = shell.ls("-1")
raise "Failed to get directory listing." unless out.status == 0

files = Array.new
out.stdout.each { |file| files
# Look through the list of files and see what we need to upload, as
# compared to what we have locally - but always upload the appcast itself
local_files = Dir.glob('*')
files.delete(APPCAST_FILENAME) # we always upload this
local_files.each do |local_file|
unless files.include?(local_file)
print "Uploading: #{local_file}... "
`scp #{local_file} deploy@#{APPCAST_SERVER}:#{remote_dir}`
puts $?.exitstatus == 0 ? "done." : "FAILED!"
end
end
rescue => e
puts "Failed: #{e.message}"
ensure
Dir.chdir(cwd)
shell.exit
end
end
end


You will of course want to modify the remote_dir, and the login credentials towards the bottom (where it does the scp command). This also relies on you having your SSH keys set up, so you don't have to enter a password when it does the scp.

You could further generalize this obviously, but this is what I have, it works fine, and I haven't needed to extract anything out further. Posting here as per a request, and hopefully it saves someone else a few minutes.

Loading mentions Retweet
Filed under  //   Cocoa   Ruby   RubyCocoa   Sparkle  

Comments [0]

RubyCocoa Rocks

My infatuation with RubyCocoa continues. I've been working on a small app for the Building Web Apps folks. I'd originally been tasked with a feature that was to be done 100% within the web app. However, some of our requirements changed, and the workflow was not efficient enough. So, essentially what we moved forward on was a desktop Mac application that could interface with NetNewsWire, as well as the Building Web Apps site. The desktop app gives us a faster way to add data into the system - rapidly processing a ton of content and injecting what is desired into the web app, yet without getting slowed down by a web app interface. AJAX or Flex, or some other web UI tech wouldn't make it any faster in this particular situation.

Thus, I pursued building a Cocoa app, but this time using RubyCocoa. I've written Objective-C apps before, and spend the bulk of my time in Ruby, but this was my first opportunity to use RubyCocoa. The combination, much like JRuby, Jython and other hybrid systems, gives you "the best of both worlds." There are cons of course (slower, a few Cocoa things you can't do, debugging is harder, etc.), but for the most part, it's really nice.

For me, the infatuation stems from letting me use the aspects of each system that I am either more efficient with, or that are easier for a particular piece of functionality, all yielding a faster, and better end result. I can iterate on the app more quickly, and get a solution to BWA faster. And, one step further in the case of RubyCocoa: having the native OS integration abilities at hand.

What's been great is the ability to use Ruby's more effective (for me at least) string processing, XML processing, and networking features; create a native Mac application - using any cool Cocoa/native features; and support for AppleScript, which was critical for this particular application. This latter feature ruled out using something like Adobe Air.

This evening I setup the second use of AppleScript/Apple Events in this app: registering a custom URL protocol for the app. Applications like Pukka and Mailplane do this. What for? Well, in this case, it allows me to create a web browser bookmarklet, that can send data from the browser to our app. It also means that it works in basically any browser on the Mac (as opposed to AppleScript not working for Firefox). Further, it allows a simple "push a button" in the browser to send the data over to the app, as opposed to having to switch to the app, pick a menu item to pull the data, or horror of horrors, copy-paste.

How do you do this? This article is short and covers how to do it in Objective-C. It's just as easy in RubyCocoa: You need to add an entry into your Info.plist file to specify the name of your URL protocol as described in the article. Then, you need to register your app as a handler for that protocol:


NSAppleEventManager.sharedAppleEventManager.
setEventHandler_andSelector_forEventClass_andEventID_(
self, :getUrl_withReplyEvent, fourcharcode('GURL'), fourcharcode('GURL'))

The fourcharcode method is my way of translating four character codes for use in Ruby. I discussed this in more detail in my last post about RubyCocoa, but here's the actual method for your pleasure:


def fourcharcode(character_code)
character_code.unpack('N').first
end

Ok, so now that you've registered your app to handle its custom URL protocol, you will get an Apple Event sent to you with the URL whenever one is opened. This is handled (as per the parameter in the registration function above), by the getUrl_withReplyEvent method:


def getUrl_withReplyEvent(event, reply)
url = event.paramDescriptorForKeyword(fourcharcode('----')).stringValue
# url now contains the complete URL as a string
# do your processing of the URL/content...
end

That's it. Pretty cool eh? Handling events from NetNewsWire is almost identical (register for them, write a handler function).

And, one more great thing I could integrate: Sparkle. Sparkle is a superb Cocoa library that does automatic application updates. It checks the web for a newer version of your app, downloads it, and installs it. Integrating it is simple, and in fact, depending on your needs, you don't have to write a single line of code. The only code I wrote for it was actually a Rake task to build the appcast and upload it to the server. Slick.

All this could only be done as a native Mac app, which means Cocoa. But, as said above, doing it with RubyCocoa gives me access to all these abilities, yet, I can do all the more heavy string and XML processing I need to do using Ruby, which is much more effective for me. Also, the web services calls and code is a lot easier for me to do in Ruby than Cocoa.

Furthermore, this is plain fun! Unlike some, I feel desktop apps still have a place, but love webapps at the same time. With RubyCocoa I can build super cool Mac apps, but do so in a language I'm happier using, yet have the power of Cocoa available to me. For me, some of the best "applications" these days are such hybrids: a web app that does your primary data storage, and gives you access to the app from "anywhere" (i.e. anywhere you can get to a browser and net connection), but a desktop app to use most of the time for faster interaction, potentially better integration on your desktop system, and so on. It's the same reason I use Mailplane (desktop app for Gmail), or PackRat (desktop app for Backpack). I suspect it's the same reason we're seeing other solutions like Adobe Air, or Google Gears. Technology is so cool, isn't it?!

Loading mentions Retweet
Filed under  //   Adobe Air   Cocoa   Ruby   RubyCocoa  

Comments [0]

Building My First RubyCocoa App - Some Notes

Today I started work on my first app built using RubyCocoa, which is now a first class citizen in MacOS X 10.5/Leopard. I had read the docs and tutorial found here. I proceeded, and ran into a few bumps along the way, so here are some notes maybe someone else will find useful...

The code for the example app RSSPhotoViewer, is not quite the same as that shown in the tutorial. Specifically:


  • The tutorial says you need to put "ib_action :method_name" after your methods that are Actions. The example code does not do that, and I found I didn't need to do it either.

  • The example code does not require osx/cocoa or include OSX, yet I had to do this in my Ruby source code in order for it to recognize the class names properly (or at least so I didn't have to prefix them with OSX).

I couldn't get Interface Builder to recognize my Ruby window controller class - i.e. it didn't show it's outlets and actions. I tried a variety of things here, but basically I finally had to go to the command line and run "rake" and let it do a command line build. I have no idea what that did differently, as I can't see any new files it generated, etc., but that resolved it - now IB can see all my outlets and actions.

My app wouldn't run, and I got a strange error in Xcode saying "The debugger is still running" etc. It appears that if your app crashes this will be the case. And, in this case, Console is your friend. Open up Console and you should see messages that will help you assess what's gone wrong.

And now for the win... dealing with Apple Events. My app wants to receive a particular apple event from NetNewsWire. NNW documents this protocol nicely, however, when registering as a handler for the event (using NSAppleEventManager.sharedAppleEventManager.setEventHandler_andSelector_forEventClass_andEventID_), you need to pass in the class and event ID that are not standard ones (they're defined by NNW/external blog protocol). Well, in Cocoa code, these are just a four character string, but as I found, a string that an unsigned long via string packing. So, it was a question of how I get these ID's in via Ruby. Luckily this turned out to be rather simple, as you can use String#unpack. And, in this case, you pass unpack "N" as the format, which is an unsigned long packed in network byte order.

Lastly, debugging. As mentioned above, Console and such are your friend. I haven't tried any shenanigans with ruby-debug or such from the command line, so that might work. But, Xcode can't debug into the Ruby code in your project (it can into the code in main.m just fine though). So, if you have weird crashes and such, check Console. Also, use NSLog, or pop alerts or what not. If someone knows a better way please do tell.

Regardless, I'm quite excited by RubyCocoa and have another couple apps that I may do with it. This app was a small one, and of course is not done in just a couple hours, but it's going to be a nice addition to the tool belt.

Loading mentions Retweet
Filed under  //   Cocoa   Mac   Ruby   RubyCocoa  

Comments [0]