さいきんのTwitterの狼藉には目が余るわッッ! wassr やろうぜ!! みたいな意見をよく目にするようになったので、QSTwitterのwassr版、QSWassrをつくってみました。15分間ハッキング。
やったこと
ほとんど s/twitter.com/api.wassr.jp/gなだけです。APIのエンドポイントを差し替えただけですね。QSTwitterは送信専用クライアントなので、おきらくでした。
とってもダーティなハック (following処理とかtwitterのがそのまま残ってる) なのですが、とりあえずでも動くものがあるほうがよいと思って公開しています。
興味をもたれた物好きな方は、ためしてみてください。
つかいかた
インストール方法、トリガーの設定方法なんかは、QSTwitterのやり方がほとんどそのまま使えます (s/QSTwitter/QSWassr/に注意)。
注意
QSTwitterといっしょに動かせません。
どういうわけか、Quicksilverのプラグインとして、RubyCocoaで書かれたものが複数存在できないようなのです (軽く調べた程度でですが)。
今後
やるとすれば。
- QSTwitterと融合
- Twitter, wassr の (両方に | 選択的に) ポストできるように
- そうすると、ほかのミニブログとも融合したいところ
- following対応
- 融合させた場合も、ちゃんとTwitter,wassrそれぞれのfollowingをうまく扱えるようにしたい
その他
ミニブログを移ると、これまで築いてきたfollow関係がもっていけないから… と躊躇しがちですが、そこはそれ、心機一転すっきりと人間関係をつくりなおすのも面白いとおもいます。ナンバーポータビリティなんか気にしない! 人間関係は定期的にリセットするんだ! 的な心情ですね。
まとめ
wassrはTwitter ライクなAPIをもってるので、Twitterクライアントをつくってるひとは対応してみては! (夏ラ希望)
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に、actionSelectorをact:に指定しておけば、ちゃんとactが呼ばれます。
疑問
- OSX.require_framework とか使わないで、QSActionProviderとかのQS frameworkが使えてるのはなぜ?
- hisaさんは、この方法だとCPU usageが高くなると懸念されているのですが、僕のところではいまひとつ高くなってるように見えません。why?
まとめ
RubyCocoaで、Quicsilverプラグインを書く方法をまとめました。
これで、あとはRubyのクラスとInfo.plistを書くだけでどんどんプラグインが書けますね。みんながんばれ!
あと、自分でもイマイチだなーと思うことでも、何かしら書いて晒すことが大事なんだなと。そんで改善していければOKなんだ。
hisaさん、ありがとうございました。
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のプラグインを書いてみました。
受けとった文字列に、”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さんにコメントいただき (!)、もっとよい方法を教えてもらいました。
こちらについても、まとめて別エントリにします。