2017/07/17 6:29 pm
Reloading extensions
Uncategorized, , ,

ブラウザのエクステンションの開発における基本的なサイクルは、ソースを書き換えて、エクステンションをリロードし、実行結果を確認する…というものだ。このサイクルを延々と繰り返す。したがってサイクルを構成する基本的な要素をちょっと最適化するだけでも、その分だけ開発は快適になる。この内、エクステンションのリロードについて考えてみたい。

かつて、Presto Opera が wasavi のファーストクラスブラウザだった頃は、リロードプロセスは実は最も洗練されていた。というのは、ページを普通にリロードすれば、ページにアタッチされている拡張の injected script 群も自動的にリロードされたからだ。賢すぎる。もちろんバックグラウンド側に修正を入れた場合はエクステンション管理ページから wasavi 全体をリロードする必要はあったものの、そういうケースはそんなにあるわけではないので、フロントエンド側がスマートにリロードされるだけで十分快適なのだった。

しかし、それはもう過去の話。現在はどうかというと、wasavi はまず Chrome で動かしてそれから Firefox や Blink Opera で動作確認という流れになっているのだが、しかしとても残念なことにいずれのブラウザも Presto Opera ほど賢くない。content script を書き換えたとしても必ずエクステンション管理ページから手動でリロードしないといけない。とても面倒くさい。とてつもなく面倒くさい。

そういうわけで、wasavi 自身に reload コマンドを実装してある。これを投入すると、メッセージがバックグラウンドとエージェントの両方に送信される。バックグラウンド側では自身を chrome.runtime.reload() によりリロードする。エージェント側ではメッセージの着信後1秒待ってページをリロードする。これでリロードの面倒臭さがかなり解消される。

ところで、Firefox の WebExtensions ベースのエクステンションを開発するための web-ext というツールがあるのだが、これを経由して Firefox を起動するとエクステンションのソースディレクトリを監視して、いずれかのファイルが更新されたら自動的にエクステンションをリロードするという機能を持っている。これはこれで便利なのだが、最新バージョンだとバグってて動かないという…。--no-reload オプションをつけないと起動しない。安心と信頼の Mozilla クオリティ。

2017/07/14 9:41 pm
Move by wrapped line
Uncategorized, , ,

Chrome と Gecko が持っている document.getSelection().modify() というメソッドがある。これは、編集可能な要素上のカーソルを任意の位置へ移動させる機能を持っている。上下左右のカーソルキー、HOME キー、END キー、およびそれらを SHIFT 併用で押下した場合の動作をコードから行わせることができる。カーソル移動の単位は文字、ワード、折り返し行の先頭及び末尾…などがある。

さて、wasavi は gjgk で物理的な行単位ではなく、表示上の折り返された行ごとにカーソルを上下させる機能があるのだが、従来この機能はかなりめんどくさい複雑なコードで実現していた。あまりに複雑すぎてバグがあってもおいそれと手を入れられないレベルである。実際 issue がいくつか来ているのだが、そういうわけでどうしたものか困っていた。

そこで、上記の modify() を使ってシンプルに書き直した。なんで最初からそうしなかったのかといえば、modify() は Presto Opera には実装されていないからだ(たぶん)。しかしもう Presto Opera 対応は終了したので、これを期に使うようにしたということだ。このほか、let や template literal、arrow function、そしてもちろん Promise や Generator なんかもぼちぼち使い始めている。

6:16 pm
WebExtensions and clipboard operation
Uncategorized, , ,

Chrome の拡張でクリップボードを読み書きするには、manifest.json の permissions に clipboardRead/clipboardWrite を追加した上で

  • 読み出す場合: バックグラウンドで適当な textarea に対して focus() し、document.execCommand(‘paste’) する。適当な textarea のその内容がクリップボードの内容で埋められる
  • 書き出す場合: バックグラウンドで適当な textarea に対して focus() し、内容を全選択した後 document.execCommand(‘copy’) する
  • つまり操作の実体はバックグラウンドになる。コンテントスクリプト側で読み書きの必要が発生したとしたら、バックグラウンドにメッセージを投げ、戻ってくるのを待つことになる。

    これが、WebExtension だとどうなるのかというと、ドキュメント自体は提供されているのだが、それによるとなんかどういう意図なのか知らないが、かなり Chrome の流儀と違う。

    まず short-lived event handler なる独自の概念が出てくる。これはつまり、ユーザーにより発生したイベント、のことらしい。しかしそれなら interactive event handler とか、あるいはそのまんま user generated event handler などと呼ぶべきものではないのか? 微妙によくわからない。何がどう short-lived なの? 意味がわからない。またもうひとつ独自の仕様があり、WebExtensions ではバックグラウンドスクリプト側でのクリップボードへの書き込みはできない。従って、コンテントスクリプト側で適当な textarea 要素に対して execCommand() しないといけない。つまり、Chrome と正反対なのである。

    で、この short-lived event handler 内であれば、clipboardWrite 権限なしにクリップボードへの書き込みは可能である。一方 short-lived ではないもの、つまりタイマによって起動された処理など、あるいはユーザーにより発生したイベント内で生成された Promise の一連の連鎖内も当てはまると思うが、そういう処理からはクリップボードへ書き込みを行うには clipboardWrite 権限を manifest.json に記述することが必要。

    ここまではドキュメントにそう書いてある通りで、その通りに wasavi を修正したならば、その通りに動作した。困るのはクリップボードからの読み出しなのであった。どうもまず、ドキュメントが言葉足らずで微妙に言いたいことがよくわからない。ドキュメントから読み取れるのは:

    • short-lived event handler 内で実行されるか否かに関係なく、常に clipboardRead 権限が必要
    • 読み込みも基本的にコンテントスクリプト側で処理?(サンプルが、適当な要素のクリックイベントを使用しているので)
    • 対象の要素は contentEditable モードでないといけない
    • コンテントスクリプト側で実行する場合、現状では textarea 要素のみに対応
    • バックグラウンド側ではいずれの要素も contentEditable モードにすることができる(できるから、何?)

    そもそもバックグラウンド側で実行できるのかどうかが明示されておらずよくわからない。クリップボードへの書き込みがバックグラウンド側では不可というのは、つまりバックグラウンドでは要素にフォーカスを当てるということができないからだそうだが、それなら読み込みも不可なんじゃないのか? しかし不可と明示されてはいないのである。わからん、ぜんぜんわからん。

    いろいろ試行錯誤してみたところ、以下のような手順でクリップボードの内容を取得することができた。

    • コンテントスクリプト側で実行する
    • ドキュメントに適当な textarea 要素を追加する。この要素のスタイルを display:none とかにはできない。フォーカスを当てる必要があるので。その代わりにスクリーン外の適当な場所に追いやる必要がある
    • textarea 要素は contentEditable 属性を true にしておく必要がある。そもそも最初から編集可能なのに、なんでこの属性が必要なのか意味不明
    • textarea にフォーカスし、document.execCommand(‘paste’) を実行する
    • しかし、Chrome と違い、textarea の内容がクリップボードの内容で埋められたりは「しない」。なんと、なんと、その代わりに document に対して paste イベントが発生する。従って、execCommand 呼び出しの前に paste イベントハンドラを追加し、その中でクリップボードの内容を取り出しておく必要がある
    • なお paste イベントは、同期的に発生する

    どうも execCommand() により paste イベントが発生するという点がミソのようだ。その点を以ってバックグラウンドでも動作可能と匂わせているのかもしれない。ということは、コンテントスクリプト側で動かす際も textarea 要素に focus() する必要はないのか? そこまでは試してない。

    とりあえず、こんな感じのコードになる。

    function getClipboard () {
      let s = '';
      function handlePaste (e) {
        s = e.clipboardData.getData('text/plain');
      }
      let buffer = $('id_of_any_textarea');
      buffer.contentEditable = true;
      buffer.value = '';
      buffer.focus();
      document.addEventListener('paste', handlePaste, false);
      document.execCommand('paste');
      document.removeEventListener('paste', handlePaste, false);
      return s;
    }

    もちろん、すでに paste イベントを他の場所でハンドリングしている場合は、そのハンドラは一旦取り外し、事が済んだ後に再度追加する…的な小細工は必要。また、これはあくまで現状での動作なので、時が過ぎればころっと変えられる恐れは十二分にある。

    あの、なんで、こんなにも Chrome の流儀と全然違うデザインにしたんですか? WebExtensions 自体が Chrome の拡張に恐ろしく馴れ馴れしく擦り寄った代物であるのに、部分的に見るとまるで作法が異なるというのはどういう意図があるのかいまいち…というか、さっぱりわからない。技術的に何か制限があってこうなっているのならわからなくもないが、そうではないとしたら一言「アホじゃないの?」としか言いようがない。

2017/07/10 12:05 pm
Replacing remote address
Uncategorized, ,

ここのサイトは CloudFlare を通しているのだが、そうするとアクセス解析的なことをする際に $_SERVER["REMOTE_ADDR"] の値が CloudFlare 内のサーバの IP アドレスになってしまい、本来のビジターのそれを得られない。

そういう場合は代わりに $_SERVER["HTTP_CF_CONNECTING_IP"] を参照する。自前で作っている PHP アプリケーションの場合はそれだけの話なのだが、このブログの場合はどうだろうか。このブログは WordPress で運用している。

must-use plugin というものを使う。これは WordPress のメインの処理に先駆けた早い段階で読み込まれる。<blog-root>/wp-content/mu-plugins/ip-override.php 的なファイルを編集し

if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
 $_SERVER["REMOTE_ADDR"] = $_SERVER["HTTP_CF_CONNECTING_IP"];
}

てな感じのコードを書いておく。

2017/07/09 8:17 am
visual in vi
Uncategorized, , ,

エラーではないが、ユーザに何か通知すべき事柄が発生する場合がある。例えばカーソルが行頭にいる時に h を押したとか。

そういう状況では、vi はビープ音を鳴らす。これが「ビービー鳴らすモード」などと揶揄されているわけだが、この動作は errorbells オプションで制御することができる。これをオフにするとビープ音は鳴らない。

ただこれだと、「ビービー鳴らすのはうっとうしいが通知があったことは知りたい」という要件に応えられない。そういうわけで、visualbell オプションを新設した。:set errorbells visualbell の状態ではビープ音の代わりに wasavi の画面がフラッシュする。

2017/07/08 6:44 am
Promise and Generator #7
Uncategorized, , , ,

さて、wasavi の根幹の動作ロジックを Promise ベースに大改造した大元の動機のひとつである、migemo 対応について考えてみたい。

とりあえず Chrome extension としての migemo についてまとめてみたい。migemo 自体のホームはここ。そして Chrome 版の migemo というのは、検索文字列に対応する migemo 的な正規表現を返す API を提供することに特化した拡張で、migemo server ということになっている。

まず edvakf さんが制作した ChromeMigemo Extension というものがかつてあった。これ自体はすでにサポートを終了しているが、ソースは公開されている。ただ困ったことに API リファレンスといったドキュメントはないので、このリポジトリ自体はエンドユーザにとってはそれほど有用ではない。API リファレンス的なものは辛うじて彼のはてなダイアリーの古い記事から得られる。

さて、これを現在サポートしているのは mono さんで、blog の記事でfork 版である Migemo Server for Google Chrome™の告知がなされている。wasavi もこれを参照する。

これを wasavi の / コマンドに組み込むわけだが。もともとこれらのコマンドは正規表現を入力して検索するものだ。これが migemo に対応すると、入力した文字列が内部で複雑な正規表現に変換された後に検索が行われるという流れになる。つまり何か入力してそれを検索するという手順は同じなのだが、入力するものの性格は全く違うということだ。migemo の場合は入力するのは文字列リテラルなのだ。

そうなると、コマンド自体を別に起こす(例えば、g/g? とか)ほうが筋がいいのかもしれない。しかし、ここはやはり / コマンドにまとめたい。

ところで vim では、正規表現中に \C というものを含めると ignorecase オプションの値にかかわらず、必ず大文字・小文字を区別して検索するようになる。\C には正規表現のメタキャラクタとしての効果は何もなく、それが正規表現中に存在するか否かに意味があるというわけだ。この考え方を流用してみよう。メタキャラクタ \M を導入し、これが検索文字列に含まれている場合は migemo で検索、そうでない場合は従来の検索というように振り分ける。

このやりかたにすると、検索文字列の入力中に「やっぱ migemo で検索しよーっと」とかその逆が簡単に切り替えられるし、また map g/ /\\M などとすることも可能なので、migemo 検索を独立したコマンドにしたいという向きにも対応できる。

というわけで、できた。

2017/07/06 7:42 pm
Marquee!
Uncategorized, , ,

wasavi が起動する際、最小の横幅・縦幅というものがあり、横の最小幅は 320px ということになっている。

このくらい幅が狭いと、ステータスラインに何かメッセージを表示する際に、その長さによっては後ろが隠れてしまうことがある。

これを解決するために、いわゆる marquee 的な動作をさせるようにしてみた。的な、というか、実際に marquee 要素で囲むのである。21 世紀も17年過ぎて marquee 要素を使うとは思わなかった…。

この marquee 要素、いつ廃止されてもおかしくない状況なのだが、とりあえず今のところは Chrome でも Firefox でも使える。ただし Chrome においてはかなりスムーズに動くのに対して Firefox ではかなりガックガクだ。

まああんまり気にしないことにしよう。

2017/07/02 8:42 pm
Promise and Generator #6
Uncategorized, , , ,

一通り修正が終わって、Selenium による Chrome 上の機能テストも一応クリアした。

一応というのは、800 超の機能テストはカテゴリごとに editing.js とか、insert.js とか分かれているのだが。それらを個別に動かすと 100% パスするものが、全てまとめて通すとぽろぽろと失敗するものがランダムに出てくる。これはなんだろうか。

もうひとつ。以前 Firefox でテストしようとしたところ、geckodriver の機能や、Selenium のサポートなどがまだ熟成されていない感じだった。やたらめったら CPU パワーを消費するし、既存のプロファイルを使用できないし、WebExtensions ベースの拡張を認識してくれないし、要素に対して sendKeys() する機能が未実装だし…など、プロダクトレベルの品質に達しているとはとても言えない状態だった。あれから数ヶ月経ったがどうだろうか。

ということで試してみたところ、

Could not convert 'text' to string

なるエラーが sendKeys() で発生する。うーんまだ実装していないのかな? と調べてみたところ、issue が上がっていた。つまるところ、sendKeys() 自体は実装済みなのだが、Selenium は入力されるべきキー情報を文字の配列の形で送出するのに対し、geckodriver 側は文字列の形で受けることを想定しているという型のミスマッチのようだ。issue 後半に掲載されているクイックパッチの通り直してみたらとりあえず動いた。javascript バインディングの Selenium の場合、スクリプト言語で実装されているおかげでこういう小回りがきくのは嬉しい。

その他、CPU パワーを浪費することもないようで、さすがにいろいろと進歩しているようだ。

2017/06/26 8:14 pm
Mount a windows share folder #3
Uncategorized

CIFS で Windows の共有ディレクトリをマウントすると Linux 側が不安定になる件。結局 nfs サーバは試していない。いや一応 WinNFSd だけ試してみたのだが、ファイル名のエンコーディングを指定できずに化けてしまうのにがっくりしてそこから追ってない。

Linux 側の現象を見てみるに、どうも一旦スリープして、復帰するとマウントの状態がおかしくなっている気がする。一旦おかしくなると Thunar は固まるし、端末からマウントしたディレクトリを覗いただけで固まって困る。

ということはスリープ時に一旦アンマウントして、復帰したらマウントしなおせばいいのか?

と思ってググってみたらそのものズバリのケースがあった。これもまた割と既知の現象だった。/etc/pm/sleep.d/ に以下のようなシェルスクリプトを置いておく:

#!/bin/sh
# Unmount CIFS share on hibernate/suspend and remount it on resume

case "$1" in
        hibernate|suspend)
                umount /media/windows/music
                umount /media/windows/pictures
                ;;
        thaw|resume)
                mount /media/windows/music
                mount /media/windows/pictures
                ;;
        *) exit $NA
                ;;
esac

なるほどねえ。

ちなみにおかしくなったマウントの状態は umount -l /media/windows/music などと -l オプションをつければ一応解消できるのだが。このオプションは、つまりアンマウントのための様々なクリーンアップは後回しにして、とりあえずファイルシステムの階層からの切り離しを優先するとのことだ。後回しにしただけなので、もしかしたらマウントがおかしくなった状態を完全に解消する方法とは言えないのかもしれない。

2017/06/22 6:44 pm
Promise and Generator #5
Uncategorized, , ,

ex の executor はかなり書き換えたわけだが、一方で vi コマンドの executor というのも一応ある。これは ex ほど大規模ではなくて、単に vi コマンドの文字列をキー入力キューに挿入するだけだ。この機能は . コマンド及び @ コマンドが利用している。つまりいわゆるマクロ機能に相当する。

今回コマンドの実行が非同期になったことで難しくなったのは、マクロの実行が完了したタイミングが取れないということで

executeViCommand('2w');
doSomethingAfterExecute();

ということができない。そこで、キー入力の最後に特別な擬似キー *macro-end* を付加するようにした。なぜマクロ完了のタイミングが重要なのかといえば、カウントというものがあるからだ。. の場合は実行する vi コマンド列がカウントを内包しているのであんまり関係ないのだが、@ の場合はそうではない。

そういうわけでこのマクロ実行完了を表す擬似キーを使う。対応するコマンドハンドラ内でカウントを見て、必要なら再度コマンド列をキー入力キューに充填したりすればいい。ところでそのハンドラは normal モードにしか存在しないので、マクロ実行完了時のモードもまた normal モードじゃいけない。つまり他のモードの場合は escape キーを再充填するなどの処置も必要。

それから、@ コマンドというのは指定したレジスタの内容を vi コマンドとして実行する機能なのだが。そうすると今回施した修正の元、たとえば @a という文字列がレジスタ a に格納されている状態で @a を実行すると実質的な再帰が起こってしまう。ちなみに vim 7.4 (2013 Aug 10, compiled Nov 24 2016 16:44:48) で試したところ、固まってしまった(固まらない時もある。よくわからない)。

というわけで、@ コマンドのもとで vi コマンドを実行している際は同じレジスタを使った @ コマンドを入れ子で使えないようにした。入れ子[email protected] が @b を呼び、それが @c を呼び…ということはできる。

Archives