Rhinocs
Rhinocs
- karino2/Rhinocs: Rhino backed Android editor only for keyboard.
- karino2/RhinocsJSPackage: JS package repository for Rhinocs. Rhinocsで使用するパッケージ。この中身のルートを指定しておく。
- karino2/RhinocsSKK: SKK port for Rhinocs Rhinocs用のSKK移植。上記のレポジトリのsubmodule
- karino2/RhinocsJSPackage: JS package repository for Rhinocs. Rhinocsで使用するパッケージ。この中身のルートを指定しておく。
RhinoをバックエンドにしたAndroidのキーボード専用エディタ。
コンセプト
キーボード前提(必須)のAndroidのエディタ。
AndroidのIME周辺のキーボードの扱いが好きになれないので、全部キーコードを自前でハンドルするエディタを作る。 テキスト入力専用で快適に長文が入力出来る事を目指す。
中途半端なタッチ対応が諸悪の根源と思っているので、 タッチでは全く使えない代わりにキーボードで快適、というものにしたい。 既存のIMEは中途半端なタッチ対応により使いづらいのでIMEも自前のものを作る(追記: SKKを移植した)
ようするに自分のBOOXをポメラっぽく使いたい。
Rhinoをバックエンドにしたemacsみたいなエディタを考えている。 ということで名前はRhino backendのemacsっぽいAndroid用エディタ、Rhinocsとした。 ただしActivityのリサイクルがあるのでemacsのようにずっとメモリ上にあるという前提の構造には出来ないからそこは違ってくると思う。
上記を満たしつつ、ちゃんとAndroidのアプリとして正しく振る舞うエディタが欲しい。PCのアプリの移植のような無理やりAndroidで動かすものではなく、Androidのアプリとして自然に振る舞いたい。 Activityのリサイクル、SAFなどを考えたエディタであって欲しい。
現時点での進捗
- Rhinoにキーイベントを渡してキーマップに登録した関数を実行する事で動くエディタ
- SKKを移植して動く
- SAF経由でのファイルのオープンと保存
- コピー・ペースト、eval_regionなどが動く
- split_windowが動く
- ミニバッファが割と動く
ToDo
優先度の高い順に思いついた事を書いておく。
- search-forward
- undo
- インテントからのオープン
- Bundleへのバッファのuriと編集位置くらい保存
- 名前をつけて保存
開発日記
とりあえず何をやったかを書いておく所。
2026-06-02 (火)
global_set_keyはロードした側で設定するように変更
switch-bufferなどをヒストリを使ったものに差し替えたりするのは、ユーザーが選ぶべきだよなぁ、と思い、 そうした選択肢はinit.js側に書くべきと気づく。 SKKなども起点となるキーはユーザーが指定するようにした。
BufferオブジェクトのisModifiedとモード行のbufferModifiedに対応
保存してないかどうかが分からないと不安になるので、変更していたら白丸を表示するようにした。 file_historyとかのバッファでも出るのがダサいが、ダサいだけで問題は無いので気にしない。
set_font_sizeとget_font_sizeを実装
一発で動く。よしよし。
2026-05-31 (日)
read_filtering_listを実装
switch-bufferで使う用に、Rhinocs_仕様検討で考えたget_floating_listを使ってリストをフィルタリングして選ぶ関数を実装した。 なんかやけにバグりやすい感じだが、低レベルAPIとはそういうものだよなぁ、という気もするので、とりあえずこのままで進む。
swtich_to_bufferを実装
長い下準備の末、ようやくswitch_to_bufferが実装出来た。これでうっかり別のファイルを開いても、作業中のバッファが失われる事は無くなったぜ。 挙動もVSCodeのCmd+pっぽくていい感じ。
バッファの新規作成とuriのないファイルの保存を実装
保存をする時にヒストリに入ってほしいのでvisit_newfile_hookというフックを追加して、複数引数対応などをやっていたら意外とはまるが無事完成。 uriにすでにあるファイルを開こうとした時は既にあるバッファを返すように修正して、 だいぶバッファの扱いもちゃんとしてきた。
2026-05-30 (土)
ミニバッファフックやミニバッファ用のキーマップを作る
switch-bufferを実装する前準備として、switch-bufferでSKKをミニバッファで使いたいので、ミニバッファ周りを少し整備して、ミニバッファは別のキーマップを使うようにする。 そしてミニバッファから抜ける時に呼ばれるhookも実装する。
g_keyMapHandler.isMiniBufferをtrueにするとミニバッファ用のキーマップスタックに切り替わり、falseにすると戻る。 ミニバッファに入ったかどうかはJS側で管理する。
hookはg_hooksにaddHook, removeHookで登録、解除する。
hook名はとりあえず以下の二つだけ。
"enter_minibuffer_hook""exit_minibuffer_hook"
2026-05-27 (水)
SKKの辞書のエントリパースをlazyにしてロードの高速化
メモリの消費くらいは確認しておくか、とHeap Dumpなどを見た所、どうも一時オブジェクトに使われて消えているように見える。 splitの行がでかいんじゃないか?という事でlazyにやればいいのかなぁ、と思ってgeminiに指示したら、 エントリのパースまでlazyにした。 別にそれは必要という根拠は無いが、コードは複雑になっているかというとそれほどでも無いし、少なくとも遅くはならないはずなのでいいか、とそのままやらせる。
ただしコードは少し気に食わないので手直しする。
メモリ消費の挙動はまだLive Telemetryでは納得出来ない振る舞いもしているが、速度は1.4secから500msecくらいまで減ってまぁまぁ一瞬になったので、もういいかな、という気分にはなる。
たぶん細かい文字列のオーバーヘッドだというのなら、文字列は全部つなげたまま持っておいてオフセットをハッシュに入れる方がメモリ効率は良さそうだが。 でも500msecならもういいかな。
switch-to-bufferの仕様検討
Rhinocs_仕様検討に移動
2026-05-26 (火)
脱continuation, Promise化、query_text_dialog
SKKの辞書登録のダイアログを非同期化したら意外とハマる。変換周辺の状態遷移はかなりきわどい想定がいろいろあるので、非同期化とかしたらいろいろ問題が出た。 この辺はそのうちちゃんと見直さないとなぁ。
read_keyなども対応して全Promise化完成
resolveが複数引数に対応してなかったり細かなバグがあったが、 一通りPromise化が終り、pending continuationを使うコードは無事なくなった。
SKKのパースの結果をkotlinのmapで保持
パース時にJSオブジェクトを作るのがRhinoのGCを呼ぶので遅い、という気がしていたので、KotlinのHashMapとdata classで持ち、lookupの時に毎回JSの配列に変換して返すようにしてみた。 バッファ関連を進めるにあたり、バッファをRhinoのホストオブジェクトにしようかなぁ、という思いもあって、 ホストオブジェクトを書いてみるか、という事で。
パースは2.5secから1.4secくらいになった。 全エントリをメモリ上に展開して持つのはいかにもセンスが無いのでそのうち直すかもしれないが、とりあえずlow hanging fruitをとっておく。
2026-05-25 (月)
脱continuation, Promise化運動 その1
capture continuationは制約が多いので、全部コールバック化してJSレイヤーでPromiseにラップする事にする。
とりあえずselect_fileをselect_file_callbackにして、request_js_loadをjs_load_callbackにしてPromiseでラップした。 こちらの方が素直でいいね。
まだquery_text_dialogとread_keyが残っているので、この二つもPromise化したい。
2026-05-24 (日)
座標計算のリファクタリング
switch-bufferはタブ補完が無いと使いものにならず、 そのためには情報を出すためのsplit-windowが無いと使いものにならない、 と気付いた為、split-windowを実装する事にする。
現状はテキストの描画位置の計算がgeminiに生成させた部分も多く、 本来はすでにある値を計算しなおしたりbaseという名前の使い方に一貫性がなかったりしたので、 座標計算周辺を手でリファクタリングする。
理解の為のリファクタリングというのは人間的な営みだよなぁ。
split-windowの実装
表示まわりをちまちまリファクタリングしてWindow位置相対で全部掛くように直したので、 満を持してsplit-windowを実装。
今の所動いている感じか?
現在は画面を上下2分割しか対応する気は無いので、 割といろいろな所で実装を手抜き出来る。 しかもあんまり3分割以上は使わないので、最後までこれでいいかなぁ。
capture continuationを使ってるAPIをコールバックベースに直そうかなぁ
思ったよりもcapture continuationの制約の多さ(Javaから呼んだ関数で使えない)がかったるいので、 コールバックベースに直そうかなぁ、という気もしている。 JSレイヤーはPromiseのAPIは割と慣れているので使ってみるとそんな嫌でも無いし。
全部コールバックベースになおしてしまえばevalなどで動かないAPIもなくなるのでかえって楽になるんじゃないか?
2026-05-23 (土)
モード行の実装
Rhinocs_仕様検討で検討した仕様の通りのモード行を実装。アプリ全体で一つとして下に表示。bufferName, column, lineNumだけ対応しておく。 右に寄せても左に寄せてもしっくりこないがとりあえず右に寄せる。
SKKのモード行を表示
SKKの現在のモードを表示するようにした。結構それっぽくなってきた。
is_bol, is_eolを実装してkill_lineを実装
bolpとeolpはあまりにもJSだと違和感があるのでis_bolとis_eolにする。bolとeolは揃えておきたいと思ってそのままで。
ミニバッファの実装
仕様がようやく頭の中で固まったので、enter_minibufferとleave_minibufferを実装してread_stringを実装してみる。 とりあえずは動いている。
タブ補完よりも絞り込みありのpopupリストをminibufferから生やしたいなぁ、という気もする。
M-xの実装とJS関数の遅延実行
evalなどではcapture continuationが動かず、そのせいでSAFとか一度yieldする必要がある関数が動かないという問題があった。 そこでrequest_function_executeというJSの関数を遅延実行するAPIを作った。
これでM-xなどでfind_fileが呼べるようになった。 interactiveに相当するものが無いのでM-xはあんまり使えないが、まぁミニバッファのテストとしては必要なのでいいだろう。
2026-05-22 (金)
いまいちな所を書き出す
- init.jsをLoadボタンを押さないとロードしない所
- set_device_idとかパッケージのディレクトリ指定とか最初にやらないといけない事が多い事
- eval_regionからpending continuationが必要な関数を実行出来ない所(これはcaptureContinuationの仕様っぽくて直せなかった)
skkの外のキーとの連携が少しバグってるかも。 C-hで途中から消せなかったり、C-jでjが入力されてしまったり。 この辺の品質は上げたい。
skk_all.jsのロードをlazyにする
ロードをlazyにする為には、ロードが終わった後に実行するコマンドを指定出来れば良さそう。
という事でrequest_load_jsにafterLoad関数をオプショナルで渡せるように変更。
init.jsをいつもロードするように変更
skk_all.jsのロードをlazyにした事でinit.jsのロードが一瞬で終わるようになったので、最初に自動でロードするように変更。 Loadボタンも無くしてだいぶ挙動が分かりやすくなる。
漢字登録をC-gでもキャンセル出来るように
typoで漢字登録ダイアログが立ち上がった時にいちいちタッチするのがかったるいのでC-gでもキャンセル出来るようにした。
2026-05-21 (木)
BOOXでC-Space効かない問題の対処
BOOXでC-Spaceが横取りされる問題と格闘。
KEY_UPは来るのでdownが来てなくてupが来る時だけ特別扱い。 アットマークがShiftとの同時押しじゃなければC-@でも良かったんだがなぁ。 しかもC-@はなんかIME切り替えが動いてしまう。
デバイスごとの保存を考える
Rhinocs_仕様検討へ移動
デバイスごとの保存を実装
以下を実装した。
- join_path
- put_pref_string
- get_pref_string
- set_device_id
- get_device_id
- get_per_device_storage_dir
ヒストリは期待通り動いてそう。
SKKの未知単語登録の実装
innerSKKは使い方が良く分からない上に普通のIMEで変換したものを入力する方がAndroid的には普通だと思うので、 ダイアログで登録するように変更した。
次はユーザー辞書の保存だ。
SKKのユーザー辞書の保存
元のコードほぼそのままでファイルを保存して動くようにはなった。 保存フォーマットには無駄にwordとかついているが、まぁまずはいいか。
これでデバイスごとに辞書を保存するようになったぜ。
あとはステータスバーあたりを終えればSKKのコードは安定するかな。
2026-05-20 (水)
マーカー仕様検討
Rhinocs_仕様検討へ移動
マーカーを実装
C-SpaceとC-x C-xはとりあえず動いてそうなのを確認。あとはbuffer_substringを実装すればeval_regionはevalで実装出来るな。
buffer_substringとeval_regionを実装
buffer_substringを作り、とりあえずeval_regionを実装してみる。 ミニバッファがまだ無いのでC-jにバインドしておく。 これでセルフ開発が捗るぜ。 まだexceptionハンドルしてないと落ちてしまう事もあるが。
クリップボード関連を実装してコピペなどを実装
コピーはクリップボードにコピーするようにする。なんかフローティングが出てきてダサいが、まぁいいか。 そしてそれらを使ってkill-regionとかも実装してとりあえずコピペ系が揃う。
最初のリリースまでの道のりを考える
現状は最初にいろいろ設定されている現状なので、ちょっと第三者には敷居が高い。 emacsユーザーくらいならすぐに分かる程度くらいまでは敷居を下げたい気がする。
まずSKKとエディタのバージョンが揃ってないとすぐに動かなくなるので、 それが一段落してからにしたい気はする。
- ステータスウィンドウ
- ユーザー辞書
この二つが揃えばSKKの必要なAPIは揃うな。
次にswitch-bufferで前のバッファには戻れるようにしたい。
あとはsave-bufferでまだ保存してなれば名前をつけて保存、はやりたいか。
この位出来たらとりあえず第三者でも試せるかな。
さらにミニバッファとsplit-windowが出来たら広く宣伝してもいいかもしれない。
この辺まで行けば最初のリリースかな。 まとめておく。
- ステータスウィンドウ
- SKKのユーザー辞書
- switch-buffer
- save-buffer-as
- ミニバッファ
- split-window
2026-05-19 (火)
js相対のreadを廃止していつもpackageルートからに
jsのファイルを読み込んでevalする時にそのファイルのパスを覚えておいてread_textとかではそこからの相対で読んでいたのだが、 これだと他のファイルで定義した関数を呼ぶ時に定義元ファイルでは無く呼び出し側の相対になってしまう事に気づいた。
Scopeオブジェクトなどを定義すればスコープを作る事は出来るが、どうもGlobalObjectのメソッドを呼ぶ時はthisObjをscopeとして渡してしまうようで、 runで渡しているscopeが来ない。
うーん、いつもパッケージのルートからの絶対パスで指定するようにしようかなぁ。それしか無い気がする。>そうした
ヒストリをなんとなく実装
見た目はダサいがとりあえず0-9の数字キーでヒストリーを開く機能は出来た。 これはバッファを使って簡単な機能を作る事が出来るようになったという事でもある。結構凄いな。
とりあえずRandomThoughtsのようなディレクトリ下に大量のファイルがあるようなものでの編集作業が我慢出来る感じになった気はする。
このヒストリの実装、file_history.jsという名前だが、この辺のjsはgitで管理してないんだよなぁ。どこに置くのがいいかねぇ。
messageを実装
ミニバッファは少し大変そうなのでまずはエコー領域として表示するだけの機能を実装。
SKKの変換候補も一定以上はここに出す。
JetBrains Monoフォントにする。
Wとaで全然幅が違ってすかすかになって見苦しいので、JetBrains Monoにした。いいね、これ。
少しドッグフードをしての雑感
新しい折り畳みキーボードはプログラムをする時は記号がまだ慣れないな。 ただこれは時間の問題な気がする。
コピペとeval_regionが欲しいなぁ。次はコピペかな。 ちょっと別のファイルいじってから戻ってくると位置が失われているのもちょっと嫌なのでバッッファ切り変えは実装したいかもしれん。
ただ、どちらかというと現時点でもかなり快適に実用出来ている事に驚く。 明らかに他のエディタ+GBoardより良い。
このキーボードから手を離さなくていい感じ、久しぶりだなぁ。 最近はVSCodeとAndroidStudioなのでこういう感じじゃなかったからなぁ。 凄く気分がいい。そうそう、こういう感じだったよね。
雑に実装したヒストリーも結構いい感じだ。こういうのをJSでちょろっと書けるのはいいな。
正直実用まで持っていけるか自信が無かったが、もう十分実用的だな。 出先で長い文章を打っていたい。
ドッグフードメモ
- 選択無しでC-x C-xでクラッシュ
- BOOXでC-Spaceでマークがセットされない
2026-05-18 (月)
SKKのKeyHandlerをself-insert的に使う事でキーマップに登録
昨晩布団の中で、keyHandlerをself-insert的に全部のキーに呼ぶようにするならそこまで書き換えも大変では無いのでは?と思いつき、朝起きて試してみる。 いい感じになった。
Objectリテラルのメソッド
Rhinoへ。
促音の子音重ねのサポート
自分があさってをasatteと入力するタイプなので、このサポートをしたい。現状は「あさtて」となる。
あれ?processRomanを見るとまさに書こうと思っていた処理が書いてあるな? その後調査を続けると、どうもsetCompositionで自分の想定してない状況があるっぽく、その対応を入れたら直った!
ファイルを開く問題の短期的な解決を考える
現状ファイルを開くのはSAFのシステムのファイル選択が開くが、これがいまいちなので快適では無い。
ディレクトリ内の最近変更したファイルから選ぶ感じになっていれば我慢出来るが、システムのデフォルトが名前順で毎回変えないと日付順にならないのが不満だ。
権限的にはrootを設定さえしておけば、その下のファイルはアプリから扱えるので、ファイルを開くのでは無くディレクトリをセットする、 という風にすれば良い。ただこれはエディタ的にはめちゃわかりにくい。
もうちょっと単純に、ヒストリーがあればいいんじゃないか? 最後10ファイルを覚えておいて、その一覧を表示して、0から9までの数字で選ぶと開ける、みたいな関数を書けばいい気がする。
これをJSレイヤーで書ければいいか。
file_historyというパッケージ的なものとして書いていこう。 ファイルを開く所自体はもう現時点でもJSで書いていて、open_uriにuriは来るが、display nameが来ないな。 これはselect_fileを多値に出来れば良さそう。destructuringがあるから配列で返せばいいか。
で、ファイルに保存するのはテキストを一括で書く手抜きなwrite_fileでも用意してやれば、あとはJSON.stringifyとか使えばいいか。
で、テキストの入力を読むためのread_key的なのを作ってやればいいか。この辺はminibufferを使った同種のemacs lispの奴にそろえておく方が本当はいんだろうが、 そういう事を考え始めると時間が掛かりそうなのでまずは目先の問題を解決するとする。
という事でfile_historyを作る事に必要なものを考えてみよう。
- DONE ... select_fileにdisplay nameも返すようにする
- DONE ... write_file的なものを作る
- DONE ... read_key的なものを作る
- DONE ... get-buffer-creatとset-buffer
このくらいでいけるか?
2026-05-17 (日)
JS周りの整理
builtins.jsをオーバーライド出来るようにした。packageDirのルートにbuiltins_override.jsがある時はそちらを読むようにする。 これでしばらくローカルで開発したあとにassets下に戻す、という風に進められるようになった。
RhinocsSKKを公開、サブディレクトリのロードをサポート
SKKを直していくのはさしあたっての優先度が高いという事で、まずはうっかり消してしまっても平気なように公開しておく。README.mdとかはおいおい。
karino2/RhinocsSKK: SKK port for Rhinocs
これに合わせてskkをサブディレクトリにした都合でサブディレクトリの相対パスでのloadをサポートした。何も考えずに移動したら辞書が読めなくなったので。
最初にinit.jsをロードするように変更して、この中からrequest_load_jsでskkをロードするようにした。 requestがついているのはContextの再入が面倒だったので遅延ロードになったから。まぁ困るまではこれでいく。
JS側のKeyMap周辺を整備
SKKをKeyMap化する為にJS側のKeyMapを整備した。久しぶりにJSでprototypeとか使ってnewとかしたなぁ。 普段Electronのアプリとかは単なるオブジェクトリテラルに関数をもたせて複雑な時もその関数をクロージャにして中で複数関数を定義する程度だが、 他からも使うような物を作るならprototypeを使った書き方もまぁアリといえばアリだな。
KeyMap側は割と整理が進んだが、SKK側をキーマップに書き直すのはかなり大変な事に気づく。全部一気にやらないと動かなくなるからなぁ、これ。 ちょっと今日は撤退しておこう。
三日目 2026-05-16 (土)
SKKが意外とすぐ動きそうなので動くまではやってしまうかな、という気分になる。
SKKの変換が動く
SKKの変換をなんとなく動かす。ちょっとskkInnerまわりは扱いが微妙なので、これよりは登録用ダイアログを作りたいな。 delete-regionとかpointを実装して一応動くようになった。
改行の入力や削除などをサポート
もともといい加減だった行末、行頭への移動や削除周辺をちゃんとUnitTestを書いて直す。面倒な所はgeminiになおしてもらったりテスト増やしてもらったり。
さしあたってのToDoを考え直す
SKKが無事動いたので使い始めまでの最低ラインをもう少し考える。
- とりあえず表示だけのminibuffer(変換候補の一覧が出せないので)
- C-x C-fなどの複数ストロークのキーマップ対応
- バッファの保存
- 移動系(次の行に行けないとか)
複数キーマップ対応
jsをassets下のbuiltins.jsにちゃんと独立させて、複数ストロークのキーマップ対応を雑に書く。
簡単なファイル入出力
C-x C-fとC-x C-sをとりあえず使える程度に実装。
移動系を充実
goal_columnを実装する事でnext_lineとprevious_lineで最初の位置を保存して移動出来るようになった。
ついでにC->とC-<も実装しておく。
さらにscroll_windowを実装してC-vとM-vを実装。
C-aとC-eも実装。これでだいたい出来たかな。
SKK-JISYO.L.gz対応とkotlinで書き直し
ロードしてみたら6sec以上掛かってGUIスレッドが長すぎとダイアログがでたりするので、Kotlinでgeminiに書き直してもらった。2.6秒くらいになった。 まだ結構GCが発生しているので減らせそうだが、我慢出来ないほどじゃないので後回し。
ドッグフードを開始
試しにこのWikiを書いてみている。結構書けるな。
やはり撥音の入力が慣れないので、自分が普段入力しているやり方に対応しよう。 ただキーボードから手を離さなくていいのは快適だな。 SKK-JISYO.Lは結構漢字も多いし。候補を戻るのはxか。
ランドスケープで使ってるとカメラの穴が邪魔だな(^^;
現在のモードが分からないのはちょっと不便だな。 やはりモードラインとミニバッファが欲しいか?
skkのコードはどう公開しようかなぁ。 今はRhinocsのレポジトリとは別のgitで管理しているのだが Rhinocs側とバージョンを合わせないと使えないんだよなぁ。
まだちょっとSKKのモードが変な状態になる事があるが、 結構つかえる。SKKはchromeOS用のkeyHandlerを無理やりつなげて動かしているので、 keymapと状態がバッティングしておかしくなる場合があるんだが、これをデバッグするよりはkeymapに直した方がたぶん無難に動くよなぁ。
やはりSKK-JISYO.Lは結構実用出来るレベルだよなぁ。もう普通に使える。
二日目 2026-05-14 (木)
Window, Point, 移動を真面目に実装
forward-charを実装するにあたり、もう少しちゃんと調べた方がいいな、という事で既存エディタを調べる>エディタ調査
調査した理解を元にGridを廃止してPointから毎回計算するようにする。ついでにスクロール周りの計算もxyzzyにそろえておく。
self-insertとキーマップをなんとなく実装
キーボードから矢印キーを処理してカーソルが動かせるようになった。 さらにself-insertも実装して文字が入力出来るようになった。
SKK、ひらがな、かたかな、asciiが動く
とりあえずinsertだけで動かせるものが動く。削除も次の行も行けないのに日本語入力が動き始めるのが日本人の作ってるエディタって感じだな。
- 辞書のロード
- 候補の扱いはxyzzyのskk-modeを真似てbufferにinsertしたりで実装したい
この2つが終わればSKKがとりあえず使えるな。まぁ後者は数日は掛かるだろうが。
作り始め 2026-05-13 (水)
とりあえずバッファを画面に描く機能はなんとなく書いた。 ファイルをロードしてスクロールもなんとなく動いている。 カーソルも描いた。
Rhinoを組み込んで、RhinoからSAFでuriを取ってくるようにリクエストして、もらってきたuriをRhinoから開くように出来た。
次はキーボードの処理だな。
文字列化してJSのonKeyDownを呼ぶまでは出来た。あとはスクロールとself insertを実装すればそれっぽくはなるな。