mikutter blog

mikutterのアナウンスなど

mikutter 4.1の新機能

mikutter 4.1の季節がやってきました。

新機能

Reactive API

mikutterは、そのほとんど全てがプラグインであり、プラグインはイベントという単純なプロトコルで通信しています。

今回の機能は、10年間で変化し、洗練されてきたイベントの利用パターンを分析し、Rxなんとかをパクったものです。

4つの機能を紹介しますが、いずれも表現を幅を広げることを目的としていて、従来の機能を使うより高速又は最適化の余地が増えています。

それでいて従来のイベントやフィルタの仕組みと完全な互換性があるので、古いプラグインを使い続けながら、高性能な新しいプラグインを開発できます。

追加されるメソッド

それぞれの機能について詳しく説明するためにブログ記事は向かないと思ったので、詳しいことはドキュメントに任せて、ここでは一行の説明と、ソースコードの例だけを紹介します。

  1. Stream

    Stream は、様々なプラグインがバラバラに発生させている同じ名前のイベントを、一本のデータの流れとして捉える機能です。ストリームイベントとして宣言されている一部イベントにしか使えません。

    いままでのイベントリスナでの表現:

    on_extract_receive_message do |datasource, messages|
      if datasource == :mastodon_appear_toots
        messages.each do |message|
          # 処理
        end
      end
    end
    

    Streamを使った同じ意味の実装例:

    subscribe(:extract_receive_message, :mastodon_appear_toots).each do |message|
      # 処理
    end
    
  2. Generate

    Streamはイベント受け取り側の話だったのに対して、Generate はイベントを発生する側に同じ考え方を適用したものです。

    今までの表現:

    user_stream do |message|
      Plugin.call(:appear, [message])
    end
    

    Generateで書かれたほとんど同じ意味になる実装例:

    generate(:appear) do |yielder|
      user_stream do |message|
        yielder << message
      end
    end
    

    generateの例の方が長くなっていますが、appearイベントにイベントリスナが登録されていない場合、 user_stream を実行しないという特徴があります。途中から購読されたり、購読解除された場合にも、ちゃんと終了したり再実行する機能もあります。要するに無駄な処理を減らせるのですが、同じことをgenerateを使わずに実現するのは困難です。

  3. collect

    Collect は、今までフィルタで [0] とか .first とか付けていたのをいい感じに省略できる機能です。コレクションフィルタとして宣言された一部のフィルタにのみ利用できます。

    今までの表現:

    Plugin.filtering(:worlds, [])&.first
    

    Collectを使った同じ意味の実装例:

    collect(:worlds)
    
  4. collection

    Collection は、上記の例で取り上げたworldsフィルタのように、様々なプラグインが列挙した値をすべてまとめたリストを得るようなフィルタのパフォーマンスを手軽に改善するための機能です。

    この機能だけは単純な具体例を挙げにくいので簡単に説明します。通常のフィルタリスナは、 Plugin.filteringcollect が呼ばれる度にフィルタリスナが実行されるため、処理が重くなることが予想されるなら自前で計算結果を変数にキャッシュするなどの配慮が必要でした。Collectionは、返すべき配列を事前に登録しておくことで、呼ばれる度にリスナを起動する必要がないようにしています。

    また、面白い機能として、自動的に ○○__add○○__delete というストリームイベントが定義されます。要するに、 collect(:worlds) のようなことができるなら、

    subscribe(:worlds__add).each do |added_world|
      # Worldが追加される度に実行される処理
    end
    

    といったことができます。ただし、 ○○__add は、起動時には登録されている全てのWorldについて一回ずつ呼ばれることに注意しましょう。

機能改善

抽出タブ

データソースをプラグインで購読しましょう

Twitterだと、 appear イベントは受信した全てのツイート、 update イベントはホームTLといった具合でした。一方で、抽出タブデータソースにも同様のものがあります。あるデータソースと全く同じものをイベントで受け取れるように配慮はしていましたが、現実には説明を見る限り同じなのにコーナーケースでの挙動が違ったり、一つ簡単な条件式でフィルタすればいいだけなのでイベントを提供していなかったこともありました。

更に、Mastodonではローカルタイムラインといった、Twitterを元にした既存のmikutterの仕組みにない概念があります。Worldが拡張可能という前提に立つと、 update などのイベントはそもそもナンセンスなのではないか、と考えました。あと、これはみんな思っていることだと思いますが、「この抽出タブで取得しているようなMessageを俺のプラグインで購読するにはどうしたらええんやー!」ということがよくあります。

そこで、mikutter 4.1からは、抽出タブデータソースを、サードパーティプラグインが購読する前提でいくつかの改善を行いました。

subscriberをコピー

f:id:toshi_a:20200623221734p:plain 抽出タブの設定画面の、データソースのリストビューを表示します。そして、使いたいデータソースを右クリックし、「subscriberをコピー」をクリックします。

その後、エディタなどで貼り付けると、以下のようなコードが出てきます。

subscribe(:extract_receive_message, :mastodon_appear_toots).each do |message|

end

あとは間の空行にいろいろ書いていくだけで、このデータソースを使ってなにかするプラグインを書けます。

データソースが購読されている間だけTootを取得するようになった

データソースは、使われている時だけ取得されていくようになりました。Mastodonでは、取得は以下のような手段で行われます。

  • ストリーミング(SSE)
  • 定期的なポーリング(REST API

したがって、無駄な接続が削減される一方、互換性のために残っている appearupdatementions といったイベントは、期待する全てのTootを取得できないかもしれません(できるかどうかは、他のプラグインが何を購読しているかによって変わってしまう)。

尤も、普通の使い方だったら、全てのアカウントのメンションを抽出タブで購読してると思うので、直ちには問題にならないと思います。

Form DSL

listview widget

f:id:toshi_a:20200623230335p:plain

Form DSLがリストビューに対応しました。今まではそこそこ複雑なGtkAPIを使う必要がありましたが、これを使うとForm DSLように単純化され、他のウィジェット同様設定項目がいい感じに保存されるようになります。

listview(
  :intent_selector_rules,
  columns: [
    [_('開く方法'), ->(record) { intents[record[:intent]] }],
    [_('対象'),     ->(record) { models[record[:model]] }],
    [_('条件'),     ->(record) { record[:str] }],
  ],
) do |_record|
  select(_('開く方法'), :intent, intents)
  select(_('対象'), :model, models)
  input '条件', :str
end

別に単純にはなりませんでした。 intent_selector_rules というUserConfigに、 [{intent: ???, model: ???, str: ???}, ...] という配列が保存されます。 行は追加、編集、並び替え、削除ができます。doブロックの内容がDialog DSLになっており、追加編集時のダイアログボックスもカスタマイズできます。

今までも、標準プラグインでは設定でリストビューを使っている場所が何箇所かありましたが、いずれも直接Gtkを使っており、細かな挙動がそれぞれのリストビューで違いました。 この機能は、それらを統一することが目的の一つです。今はIntentだけが使っていますが、(うまくいけば)他のリストビューもこちらの実装に切り替わっていくでしょう。

keybind widget

ショートカットキーを設定させる、 keybind ウィジェットを新たに追加しました。

mikutter 3.6ではSetting DSLForm DSLに機能が切り出されており、Setting DSLのほか、Dialog DSLなどで利用されています。この変更はUI周りの整理の一環として為されました。ショートカットキーを設定したいような機能はmikutterコマンドにしたほうが便利なので、通常プラグイン開発者がこれを使うことはないと思います。そのうちショートカットキープラグインも、これを使って実装し直したいなあ。

Intent

リスト、ダイアログの改善

前述のForm DSLのlistviewを使ってUIを再実装しています。

設定>関連付け の画面で、既に設定されているルールをドラッグ&ドロップで並び替えできるようになりました。

Intentは、上のルールから順番に走査し、最初にマッチしたものを使います。一度「https://example.com/ から始まる」のようなルールを作ってしまうと、あとで「https://example.com/images/ から始まる」といったルールを追加しても無視されるので、最後に持っていきたいルールを追加し直す必要がありましたが、今後は単に並び替えることで対応できます。

Mtkモジュールのすべてのメソッドがdeprecatedに

Setting DSLが登場するより前(たぶん8年くらい前)のmikutterで使われていた、設定みたいなUIを表現するためのユーティリティモジュールを正式にdeprecatedにしました。今後、このモジュールのメソッドを呼ぶと警告が出ます(使っている人はいないと思うけど)。

とくに、 Mtk.dialog は、呼び出すだけで絶対にFiberErrorでmikutterがクラッシュし、互換性を保ったままこれを修正するのは不可能でした。今後は、ほとんど同じことができるDialog DSLを使ってください。

予約実行

Delayer.new に、delayという名前付き引数が追加されました。数値で秒数を渡すと、その秒数だけ待ってからブロックを実行します。Timeオブジェクトを渡すことも出来、その場合はその時刻になったらブロックが実行されます。

以前からSerialThreadを使えば同様のことが出来ましたが、Delayerは一切別のスレッドを作りません。

画像キャッシュ管理方法の変更

アイコンやスキンなどで使われている画像リソースの管理方法が変更されました。

メモリキャッシュ

一度使った画像をメモリ上にロードしておいて、次以降に使われるときはローカルストレージやネット上から画像を取得せず、メモリ上から即座に返す仕組みがありました。 しかし、瞬間的に何度か使われた画像は、事実上再起動するまでずっとメモリ上に記録することがあり、メモリを過剰に消費する一因となっていました。

今後はファイルキャッシュがある前提で、WeakRefを使ってメモリキャッシュを保持するようになります。

ファイルキャッシュ

画像をファイルとしてキャッシュする機能があります。しかし、不具合と言って良いくらいキャッシュされる確率が低く、ほとんど効果がありませんでした。

mikutter 4.1からは、画像のファイルキャッシュとして消費してもよい容量上限を決めておいて、その範囲内で貪欲にキャッシュするようになります。ウェブブラウザだってストレージにコンテンツをキャッシュするので、ユーザの期待通りになったといえるでしょう。

書き忘れていること

書き忘れていることです。

リリース時期

例によってまだバグっているところもありますが、概ね問題なく使えているので、6/27のdevelopブランチの状態を4.1.0-alpha1とする予定です。

6/30追記

27日にパソコンぶっ壊れたので、7/4にalpha1出します