2024-02-17

Emacsのコンテキストメニューをキーから開く

Emacs28から(?)context-menu-modeというグローバルマイナーモードがあって有効にしてどこかを右クリックするとコンテキストメニューが出ます。

diredでファイルを右クリックした画面
図1: diredでファイルを右クリックした画面

常々「何が出来るのかわかんねーよC-h mは見づらいし、マニュアル読め? あんなの全部読めるわけ無いだろ、というか覚えておけるわけ無いだろ」と思っている人間にはとても素晴らしい機能だと思います。

左手でポテチをつまみながらリラックスして右手でマウスを握って操作をしたい人にも良いですね。

といっても普通にキーボードで操作しているときには逆に使いづらいのです。このメニューはMS-Windowsだとアプリケーションキー(またはShift+F10)でも開けるのですが右クリックと同じメニュー(x-popup-menuによる)が出てきてしまいます。もちろん矢印キーで操作できるのですが、C-n等は使えません。私はいまだにXKeymacsを使っているので一般的なWindowsアプリケーションはたいていEmacsキーバインドで使えるのですが、Emacsには適用されないように設定しています。Emacsはそんなの無くてもEmacsキーバインドで使えますし、XKeymacsの不完全なエミュレーションを使う必要は無いですからね。でもx-popup-menuで表示されるWindows標準のメニューUIにも効果が無くなってしまうわけです。困った困った。

メニューバーの方も同じ問題があってF10を押すとメニューバーにキーフォーカスが移るのですが、こちらも矢印キーしか受け付けません。(ちなみに私は機能の存在を発見しやすくするためにメニューバーは表示したままにしています。使うことはそれほど多くはありません)

ただし、メニューバーを非表示にしているときはF10を押すとCUIでメニューが出てきます。menu-bar-openのコードを呼んでみるとtmm-menubartmm-promptという流れになっています。試しに M-: (tmm-prompt (context-menu-map)) を実行してみるとCUIでコンテキストメニューが表示されました。

diredで(tmm-prompt (context-menu-map))を評価した画面
図2: diredで(tmm-prompt (context-menu-map))を評価した画面

見た目が少しイモっぽいですがちゃんとメニューとして機能します。同じ物が二つ表示されてるのは vertico-mode のせいですね。後でどちらかを非表示にしないと。

とりあえず次のようにしておけばアプリケーションキーやShift+F10でtmm-promptを使ったコンテキストメニューが表示できます。

(with-eval-after-load "mouse"
  (defun my-context-menu-open-tmm ()
    (interactive)
    (tmm-prompt (context-menu-map)))

  (define-key global-map (kbd "S-<f10>") 'my-context-menu-open-tmm)
  (define-key context-menu-mode-map (kbd "<apps>") 'my-context-menu-open-tmm)
  (define-key context-menu-mode-map (kbd "<menu>") 'my-context-menu-open-tmm))

というかcontext-menu-openのdocstringには「Start key navigation of the context menu.」って書かれてるんですけどね。うーん……。context-menu-open関数自体を置き換えてしまってもいいかもしれませんね。もしくはremapする?

まぁそもそも始めから M-` を押してtmm-menubarを開くのでもたいていの場合は間に合ってしまいますが。今のところ内容はほとんど被っているようですし。もう少し現在の位置に応じて色々変えてくれるともっと使いやすくなりそうですね。org-modeなんてTableの位置でもないのにTableなんてメニュー項目を出してくるなと言いたい。

(追記)

Verticoとの兼ね合いをどうするかについて。とりあえずtmm-prompt関数ではVerticoを一時的に無効にしてみました。Verticoを使うと1文字入力だけでメニュー項目を選択出来なかったので。ヘルプを消したり"==>"を":"にしたりして見た目もちょっとスッキリに。

(defun my-tmm-prompt:around (oldfun &rest args)
  (let ((tmm-completion-prompt "")
        (tmm-mid-prompt ":")
        (completion-show-help nil))
    (if (and (boundp 'vertico-mode) vertico-mode)
        ;; verticoがある場合
        (if t
            ;; verticoを無効にする場合
            ;; 一時的にverticoを無効にして実行
            (unwind-protect
                (progn
                  (vertico-mode -1)
                  (apply oldfun args))
              (vertico-mode 1))
          ;; verticoを使う場合 (いまいち)
          (cl-letf (((symbol-function 'tmm-add-prompt) #'ignore)
                    (vertico-count 20))
            (apply oldfun args)))
      ;; そのまま実行
      (apply oldfun args))))
(advice-add 'tmm-prompt :around 'my-tmm-prompt:around)