Dockless RubyMotion menubar app with start at login
This is a guide to create a RubyMotion OS X app that runs on the menubar (without a dock icon), is compliant with App Sandboxing and capable to launch at login. Although this sounds fairly straightforward I found it a bit more complex than anticipated. The following process describes a solution that should be accepted to the App Store (I haven't submitted it yet). It isn't perfect, but it will take you far.
Here's what you'll need to setup:
- A RubyMotion OS X menubar app that doesn't display a dock icon
- Another RubyMotion "helper app" with a dock icon. This app is stored within the main app. It's launched by the operating system and does nothing but start the menu bar app and close itself again.
- Modify the build process of your primary app to include the helper app
- A small addition to your primary app to tell the operating system to start the helper app at login (which subsequently launches the primary app)
To get a better understanding why all this is necessary, I recommend you read Delite Studio's Blog Post. If you're just a busy cowboy, keep reading.
Before you start, make sure to have a clean git working copy. None of this is intuitive, so make sure you can discard the changes easily.
1. Configuring your menubar app
So you've decided that your app should only run in the menubar. Welcome to the cool kids! Let's hope there's space left for you in the VIP-area at the top of the screen.
I'm guessing you already created your app. Perhaps you even configured it to run as a dockless menu bar app. If not, add try adding this to your Rakefile:
app.info_plist['NSUIElement'] = 1
As for the menu bar icon, you'll want to do something like:
def setup_menu_bar
@status_item = NSStatusBar.systemStatusBar.statusItemWithLength(24.0)
@status_item.setHighlightMode(false)
@status_item.setImage(NSImage.imageNamed("Status"))
@status_item.setTarget(self)
@status_item.setAction('clicked_menu_bar:')
end
def clicked_menu_bar
p "You'll take it from here"
end
Not a lot of surprises. Here's how to make it launch at login.
2 Adding the helper application
It's Apple's recommendations that you use a helper application to launch your menubar app. In RubyMotion, this means creating a new OS X project and include it in the build process of your main app.
Thankfully the helper application is very simple. I've created a RubyMotion template to make the setup process a bit easier. In the following MyRubyMotionAppName
should be the current name of your menu bar app.
cd into your menu bar app RubyMotion project:
$ cd ~/path/to/MyRubyMotionAppName
Inside that project, create a new project using the custom app-launcher-template:
$ motion create --template=git@github.com:JonasNielsen/app-launcher-template.git MyRubyMotionAppName-app-launcher
Make sure to rename MyRubyMotionAppName
with the name of your menu bar app.
Basically, all this app does is launching the menu bar app and close itself again (/MyRubyMotionAppName/MyRubyMotionAppName-app-launcher/app/app_delegate.rb):
class AppDelegate
def applicationDidFinishLaunching(notification)
app_name = "MyRubyMotionAppName"
if NSWorkspace.sharedWorkspace.launchApplication(app_name)
NSApp.performSelector("terminate:", withObject:nil, afterDelay: 0.0)
else
raise "Could not open app with name #{app_name} - is the name correct?"
end
end
end
cd into your helper application and build and run it to confirm that the menu bar app is launched:
$ cd MyRubyMotionAppName-app-launcher/
$ rake
You probably want to update the Rakefile of the helper application with a correct app.identifier
and perhaps a dock icon.
3. Include the helper application in the build process of your menubar app
Add this to the bottom of your Rakefile.rb
:
class Motion::Project::App
class << self
#
# The original `build' method can be found here:
# https://github.com/HipByte/RubyMotion/blob/master/lib/motion/project/app.rb#L75-L77
#
alias_method :build_before_copy_helper, :build
def build platform, options = {}
# First let the normal `build' method perform its work.
build_before_copy_helper(platform, options)
# Now the app is built, but not codesigned yet.
destination = File.join(config.app_bundle(platform), 'Library/LoginItems')
info 'Create', destination
FileUtils.mkdir_p destination
helper_path = File.dirname(__FILE__)+'/MyRubyMotionAppName-app-launcher/build/MacOSX-10.8-Development/MyRubyMotionAppName-app-launcher.app'
info 'Copy', helper_path
FileUtils.cp_r helper_path, destination
end
end
end
Double check the helper_path
and rename MyRubyMotionAppName with the name of your app.
Now when building your menu bar app, your helper application should be contained in the Contents folder of MyRubyMotionAppName.app.
All that's left now is to allow your users to launch the app at login. This is done with the ServiceManagement framework. Add it the Rakefile of your menubar app:
app.frameworks += [ 'ServiceManagement']
Finally, integrate following function with your UI:
def start_at_login enabled
url = NSBundle.mainBundle.bundleURL.URLByAppendingPathComponent("Contents/Library/LoginItems/MyRubyMotionAppName-app-launcher.app") # path
LSRegisterURL(url, true)
unless SMLoginItemSetEnabled("com.your-name.MyRubyMotionAppName-app-launcher", enabled) # identifier
NSLog "SMLoginItemSetEnabled failed!"
end
end
Again, make sure to double check identifier and path to your helper app.
Final caveats
That's it! You should be good to go now. Don't forget that Apple forbids auto-launch at startup without explicit consent from the user. Unfortunately, you can't just throw the above piece of code into your AppDelegate.
Also, rumors are that launch at login using this method will only work if the app is placed inside the user's /Applications folder (because of App Sandbox requirements). Since you plan to distribute via the App Store, that shouldn't be an issue.
Credits
I would never have figured out how to do this, if it wasn't for the help of a few fine gentlemen. All credits goes to Delite Studio's Start dockless apps at login with App Sandbox enabled and Eloy Durán's invaluable RubyMotion interpretation of the article
Thanks guys!
Eloy @alloy
Me @jonasnielsen
Written by Jonas Bruun Nielsen
Related protips
2 Responses
This helped me immensely! Thanks for posting it!
This is awesome. Thanks for sharing.