Mapping more modes

wasavi の issue として、bound/bound_line に normal ではない個別のマッピングを行いたい、というものがある。

現状では、normal/bound/bound_line は MapManager 内部の command マップに収斂、insert/overwrite は edit マップに収斂、それ以外のモードはマップ不可、という形になっている。したがって、要件に応えるには、単に bound/bound_line 用の個別のマップを設けるだけでいい。

ただ、インターフェースの問題がある。ex コマンド [cci]map[/cci] は command マップを操作する。一方 [cci]map![/cci] は edit マップを操作する。[cci]![/cci] の有無の二者択一のため、第3のマップが入り込む余地がない。

vim では、これを nmap/vmap/xmap/smap/omap/imap/lmap/cmap… とむやみに ex コマンドを増やすことで対処している。さらに再帰的なマップ展開をしない版である nnoremap/vnoremap/xnoremap/snoremap/onoremap/inoremap/lnoremap/cnoremap… という群もあり、総数としてはかなりのものになっている。

これ、果たしてわかりやすい仕様なんだろうか。だいたいなんなの onoremap とか inoremap って。初見じゃあ己マップと祈れマップとしか読めないよ。何をするコマンドなのかさっぱりわからない。

どうもこんな感じに ex コマンドをホイホイ新設するのは良くない設計に思える。加えて自己記述性の低いコマンド名はもっと良くない。

そんなわけで、wasavi では noremap かどうかを指定するのは map コマンドに与えるアトリビュートという形式にしてある。アトリビュートとはつまり
:map [noremap] gh ^
などと LHS の前に角括弧つきで指定する部分のことだ。ちなみに noremap というアトリビュート名もなんかいまいちなので、例えば final にしようかと考えている。

で、定義先のマップもアトリビュートで指定するようにしてはどうだろうか。例えば
:map [bound,final]
みたいな感じ。map コマンドを投入してかつアトリビュートでマップを指定しなかった場合に選択されるデフォルトのマップは normal と bound の和集合になる。一方、map! コマンドの場合は対象は edit マップ。

また、引数なしで map/map! コマンドを投入した場合は現在定義されているマップを表示するが、その場合には対象となるマップも表示したほうがいいかもしれない。

Xmodmap alternatives?

Xubuntuで、Caps と Ctrl を交換するだとか、その他細々としたキーボードのリマップを、xmodmap でやっている。が、なんだか知らないが、xmodmap はとっくの昔に Obsolete らしいのであった。さらに困ったことに、数時間おきに xmodmap によるリマップが勝手にリセットされるのだ。いや正確に言うと数時間おきというのも違い、ランダムというしかないタイミングでリセットされる。わからん。どういう嫌がらせなんだ。

で、xmodmap の代替は xkb らしいのだけど。ちょっと調べてみてなんかめんどくさそうだなーまあ xmodmap でいいかな…というのをかれこれ年に3回位繰り返している。

しかしせっかくなので、いろいろ試行錯誤してみた。xkb は基本的に言えば、物理キーに対してどの論理キーとして振る舞わせるかを制御する。また Shift−Level なんかも制御する。それらの情報は /usr/share/X11/xkb 以下にテキストファイルとして保持されている。各ユーザのホームに個別の .xkbrc ファイルみたいなのを置いて上書きするような仕組みはない。やろうとするなら、.xinitrc なり .xsession なりで setxkbmap を呼ぶことになると思う。

さて実際にキーの割当を記述するのは、symbols/ サブディレクトリ以下のファイル群となる。
/usr/share/X11/xkb $ ls symbols
af by es hu kz mm pt terminate
al ca et ie la mn ro th
altwin capslock eu il latam mt rs tj
am cd eurosign in latin mv ru tm
apl ch fi inet level3 nbsp rupeesign tr
ara cm fo iq level5 nec_vndr se tw
at cn fr ir lk ng sgi_vndr typo
az compose fujitsu_vndr is lt nl sharp_vndr tz
ba ctrl gb it lv no shift ua
bd cz ge jp ma nokia_vndr si us
be de gh ke macintosh_vndr np sk uz
bg digital_vndr gn keypad mao olpc sn vn
br dk gr kg md pc sony_vndr xfree68_vndr
brai ee group kh me ph srvr_ctrl za
bt empty hp_vndr kpdl mk pk sun_vndr
bw epo hr kr ml pl sy

ここにたくさんのファイルがあるわけだが、個々のファイルはそれら自身が行うキー定義量によって実質的に階層化されている。例えば pc ファイルはほぼすべてのキーに対して割当を定義する。言語コードっぽいファイルは各言語に応じた部分だけを定義する。ctrl や altwin、shift といったファイルは、それぞれの個別のキーをどのように振る舞わせるかだけを定義する。このようにレイアウトや言語から独立して、ほんの一部の定義だけを行うものを option と呼ぶ。

言語による定義の場合、さらにその言語内でいくつかのバリエーションが必要になる場合がある。その場合は、例えば us ファイルであればそのファイル内で、基本の定義を継承し、必要な部分だけを変更したものを定義する。そのような定義を variant と呼ぶ。

これらのファイルから任意のものをピックアップし、定義量の大きいファイルから小さいファイルへ順々に定義を行うことで任意のキーレイアウトが完成する。

で、実際にキーボードの種別を指定するには /etc/default/keyboard ファイルを

XKBMODEL="pc105"
XKBLAYOUT="us"
XKBVARIANT=""
XKBOPTIONS=""

と言った感じに記述する。model -> layout -> variant -> option という並びがまさに前述の、定義量の大きい順から小さい順というトピックに対応している。

ということを踏まえた上で、行いたいリマップは:

  1. 左の Ctrl と、Capslock を交換する
  2. Menu キーを Compose キーにする
  3. 右の Alt キーを、F20 キーにする

1. と 2. については、標準でオプションが定義されているので、前述の keyboard ファイル中の XKBOPTIONS に “ctrl:swapcaps,compose:menu” という値を与えればいい。問題は 3. だ。3. を実現するには、us の variant を定義するか、あるいは新しい option を定義するか…が正道に思える。

しかし、実際に行うのはそのどちらでもない。というのは variant や option を新設するには、rules/ 以下のいろいろなファイルを併せていじる必要があり、とても面倒だからだ。しかもそれらの編集が間違っていると、X 上の文字入力が一切できない、つまりログインスクリーンから進めないというかなり困る状態になってしまう。実際にやらかして、アワワワワ…となった。

実際に行うのは、上記で参照している option の定義中に 3. の定義を忍び込ませるというものだ。かなりイケてないが、たぶんこれが xkb の定義ファイルを最小限いじるだけで済むベストな方法だ。us ファイルを使わないのは、それが定義順の左の方に位置するので、知らない定義で上書きされる可能性が高いからだ。option なら定義の最後なので上書きされる恐れはかなり小さい。

swapcaps の定義は ctrl ファイルの

// Swap the functions of the CapsLock key and the left Ctrl key.
partial modifier_keys
xkb_symbols "swapcaps" {
replace key { [ Control_L ] };
replace key { [ Caps_Lock ] };
};

というセクションなので、この中に

replace key { [ F20 ] };

という行を追加する。そして祈りながら再起動する。これで xmodmap でやっていたリマップを xkb で再現できた。

Screen Casting

wasavi の Readme の最初の画像2つは、素の textarea と、それに対して wasavi を起動した状態の対となるものにしてある。

ところで Twitter でたまに wasavi について tweet される際、それは最初の画像を添付してあったりする。前述の通り最初の画像は単に素の textarea の画像なので、wasavi についての説明になっていない。例えば:

これを解消するために、2つの画像をまとめ、スクリーンキャストによる gif アニメーションをこさえたい。素の textarea から wasavi を起動し、編集し、書き込んで終了するまでを録画すればいい。というわけで、gtk-recordmydesktop をインストールし、録画し、ogg theora 動画を生成した。それを適当なオンラインのコンバータで gif に落とした。

gif に落とす際のオプションとかはなかったのでディザが目立つが、まあこんなもんだろう!

* * *

適当なオンラインのコンバータに頼るのはやめ、ogv → gif 変換はローカルで ffmpeg により行うようにした。見苦しいディザは追放した、はず。

Opening an options page

wasavi のオプションページというものを開くために、manifest.json に options_page キーを含めているが、実はこれは旧式である。現行は options_ui キーになっている。options_ui でどのようにオプションページを開くかどうかについてさらに新旧があり、古い方はオプションページをタブで開き、新しい方は chrome://extensions ページにオーバーレイする形で開く。これは TabQueue などの他の Chrome エクステンションでも出てきた話題だ。

さて wasavi を WebExtensions で動かした場合、options_page キーを認識しないようだ。そこで options_ui の、旧式の方に移行した。旧式の方は公式に obsolete であり、今にも削除されそうな勢いである…ということは、ちょっと気にかけておかないといけない。

WebExtensions で動かした場合、options_ui 旧式の場合は、拡張の詳細ページにオプションページを開くボタンが追加される。新式の場合は、オーバーレイではなく、拡張の詳細ページにオプションページが埋め込まれた形で表示される。へー。

Chrome では個々のエクステンションの設定をエクステンション一覧の特別な、モーダルな状態として捉えているが、Firefox ではエクステンション一覧から遷移する独立したページとして捉えている。どちらかというと Firefox の設計のほうが優れてるように思える。ブラウザの設定も昨今の多くのブラウザでは Web ページの1つとして扱われている。であればやはり様々なページを遷移する UI であることを強く意識して設計すべきであり、オーバーレイというモーダルな状態に安易に頼るべきでない。

mozc is…

xubuntu デスクトップで日本語を入力する際、fcitx+mozc という環境にしている。mozc というのは Google 日本語入力のオープンソース版で、違いは Google 日本語入力ほど賢くないということだ。

どれくらい賢くないかというと、「よくみるとかわいい」と打ち込んで変換すると第1に候補として上がるのが「よく覧ると革いい」になったりするのだ。まあ「覧る」でも間違いじゃないだろうけど、いやーそれ第1候補かな? と思うし、それにどんだけ革が好きなのかとも思う。

意味が分からないのは、常に賢くないわけではなく、時々この通りパーになるのである。夏バテでもしてるのかな。

Reloading extensions

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

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

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

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

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

Move by wrapped line

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

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

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

WebExtensions and clipboard operation

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 要素を追加する。この要素のスタイルを [cci]display:none[/cci] とかにはできない。フォーカスを当てる必要があるので。その代わりにスクリーン外の適当な場所に追いやる必要がある
    • 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 の拡張に恐ろしく馴れ馴れしく擦り寄った代物であるのに、部分的に見るとまるで作法が異なるというのはどういう意図があるのかいまいち…というか、さっぱりわからない。技術的に何か制限があってこうなっているのならわからなくもないが、そうではないとしたら一言「アホじゃないの?」としか言いようがない。

Replacing remote address

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

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

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

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

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

visual in vi

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

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

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