mikutterの最新の情報は、mikutter blogに引っ越しました。

2012年9月16日日曜日

mikutter 0.2 プラグイン移行ガイド

1 はじめに
まだバグは致命的なもの含めていくつか残ってますが、テストしてもらえる品質になったと判断したので、mikutter 0.2 を mikutter の trunk にマージしました。 今回のアップデートで、0.1 系と互換性が失われた部分がいくつかあり、プラグインを開発している人にはお手数ですが自分のプラグインに手を入れてもらわなけばいけなくなりました。

それもこれもクーラーが壊れたのが悪いんです。で、みなさんのプラグインをどうやって移行していけばいいかを簡単に説明します。 今回は、あくまで0.1用のプラグインが「動く」ようになることを主眼に説明します。 つまり、すべてをモダンな形式に書きなおす訳ではなく、互換性のあるところはできるだけ触らないようにして、 最小ステップで動くようにする方法を紹介します。 したがって、0.2対応のプラグインを書くためのドキュメントとしてはあまり使えません。

2 ざっくり
0.2 の目玉の変更はGUI関連に大幅に手が入ったことです。 これにより、複雑なGUIをもったプラグインが動作しない場合があります。 また、mikutterコマンドも変わったので、 もしあなたの作成したプラグインにmikutterコマンドを提供しているものがあるなら、 基本的には必ず書き替えなければならないと考えてください。

3 タブ
今までは、Gtkオブジェクトを作成して、 mui_tab_regist イベントでそれをGUIに渡して、適切な場所に配置されていました。 これからはGtkは基本的には使いません。全てはUI DSLを介して操作し、それをツールキットプラグインが表示する というスタイルをとっています。こうすることで、mikutterをGtk以外で操作する方法を提供する余地を持たせたい、 という目論見があります。 とはいうものの、UI DSLはmikutterプラグインのほとんどのニーズを満たすために最適化されているため、複雑な インターフェイスを定義することには向いていません。望むなら nativewidget メソッドを使ってタブの中に Gtkオブジェクトを直接埋め込めますし、 mui_tab_regist も非推奨ですが残されています (そして、いくつかのmikutterプラグインは未だにこの古い方法でタブを構築しています)。 ぶっちゃけて言えば mui_tab_regist はそのまま使える。Gtk::TimeLineもそのままでいい。 (Gtk::TimeLineは昨日あたりまでバグで動作してませんでしたが、今はもう大丈夫です)

あとどうでもいいんですけど、registっていう英単語はないんですってね。なんであると思い込んでたんだろう。 多分みんな使ってるから、調べもせずに日常的に使うようになってしまったんでしょうけど、 イベント名なので単純に変えられないし。早くこんな黒歴史イベント消し去りたいです。 このイベントを使わずにもっとクールに書く方法はあるんですが、今回の範囲外なのでカット。

4 設定
設定はポップアップウィンドウになりましたが、プラグインからsettings DSLを 使って設定を挿入することに変わりはありません。 これについても、内部のAPIは完全に互換性があります。

5 廃止されたイベント
内部でしかほとんど使っていなかったような、Gtk関係のフィルタを削除しました。残念ながら、見た目はほとんど 変わっていませんが、mikutterのUIの構造は大幅に変わっているので、安直に代替できるものはほとんどの場合ありません。 Gtkウィジェットのツリー構造を直接操作するようなプラグインも(多分見たことないけど)動かなくなります。

6 mikutter コマンド
おそらく一番多くの人にとって問題になるのはmikutterコマンドです。これは今回どうしても変更せざるを得ませんでした。

6.1 command メソッド
従来はmikutterコマンドを追加するためにはフィルタを用いていましたが、これからは command メソッドを使用します。 まずは従来の書き方をおさらいしてみましょう。
# in 0.1.x
on_command do |menu|
  menu[:reply] = {
    :slug = :reply,
    :name => '返信',
    :condition => lambda{ |ms| ms.map(&:message).all? &:repliable? },
    :exec => lambda{ |ms| ms.first.timeline.reply(ms.first.message, :subreplies => ms.map(&:message)) },
    :visible => true,
    :role => :message
  }
  [menu]
end
このモデルにはさまざまな問題があって、まずフィルタで受け取ったHashに、スラッグをキーにしてHashを入れる必要がありますが、 中に入れるハッシュの中にもスラッグを書いておかなければいけません。2つが違うと不具合が起こるので、 あまり好ましいAPIではありませんでした。 更に、フィルタなので、最後に引数を配列で返す必要があります。だいたいはスニペットで書いてしまうのでこれが問題になることは少ないですが、 ミスをする原因にしかなりません。 0.2では以上の点を改善しています。
# in 0.2
  command(:reply,
          name: '返信',
          condition: Plugin::Command[:CanReplyAll],
          visible: true,
          role: :timeline) do |opt|
    opt.widget.create_reply_postbox(opt.messages.first.message,
                                    subreplies: opt.messages.map(&:message)) end    
commandメソッドが追加されました。
command(slug, options, &proc)
slug にスラッグを設定して、 options は従来渡していたHashと同じ物を渡します。ポイントは、slugを指定する必要が なくなったことです。 slug に設定されている値が流用されます。 更に、 :exec キー(コマンドの実行内容)も不要になり、 &proc を使うようになりました。これも slug のように中で Hashが加工されているとお考えください。というかそうなってます。

6.2 condition
condition キーに、従来は無名関数を渡していましたが、例では Plugin::Command[:CanReplyAll] という値が 設定されています。 これは単純に、よくある条件を定数にまとめたもので、あえて使う必要はありません。 すべての定数は mikutterの「core/plugin/command/conditions.rb」にまとめられています。 実は引数に渡される構造体にも若干の変化があるのですが、それは次の実行部分の話にまとめます。

6.3 ロール
どのウィジェット上でコマンドを実行するかを指定する部分で、条件や実行時に渡ってくる引数が変わります。 ただ、今まであった :message:message_select のような、 直接存在しないウィジェットに紐づいていないロールは 廃止されています 。 0.2でも使用できるロールは、実際のウィジェットとして存在するもの、すなわち :timeline, :postbox です。 また、ウィジェット全てにロールが割り当てられたので、今までにはなかった :tab, :pane, :window, :profiletab, :profile が新たに使えるようになりました。移植するのに新しいロールを使うことはない と思うのでここでは割愛します。

6.4 条件及び実行時の引数
いままでは、ロールによって渡される引数が違いましたが、 これからはどのロールでも同じ構造体が渡されるようになりました。
Plugin::GUI::Event = Struct.new(:event, :widget, :messages)
event には、mikutterコマンドがどのような入力によって実行されたかがシンボルで入っています。 基本的には :contextmenu (右クリックメニューから選択された)か :keypress (ショートカットキー)です (mikutter 0.2 からはmikutterコマンドを呼ぶデバイス等も拡張できます。だから :gamepad とか :kinect とかが入ってくる可能性があります)。 widget は、イベントが発生したウィジェットです。ロールが :timeline なら、必ずPlugin::GUI::Timeline のインスタンスが入ります。 最後の :messageswidget のアクティブな子ウィジェットを再帰的に探索し、タイムラインがあれば、 そのタイムラインでイベント発生時にアクティブだった Message の配列が渡されます。 これは Plugin::GUI::Timeline#selected_messages の戻り値ですが、 選択されているツイートに対して実行されるコマンドには必ずこの配列の内容を使ってください。 イベント発生時と実行時には微妙なタイムラグがある可能性があり、コマンドを実行した時に選択していたツイートと、 実際に実行された時に選択しているツイートが違う可能性があるからです。

6.5 具体例等
mikutterの core/plugin/command/command.rb に、たいがいのコマンドが定義されているので参考になると思います。 フィーリングでコピったらいけるでしょう。

7 定義ファイル
プラグインによっては、正しく書いているのにNoMethodErrorなどでクラッシュすることがあります。 理由はたいがい、guiプラグイン等がプラグインDSLにあとからメソッドを足しているケースがあり、これらの プラグインよりそのプラグインが先にロードされていることが原因なんじゃないでしょうか。 0.2からはどのプラグインに依存しているかといった情報を書く定義ファイルが追加されたので、 依存するプラグインがあればそれを書いておくべきです。そうすれば、依存しているプラグインを先にロードしますし、 依存するプラグインが一つでも存在しなければ、ロードされません。

7.1 定義ファイルの書式
spec という名前のファイルを作成して、中にYAMLで記述します。 例えばダイレクトメッセージプラグインはこんなかんじです。
name: Direct Message
slug: directmessage
description:
  mikutterにリア充御用達機能ダイレクトメッセージを追加します。
  送受信したすべてのDMを見るタブと、各ユーザのプロフィールにそのユーザと自分がやり取りしたDMを表示するタブを追加します。
depends:
  mikutter: "0.2"
  plugin:
    - gui
    - gtk
version:
  "1.1"
author: toshi_a
  • name プラグインの通称です。日本語でもいいです。現在は使われませんが、インストールしてるプラグイン一覧みたいなのをそのうち作ってみたいですね。
  • slug プラグインのスラッグ。依存関係の解決に使われます。
  • description プラグインの簡単な説明。
  • depends/mikutter 想定するmikutterのバージョン。ここに書いてあるバージョンと現在のmikutterのバージョンに 互換性がなければプラグインがロードされません。
  • depends/plugin 依存するプラグインのスラッグ。今のところ、GUIがないと意味がないようなプラグインは gui を、 Gtkクラスを少しでも使っている場合は gtk を指定してください。DirectMessage は両方指定しています。 一つしかない場合も配列で指定してください。
  • version プラグイン自体のバージョン。今のところ特に用途はありません。
  • author プラグインの開発者のTwitterアカウントのscreen_name。これも今のところ特に用途はない。
8 まとめ
今回は当初の設計では想定していなかったことをいくつか実現するために、内部を大幅に書き換えたので、いくつか プラグインの互換性がなくなってしまいました。mikutterは今まで、必ず過去のプラグインも動くようにしていたので、 今回手を入れたGUIまわりは、最初の設計が誤っていたと言わざるを得ないでしょう。 

難しく言えば、クライアントに新たなユーザエクスペリエンスを提供するためにはアーキテクチャにイノベーションを起こすことが必要でした。 その代償としてコンパチビリティを失うソリューションしかトゥギャザーできなかったのは残念ですが、逆に言えば、 長いスパンで見ればプラグインデベロッパの開発コストを大幅に減らすオポチュニティと捉え、 プラグインDSLを拡張することによって新たなmikutterのコアコンピタンスを創出することができたので、 私とみなさんにとってWin-Winの変更になったと思う次第です。

mikutterのプラグインは本当にいろんなことができます。だから場合によってはここにある内容だけでは 移植することができないかもしれません。あと、なんか書き忘れてる気もします。 だから何かあれば適当に聞いてくれたらいいと思います。適当に答えますんで。