知易行难

Posts Tagged ‘GmailNotifr’

GitHub Pages

Dec 19, 2008

Tags


From GitHub blog:

GitHub Pages allow you to publish web content to a github.com subdomain named after your username. With Pages, publishing web content becomes as easy as pushing to your GitHub repository.

To create your own pages, follow these steps:

  1. Create repository named you.github.com, where you is your username
  2. Add some pages (index.html or index.md for example) to the repository and push
  3. Wait a few minutes for your pages to be created for the first time
  4. Visit you.github.com and see the simple yet amazing feature working

I’ve already created my own: ashchan.github.com

It’s time to learn the Jekyll site generator and play with it.

Update:

Dr Nic wrote up an awesome post: Migrating project websites to github pages with sake tasks, new websites with jekyll_generator

Update 2:

GitHub now offers Pages Generator: see here. Really cool! It took me 1 minutes to generate the project page for Gmail Notifr: http://ashchan.github.com/gmail-notifr/

Gmail Notifr Changelog

Gmail Notifr is an Open Source RubyCocoa Gmail Notifier for Mac OS X.

View project detail here.

You can fetch the source from:

Feedbacks are welcome! Leave a message in the comments area below.

CHANGELOG

0.3.0 (Jan 7, 2009)

  • Multiple accounts support, finally!

Note: 0.3.0 is the very first version that supports multiple accounts. It might be very buggy, ‘use it at your own risk’ :-)

0.2.8 (Dec 7, 2008)

  • Do not notify if unread messages unchanged
  • Open Inbox with https

0.2.7 (Nov 20, 2008)

  • Bugfix: Exit sometimes due to onClick error of the grwol notification
  • Handle Gmail feed connect timeout error

0.2.6 (Nov 14, 2008)

  • Bugfix: Crash when set launch at login
  • Bugfix: Crash on launch
  • Do not show count number if there is no new message

0.2.5 (Nov 12, 2008)

  • Play the sound when selected in Preferences window

0.2.4 (Nov 7, 2008)

  • Added growl and sound notification settings to Preferences
  • Got rid of the ssl warning in system log: “peer certificate won’t be verified in this SSL session”

0.2.2 (Nov 3, 2008)

  • Goto inbox when click on the growl notification
  • Show sender names and subjects of the first three messages

0.2.1 (Oct 31, 2008)

  • Bugfix: Crash when registering defaults.

0.2.0 (Oct 30, 2008)

  • Added Growl support.
  • Play the “Blow” sound when there’re new messages.

0.1.3 (Oct 29, 2008)

  • “Open Inbox” goes to the proper url instead of the normal gmail url if hosted domains account is used.

0.1.2 (Oct 4, 2008)

  • initial release.

Write RubyCocoa Application that Launches at User Login

I wanted Gmail Notifr to run automatically when I log in, so I added a preference item called “Launch at login”. In this article I’ll show how to implement this feature in RubyCocoa.

Gmail Notifr Preferences

Let’s assume the Gmail Notifr’s path is “/Applications/Gmail Notifr.app” for now, this will be used to identify our application in login items:

notifr_app = "/Applications/Gmail Notifr.app"

Use CFPreferencesCopyValue to fetch preference value for login items:

cf = OSX::CFPreferencesCopyValue(
  "AutoLaunchedApplicationDictionary",
  "loginwindow",
  OSX::KCFPreferencesCurrentUser, 
  OSX::KCFPreferencesAnyHost
)

The first two parameters specify that we need to fetch value for auto launched applications (login items), and the third parameter specifies that we want the value for the current user.

Now cf holds information about all login items, it’s easy to check if our application has already been sitting there:

auto_launch_enabled = cf.any? { |app| app.valueForKey("Path") == notifr_app }

Note: each item has a key named “Path” storing the application path.

To change the preference (set our application as login item or remove it from login items), we need a mutable copy of cf:

mcf = cf.mutableCopy

Then, to remove the application if it’s already there:

mcf.each do |app|
    mcf.removeObject(app) and break if app.valueForKey("Path") == notifr_app
end

Or to add it as a new login item:

item = OSX::NSDictionary.dictionaryWithObject_forKey(notifr_app, "Path")
mcf.addObject(item)

At last, we have to write the new value back:

OSX::CFPreferencesSetValue(
  "AutoLaunchedApplicationDictionary",
  mcf,
  "loginwindow",
  OSX::KCFPreferencesCurrentUser,
  OSX::KCFPreferencesAnyHost
)
OSX::CFPreferencesSynchronize(
  "loginwindow",
  OSX::KCFPreferencesCurrentUser,
  OSX::KCFPreferencesAnyHost
)

In the world of reality, user of Gmail Notifr would install it anywhere he/she likes, so it’s better we get the application path in a more flexible way:

notifr_app = NSBundle.mainBundle.bundlePath

Pretty easy right? Now see what we’ve done:

Login Items

Note: in RubyCocoa it’s unnecessary to call methods exactly as in Objective-C, for example, instead of writing

app.valueForKey("Path")

we can get the value for key “Path” in ruby’s usual way

app["Path"]

I keep the code more like Objective-C so it’s easier to reference Objective-C Cocoa APIs.

PS: example code here were extracted from the source code of Gmail Notifr hosted on GitHub.

Want to learn RubyCocoa? Read this book:

Reduce CPU usage of RubyCocoa app

I found Gmail Notifr consumed 2% of CPU usage, even when it’s idle (Gmail Notifr checks new mails at an interval that could be configured by user). It was really odd as the app doesn’t do much job other than visiting the gmail feed and fetching the new mails count.

So I ran several Xcode RubyCocoa example apps provided by Apple. They seemed to act like that also.

A quick google search brought me to this discussion: CPU Usage. Not knowing what kind of problem they had, I tried adding the OSX.ruby_thread_switcher_stop to the application controller and test it. Wow, the cpu usage became low as 0%.

Rubycocoa cpu Usage

For now I have no idea if this would cause any problem to the app, I’ll just keep the code and see if Gmail Notifr runs normally.

Want to learn RubyCocoa? Read this book:

RubyCocoa: Store User Password in Keychain

When writing Cocoa software for Mac, it’s a better idea to store user password in Keychain because I don’t believe my own code could do such an important job well. So I did this for Gmail Notifr.

There’re few documents I can find on RubyCocoa Keychain coding. The best one for me is this post: RubyCocoa and Keychain access

Before start, generate metadata for the Security Framework:

mkdir ~/Library/BridgeSupport
gen_bridge_metadata -f Security -o ~/Library/BridgeSupport/Security.bridgesupport

The above post suggests that put the bridgesupport file in /Library/BridgeSupport, but ~/Library/BridgeSupport will also work.

So we need the Security.bridgesupport file to write RubyCocoa code with Keychain, how do we deploy the software? I guess we can do one of the following:

  • Ask the users to generate the metadata, which is kinda rude and silly;
  • Copy the file to the proper location when users install the app;
  • Embed the file in app bundle.

The last one is my favorite option and I’ve found it not very hard to implement. Here’s the code to load the Security Framework and the bridge file:

require 'osx/cocoa'
OSX.require_framework 'Security'
OSX.load_bridge_support_file(NSBundle.mainBundle.pathForResource_ofType("Security", "bridgesupport"))

Don’t forget to add the Security.bridgesupport file to Xcode project.

Basically, we only need Keychain to store and retrieve password. To do so, we just need to learn three Cocoa methods:

  • SecKeychainAddGenericPassword
  • SecKeychainFindGenericPassword
  • SecKeychainItemModifyContent

Use the first one to add a password to Keychain:

SERVICE = "MyKeychainApp"
...

error = SecKeychainAddGenericPassword(
    nil,
    SERVICE.length,
    SERVICE,
    username.length,
    username,
    password.length,
    password,
    nil)

The first nil parameter specifies that we’re going to use the default Keychain (why not?). SERVICE is a const string I use to represent the application. The last nil parameter tells the method that I don’t need it to return a SecKeychainItemRef object.

Chances are your users might want to change the password later. Calling the method again with a different password, the password stored in the Keychain will not be updated! (password can be showed in the Keychain Access application)

This is because when a Keychain item already exists for a username (Keychain item is identified by service name and username), the method call will fail. The sweet method returns errSecDuplicateItem in such case, then we can use the second method to update password:

error = SecKeychainAddGenericPassword(
    nil,
    SERVICE.length,
    SERVICE,
    username.length,
    username,
    password.length,
    password,
    nil)

if error == OSX::ErrSecDuplicateItem
    status, *data = SecKeychainFindGenericPassword(
        nil,
        SERVICE.length,
        SERVICE,
        username.length,
        username)
    password_length = data.shift
    password_date = data.shift #password data
    item = data.shift #SecKeychainItemRef
    SecKeychainItemModifyContent(item, nil, password.length, password)
end

SecKeychainFindGenericPassword returns a status code and an array of data, which contains the password length, password data, and a pointer to the object of the password.

Pass this pointer and the new password to the SecKeychainItemModifyContent method to update the password. The nil parameter tells the method we don’t have any attribute to set for the item.

When we use SecKeychainFindGenericPassword to get the password, it returns the password length and the password data (which is an ObjcPtr). Get the string value of the password is easy now:

password = password_data.bytestr(password_length)

Happy RubyCocoa programming!

Want to learn RubyCocoa? Read this book:

Archives: Monthly or