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?