Home > Tags > 208

208

もっとRubyCocoaでQuicksilverプラグインを書く

RubyCocoaを使ってQuicksilverプラグインを書く の続編です。 前回書いたあと、hisaさんからアドバイスをもらい、RubyCocoaプラグインを書くことができるようになりました。

ポイントは以下の2つ。

  • NSPrincipalClass
  • RubyActionClass < OSX::QSActionProvider

コードは、CodeReposのものをupdateしておきました。 hisaさんコードがたくさん入っていますが、煮るなり焼くなり好きにせよとのことなので、前回と同じ修正BSDライセンスとします。

NSPrincipalClass

Quicksilver プラグインに限らず、Cocoaで何らかのBundleを書く際には、Info.plistの NSPrincipalClass にクラス名を指定しておくと、そのクラスの +load() メソッドが呼ばれてプラグインが組み込まれるようです。 なので、ここでRBBundleInit()を呼んでRubyCocoaを初期化すると。とてもスマート。 前回はこのことを知らずに、無理矢理なハックをしていたのでした。

RCLoader.m:

ここでは、NSPrincipalClassにRCLoaderというクラスを指定しています。

@implementation RCLoader
 
+ (void)load {
  static bool installed = 0;
 
  if (! installed) {
    if (! RBBundleInit("load_ruby.rb", [self class], self)) {
      installed = true;
    }
  }
}

load_ruby.rb:

そんで、RubyCocoaの初期化の中で、必要な.rbを読み込んでおく、と。

def load_ruby_programs(bundle, logger)
  path = bundle.resourcePath.fileSystemRepresentation
  rbfiles = Dir.entries(path).select {|x| /\.rb\z/ =~ x}
  rbfiles -= [ File.basename(__FILE__) ]
  rbfiles.each do |path|
    require( File.basename(path) )
  end
end
 
OSX.init_for_bundle do |bundle, param, logger|
  load_ruby_programs(bundle, logger)
end

RubyActionClass < OSX::QSActionProvider

前回説明したように、QuicksilverのActionを記述するには、QSActionProviderを継承したクラスでメソッドを用意します。 今回は、RubyのクラスでQSActionProviderを実装します。RubyCocoaのパワーがすごすぎる。

class RubyAction < OSX::QSActionProvider
  def act(arg)
    val = arg.stringValue
    OSX::QSObject.objectWithString('Hello world, ' + val)
  end
end

あとは、このRubyActionクラスを、Info.plistのactionClassに、actionSelectoract:に指定しておけば、ちゃんとactが呼ばれます。

疑問

  • OSX.require_framework とか使わないで、QSActionProviderとかのQS frameworkが使えてるのはなぜ?
  • hisaさんは、この方法だとCPU usageが高くなると懸念されているのですが、僕のところではいまひとつ高くなってるように見えません。why?

まとめ

RubyCocoaで、Quicsilverプラグインを書く方法をまとめました。 これで、あとはRubyのクラスとInfo.plistを書くだけでどんどんプラグインが書けますね。みんながんばれ!

あと、自分でもイマイチだなーと思うことでも、何かしら書いて晒すことが大事なんだなと。そんで改善していければOKなんだ。 hisaさん、ありがとうございました。

Write a Quicksilver plugin with RubyCocoa

I wrote a Quicksilver plugin with RubyCocoa, that adds “hello world” to the passed string.

  • Xcode project codes : in CodeRepos
  • License : revised BSD

It should work on Tiger/Leopard if RubyCocoa is installed.

How does it work

As I mentioned in QuartzComposer CustomPatch with RubyCocoa, we just call RBBundleInit() function in plugin initializaiton phase to write some plugin with RubyCocoa. But wait, where should be the initializaiton code in Quicksilver plugin ?

We write actual plugin behavior in the class that inherits QSActionProvider, so I tried to call RBBundleInit() in the init() method in that class… however, that resulted in crashing Quicksilver :(

Then I called RBBundleInit() at only first time in performActionOnObject() of the actual Action class, and made references between Objective-C and Ruby class instances.

After that, I implemented an actual action behavior in Ruby class, and delegates from Objective-C to Ruby method, thats’ all. This is ugly, confusing, … I know, but it does work well :)

Code Snippets

ActionProvider in Objective-C:

 (QSObject *)performActionOnObject:(QSObject *)dObject{
  // initialize RubyCocoa
  static bool loaded = false;
  if (!loaded) {
    if (RBBundleInit("qs_action.rb", [self class], self)) {
      NSLog(@"[RubyCocoaPluginAction.performActionOnObject] RBBundleInit failed"
);
      abort();
    }
    loaded = true;
  }
 
  // delegate actual action to Ruby class
  QSObject *ret = [QSObject objectWithString:[rb_ act:dObject]];
  return ret;
}

RubyCocoa side:

class Action
  def initialize(logger)
    @logger = logger
  end
 
  # write something great :)
  #  - arg : QSObject
  def act(arg)
    val = arg.stringValue
    @logger.info(val)
    'Hello world, ' + val
  end
end # Action
 
require 'osx/cocoa'
OSX.init_for_bundle do |bdl, owner, log|
  # bdl    - the bundle related with the 2nd argument of RBBundleInit
  # owner  - the 3rd argument of RBBundleInit as optional data
  # log    - logger for this block
 
  act = Action.new(log)
  owner.setInstance act
end

Future works

  • Better initializaiton. It should be in constractor of some class)
  • Inherits QSActionProvider by Ruby class (more pure Ruby)
  • Use ns_import ?

It should not be “With RubyCocoa”, but rather “By RubyCocoa”.

Conclusion

I made a start point to write a Quicksilver plugin by RubyCocoa. It is timing of initialization that is to be considered about writing some bundle in RubyCocoa. I made it in this article somehow. This article is for someone who wants to create Quicksilver plugin by Ruby, not learning unfamiliar Objective-C.

Reference

RubyCocoaを使ってQuicksilverプラグインを書く

RubyCocoaを使って、Quicksilverのプラグインを書いてみました。 受けとった文字列に、”hello world” をくっつけるだけのものです。

プロジェクト一式のコードは、CodeReposに。 ライセンスは修正BSDで。 RubyCocoaがインストールされていれば、TigerでもLeopardでも動くと思います。

仕組み

RubyCocoaでQuartzComposer CustomPatchのときに書いたように、 CocoaアプリじゃなくてプラグインをRubyCocoaで書くには、RBBundleInit() という関数をプラグインの初期化の際に呼んであげればよいわけです。 ところが、Quicksilverプラグインの初期化ってどこなんだろうという。

QSActionProviderを継承したクラスで実際のプラグインの動作 (Action) を記述するので、このクラスのinit()で RBBundleInit() を呼べばいいかなーと考えてやってみたところ、Quicksilverをクラッシュさせてしまいます。こまった。

しかたないので、Actionを記述する performActionOnObject() の中で、RBBundleInit()を最初の一度だけ呼ぶようにしました。その中で、Objective-CからRubyクラスのインスタンスを参照できるようにしておきます。

あとは、RubyのクラスでActionを実装し、Objective-CからRubyのメソッドを呼んで移譲します。これでできあがり。 かなり強引で美しくないのですが目的は果たせるかな、というダーティハックです。

コード片

Objective-CのActionProvider:

 (QSObject *)performActionOnObject:(QSObject *)dObject{
  // initialize RubyCocoa
  static bool loaded = false;
  if (!loaded) {
    if (RBBundleInit("qs_action.rb", [self class], self)) {
      NSLog(@"[RubyCocoaPluginAction.performActionOnObject] RBBundleInit failed"
);
      abort();
    }
    loaded = true;
  }
 
  // delegate actual action to Ruby class
  QSObject *ret = [QSObject objectWithString:[rb_ act:dObject]];
  return ret;
}

RubyCocoa側:

class Action
  def initialize(logger)
    @logger = logger
  end
 
  # write something great :)
  #  - arg : QSObject
  def act(arg)
    val = arg.stringValue
    @logger.info(val)
    'Hello world, ' + val
  end
end # Action
 
require 'osx/cocoa'
OSX.init_for_bundle do |bdl, owner, log|
  # bdl    - the bundle related with the 2nd argument of RBBundleInit
  # owner  - the 3rd argument of RBBundleInit as optional data
  # log    - logger for this block
 
  act = Action.new(log)
  owner.setInstance act
end

あと、やるとしたら

  • もうちょっときれいに初期化する (何らかのクラスのコンストラクタが望ましい)
  • QSActionProviderをRubyのクラスで継承する (よりRubyだけで書けるように)
  • ns_importとかつかってみる?

「RubyCocoaを使って」 よりも、「RubyCocoa」、スマートにプラグインを書けるようにしたいものです。 まぁ、まずは第一歩ということでひとつ。

まとめ

RubyCocoaを使って、Quicksilverプラグインをつくるためのとっかかりについて書きました。 RubyCocoaでバンドルを書くときに悩むのは初期化のタイミングだと思います。 今回はここを無理矢理解決しました。 Objective-Cを学んでる時間があったら、慣れ親しんでるRubyでちゃちゃっと コード書きたいよ! という方の参考になればと思います。

参考


2008.02.03 追記

RubyCocoaのhisaさんにコメントいただき (!)、もっとよい方法を教えてもらいました。 こちらについても、まとめて別エントリにします。

QSTwitter 1.2

QuicksilverからTwitterに投稿するプラグイン、QSTwitterを1.2にアップデートしました。 ダウンロードはこちら

変更点は2つ。

‘+’ 問題 を解決

1.1までは、‘+’を含むメッセージを入力するとスペースに変換されてしまうという問題がありました。 これは、NSString.stringByAddingPercentEscapesUsingEncoding が ‘+’ をエスケープしてくれないことに起因していたのですが、今回はやっつけで /+/%2B/g するようなクイックハックで解決しています。

かわいいアイコン :)

Twitter Development Talk のアイコンがかわいかったので、使っていい? とMLで聞いたところ快諾されたので、使うことにしました。(Thanks Alex !) プラグイン選択画面、プラグインの設定画面、そしてメッセージを送るときに表示されます。 ドット絵風でとってもキュート。


ぜひぜひお使いくださいね。

QSTwitter 1.1 (Quicksilver Twitter Plugin)

QSTwitter (Quicksilver Twitter Plugin) 1.1を公開します。

QSTwitter.zip をダウンロードしてダブルクリック!

変更点は、Twitterエントリの”from=…”のところに、”QSTwitter”が入る、というものです。ユーザーエージェントみたいなものですね。


おまけ : どうやって自作Twitterアプリでfrom=”…”に自分のアプリ名を入れるか

  • Twitterの中の人にメールを送る
  • 自分アプリで投稿するときに “source=YourAppName“というパラメータを追加
  • Twitter側の対応を待つ

でした。 僕の場合は、10日ほどで対応してもらえました。Twitterの中の人に感謝!

参考 : Twitter Development Talk::”from Web”

Quicksilverプラグインのつくりかた

Quicksilverでプラグインをつくる方法をチュートリアルにしてまとめました。

このチュートリアルを読むことで、以下のものがつくれるようになります。

  • 基本的なQuicksilverプラグイン
  • プラグイン用の設定画面
  • 設定画面からのデータ取得

目次

  • ここでの環境
  • 必要なもの
  • 準備
  • つくりはじめる
  • PreferencePaneをつくる
  • Actionのコードを書く
  • おわりに
  • 参照

ここでの環境

  • Mac OS X Leopard 10.5.1
  • Quicksilver B53 (3814)
  • Xcode 3.0

必要なもの

準備

ビルドできるようになるまでの準備をします。

1. ダウンロードしたXcodeのテンプレートをインストール

Xcode 3.0 からは、カスタムテンプレートを置く場所が /Developer/Library/Xcode/Project\ Templates/ になっています。 (VIRTUOSO VIRTUOSOのところでは、 /Library/Application Support/Apple/Developer Tools/Project Templates/ に置くよう書かれている)

ここでは、 /Developer/Library/Xcode/Project\ Templates/Bundle/Quicksilver\ Plug-in というパスになるように、zipを展開したディレクトリを置きます。

2. Xcode プロジェクトをつくる

1がうまくいっていれば、このように Quicksilver Plug-in が選択できます。

すすめると、こんな画面に。

このチュートリアルでは、設定画面 (PreferencePane) もつくるので、 QSInterface.frameworkにもチェックして、リンクされるようにしておきましょう。

3. Source Trees

プラグインをビルドするには、Quicksilverのフレームワークが必要です。 ダウンロードしておいたQuicksilver Developer版の中に、フレームワークがあるのでそこをXcodeに指定します。

XcodeのPreferencesを開き、Source Treesタブに移動して、画面のように設定します (Pathのところは、Quicksilver Developer版を置いた場所に読み替えてください。 例:/Applications/QS-dev.app/Contents/Frameworks)

つくりはじめる

フレームワークにのっかってビルドできるところまでいってみましょう。

4. Info.plist

さて、ここまでで準備が完了しました。 ここからは実際にプラグインをつくっていくことになります。

プラグインの情報のほとんどは、Info.plistに書くことになります。 .plistなファイルはXMLなので、Property List Editorで開くと編集にラクができます。

デフォルトのInfo.plistをProperty List Editorで開くと、次のような感じ。

個々のキー/値については、 Anatomy of a plugin - Info.plist Part 1Plist Specification が詳しいです。 ここでは、最小限の設定項目を埋めていきます。

5. QSPlugin

プラグインについてのメタ情報を書きます。

Before | After:

6. QSActions

実際にプラグインが何をするかについての情報を書きます。 まず、QSActionsTemplateからQSActionsに名前を変えておきます。

nameのところの値をタイプすることで、Quicksilverからこのプラグインが呼ばれるので、 それっぽい値を入れておくとよいでしょう。

directTypesのところで、受けつける入力の種類を判定するぽいです。 ここでは “*”を指定してなんでもこいにしてみました。

Before | After:

7. QSRegistration

いろいろ設定項目はありますが、ここではPreferencePaneについて。 まず、QSRegistrationTemplateからQSRegistrationに名前を変えておきます。

QSPreferencePanesの中に、classを指定できるところがあります。 ここには、後でつくるPreferencePane用のクラス名を指定します。

Before | After:

8. ビルド

そろそろこのページを見ながらちまちまと作業をするのにも飽きてきたことででしょう。 ここらでビルドして、実際に動いてるのを見ようではないですか。

Xcodeでふつうにビルドすると、*.qsplugin というファイルが生成されます。 これがQuicksilverのプラグインなのですね。

ダブルクリックすると、Quicksilverに “プラグインをインストールする? “みたいに聞かれるのでYesで進みましょう。

9. 動いてるのを見る

「.」でテキスト入力モードにして何かうちこみ、TABでActionに移りましょう。 6.で設定してある名前を入力すればほら、いまつくったプラグインが呼び出せるではないですか! (とはいえ、いまのところ何もActionを実装していないので、実行したところで何も起こりません)

次に、QuicksilverのPreferenceを開いてみます。 左のリストに、いまつくったプラグインがあると思います。やったね !

PreferencePaneをつくる

ここからがお楽しみです。

10. PreferencePaneのクラスをつくる

File→Newから、Objective-C class をつくります。 ファイル名は、7で指定してたクラス名にするとよいでしょう。

@interfaceを、QSPreferencePaneを継承するように書き換えます。

QSPluginTutorialPreferencePane.h:

#import <qsinterface/QSPreferencePane.h>
 
@interface QSPluginTutorialPreferencePane : QSPreferencePane {
}
 
@end

@implementationに、mainNibNameというメソッドを加えます。 このメソッドは、次につくるnibファイルの名前 (から.nibを除いたもの) を返すようにします。

QSPluginTutorialPreferencePane.m:

#import "QSPluginTutorialPreferencePane.h"
 
 
@implementation QSPluginTutorialPreferencePane
 
-(NSString *)mainNibName {
	return @"QSPluginTutorialPreference";
}
 
@end

ここまでのプロジェクトはこんな感じになるでしょう。

11. NIBをつくる

File→Newから、Window NIB をつくります。

12. File’s OwnerのクラスをPreferencePaneのクラスにする

このへんを 参考にして、File’s Ownerのクラスを、10でつくったクラスにします。

まず、QSPreferencePane.hと10でつくったクラスのヘッダファイルを、 NIBのウィンドウにドラッグ&ドロップします。

次に、File’s Ownerのオブジェクトを選択した状態でObject Identityの設定ウィンドウに行き、 Classを10でつくったクラス名にします。

13. File’s OwnerとWindowをつなげる

File’s OwnerのオブジェクトからCtrl+ドラッグしてWindowにドロップすると、 オブジェクトをつなげることができます。 Outlets _windowと出るので、_windowを選択しましょう。

14. コントロールを配置

好きなコントロールをWindowにつけましょう。 ここでは、シンプルなText Fieldコントロールをつけました。

15. 動かす

ふたたびXcodeでビルドし、.qspluginをつくり、インストールします。 QuicksilverのPreference→つくっているプラグインを選ぶと、ちゃんと右側にいまつくったText Fieldが 表示されることでしょう。やった !

16. バインドする

しかしこのままでは、設定画面で入力した値をどうやって使うのか分かりませんよね。 ここで、Cocoa Binding というテクニックをつかいます。

14で配置したコントロールを選択してBindngs設定ウィンドウに行き、次のように設定します。

  • Bind to: Shared User Defaults
  • Controller key: values
  • Model Key Path: 好きな文字列 (他のところからこの値を参照するときに使うキー)

ここでは、Model Key Path を QSPluginTutorial.someKey という値にしました。

Actionのコードを書く

したいことを何でも書くところ。

17. 入力 + 設定から得た値 => 出力

Actionのクラスの実装を書きます。 ここでは、入力された文字列に、PreferencePaneから得た値を組合せて、出力にすることにしました。 コードは以下のようになります。

QSPluginTutorialAction.m:

@implementation QSPluginTutorialAction
 
- (QSObject *)performActionOnObject:(QSObject *)dObject{
    // get value from Preference Pane
    id values = [[NSUserDefaultsController sharedUserDefaultsController] values];
    NSString *value = [values valueForKey:@"QSPluginTutorial.someKey"];
 
    QSObject *ret = [QSObject objectWithString:
      [NSString stringWithFormat:@"%@ hello, %@",[dObject stringValue], value]];
 
    return ret;
}
@end

18. 完成 !!!

長い道のりでしたが、ついにこれで終わりです。

ビルドし、インストールして、PreferencePaneから値を入れておきます。

テキストを入力し、Actionを選択して実行します。

もういちどQuicksilverを起動すると、入力のところに結果が入っています。やったね !

おわりに

Quicksilverでプラグインをつくる方法をチュートリアルにしてまとめました。

Quicksilver Twitter Plugin (日本語) で、Quicksilverのプラグインをつくっているとき、まとまった情報がない、情報が古い、日本語の情報が見当たらない、と何かと苦労してました。 このチュートリアルでQuicksilver野良プラグインを書く人が増えればいいなあと。 (そして素晴しいプラグインが増えて僕も幸せになれればいいなぁとw)

Catalogの扱い方とかまだ調べてみたいところがたくさんあるので、情報持ってる方いればぜひご連絡ください。

参照

Vacuous Virtuoso: ほぼ唯一のQuicksilverプラグインづくりの情報源。 基本はここを以下の順に読むとよいです。

docs.blacktree.com: 本家。でもドキュメントが散逸してます…

Quicksilver Twitter Plugin

Update (2008.3/5) : More Information is in Trac. Please check and see the latest version there. Text below is about version 1.0, and current version is 1.4 including more features.


I wrote a small Quicksilver plugin to send messages to Twitter directly.

Advantages

  • Easy to install. No additional software required to be installed
  • Configurable via Quicksilver Preference
  • You can send messages containing non-ASCII characters

Download

QSTwitter.zip (1.2) (2008.01.23)

Environment

Tested under Leopard 10.5.1, Quicksilver B53 3814.

Install

Unzip the downloaded file and double-click it. that’s all !

Configuration

You will find “Twitter option” in the Quicksilver preference pane. Enter your screen name and password there.

Usage

  1. Activate Quicksilver (by Ctrl-SPACE or so)
  2. type . (period/dot) key to enter text input mode
  3. type a message
  4. hit TAB to move into Action
  5. type “Twitter”
  6. return !

The more familar you are with it, the faster you can send messages than ever.

Screencast

Seeing is Believing, as you know :) I don’t know why the video is collapsing for first 15 seconds. Sorry for inconvinience.

Code

You can see the code in CodeRepos.

License

Considering…

Restriction

  • I could not post messages throught HTTP proxy. Let me know if anyone succeeded to make it.

ChangeLog

  • 1.2 (2008.01.23) : fixes the bug that ‘+’ is not shown in status, uses pretty girl icon .
  • 1.1 (2007.12.22) : now it shows “QSTwitter” in your post on “from …” .
  • 1.0 (2007.12.13) : initial release.

There seems no comprehensive documents to develop Quicksilver plugin as far as I searched. In order to share my experience to avoid falling into pitfalls, I will write an tutorial developing a Quicksilver plugin under up-to-date environment later.

Quicksilver Twitter Plugin (日本語)

Quicksilverから直接Twitterにメッセージを送ることのできるプラグインを書きました。

ダウンロード

QSTwitter.zip (1.2) (2008.01.23)

インストール

zipを展開してでてきたpluginファイルをダブルクリックするだけ。

設定

Quicksilverの設定画面に、”Twitter option” という項目が現れるので、そこに自分のscreen nameとパスワードを入れます。

使い方

  1. Quicksilverを起動 (Ctrl-SPACEとかで)
  2. . (ピリオド) キーを押して、テキスト入力モードに
  3. メッセージを打ち込む
  4. TABでActionに移動
  5. “Twitter”と入力
  6. return !

慣れると一瞬です。

スクリーンキャスト

百聞は一見に如かずで。 開始15秒くらい見苦しくなっています。すみません。

コード

CodeReposに置いてあります。

ライセンス

修正BSD

売り

  • インストールが簡単
    • QuicksilverからTwitterに投稿するには、他にTweetとか選択肢がありますが、スクリプトをコピペしたりKeychainに設定を書かねばならなかったりで面倒です。
  • 日本語が通る
    • Quicksilverでは日本語の扱いに難があるとか聞きますが、少なくとも僕が試している限り (Leopard + AquaSKK + Quicksilver B53) では問題なく入力できています。
  • 超はやい
    • IMやコマンドライン、IRCから入力すればいいじゃん! という声もありましょうが、僕にとってはそれぞれのアプリにコンテキストスイッチするコストが高すぎまして。Quicksilverなら、いつでもどこからでもすぐ呼び出せて、ささっとメッセージを入力できます。

制限事項

  • プロキシ越しでのpostができてません → 1.3 で入れました。

ToDo

  • ライセンス決める
  • “from…” にsignatureを入れてもらう → 1.1 で入れました。
  • かわいいアイコン → 1.2 で入れました。

ChangeLog

  • 1.2 (2008.01.23) : ‘+’を含むメッセージを送るとスペースに変換されてしまう問題を解決。また、Twitterの女の子のアイコンをつかうようにしました。
  • 1.1 (2007.12.22) : “from…” のとこに、”QSTwitter” を表示するようにしました。
  • 1.0 (2007.12.13) : はじめのリリース。

Quicksilverのプラグインを書く方法は、うまくまとまったものが存在しないのが2007.12/13の現状です。いろいろはまったポイントがあったので、あとでそのへんのノウハウもまとめて書きます。

Home > Tags > 208