Category Archives: 未分類

2025-12-10 ,

org-captureをタッチで使いやすくする

Android版のEmacsを使ってみて、org-captureが使いづらいというのはすぐに気がつきました。私はorg-captureコマンドを C-c r に割り当てていますが、Ctrlを押さなければならない時点でタッチパネルからでは使いづらいです。その後もテンプレートを選ぶのにキー入力が必要ですし、最後の C-c C-cC-c C-k もタッチでは打ちづらいです。org-captureは「キーボードで素早くノートを取る」ことに最適化されていますが、これがタッチでは逆に障害となっています。

なので、まずはテンプレートの選択はGUIメニューで行うようにします。

テンプレート選択メニュー
テンプレート選択メニュー

そして入力が終わった後の C-c C-cC-c C-wC-c C-k と書いてある部分を押せるようにします。

ヘッダーラインにキャプチャ終了アクションが表示されている
ヘッダーラインにキャプチャ終了アクションが表示されている

作成したコードは次の通りです。

;;; org-captureをタッチで使いやすくする。

;; マウス/タッチ使用時はテンプレート選択をx-popup-menuにする。

(defun my-org-mks (table _title &optional prompt specials)
  (unless prompt (setq prompt "Select: "))
  (x-popup-menu t
                (list
                 prompt
                 (cons ""
                       (nconc
                        (cl-loop for item in table
                                 when (cddr item) ;; Prefixの説明を除く
                                 collect (cons (cadr item) item))
                        (cl-loop for item in specials
                                 collect (cons (cadr item) (car item))))))))

(defun my-org-mks-around (old-fun &rest args)
  (if (use-dialog-box-p)
      (apply #'my-org-mks args)
    (apply old-fun args)))

(advice-add 'org-mks :around 'my-org-mks-around)

;; header-lineのキーヘルプをボタンにする。

(defun my-org-capture-make-clickable (str keymap)
  "STRの中にあるfaceがhelp-key-bindingである部分をボタンにして押せるようにします。"
  (setq str (copy-sequence str))
  (let ((pos 0))
    (while pos
      (let ((face (get-text-property pos 'font-lock-face str))
            (next (next-property-change pos str)))
        (when (eq face 'help-key-binding)
          (let* ((end (or next (length str)))
                 (key-str (substring str pos end))
                 ;; ↓ここはKEY-STRによっては良くないかもしれない。
                 (command (lookup-key keymap (kbd key-str))))
            (when command
              (let ((km (make-sparse-keymap)))
                (define-key km [down-mouse-1] command)
                (define-key km [header-line down-mouse-1] command)
                (put-text-property pos end 'keymap km str)
                (put-text-property pos end 'pointer 'hand str)))))
        (setq pos next))))
  str)
;; EXAMPLE: (my-org-capture-make-clickable (substitute-command-keys "\\<org-capture-mode-map>Capture buffer.  Finish `\\[org-capture-finalize]', refile `\\[org-capture-refile]', abort `\\[org-capture-kill]'.") org-capture-mode-map)

(defun my-org-capture-init ()
  (defvar org-capture-mode-map)
  (setq header-line-format
        (my-org-capture-make-clickable
         ;; ここはheader-line-formatを指定しても良いのですが、そのままだと
         ;; 長すぎてC-c C-kが画面の右からはみ出して押せないので短くします。
         (substitute-command-keys "\\<org-capture-mode-map> Finish:\\[org-capture-finalize] Refile:\\[org-capture-refile] Abort:\\[org-capture-kill]")
         org-capture-mode-map)))

(add-hook 'org-capture-mode-hook 'my-org-capture-init)

テンプレート選択のGUIメニュー化はorg-mks関数にadviceを仕込むことで実現しました。そこではy-or-n-p等がやっているように、use-dialog-box-pを使ってマウス・タッチ操作からのコマンド起動かを判断して、必要ならGUIでメニューを表示します。 my-org-mksx-popup-menuを使ってテンプレート選択メニューを表示するorg-mksの代替物です。私はあまり複雑なテンプレートの指定をしていないので、これでorg-mksの全てのユースケースをカバーしているかは分かりません。org-mksはざっと調べた限りorg-captureorg-insert-structure-templateでしか使われていないみたいです。

終了アクションをタッチでできるようにするために header-line 上の C-c C-c などと書かれている部分をボタン化することにしました。 window-tool-bar を使って上に○×ボタンを表示するのも面白いかなと思ったのですが、面倒なのでこの方法にしました。 my-org-capture-make-clickable 関数は、文字列(header-line-format)中の特殊なface(help-key-binding)が指定されている部分に、keymapテキストプロパティを追加してdown-mouse-1イベントに反応するようにします。kbdを使っている部分がキー設定によっては正しく動かない可能性はありますが(キー割り当てが M-x org-capture-finalize のように表示されている場合など)、デフォルトの状態なら問題は無いでしょう。

org-captureコマンドはすでにメニューバーにも追加していましたが、ツールバーにも追加しておきます。

メニューバーに「+」ボタンを追加したところ
メニューバーに「+」ボタンを追加したところ

そのあたりのコードは以前の記事に追記しておきました。

Emacsのツールバーをカスタマイズする | Misohena Blog

これでスマートフォンからの入力がさらに便利になりました。

org-captureは前々から少し疑問を感じていた仕組みではありました。私は特定のファイルに新規エントリーを追加するという単純な目的にしか使っていません。それならもっと単純なやり方がある気がします。テンプレートを選んだら、即特定のファイルを開いて新規エントリーを挿入し、ポイントを適切な位置に移動すれば十分です。キャンセルしたければundoすれば良いのですから。なので今回の改良ではノートのキャプチャを行う仕組み自体を書き直そうかとも思いました。

しかしorg-captureにはノートを取った後に何事もなかったかのように元の状態に戻るという特徴があります。思いついた時にノートを取って、またすぐに元の作業に戻る。それがorg-captureの良いところなのかもしれないなと思い、今回はorg-captureを活かす改良で留めてみました。また不満を感じたら全体を書き直すかもしれません。

2025-12-09

Emacsのコンテキストメニューに「Close」を追加する

AndroidのEmacsを使っていると、なぜかふとバッファを長押ししてコンテキストメニューを表示し、そこから「Close」を選びたくなるときがあるんです。もちろんそこには「Close」なんて無いので「あっ……」と思うわけですが。

バッファを閉じたいならツールバーに×ボタンがありますし、メニューバーも出しているので「File」→「Close」を選ぶという手もあります。でもなぜかコンテキストメニューの中から「Close」を選びたくなるときがあるのです。

それがなぜかはよく分からないのですが、とりあえず追加してみます。

私はcontext-menu-mode(Emacs 28で追加)を有効にしていて、以前書いたようにタッチの長押しでそれが表示されるようにしているのでコンテキストメニュー自体はすでに表示できます。

長押しでコンテキストメニューを開く(Emacs 30) | Misohena Blog

なので後は単純にコンテキストメニューに項目を追加すればいいだけです。

コンテキストメニューに項目を追加するにはcontext-menu-functions変数に関数を追加すれば良いのでした。

(defun my-close-buffer-and-window ()
  (interactive)
  (let ((window (selected-window)))
    (when (kill-buffer)
      ;; すぐに削除するとタッチ処理部分のwith-selected-windowが削除
      ;; したウィンドウを選択しようとしてエラーになる。
      ;; なのでタイマーで遅延する。
      (run-with-timer
       0 nil
       (lambda ()
         (when (and (window-live-p window) (window-deletable-p window))
           (delete-window window)))))))

(defun my-context-menu-function (menu _click)
  (define-key menu [my-close-buffer-and-window]
              '(menu-item "Close" my-close-buffer-and-window))
  menu)

(add-hook 'context-menu-functions 'my-context-menu-function 0)

望んでいる動作というのはおそらくただバッファを閉じるだけでなくウィンドウも閉じたいのだと思います。

ウィンドウを閉じるだけなら以前モードラインをドラッグしてウィンドウを消せるようにしましたが、それだとバッファが残ってしまいますからね。

モードラインをドラッグしてウィンドウを消す | Misohena Blog

それに今はサイドウィンドウで仮想キーボードを表示しているので、うまく狙ったウィンドウが消せないことがあるのです。

最初はプレフィックス付きでquit-windowを呼び出そうと思いました。私はデスクトップ上では表示専用のバッファをよくC-u qで閉じることがあります。ポップアップしたウィンドウをバッファも含めて消してくれるので。

でもこの「Close」にquit-windowを割り当ててみてもどうもしっくりこないのです。quit-windowは前の状態を復元しようとしますが必ずウィンドウを閉じてくれませんからね。

なので上のコードでは素直にバッファとウィンドウを同時に削除することにしました。

ただし、長押しで無理矢理コンテキストメニューを出した弊害で、そこからウィンドウを削除するとどこからともなくwindowがらみのエラーが発生します。調べてみると、touch-screen.elの中でwith-selected-windowを使用しているところがあって、それが私が削除したウィンドウを選択しようとしてエラーが出るみたいなのです。なのでkill-buffer-and-windowは使えません。仕方が無いのでタイマーで後から削除するようにしました。

コンテキストメニューに「Close」を追加したところ
図1: コンテキストメニューに「Close」を追加したところ
2025-12-05

Emacsの中で動く仮想キーボードを作る

Emacs 30からmodifier-bar-modeというのが追加されました。これはツールバーにControl、Shift、Meta、Alt、Super、Hyperといった修飾キーに相当するボタンを追加するモードです。つまりツールバーでそれらのボタンを押してから通常のキーを押すと、その通常のキーがControlキーやらShiftキーやらと一緒に押したことになるわけです。これはおそらくAndroidの仮想キーボード(Emacsではオンスクリーンキーボードという呼び方で統一されていますしソフトウェアキーボードという呼び方も普通だと思いますが、ここでは仮想キーボードと呼ぶことにします)と併用するために追加されたのではないでしょうか。普通の仮想キーボードにはそういった修飾キーはありませんからね。

modifier-bar-mode
図1: modifier-bar-mode

私も今年Android版のEmacsを利用し始めましたが、このmodifier-bar-modeは当然試しました。

しかし単に修飾キーが欲しいだけならHacker's Keyboardのような豪華な仮想キーボードを使えば十分です。

このように手軽に押せるツールバー上のキーが欲しくなるシチュエーションは、日本語用の仮想キーボードを表示しているときや、そもそも仮想キーボードを非表示にしているときではないでしょうか。それならもっと色々なキーがツールバー上にあっても良いでしょう。

ということで作ったのがこちらのツールバー。

my-tool-bar-kbd-mode
図2: my-tool-bar-kbd-mode

modifier-bar-modeを真似た上で他にも様々なキーを追加しました。これで仮想キーボードをONにしたり切り替えたりしなくてもよく使うキーが押せるようになりました。

しかしこのツールバーを使っていると不満も出てきます。アルファベットがqwerty配列ではないので使いづらいです。キーのサイズも小さくて押しづらい。そして一番はやはり「もっと沢山キーが欲しいよ!」ということです。人間の欲望に限りはありません。

それならいっそのこと……というわけで、Emacsの中で動く仮想キーボードを作ることにしました(以前どこかで見たような気もしたのですが、ちょっと探したくらいでは見つからなかったんですよね)。

Emacs Virtual Keyboard(el-vkbd)

それでできたのがこちら。

misohena/el-vkbd: A software keyboard implemented in Emacs Lisp that runs inside Emacs.

次のスクリーンショットで上側に表示しているのがその仮想キーボードです。

vkbdと通常の仮想キーボードを両方表示
図3: vkbdと通常の仮想キーボードを両方表示

こうやって既存の仮想キーボードと併用することで日本語もフリック入力で打てますし切り替え無しでほぼ全てのキーが入力できます。(この配列は括弧がシフト無しで入力できるようになっています。素晴らしくないですか!?)

キー配列も数種類用意してあります。次のように特殊キーのみの配列を使えばmodifier-bar-modeの代わりとしても使えます。

vkbdは特殊キーのみの表示で通常の仮想キーボードと併用する(関係ないけどこの後ろのBASIC世代にはたまらなく懐かしくありません?)
図4: vkbdは特殊キーのみの表示で通常の仮想キーボードと併用する(関係ないけどこの後ろのBASIC世代にはたまらなく懐かしくありません?)

もちろん配列は自由にカスタマイズ可能です。標準的なカスタマイズ方法としては、 M-x customize-variable vkbd-layout-list というのを用意してあります。

仮想キーボードの表示場所はサイドウィンドウ、子フレーム、独立フレームから選べるようになっています(内部的にはcontainer-typeと呼んでいます)。最初は子フレームとして作ったのですが、後からサイドウィンドウとして表示できるようにもしました。結局同じフレームにウィンドウを分割して表示した方が使いやすいと思います。

子フレームとして表示している状態
図5: 子フレームとして表示している状態

仮想キーボードのタイトルバーにあるボタンは左から「閉じる」「メニュー」「10x7配列へ切替」「特殊キーのみ配列へ切替」「ネイティブ仮想キーボード無効化トグル(Android時のみ)」です。もちろんこれらタイトルバーの構成は自由にカスタマイズ可能です(M-x customize-group vkbd-title-bar)。

タイトルバーのボタン
図6: タイトルバーのボタン

メニューからは「キー配列の変更」「フレームタイプ変更(ウィンドウ、子フレーム、独立フレーム)」「サイドウィンドウの方向(ウィンドウ表示時のみ)」「カスタマイズバッファの表示」が選べます。

キーボードメニュー
図7: キーボードメニュー

メニューやボタンから選んだ状態は即座に反映され、デフォルトでは ~/.emacs.d/vkbd ファイルに保存され次回起動時には同じ状態からスタートします(保存先は M-x customize-variable vkbd-global-keyboard-user-data-storage で変更可能。項目毎にどこに保存するかをカスタマイズできる過度に複雑な仕組みをご用意しました)。

Android環境では通常の仮想キーボードを無効化するボタンも付けました。それを押すと通常の仮想キーボードは表示されなくなり、もう一度押すと元の設定に戻ります(どのようなときに仮想キーボードを表示するかは元々Emacsの設定である程度制御できます)。

通常の仮想キーボードを無効化すると、完全にこの仮想キーボードだけでEmacsを操作することも可能です。

vkbdのみ使用
図8: vkbdのみ使用

また、vkbd-replace-osk-modeというグローバルマイナーモードも作りました。これは通常の仮想キーボード(Emacsではオンスクリーンキーボードと呼びます)をできるだけこの仮想キーボードで置き換えるモードです。Emacsでは(設定によりますが)画面をタッチしたときに、そのタッチした場所が編集可能なら自動的に仮想キーボードを表示するしくみがあります。他にもミニバッファからの入力を開始したときなど、仮想キーボードを自動的に立ち上げるタイミングが複数あります。vkbd-replace-osk-modeを有効にすると、それらのタイミングで通常の仮想キーボードは出ずに代わりにこの仮想キーボードが出現します。

もちろん100% Emacs Lispで実装されていますので、動作はAndroidに限りません。デスクトップ上でも使用できます。

Windowsで使う
図9: Windowsで使う

マウスから手を離したくない、キーボードが遠い、といった時には使ってみても良いかもしれません。(私はあまり使わないと思いますが)

実装について

実装の一番のキモはやはりどうやってマウス/タッチイベントをキー入力に変換するのかという点でしょう。

このプロジェクトはmodifier-bar-modeに端を発していますから、その方法を参考にするのが自然な流れでした。

それはinput-decode-mapという変数を使うことです。

Translation Keymaps (GNU Emacs Lisp Reference Manual) (ayatakesi氏の日本語訳)

相変わらずマニュアルを読んでも頭に入ってこないので色々試して理解したこと:

  • input-decode-mapというキーマップがある。
  • input-decode-mapにはキーシーケンスに対してコマンドでは無く変換関数を割り当てる。
  • (read_key_sequence 中に)イベント(キーやマウス、タッチ等)が発生したらその組み合わせ(キーシーケンス)に対応するinput-decode-mapに割り当てられた変換関数が呼ばれる。
  • 変換関数はpromptという引数一つだけを受け取る。これは元々は read_key_sequence の引数。ほとんどの場合無視して良いらしい。
  • 変換対象のイベントはcurrent-key-remap-sequence変数から取得できる。その値はベクトルで、基本的にキーマップに設定した(変換関数を呼び出すトリガーとなった)キーシーケンスと同じものが入っているはずなので必ずしも参照しなくてよいが、マウスやタッチイベントでは座標等の細かい情報はここからしか入手できない。
  • 変換関数は変換後のイベントをベクトルで返す。
    • 空ベクトル([])を返すと変換元のイベントは無かったことになるみたい。
    • nilを返すと変換無しとなり、何も変換せずに元のイベントを返すのと同じになるみたい。
    • 返すベクトルに沢山のイベントを詰め込んでも、それが全部使われるわけではない。あくまで1回のキーシーケンス読み取りに使われる分だけが処理対象。

というわけで、まずはinput-decode-map[down-mouse-1] やら [touchscreen-begin] やらに対して独自の変換関数を登録(例えば (define-key input-decode-map [down-mouse-1] #'my-translate-event) みたいにして)。そして変換関数は、変換元イベントが仮想キーボードのキー上で発生したのであればそのキーに対応するキーイベントを生成して返します(実際には押し下げから離すまでの待ち、修飾キーの適用、キーリピートなど様々な処理がこの間に必要になります)。もし仮想キーボードと関係ない場所のイベントであれば他に悪影響が及ばないように速やかにnilを返します。

実際にやってみるとC-xの後に down-mouse-1 が来ないで mouse-1 は来るとか frame-switch イベントが邪魔とかよく分からない挙動が色々とありましたが、どうにかうまくマウス/タッチからキーイベントを生成することが出来ました。

しかしこれだけでは入力できないシチュエーションがチラホラありました。それもそのはず。input-decode-mapread_key_sequence 関数からしか使われないからです。その他の入力関数を使用している場合はinput-decode-mapによる変換は行われません。その他の入力関数とはread-eventread-charread-char-exclusiveのことです。

このあたりのEmacsの入力関数には何があるのかということは前回図解しました。

よく分かるEmacs Lisp入力関数関連図 | Misohena Blog

つまり入力関数には read_filtered_event 系(read-eventread-charread-char-exclusive)と read_key_sequence 系の2系統があり、その上に read_char という関数があるという構造でした。

read_char のレベルで何かイベントを変換するような仕組みがあれば良かったのですが、私には見つけられませんでした(長いのでろくに読んでいません)。

そこでread-eventread-charread-char-exclusiveadvice-addで:around adviceを仕掛けて動作を上書きし無理矢理返ってきたイベントを変換することに。read-charread-char-exclusiveは元の挙動ではマウス/タッチイベントが返ってこないので、read-eventを呼んで返ってきたイベントが文字で無ければリトライしたりエラーにするといった方法で対処しました。要するにread-charread-char-exclusive、そこから呼ばれる read_filtered_event が内部でやることを、Emacs Lispレベルで再現して対処しました。それなりにうまく再現できたと思いますが、不安は残るので(特にtext-conversionの切り替えまわり)仮想キーボードが表示されていない場合はちゃんとadviceを取り除くようにしてあります。

これでほとんどのケースで仮想キーボードに対するマウス/タッチイベントをキーイベントに変換できるようになりました。

これらはできるだけ表示方法とは切り離して実装してあります。なので表示部分だけをごっそり別なものに入れ替えることも理論上は可能です。その入れ替えるための仕組みも一応styleという名称ですでに入っています。ただし、その実装は現在のところテキストで表示するvkbd-text01-styleという一種類のみになっています。SVGで表示するスタイルも作りたいなと思ったのですが、思いのほかテキストだけで十分綺麗な物が出来てしまったので、作る意欲はどこかへ飛んで行ってしまいました。

Emacs Lisp実装の仮想キーボードの限界

現段階でもうまく入力できないシチュエーションがいくつか確認できています。

一つは仮想キーボードを専用フレーム(子フレームまたは独立フレーム)で表示している場合。この時、仮想キーボードをクリックするとフォーカスが仮想キーボードを表示しているフレームに移り switch-frame イベントが発生します。そして様々なフレーム切り替え処理が行われます。その影響なのか詳しくは調べていませんが、そのタイミングで入力受け付け状態が終了してしまうプログラムがいくつかありました。transient.el(Magit等)やset-transient-map(text-scale-adjust等)が典型的です。ウィンドウ表示にするとフレーム遷移が発生しないので問題が発生しません。基本的に生成したキーイベントは入力対象のバッファ、ウィンドウ、フレームに選択やフォーカスを戻してから返さないといけないので、必然的にフレーム選択やフォーカスの遷移は打鍵毎に2回生じることになります。別フレームで表示していると他にも様々な問題があります。基本的にフォーカスが当たっていないフレームではマウスイベントが発生しないようなので、キーリピート中のmouse upイベントが発生せずキーが押しっぱなしになってしまうという問題もありました(従って現状ではキーリピートはウィンドウ表示時のみ可能です)。

もう一つはウィンドウ表示時の問題。現在ウィンドウ表示時はサイドウィンドウを使用して仮想キーボードを表示しますが、既存のプログラムの中にはサイドウィンドウを使うものが数多くあります。例えばtransient.el(Magit等)はキーメニューをデフォルトでは下側にサイドウィンドウとして表示します。which-key-modeもヘルプを下側にサイドウィンドウとして表示していました。もし下側に仮想キーボードを表示していると、それらがサイドウィンドウを表示したときに消されてしまい、キー入力が出来なくなってしまいます。どちらもキーを受け付ける専用の状態の時に起こるので、その時にキーが打てないのは困ります。現在はサイドウィンドウのスロット番号にランダムな数字を入れておいて完全に消されないようにしていますが、それでも一部が隠れてしまい狙ったキーが入力できないときがあります。私はサイドウィンドウをあまり積極的に使っていないので、仮想キーボードの配置場所を上側に移すことで回避できました。上下両方に何かのサイドウィンドウを表示している人は困るかもしれません。

他にもたまにキーを押したつもりがマウスイベントとして解釈されてエラーが発生する場合があるような気がします。

いくつか問題はありますが、とは言え実用的にはほとんど困らない程度にはなかったかなと思っています。