views:

93

answers:

0

I'm writing an OS X Service with MacRuby. It upcases the selected text. It mostly works, but… well, here's all of it:

#!/usr/local/bin/macruby
# encoding: UTF-8
framework 'Foundation'
framework 'AppKit'

class KCUpcase
  def upcase(pasteboard, userData: s_userdata, error: s_error)
    incoming_string = pasteboard.stringForType "public.utf8-plain-text"
    outgoing_string = incoming_string.upcase
    pasteboard.clearContents
    pasteboard.setString(outgoing_string, forType: "public.utf8-plain-text")
  end
end

NSLog "Starting…"
NSRegisterServicesProvider(KCUpcase.new, "Upcase")
NSLog "Registered…"
NSRunLoop.currentRunLoop\
  .acceptInputForMode(NSDefaultRunLoopMode, 
           beforeDate:NSDate.dateWithTimeIntervalSinceNow(10.0))
NSLog "Done."

It's just a Foundation tool, not part of an Application.

Now, see the NSRunLoop… line? That doesn't really work. The program exits imediately. I suppose the loop runs once and then exits. Anyhoo, the fact is that it's definititely not waiting 10s for input. So, here's what I did instead:

NSRunLoop.currentRunLoop.runUntilDate NSDate.dateWithTimeIntervalSinceNow(60.0)

And that works, but naturally the program sticks around for 60s, and it's a kludge. So I implemented the whole thing in Objective C (Including KCUpcase, which is not shown). And… it works. With manual memory management. Once I switch to GC (-fobjc-gc-only), it exits imediately same as the MacRuby version.

#import <Foundation/Foundation.h>
#import "KCUpcase.h"

int main (int argc, const char * argv[]) {
    NSLog(@"Starting…");

    NSRegisterServicesProvider([[KCUpcase alloc] init], @"KCUpcase");
    NSLog(@"Registered…");

    [[NSRunLoop currentRunLoop]
        acceptInputForMode:NSDefaultRunLoopMode
                beforeDate:[NSDate dateWithTimeIntervalSinceNow:10.0]];
    NSLog(@"Done.");

    return 0;
}

But, alas, the fix is easy: because this is a Foundation tool (not an NSApplication), it seems I have to start GC manually by calling objc_startCollectorThread. Here:

#import <objc/objc-auto.h>
// ...
NSLog(@"Starting…");
objc_startCollectorThread();
NSRegisterServicesProvider([[KCUpcase alloc] init], @"KCUpcase");
// ...

Ok, but what's up with MacRuby then? Let's throw it into the mix:

#import <MacRuby/MacRuby.h>
// ...
NSLog(@"Starting…");
objc_startCollectorThread(); // This magic stops working once we add MacRuby
[[MacRuby sharedRuntime] evaluateString: @"$stderr.puts 'hi from macruby'"];
NSRegisterServicesProvider([[KCUpcase alloc] init], @"KCUpcase");
// ...

And, again, it's not waiting in the loop. And, again, ussing the runUntilDate: kludge instead of acceptInputForMode:beforeDate: works:

NSLog(@"Starting…");
[[MacRuby sharedRuntime] evaluateString: @"$stderr.puts 'hi from macruby'"];
NSRegisterServicesProvider([[KCUpcase alloc] init], @"KCUpcase");
NSLog(@"Registered…");
// Hmmm…
[[NSRunLoop currentRunLoop]
    runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10.0]];
NSLog(@"Done.");
return 0;

So, I suppose I'm missing something terribly obvious. Please enlighten me.


And by the way, the full MacRuby version of the project is available here (download) with a Rake task that'll build and install it in ~/Library/Services. Then you need to enable its checkbox in Services in the Keyboard Preference Pane (once).

(or git clone git://gist.github.com/537075.git)

Aside: Interestingly, I tried calling NSLog inside the MacRuby string, and it raised NoMethodError. What gives?