Multiple Instances #2

そういうわけで、同時に複数の wasavi を起動できるようにした。

最も大掛かりにコードの変更を受けたのは agent.js で、一旦バラバラにして組み直したような格好だ。しかしこれでコード内の役割分担が割と明確になったので結果的にはよかった。

次に控えている issue はちょっと難しい。

Multiple Instances

issue #118

現状の実装だと、あるページに textarea が複数あった場合、wasavi の起動は排他的だ。つまり複数の wasavi を同時起動することはできない。この制限を取り払ってほしいという issue。

これを解決するとして、最も影響を受けるのはエージェントだ。まず wasavi を起動する際にフレームごとにユニークな ID を振るようにして、メッセージングの際はそれをやりとりするようにしないといけない。特にメッセージを受信した場合はフレーム ID を元に複数の wasavi のインスタンスから正しい宛先を探しだし、適切にディスパッチする必要がある。

これはこれとして、もともとエージェントの構造は若干とりとめなく関数を書き連ねてた形になっていたのが気になっていたので、大掛かりに見直す切っ掛けとしては良い。

Tegaki #2

akahukuplus-tegaki-in-summary2

ふたばのオリジナルの手描き機能では、手描き画像でスレを立てることはできない設定になっている。少なくとも虹裏 may ではできない。これはきっと理由があるのだと思うけど、まあブラウザのエクステンションでは特にそういう制限をかける必要はあるまいということで、立てられるようにした。

このとき、手描き画像のキャンバスサイズは 640×480 に拡大されるようになっている。これは Tegaki Draw and Tweet の画像サイズに準じた。

ちなみにオリジナルの手描き機能を送信する際の内部的なフォームデータは、baseform という hidden input 要素である。手描きの flash が逐一この要素に画像を base64 でエンコードした文字列を設定する。これとは別に、従来通りの普通にファイルを送信する要素は upfile である。すなわち可能性としてはファイルと手描き画像を両方同時に送信することが可能なのだが、試していないがたぶん upfile のほうが優先される。

赤福プラスでは baseform 要素を特に積極的に使う必要はない。というのは手描き画像を保持する canvas 要素から blob データを得て、送信フォームの構築時に upfile に割り当てればいいだけの話だからだ。そんな細かいことをブラウザだけでできるようになったんだねぇとしみじみしちゃうが、とりあえずレス画像としての手描きは baseform、スレ画像としては upfile を使うようにしてある。

ついでに 1 回限りの undo やペンサイズを変更するためのショートカットなどを追加。

Pick it up

とある事情でいわゆるカラーピッカーが入り用になった。そこで、とりあえずググった所ものすごくたくさんある。なぜプログラマはカラーピッカーを書きたがるのか?

ちなみに、今回欲しいカラーピッカーの要件は:

  • シンプルでいい。HSV 方式だけでいい
  • 付加的な画像ファイルやCSSファイルを必要としないほうがいい。javascript ソースだけで動けばいい
  • jQuery 等々には依存しないほうがいい
  • 最後に選択した色を憶えててくれて、再選択できるといい
  • hex 表記との相互変換ができるといい
  • 表示開始のタイミングを呼び出し側で制御できるといい

最後の表示開始タイミング云々というのは、既存のライブラリの中には、ドキュメントの load イベント時に、カラーピッカーを起動させるべく特別なクラス名などが付加された要素に対してアタッチするようなものも少なくないのである。しかし静的なページならそれでもいいけど、動的なページだとかなり相性が悪い。そんなわけで表示開始のタイミングは呼び出し側で制御したい。こんな感じで:

targetElement.addEventListener('click', function (e) {
startColorPicker(e.target, {
onchange: function (color) {
},
ok: function (color) {
},
cancel: function () {
}
});
}, false);

ところが、この要件に合致する既存のライブラリがなかなか見つからないのである。そもそも、jQuery プラグインとして作ってあるものが大半だったりする。別に jQuery プラグインだからダメというわけではない。最初から jQuery ありの前提で書いてたアプリに組み込むのならまったく問題ない。しかしもともと jQuery なしで開発してたものにたかがカラーピッカー 1 つのために jQuery を読み込ませるのはかなりとっても相当嫌だ。jQuery プラグインと同時に、依存なし版も合わせて作って公開してくれれば更に多くの人に使われると思うのだけど。

そんなわけでググれどググれど見つからないので、しょうがないので自分で書く羽目になってしまったのである。なんでこんなものがないんだ。かなりプンプンだ。

color-picker

そんなこんなでドバババと書いたのであった。

Compatibility to VimFx #3

色々やりとりしてみた所、何やら思いがけない方向に進んだ。

まず、wasavi 側で VimFx を意識した処理をする必要はなくなった。逆に、VimFx 側で wasavi の iframe を contenteditable な要素として扱ってもらえるようになった。のだが、途中から wasavi が起動中に blur させる方法がないのが問題、という話になり、正直なところへぁ? なんのこと? という気分に。

どうも VimFx の作法として、編集可能な要素がフォーカスを持っている場合は VimFx は自身のキーバインドを使用しないというものがあるようだ(逆に言うと、編集可能な要素の振る舞いを上書くことは VimFx ではできない)。ただし、編集可能な要素上で esc キーを押すとフォーカスを外す、つまり VimFx のキーバインドが再度有効になるようにはなっている。したがって、例えば通常の textarea で編集中に他のタブに切り替え、必要なものをコピー、戻って貼り付けなどという一連の動作をすべてキーボードで行うには、

  • textarea 上で esc
  • gt (次のタブへ)
  • yf 等々で必要なものをコピー
  • gT (前のタブへ)
  • gi (先頭、あるいは最後にフォーカスした編集可能要素に再度フォーカス)
  • wasavi 上で [cci]”*p[/cci]

というようなキーボード操作になる。で、これを可能とするために、wasavi も esc で blur して欲しいということのようだ。

というわけで、そういう風にした。ただ、この常にこの仕様で動作させて良いのか? という懸念はある。

  • vi 使いの中にはノーマルモードに戻るために esc を連打する人もいる
  • そうでなくても、うっかりノーマルモード上で esc を押してしまうことは有り得る
  • あくまで VimFx の作法である。VimFx がインストールされていれば、wasavi がフォーカスを失っても即 [cci]gi[/cci] で戻すことはできるが、インストールされていなければポインティングデバイスで wasavi をクリックし直すしかない

なので、新しいオプション [cci]esctoblur[/cci] を導入し、これがオンの場合のみに blur 処理を行うようにした。

Compatibility to VimFx #2

動作するようにはなったのだが、やはりイベントハンドラの実引数として渡された何かを取っておいてハンドラのスコープの外で使うというやり方はちょっと嫌な感じではある。

VimFx の issue に何か参考になるものがないのかなと見てみたら、丁度よく wasavi が動かない、正確には Pterosaur や CodeMirror や wasavi 等々、自前で色々キー入力を消費するタイプの拡張と競合する、というが上がっていたので乗っかってみよう。

Compatibility to VimFx

Firefox の拡張には、キーボード周りを含めたブラウザのインターフェースを「強力に」書き換える能力があるわけなのだが、それらと wasavi の相性は一般的によくない。wasavi もまたすべてのキーボード入力を自分で消費するからだ。すると、あるキー入力が他の拡張に横取りされてしまうとか、wasavi と同時に処理されてしまうと言った不具合が起きる。

それを避けるために、VimperatorKeysnail 用のプラグインスクリプトを用意してある。これらは主に 2 つの仕事を受け持つ:

  1. タブの切り替えを監視し、アクティブなタブのドキュメント上に実行中の wasavi が存在するかどうかで対象の拡張を一時的にサスペンドさせるかを制御する(ちなみにこの辺の処理は e10s が有効だと完璧に破綻する: プラグインは Chrome 権限上で動いている、そして content を直接触るためだ)
  2. ドキュメントのカスタムイベント [cci]WasaviStarted[/cci] [cci]WasaviTerminated[/cci] を監視し、各イベントに応じて適宜対象の拡張を一時的にサスペンドさせるかを制御する

ところで、こういったインターフェースを再定義する拡張はもちろんこれだけではない。issue では VimFx との相性がよくないというものが上がっているので、とりあえずそれに対応してみることにしよう。

* * *

VimFx というのはなんぞやというと、実はあまり知らないのだが、本質的には Vimperator と同様に Firefox のインターフェースを vim っぽくする系の拡張である。ただし、Vimperator がインターフェースを「ドラスティックに」変えるのに対して VimFx はもうちょっと大人しいようだ。

プラグインの機構はない。ただし、Chrome 権限のスクリプトから使用できる API が提供されている。

さて、対象の拡張が変わったとしても、やるべきことは上記の 2 点なのは変わらない。ただし 1 は上記のとおり、e10s を見越して content 内で処理する必要がある。普通の DOM のイベントでタブがアクティブ・非アクティブになったというのを判定するには Page Visibility という割と新し目の API で定義される visibilitychange イベントを使えば良いだろう。一方で、wasavi の状態によって他の拡張にアクセスするのは content ではなくバックエンドの chrome スクリプトでなければならないので、つまり visibilitychange イベントでやることはアクティブなタブだったらバックエンドにメッセージを投げる、ということになる。そんなわけで:


window.addEventListener('visibilitychange', function () {
if (document.hidden) return;
extension.postMessage({
type: 'visibility-change',
wasaviRunning: !!wasaviFrame
})
}, false);

というような感じ。

このメッセージをバックエンドで受け取り、wasavi が起動しているようなら VimFx をサスペンドさせればいいのである。

* * *

というわけで色々作って、VimFx の API を使ってどうこうするところまで来たのだが。

ドキュメントによれば、API はそれを提供するオブジェクトのインスタンスを通して使用する。インスタンスの取得は

let {classes: Cc, interfaces: Ci, utils: Cu} = Components
Cu.import('resource://gre/modules/Services.jsm')
let apiPref = 'extensions.VimFx.api_url'
let apiUrl = Services.prefs.getComplexValue(apiPref, Ci.nsISupportsString).data
Cu.import(apiUrl, {}).getAPI(vimfx => {

// Do things with the `vimfx` object here.

})

こんな感じで、ここで [cci]getAPI[/cci] に渡しているコールバックに渡される仮引数 vimfx がそのインスタンスなのだが。困ったことにその中に VimFx をサスペンドさせるようなそのものズバリの機能はないのである。

というより VimFx 自体にサスペンドという概念がない。その代わり、すべての入力を素通りさせて VimFx は関与しない ignore モードというのがあって、normal モードから ignore モードへ遷移することで結果的にサスペンドが行える。で、このモードを遷移するための [cci]enterMode()[/cci] メソッドはどこに属しているのかというと VimFx のコマンド群のそのハンドラに渡される vim パラメータが持っているらしい。しかしこれを API から直接的に得る手段はない。うーんどうすれば……。

しかし、間接的に得る手段はある。[cci]vimfx.on()[/cci] でいくつかのイベントハンドラを登録することができて、その時も vim パラメータが渡される。これを取っておいて、必要なときに使えばいけるかもしれない。

* * *

というわけで、期待した動きをするようになった。

The Traditional vi

issue #113

これはいわゆる vim の compatible mode を実装してくれという要望なのだが、あれを実装するのは大変そうだ。具体的にはテストが大変だ。様々なオプションのオン・オフの組み合わせを網羅するとテスト項目が半端ないものになってしまう。

件の issue の方は最低でも traditional な vi の uu だけでもいいということなのでそういう方向で行ってみたい。

vi の uu というのは、u で一旦 undo した後、続けて u を押すと今度は redo 動作になる、という不思議な動作のことだ。この動作のことを一般に何と言うのかわからないが、とりあえず flipping undo と呼んでおこう。

これをどうやって実装するかは、素直に boolean なフラグを保持しておいて(以下 flipped)、u が押された時にそれを元に動作を振り分けということになるだろう。

いくつか気になる点:

  • flipping undo とふつーの undo をどう切り替えるか? vim では

    :set compatible
    :set cpoptions+=u

    とすることで flipping undo モードになる。しかし前述の通り [cci]compatible[/cci] や [cci]cpoptions[/cci] の類はあんまり実装したくない。ちなみに vim では、flipping undo モードにするための方法はもう一つあり、それはつまり [cci]undolevels[/cci] を 0 にするというものだ。じゃあこれでいいかな。
  • wasavi では vim と同様、[cci]^R[/cci] に redo を割り当てている。flipping undo の場合これは何をするべきなのか? vim の場合は、どうも flipped 変数を変更しない undo として振る舞うようだ。つまり flipping undo 時の u コマンドは

    if (flipped)
    redo();
    else
    undo();
    flipped = !flipped;

    という動作になる。[cci]^R[/cci] もほぼ同じなのだけど、最後の flipped 変数の変更だけは行わない。

Two of three

issue をちまちま片付けていたのだがなぜが次々と新しい issue が上がってくる。なにこれ…と思ったら、3 日に Hacker News で wasavi が取り上げられていたようだ。

前にも書いたが、書いたプログラムが Hacker News と Reddit と Slashdot で取り上げられたらまあある程度浸透したのかもしれないと勝手に思っているのである。Reddit にはすでに取り上げられていて、今回は Hacker News なので 2/3 のスコアになった。

残りは Slashdot なのだが。しかし最近の Slashdot はなんか知らないがとても gdgd な状況になっているようなので、まあいいかなという気分になりつつある。

Access local files from wasavi #4

Chromeでローカルファイルの読み書きの目処が立ったということで、Firefox にも同じ機能を実装してみる。Firefox の拡張からローカルファイルを操作するための API はいくつかあるが、新規に実装するということで最もナウい OS.File を使う。どれくらいナウいのか、なんと言っても完全に Promise と融合したインターフェースになっていると言うことだろう。これは今風だ。ナウいぜイマいぜ超マブいぜ。

ちなみに Presto Opera に対しては同じ機能を実装することはできない。Presto Opera 上の wasavi で file: スキームのファイルの読み書きを試そうとしても必ずエラーになる。前の記事の通り、Opera Unite がポシャらなければそれを経由して何とかなったんだけど……。

もう一つちなむと、かねてから記事にしている通りビューとモデルの分離という大きな作業も並行して行っている。そして、それが完了したバージョンから、wasavi はもはや Presto Opera 向けにはリリースしない。

* * *

実装した。

Chrome と Firefox でローカルファイルの読み書きで最も違うのは、後者はローカルファイルシステムそのものを取り扱うことができるのに対し、前者は LFO アプリケーションというラッパーを通す際にマウント位置が自由であることで実質的にローカルファイルシステムが仮想的になってしまうということだ。これはファイルシステムのルート以外をマウントした場合に違いがはっきり現れる。

実際のファイルシステム上の /home/akahuku を LFO にマウントする。このとき、wasavi を起動する。wasavi 上のカレントディレクトリは [cci]/[/cci] なので、Chrome の場合は wasavi 上で [cci]:r [/cci] したとき補完されるファイル群はホームディレクトリ直下のファイルだが、Firefox の場合はルート直下のファイルになってしまう。

LFO でどこでもマウントできるという仕様自体に何か問題があるわけではないので、これは運用でカバーすべき問題かもしれない。つまり Chrome 版 wasavi でローカルファイルを扱うための現実的な作業手順というのは、

  • LFO でドライブのルートをマウントする
  • wasavi の exrc で [cci]cd /path/to/home[/cci] する

ということになる。

* * *

ローカルファイルアクセスが可能になることでもう一つ影響を受けるかもしれないものがあって、それは exrc だ。ホームディレクトリに .wasavirc を置いておいたほうが楽な場合は多々あることだろう。ただし、ここでも若干問題があり、Chrome の filesystem では現在のユーザのホームディレクトリがどこかを得る手段がない(そもそも現在のユーザというものが、OS にログインしているアカウントなのか、ブラウザの同期サービスにログインしているアカウントなのか?)

これは、LFO でホームディレクトリがどこかも登録させるようにすべきだろうか?