通知センターでHello, world!

Mountain Lionの新機能の一つ、通知センターをうまく使えないかと試してみた記録。今まではgrowlnotifyコマンドを利用して、AppleScriptなどの処理の状態を知る手がかりにしていた。できることならOSX標準の仕組みだけで通知したいのだ。

Objective-C

  • CocoaObjective-Cで書かれている。Objective-CCocoaのために存在する、と言えるかもしれない。
  • だから、最初はObjective-Cで考えることが、理解への近道だと考えた。
#import "AppDelegate.h"

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    // Insert code here to initialize your application
    NSUserNotification *myNotification = [[NSUserNotification alloc] init];
    myNotification.title = @"Hello, world!";
    myNotification.informativeText = @"Nice to meet you.";
    [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:myNotification];
    
    [NSApp terminate:self];
}

@end

できた!

  • 通知をクリックすると、その通知を登録した元のアプリケーションを起動する仕様のようだ。
  • 通知をクリックするたびに、新たな通知が表示された。

RubyCocoa

  • XcodeRubyCocoaのアプリケーションを作ろうと思ったが、雛形プロジェクトがが見当たらない...。
  • もしやMountain LionではrubyCocoaが標準インストールされなくなってしまったのか、と確認してみると...
$ ruby -e "require 'osx/cocoa';puts OSX::RUBYCOCOA_VERSION"
1.0.1
  • しっかりと/System/FrameworksにRubyCocoa 1.0.1がインストールされている!
  • ならば、Xcodeが利用するRubyCocoa用の雛形プロジェクトをインストールすればいいのだ。
$ svn export --force http://rubycocoa.svn.sourceforge.net/svnroot/rubycocoa/trunk/src/template/Xcode4.4/Templates ~/Library/Developer/Xcode/Templates
  • Ruby-Cocoa Applicationを新規作成して、AppDelegate.rbに以下のコードを書いて完成した。
class AppDelegate < OSX::NSObject
  def applicationDidFinishLaunching_(notification)
    # Insert code here to initialize your application
    myNotification = OSX::NSUserNotification.alloc.init
    myNotification.title = "Hello, world!"
    myNotification.informativeText = "Nice to meet you."
    OSX::NSUserNotificationCenter.defaultUserNotificationCenter.deliverNotification_(myNotification)
    
    OSX::NSApp.terminate_(self)
  end

end

できた!

MacRuby

  • RubyからCocoa環境を利用するもう一つの手段がMacRubyである。
  • MacRubyOSX標準ではインストールされていないので、自分でダウンロードする必要がある。
  • RubyCocoaOSXモジュール配下にCocoaクラスを追加して拡張しているのに対し、
  • MacRubyではRuby本体のソースコードを拡張して、Cocoa環境にアクセスしている。
  • そのため、MacRubyではすべてのオブジェクトがNSObject(Cocoaのルートオブジェクト)を継承して、よりRubyCocoaの親和性が高まっているのだ。
$ ruby -e "require 'osx/cocoa'; p OSX::NSObject.ancestors"
[OSX::NSObject, OSX::OCObjWrapper, OSX::NSKeyValueCodingAttachment, OSX::NSKVCAccessorUtil, OSX::ObjcID, Object, Kernel]
$ ruby -e "require 'osx/cocoa'; p Object.ancestors"
[Object, Kernel]
$ macruby -e 'p NSObject.ancestors'
[NSObject, Kernel]
$ macruby -e 'p Object.ancestors'
[NSObject, Kernel]
  • MacRubyをダウンロード&インストールすると、すでにXcodeの雛形プロジェクトにもMacRuby Applicationが追加されていた。
  • さっそく以下のように入力して完成と思いきや、実行するとエラーが出る...。
class AppDelegate
    attr_accessor :window
    def applicationDidFinishLaunching(a_notification)
        # Insert code here to initialize your application
        myNotification = NSUserNotification.alloc().init()
        myNotification.title = "Hello, world!"
        myNotification.informativeText = "Nice to meet you."
        NSUserNotificationCenter.defaultUserNotificationCenter().deliverNotification(myNotification)
        
        NSApp.terminate(self)
    end

end

できた!

Cocoa-AppleScript Applet.app

  • Lion以降、いつのまにかAppleScript エディタにファイル >> テンプレートから新規作成というメニューが追加されていた。
  • そこでCocoa-AppleScript Applet.appを選択すると、AppleScriptからCocoa環境を利用したアプリが作れるようになっていた。


set myNotification to current application's NSUserNotification's alloc()'s init() set myNotification's title to "Hello, world!"
set myNotification's informativeText to "Nice to meet you."
current application's NSUserNotificationCenter's defaultUserNotificationCenter's deliverNotification_(myNotification) quit --quit重要--これがないと通知されない--謎

  • 実行する時は「アプリケーションを実行」ボタンを押すのだ。(「スクリプトを実行」ではCocoaにアクセスできず、エラーになる)

できた!

AppleScript

  • Cocoa-AppleScript Applet.appでは、Cocoaの機能を使えて大変便利なのだが、
  • 通常の.scpt形式のAppleScriptでは、Cocoaにはまったくアクセスできない...。(非常に残念)
  • ならば、力技で.scpt形式のAppleScriptから、Cocoa-AppleScript Applet.appを作成して、実行して、破棄してみた。
  • システム環境設定 >> アクセシビリティ >> 「補助装置にアクセスできるようにする」チェック入 にしておく必要あり。


tell application "AppleScript Editor" to activate

delay 1

tell application "System Events" to tell process "AppleScript Editor"
pick menu item "Cocoa-AppleScript Applet.app" of menu "テンプレートから新規作成" of menu item "テンプレートから新規作成" of menu "ファイル" of menu bar item "ファイル" of menu bar 1
end tell

tell application "AppleScript Editor"
set document 1's text to "set notification to my NSUserNotification's alloc()'s init()
set notification's title to \"Hello, world!\"
set notification's informativeText to \"Nice to meet you.\"
my NSUserNotificationCenter's defaultUserNotificationCenter's deliverNotification_(notification)
quit"
activate
end tell

tell application "System Events" to tell process "AppleScript Editor"
pick menu item "アプリケーションを実行" of menu "スクリプト" of menu bar item "スクリプト" of menu bar 1
end tell

delay 1

tell application "AppleScript Editor" to close document 1 saving no

できた!(まったく実用的ではないが...)

AppleScriptRubyCocoa

  • 上記のような面倒なことをしないで、AppleScriptからRubyを実行して、RubyCocoaを使えばいいじゃないかと、ふつうは考える。
  • 当然、自分もやった。その結果どうなったかというと...


set ruby_cocoa to "require 'osx/cocoa'
myNotification = OSX::NSUserNotification.alloc.init
myNotification.title = 'Hello, world!'
myNotification.informativeText = 'Nice to meet you.'
OSX::NSUserNotificationCenter.defaultUserNotificationCenter.deliverNotification(myNotification)"
do shell script "/usr/bin/ruby -e " & quoted form of ruby_cocoa

error "-e:5: undefined method `deliverNotification' for nil:NilClass (NoMethodError)" number 1
  • Ruby-Cocoaアプリケーションでは動いていたコードが、エラーになってしまう...。
  • nil:NilClassに対して、deliverNotificationメソッドは定義されていない、と警告される。
$ irb
    
=> true
=> nil
  • irbで試してみると、nilが返っている。
  • どうやら、アプリケーションの実行環境以外から通知センターを取得しようとしてもnilになってしまう仕様のようだ。
  • そう思って通知センターを見ると、確かにアプリケーション単位で通知が区分されている。
  • 通知センターを簡単に利用できそうな気がしてやってみたが、それほど簡単ではなかった...。

terminal-notifer

  • そうなると、頼みの綱はこれ。やはり、通知センターを利用することを目的に開発されたterminal-notiferコマンドは、使いやすい!
$ terminal-notifier -message 'Hello, world!'
できた!(とっても手軽)
  • タイトル・サブタイトルも指定する。
$ terminal-notifier -message 'Hello, world!' -title 'タイトル' -subtitle 'サブタイトル'
  • 同じIDの通知は上書きする。
$ terminal-notifier -message 'Hello, world! 1回目' -group 0
∗ Notification delivered.
$ terminal-notifier -message 'Hello, world! 2回目' -group 0
∗ Removing previously sent notification, which was sent on: 2012-09-13 01:59:41 +0000
∗ Notification delivered.
  • 通知をクリックしたら、アプリをアクティブにする。(この例:iTunesをアクティブにする)
$ terminal-notifier -message 'Hello, world!' -activate com.apple.iTunes
  • 通知をクリックしたら、URLを開く。(この例:ザリガニが見ていた...。を開く)
$ terminal-notifier -message 'Hello, world!' -open 'http://d.hatena.ne.jp/zariganitosh/'
  • 通知をクリックしたら、コマンドを実行する。(この例:警告音を鳴らす)
$ terminal-notifier -message 'Hello, world!' -execute 'afplay "/System/Library/Sounds/Blow.aiff"'
$ terminal-notifier -message 'Hello, world!' -execute 'osascript -e "beep"'
$ terminal-notifier -message 'Hello, world!' -execute "ruby -e \"require 'osx/cocoa'; OSX::NSSound.soundNamed('Blow').play; sleep 1\""

AppleScriptライブラリのmessageハンドラ

  • 今までgrowlnotifyを利用してきたAppleScriptのmessageハンドラは、以下のように修正した。
  • terminal-notifierで-messageの先頭が「<」で始まっていると、なぜかエラーが出て通知されなかった。
  • 上記対策のため、-messageの先頭に必ず「> 」を付加する仕様にした。
 message("タイトル", "日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであって、平和を愛する諸国民の公正と信義に信頼して、われらの安全と生存を保持しようと決意した。われらは、平和を維持し、専制と隷従、圧迫と偏狭を地上から永遠に除去しようと努めてゐる国際社会において、名誉ある地位を占めたいと思ふ。われらは、全世界の国民が、ひとしく恐怖と欠乏から免れ、平和のうちに生存する権利を有することを確認する。")
 on message(title, msg)
   try
     --msgの文字数を350文字に制限する(\"等のエスケープ記号はカウントしない)
     set msg to (msg's items 1 thru 350 as text) & "……"
   end try
   
   try
     set option to ""
     if title"" then set option to option & " -title " & quoted form of title
     do shell script "terminal-notifier -message " & quoted form of ("> " & msg) & option
   on error
     activate
     display dialog msg buttons {"OK"} default button "OK" with title title with icon 1 giving up after 4 --with icon note:=1 caution=2 stop=0
   end try
 end message
できた! OSX標準ではなくなってしまったが、この辺が落としどころ。