2025-03-12 ,

Android版Emacsのためにした設定

先日Android版のEmacsを導入してみましたが、その後にした設定をまとめました。

これまでの設定:

以下はそれ以外の細かい設定です。

起動したらorgファイルを開く

私はこれまでEmacsで起動画面のカスタマイズなどは特にしていませんでした。とは言ってもデフォルトのスプラッシュスクリーンくらいはOFFにしていたので(つまり (setq inhibit-splash-screen t))、起動したらscratchバッファが表示される状態でした。

PCならこれで全く困らずそこから必要に応じてファイルやディレクトリを開けば済むわけですが、Androidスマホだと何を開くにも小さなボタンを何回も押さなければならず苦痛です(メニューバーの中のブックマークの位置と来たら……)。

なのでとりあえずデフォルトのorgファイルを最初から開くことにしました。その名も phone.org

とは言ってもやることは init.el の最後でfind-fileするだけです。一応OSとファイルの存在くらいは確認しておきましょうか。

(when (eq system-type 'android)
  (let ((home-file "~/my-org-files/phone.org"))
    (when (file-regular-p home-file)
      (find-file home-file))))

org-modeでメニューを作る

そうして開いたorgファイルにはよくアクセスするファイルやディレクトリへのリンクを書いておくわけです。

しかしそれだけでは足りません。よく使うコマンドもタッチで実行できるようにしておきたいところ。そんな時に便利なのが elisp: リンクです。 [[elisp:(message "Hello")][ハロー]] などと書けば押すとEmacs Lispの式が評価されるリンクが作成できます。description(ハローの部分)には画像を指定したりも出来るので工夫次第で綺麗な画面を作ることも出来ることでしょう。他人からもらったorgファイルだと何をされるか分からないので危険なリンクですが、自分が作ったものなら何の問題もありません。一応評価するかyes/noで聞かれるのでキーボードからだとy・e・s・RETと4ストローク必要ですが、Androidであればダイアログのyesをタップするだけです。むしろちょうど良いくらいです。

例えば次のような感じです(適当に似たようなものをでっち上げたので動作未確認)。

#+TITLE: Android用ホームファイル

- [[elisp:(my-ssh-setup)][ssh-agentの起動]]

- [[file:/data/data/com.termux/files/home/][Termux Home]]

- [[file:/data/data/org.gnu.emacs/files/][Emacs Home]]

- [[file:/sdcard/][SDCARD]]

- [[file:~/my-org-files/][Orgファイルたち]]
  - [[elisp:(my-git-pull "~/my-org-files/")][(my-git-pull)]]
  - [[elisp:(my-git-commit-push "~/my-org-files/")][(my-git-commit-push)]]
  - [[file:~/my-org-files/phone.org][phone.org]]
  - [[file:~/my-org-files/todo.org][todo.org]]
  - [[elisp:(org-agenda nil "a")][Agenda]]

- [[file:~/my-emacs-config/][Emacsの設定ファイル]]
  - [[elisp:(my-git-pull "~/my-emacs-config/")][(my-git-pull)]]
  - [[elisp:(magit-status "~/my-emacs-config/")][(magit-status)]]
  - [[file:~/my-emacs-config/init.el][init.el]]
  - [[file:~/my-emacs-config/early-init.el][early-init.el]]

各種ディレクトリへのリンクの他に、Gitのpushやpull、org-modeのアジェンダの表示なんかを入れておくと良いでしょう。これでAndroidからいつでも予定を確認できます。

Gitのためにあらかじめ次のような関数を用意しておきます。

(defun my-git-pull (dir)
  (let ((default-directory dir))
    (vc-pull)))

(defun my-git-commit-push (dir)
  (let ((default-directory dir))
    (unless
        (and (zerop (shell-command "git commit . -m \"Update\"" "*my-git-push*"))
             (zerop (shell-command "git push" "*my-git-push*")))
      (pop-to-buffer "*my-git-push*"))))

~/my-org-files/ の下は同期ソフト代わりにGitを使うようなイメージです。将来的には変更がぶつかったらある程度自動的にマージ(rebase?)するような仕掛けも欲しい所。

~/my-emacs-config/ の下はある程度ちゃんとコミットメッセージを書くためにとりあえずMagitを起動するリンクを載せておきましたが、Magitはなぜかものすごく遅いのでvc-checkinとvc-pushの方がいいかもしれません。

リンクを開くときに新しいウィンドウを開かない

使っていると何かにつけて新しいウィンドウを開いてくるのが気になります。PCでは分割ウィンドウが不要ならC-x 1を押せば済む話ですが、タッチ操作がメインの場合はウィンドウを閉じるのにも一手間必要です。画面が小さいので分割されると見づらくなってしまうということも関係しているのでしょう。

先日の設定(モードラインをドラッグしてウィンドウを消す)でウィンドウを簡単に閉じられるようになったとはいえ、そもそも最初から新しいウィンドウを開かなければいい話です。とは言え別ウィンドウを開くのが必ず悪いかと言われればよく分からないので、とりあえず気になったところだけ直すことにします。display-buffer-alistあたりを変更しようかとも思いましたが、とりあえず個々のコマンドの設定を変更してみます。

;; org-modeのリンクを開くときに別ウィンドウを開かない
(setf (alist-get 'file org-link-frame-setup) 'find-file)

;; org-agendaで別ウィンドウを開かない
(setq org-agenda-window-setup 'current-window)

;; Dired内でファイルをタップしたときに別ウィンドウを開かない
(define-key dired-mode-map [mouse-2] #'dired-mouse-find-file)

;; Diredから他のディレクトリを開くときに元のDiredバッファをkillする
;; (`dired-mouse-find-file'の挙動に影響する。
;;   dired-mouse-find-alter-fileは存在しない)
(setq dired-kill-when-opening-new-dired-buffer t)

おそらく他にも同じように感じる場所があると思いますが、気がついたら逐一設定していくことにします。

org-captureの保存先

ディレクトリ構成が変わったのでorg-captureの保存先も変える必要があります。……と思ったのですが、その後 ~/ とシンボリックリンクを組み合わせてPCと同じパスになるようにしてしまったので設定は不要になりました。

ファイルの同期

PCではファイルの保存やEmacsの終了のタイミングでファイルを同期するような仕組みを整備していましたがスマホでは止めておきました。電波が入らないところで使っている可能性があるので。とりあえず手動で同期しようと思います。

物理キーボードからのIMEのON/OFF

Bluetoothのハードウェアキーボードを接続してみたのですが、IMEのON/OFFの方法がよく分かりませんでした。半角/全角を押すと切り替わるように見えてOFFなのにM-<やM->を押すと<や>が入力されてしまったり、かと思えばそれらはコマンドとして認識されるけど半角/全角切り替えはできなかったり。

調べてみると、これはどうもtext-conversion-styleoverriding-text-conversion-styleが影響しているようです。

text-conversion-styleはIMEの挙動を指定するバッファローカル変数です。nilのときIME無効、非nilのとき有効になるようです。非nilの中でも、シンボル action やシンボル password といった指定もあります。ザッと検索してみたところ、text-mode(org-modeはここに含まれます)やprog-mode(一般的なプログラミング用のモードはここに含まれます)では tcomint-mode(shell-mode等がここに含まれます)やminibuffer-modeでは actionread-passwdでは password が指定されているようでした。つまりバッファ毎にそのバッファに入力される文字の種類・性質を指定するという側面があるようです。

それでtext-conversion-styleがnilのバッファではIMEを介さない入力が可能で、tのバッファでは常にIMEを介した入力になってしまうということのようです。極端だってば。(注: この辺りの挙動はIMEによっても若干異なるようです。Gboardはtext-conversion-styleがnilの時でもGboardのショートカットキーを完全に無効にすることはできませんでした。M->を押すと何か不可解なエラーが出たり、M-tを押すとGoogle翻訳を使おうとしたりします。ATOK Passport Proはショートカットキーが干渉することはありませんでしたが、text-conversion-styleがtのときは半角文字もIME経由でバッファへ直接挿入されるためorg-modeのスピードコマンドやedebugのアルファベット1文字のキーが使えなかったりしました)

text-conversion-styleの効果はグローバル変数overriding-text-conversion-styleで上書きできます。デフォルトの値はシンボル lambdatext-conversion-styleの値を尊重します。それ以外の値が指定されている場合は、text-conversion-styleは無視してoverriding-text-conversion-styleの値が使われるようです。

ということはこのoverriding-text-conversion-styleを切り替えるコマンドを作ればIMEの挙動をユーザーが明示的に指定出来るようになるはずです。

(defun my-toggle-text-conversion-style ()
  "`overriding-text-conversion-style'を`lambda'とnilとの間で切り替えます。"
  (interactive)
  (cond
   ((eq overriding-text-conversion-style 'lambda)
    (setq overriding-text-conversion-style nil)
    (set-text-conversion-style text-conversion-style))
   ((null overriding-text-conversion-style)
    (setq overriding-text-conversion-style 'lambda)
    (set-text-conversion-style text-conversion-style))))

それを私はCtrl+変換に割り当てました。私は普段PCでもこのキーでIMEのON/OFFを切り替えているので。

(define-key global-map [C-henkan] #'my-toggle-text-conversion-style)

半角/全角は英数/カナ切り替え(カナロック)のようなものだと考えることにします。

ちなみにハードウェアキーボードの細かいレイアウトはshiftrot/caps2ctrlkcmファイルを独自にカスタマイズして調整しています。

line-spacingや文字サイズの調整

指で位置を指定することを考えるとline-spacingは大きめが良いでしょうね。使うフォントにもよると思いますが。文字サイズは視力との兼ね合いでしょうか。

おしまい

これでAndroid版のEmacsを触って最初に思いついた設定は一通り終わりました。

最初はAndroid版のEmacsなんて使い物になるの~? と疑っていましたが、思っていた以上に使える道具だということが分かってきました。Orgzlyはもう私には必要ありません。Android用に作られた昔からあるテキストエディタアプリも用済みです。タッチ操作で普通に編集できるので遜色ありません。長押しメニューが圧倒的に便利です。Dropbox等のクラウドストレージとの連携が必要な人だとまだその手の機能を搭載したアプリの方に分があるかもしれません……ってTrampのrcloneメソッドがあるんですね(使ったことはありません)。

すごいのはほぼ全ての設定がPCと同一だということです。Androidのためだけに設定したことは、これまでに紹介したものしかありません。

もちろん今後も触っていて思いついた改善をしていこうと思います。決して尽きることはないでしょう。

2025-03-11

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

Emacs 30からタッチ操作がサポートされました。これまでもOSによってはタッチ操作がマウス操作に変換されて操作可能だった場合もあると思いますが、Emacs 30からはEmacs自体がタッチイベントを認識してEmacs Lispからきめ細かい制御が可能になりました。

タッチスクリーンと言えばスマホやタブレットを連想しますが、PCにおいてもタッチスクリーン付きのディスプレイが搭載されていればタッチ操作が可能です。試しにタッチスクリーン付きの少し古めなノートPC(Windows 10)を使ってみたところ問題なく操作できました。

タッチスクリーンでできる操作についてはEmacs マニュアルの6.1 Using Emacs on Touchscreens(日本語訳)に書いてあります。それによればデフォルトでタップ、スクロール、ドラッグ、ピンチといった操作がサポートされています。マウスイベントへの変換も行われるので、従来のマウス用に用意された操作もタッチスクリーンからある程度実行可能です。

ある程度ということはもちろん全てではありません。マウスにはボタンが複数付いているのでかなり複雑な操作も可能ですが、現在のEmacsのタッチスクリーン操作はそれを全て再現できるようには出来ていません(加えて修飾キーとの組み合わせもサポートしていないようです)。

タッチで再現できない最たるものが右クリックでしょう。私は少し前からcontext-menu-modeを使用しているのでPCでは右クリックでコンテキストメニューが表示されますが、タッチ操作からではそれができません。スマホにおいても長押しでメニューが出るというのはかなり一般的な操作だと思います。

というわけで次のようなコードを書いて長押しでコンテキストメニューが表示されるようにしてみました。

(defun my-touch-screen-handle-point-up:around (old-fun point prefix canceled)
  (when (and (null touch-screen-aux-tool)
             (memq (nth 3 touch-screen-current-tool) '(held drag))
             (null prefix)
             (not canceled))
    ;; 右ボタン押し下げを再現するなら次のようにする。
    ;; ただ、続く解放イベント(mouse-3やdrag-mouse-3)も再現しないと色々良くない。
    ;; (throw 'input-event (list 'down-mouse-3 (cdr point)))
    ;; なので、とりあえず単にコンテキストメニューを開くだけにしておく。
    (context-menu-open)
    ;; 後続の処理(仮想キーボードの表示)をブロックするか少し迷う。
    )
  (funcall old-fun point prefix canceled))

(advice-add #'touch-screen-handle-point-up :around
            #'my-touch-screen-handle-point-up:around)

実際に使ってみた図:

タッチ長押しの後ドラッグして離したところ(Windows 10)
図1: タッチ長押しの後ドラッグして離したところ(Windows 10)

AndroidとWindows 10の両方で動くことを確認しました。

Emacsのタッチ操作に反応する部分はtouch-screen.elに書かれています。複雑なので全部は読んでいませんが、長押し、ドラッグ、解放に関する流れを大ざっぱに追ってみました。長押しするとheldという状態に移行し、そのまま指を動かすとdragという状態に移行します。heldまたはdragの状態で指を離したとき、デフォルトではなぜか仮想キーボードを開く処理が行われるのですが(おそらく選択した範囲に対して仮想キーボードでC-wやM-wしろということなのかもしれません)、上のコードはその前に割り込んでコンテキストメニューを表示させます。

コンテキストメニューが不要ならそのままフレームをタップしたり(Windows 10)、ウィンドウ内の適当な場所をタップしたり(Android)すればコンテキストメニューは閉じます。その時ポイントやアクティブリージョンが変わったりはしません(注: Windows 10の時はウィンドウ内をタップするとメニューが閉じるだけで無くポイントも移動してしまいました。フレームのタイトルバー等をタップしましょう)。なのでコンテキストメニューを表示させることにそれほど害は無いでしょう。

むしろそのままカットやコピーが選べるのはかなり楽なはずです。

上のコードでは右クリックを再現するのでは無くcontext-menu-openを直接呼び出しています。本来は右クリックを再現した方が良いと思うのですが、マウス右ボタン押し下げイベント(down-mouse-3)だけでなく解放イベント(mouse-3またはdrag-mouse-3)も一緒に再現するのが面倒なので止めておきました。パッケージによってはdown-mouse-3やmouse-3でメニューを表示しているものがあるので、本来ならちゃんと再現した方が用途が広がると思います。

マニュアルにも書いてありますが、より良い選択操作のため次の変数をtにしておくと良いでしょう。

(setq touch-screen-word-select t ;; 単語単位の選択(日本語だとちょっとつらい)
      touch-screen-extend-selection t ;; 後からポイント長押しで選択範囲の拡張
      touch-screen-preview-select t) ;; ミニバッファにドラッグ中の場所を表示

(相変わらずEmacsというのはこの手の便利機能をデフォルトで有効にしたがりませんね。これだから初心者が逃げていくのです。使っていて邪魔だなと思ったらnilにすれば良いだけです)

何はともあれ、AndroidでのEmacsが大分使いやすくなってきました。

2025-03-08

Emacsでキー入力を補助するためのツールバーを改善する

Emacs 30からmodifier-bar-modeが追加されました。これは修飾キーの入力ボタンを表示するグローバルマイナーモードです。

M-x modifier-bar-modeとタイプすると次のようなツールバーが表示されます(ツールバーの一種なので、先にtool-bar-mode等でツールバー自体も表示しておく必要があると思います)。

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

見ての通りボタンはCtrl、Shift、Meta、Alt、Super、Hyperの六つです。例えばC-M-iと入力したければCtrlボタンとMetaボタンを押してからiを押せば良いということになります。

もちろん普通のキーボードを使っていれば必要になることはほとんど無いと思いますが、AndroidでCtrl等が無いソフトキーボードを使っているときは重宝するでしょう。PCでも左手でポテチをつまみながら右手でマウスを握りリラックスしながらポチポチやってるときには便利かもしれません。

とは言え、実際Androidスマホでこれを使ってみたところ、案外痒いところに手が届かないと言いましょうか、これじゃ全然足りないよ! という気持ちになりました。

スマホで使用するソフトキーボードというのは本当にキーが小さくて押しづらいです。Hacker's Keyboardはちゃんと修飾キーも押せるソフトキーボードですが、Ctrl、xと押すだけでもキーが小さすぎて神経を使います。1キーでC-xまで入力させてほしいのです。他にも1ボタンで入力できれば便利なキーは沢山あります。

そこで、このmodifier-bar-modeを元に自分なりのキー入力補助ツールバーを作ってみました。それを有効にすると次のようになります。

my-tool-bar-mode
図2: my-tool-bar-mode
  • 修飾キーは C- S- M- の三つで簡素に表示します。
  • A- s- H- はスペースの都合で諦めました。どうせ使わないでしょう。
  • C-x C-c というプレフィックスはやはり1ボタンで入力したいところ。
  • 追加で C-xC-c の後にさらに C- を続けるパターンもボタン化しました。
  • M-x はメニューのEdit→Execute Commandでも呼び出せますが、やはり1ボタンにしたいです。
  • C-g もなんだかんだ言って欲しくなります。
  • 矢印キーの上と下は普通の日本語IMEを使っているときに重宝します。GboardにせよATOKにせよ左右キーはあっても上下キーは無いからです。

というわけで以下ソースコード。

(require 'tool-bar)

(defconst my-tool-bar-svg-alist
  '(("C-" . "<g><path d=\"M9.0 5.2C9.0 5.0 9.0 4.6 8.5 4.6 8.2 4.6 8.1 4.7 8.0 4.8 8.0 4.9 8.0 4.9 7.8 5.4 7.2 4.9 6.5 4.6 5.6 4.6 3.5 4.6 1.6 6.8 1.6 9.8 1.6 12.9 3.5 15.1 5.6 15.1 7.6 15.1 9.0 13.6 9.0 12.1 9.0 11.6 8.6 11.6 8.4 11.6 8.3 11.6 7.9 11.6 7.9 12.1 7.7 13.7 6.4 14.1 5.7 14.1 4.2 14.1 2.8 12.3 2.8 9.8 2.8 7.3 4.2 5.6 5.7 5.6 6.7 5.6 7.6 6.3 7.8 7.7 7.9 8.0 7.9 8.2 8.4 8.2 9.0 8.2 9.0 7.9 9.0 7.6Z\" /><path d=\"M16.7 10.4C16.9 10.4 17.5 10.4 17.5 9.8S16.9 9.2 16.7 9.2H11.4C11.2 9.2 10.6 9.2 10.6 9.8 10.6 10.4 11.2 10.4 11.4 10.4Z\" /></g>")
    ("S-" . "<g><path d=\"M5.9 10.3C6.3 10.4 7.7 10.8 7.7 12.2 7.7 13.1 7.0 14.0 5.7 14.0 5.2 14.0 4.4 14.0 3.7 13.5 3.0 13.1 3.0 12.4 3.0 12.1 2.9 11.8 2.9 11.5 2.4 11.5 1.8 11.5 1.8 11.9 1.8 12.2V14.4C1.8 14.6 1.8 15.1 2.3 15.1 2.7 15.1 2.8 14.8 2.9 14.3 3.6 14.8 4.6 15.1 5.7 15.1 7.6 15.1 8.8 13.6 8.8 12.1 8.8 11.1 8.2 10.4 8.0 10.1 7.3 9.5 6.9 9.4 5.6 9.1L4.2 8.8C3.5 8.6 2.9 8.0 2.9 7.2 2.9 6.3 3.7 5.5 4.9 5.5 6.9 5.5 7.1 7.1 7.2 7.6 7.3 8.0 7.4 8.1 7.8 8.1 8.4 8.1 8.4 7.7 8.4 7.4V5.2C8.4 4.9 8.4 4.5 7.9 4.5 7.5 4.5 7.4 4.8 7.2 5.3 6.6 4.7 5.7 4.5 4.9 4.5 3.1 4.5 1.8 5.8 1.8 7.3 1.8 8.4 2.6 9.5 4.0 9.9 4.0 9.9 5.6 10.3 5.9 10.3Z\" /><path d=\"M16.7 10.4C16.9 10.4 17.5 10.4 17.5 9.8S16.9 9.2 16.7 9.2H11.4C11.2 9.2 10.6 9.2 10.6 9.8S11.2 10.4 11.4 10.4Z\" /></g>")
    ("M-" . "<g><path d=\"M5.8 8.7C5.6 9.3 5.4 9.8 5.3 10.3H5.3C5.1 9.5 3.7 5.2 3.6 5.1 3.5 4.7 3.1 4.7 2.8 4.7H1.9C1.6 4.7 1.2 4.7 1.2 5.2 1.2 5.7 1.6 5.7 2.1 5.7V13.8C1.6 13.8 1.2 13.8 1.2 14.3 1.2 14.8 1.6 14.8 1.9 14.8H3.3C3.5 14.8 3.9 14.8 3.9 14.3 3.9 13.8 3.6 13.8 3.0 13.8V5.9H3.1C3.2 6.7 4.3 9.9 4.4 10.1 4.5 10.5 4.7 11.1 4.8 11.2 4.9 11.3 5.1 11.5 5.3 11.5 5.5 11.5 5.8 11.3 5.9 11.1 5.9 11.0 7.4 6.8 7.6 5.9H7.6V13.8C7.0 13.8 6.7 13.8 6.7 14.3 6.7 14.8 7.1 14.8 7.4 14.8H8.7C9.0 14.8 9.4 14.8 9.4 14.3 9.4 13.8 9.0 13.8 8.5 13.8V5.7C9.0 5.7 9.4 5.7 9.4 5.2 9.4 4.7 9.0 4.7 8.7 4.7H7.8C7.1 4.7 7.0 4.9 6.9 5.3Z\" /><path d=\"M16.7 10.3C16.9 10.3 17.5 10.3 17.5 9.8S16.9 9.2 16.7 9.2H11.4C11.2 9.2 10.6 9.2 10.6 9.8S11.2 10.3 11.4 10.3Z\" /></g>")
    ("A-" . "<g><path d=\"M6.2 5.1C6.1 4.5 5.8 4.4 5.3 4.4 4.8 4.4 4.6 4.4 4.4 5.1L2.4 13.8C2.2 13.8 1.8 13.8 1.7 13.8 1.5 13.9 1.4 14.1 1.4 14.3 1.4 14.8 1.8 14.8 2.1 14.8H3.8C4.1 14.8 4.5 14.8 4.5 14.3 4.5 13.8 4.2 13.8 3.6 13.8L4.0 12.1H6.7L7.0 13.8C6.4 13.8 6.1 13.8 6.1 14.3 6.1 14.8 6.5 14.8 6.8 14.8H8.5C8.8 14.8 9.2 14.8 9.2 14.3 9.2 14.1 9.1 13.9 8.9 13.8 8.8 13.8 8.5 13.8 8.2 13.8ZM5.3 5.8H5.3L6.4 11.1H4.2Z\" /><path d=\"M16.7 10.3C16.9 10.3 17.5 10.3 17.5 9.7S16.9 9.1 16.7 9.1H11.4C11.2 9.1 10.6 9.1 10.6 9.7S11.2 10.3 11.4 10.3Z\" /></g>")
    ("s-" . "<g><path d=\"M5.9 10.5C5.5 10.5 5.2 10.4 4.8 10.3 4.3 10.3 3.2 10.1 3.2 9.4 3.2 9.0 3.7 8.5 5.3 8.5 6.7 8.5 6.9 9.0 6.9 9.4 7.0 9.7 7.0 10.0 7.5 10.0 8.1 10.0 8.1 9.6 8.1 9.3V8.1C8.1 7.9 8.1 7.5 7.6 7.5 7.2 7.5 7.1 7.7 7.1 7.8 6.4 7.5 5.6 7.5 5.3 7.5 2.5 7.5 2.1 8.8 2.1 9.4 2.1 10.9 3.9 11.2 5.4 11.5 6.2 11.6 7.6 11.8 7.6 12.7 7.6 13.3 7.0 13.9 5.4 13.9 4.7 13.9 3.7 13.7 3.3 12.4 3.2 12.1 3.2 11.9 2.7 11.9 2.1 11.9 2.1 12.2 2.1 12.6V14.2C2.1 14.5 2.1 14.9 2.6 14.9 2.8 14.9 3.1 14.9 3.3 14.2 4.1 14.8 5.0 14.9 5.4 14.9 8.1 14.9 8.6 13.5 8.6 12.7 8.6 11.0 6.4 10.6 5.9 10.5Z\" /><path d=\"M16.7 10.3C16.9 10.3 17.5 10.3 17.5 9.7S16.9 9.1 16.7 9.1H11.4C11.2 9.1 10.6 9.1 10.6 9.7S11.2 10.3 11.4 10.3Z\" /></g>")
    ("H-" . "<g><path d=\"M8.2 5.6H8.6C8.9 5.6 9.3 5.6 9.3 5.1 9.3 4.6 8.9 4.6 8.6 4.6H6.7C6.4 4.6 6.0 4.6 6.0 5.1 6.0 5.6 6.4 5.6 6.7 5.6H7.1V9.0H3.5V5.6H4.0C4.2 5.6 4.6 5.6 4.6 5.1 4.6 4.6 4.2 4.6 4.0 4.6H2.0C1.7 4.6 1.3 4.6 1.3 5.1 1.3 5.6 1.7 5.6 2.0 5.6H2.4V13.7H2.0C1.7 13.7 1.3 13.7 1.3 14.2 1.3 14.7 1.7 14.7 2.0 14.7H4.0C4.2 14.7 4.6 14.7 4.6 14.2 4.6 13.7 4.2 13.7 4.0 13.7H3.5V10.0H7.1V13.7H6.7C6.4 13.7 6.0 13.7 6.0 14.2 6.0 14.7 6.4 14.7 6.7 14.7H8.6C8.9 14.7 9.3 14.7 9.3 14.2 9.3 13.7 8.9 13.7 8.6 13.7H8.2Z\" /><path d=\"M16.7 10.2C16.9 10.2 17.5 10.2 17.5 9.6S16.9 9.1 16.7 9.1H11.4C11.2 9.1 10.6 9.1 10.6 9.6S11.2 10.2 11.4 10.2Z\" /></g>")
    ("Mx" . "<g><path d=\"M5.8 8.6C5.6 9.2 5.4 9.6 5.3 10.1H5.3C5.1 9.3 3.7 5.0 3.6 4.9 3.5 4.5 3.1 4.5 2.8 4.5H1.9C1.6 4.5 1.2 4.5 1.2 5.0 1.2 5.6 1.6 5.6 2.1 5.6V13.7C1.6 13.7 1.2 13.7 1.2 14.2 1.2 14.7 1.6 14.7 1.9 14.7H3.3C3.5 14.7 3.9 14.7 3.9 14.2 3.9 13.7 3.6 13.7 3.0 13.7V5.8H3.1C3.2 6.5 4.3 9.7 4.4 9.9 4.5 10.3 4.7 10.9 4.8 11.1 4.9 11.2 5.1 11.3 5.3 11.3 5.5 11.3 5.8 11.2 5.9 10.9 5.9 10.8 7.4 6.6 7.6 5.8H7.6V13.7C7.0 13.7 6.7 13.7 6.7 14.2 6.7 14.7 7.1 14.7 7.4 14.7H8.7C9.0 14.7 9.4 14.7 9.4 14.2 9.4 13.7 9.0 13.7 8.5 13.7V5.6C9.0 5.6 9.4 5.6 9.4 5.0 9.4 4.5 9.0 4.5 8.7 4.5H7.8C7.1 4.5 7.0 4.8 6.9 5.2Z\" /><path d=\"M14.5 11.0 16.4 8.6H17.0C17.3 8.6 17.7 8.6 17.7 8.1 17.7 7.5 17.3 7.5 17.0 7.5H15.1C14.8 7.5 14.4 7.5 14.4 8.0 14.4 8.6 14.8 8.6 15.2 8.6L14.0 10.2 12.7 8.6C13.2 8.6 13.5 8.6 13.5 8.0 13.5 7.5 13.1 7.5 12.9 7.5H10.9C10.7 7.5 10.2 7.5 10.2 8.1 10.2 8.6 10.7 8.6 10.9 8.6H11.6L13.5 11.0 11.5 13.7H10.8C10.6 13.7 10.1 13.7 10.1 14.2 10.1 14.7 10.6 14.7 10.8 14.7H12.8C13.0 14.7 13.4 14.7 13.4 14.2 13.4 13.7 13.1 13.7 12.6 13.7L14.0 11.6 15.5 13.7C15.0 13.7 14.6 13.7 14.6 14.2 14.6 14.7 15.1 14.7 15.3 14.7H17.3C17.5 14.7 17.9 14.7 17.9 14.2 17.9 13.7 17.5 13.7 17.3 13.7H16.6Z\" /></g>")
    ("Cx" . "<g><path d=\"M9.0 5.0C9.0 4.7 9.0 4.3 8.5 4.3 8.2 4.3 8.1 4.5 8.0 4.6 8.0 4.7 8.0 4.7 7.8 5.1 7.2 4.7 6.5 4.3 5.6 4.3 3.5 4.3 1.6 6.6 1.6 9.6 1.6 12.6 3.5 14.9 5.6 14.9 7.6 14.9 9.0 13.4 9.0 11.9 9.0 11.3 8.6 11.3 8.4 11.3 8.3 11.3 7.9 11.3 7.9 11.8 7.7 13.4 6.4 13.8 5.7 13.8 4.2 13.8 2.8 12.1 2.8 9.6 2.8 7.1 4.2 5.3 5.7 5.3 6.7 5.3 7.6 6.1 7.8 7.5 7.9 7.7 7.9 8.0 8.4 8.0 9.0 8.0 9.0 7.7 9.0 7.3Z\" /><path d=\"M14.5 11.0 16.4 8.5H17.0C17.3 8.5 17.7 8.5 17.7 8.0 17.7 7.5 17.3 7.5 17.0 7.5H15.1C14.8 7.5 14.4 7.5 14.4 8.0 14.4 8.5 14.8 8.5 15.2 8.5L14.0 10.2 12.7 8.5C13.2 8.5 13.5 8.5 13.5 8.0 13.5 7.5 13.1 7.5 12.9 7.5H10.9C10.7 7.5 10.2 7.5 10.2 8.0 10.2 8.5 10.7 8.5 10.9 8.5H11.6L13.5 11.0 11.5 13.7H10.8C10.6 13.7 10.1 13.7 10.1 14.2 10.1 14.7 10.6 14.7 10.8 14.7H12.8C13.0 14.7 13.4 14.7 13.4 14.2 13.4 13.7 13.1 13.7 12.6 13.7L14.0 11.6 15.5 13.7C15.0 13.7 14.6 13.7 14.6 14.2 14.6 14.7 15.1 14.7 15.3 14.7H17.3C17.5 14.7 17.9 14.7 17.9 14.2 17.9 13.7 17.5 13.7 17.3 13.7H16.6Z\" /></g>")
    ("CxC" . "<g><path d=\"M9.0 5.0C9.0 4.7 9.0 4.3 8.5 4.3 8.2 4.3 8.1 4.5 8.0 4.6 8.0 4.6 8.0 4.7 7.8 5.1 7.2 4.6 6.5 4.3 5.6 4.3 3.5 4.3 1.6 6.5 1.6 9.5 1.6 12.6 3.5 14.8 5.6 14.8 7.6 14.8 9.0 13.3 9.0 11.8 9.0 11.3 8.6 11.3 8.4 11.3 8.3 11.3 7.9 11.3 7.9 11.8 7.7 13.4 6.4 13.8 5.7 13.8 4.2 13.8 2.8 12.0 2.8 9.5 2.8 7.1 4.2 5.3 5.7 5.3 6.7 5.3 7.6 6.1 7.8 7.4 7.9 7.7 7.9 8.0 8.4 8.0 9.0 8.0 9.0 7.6 9.0 7.3Z\" /><path d=\"M14.5 10.9 16.4 8.5H17.0C17.3 8.5 17.7 8.5 17.7 8.0 17.7 7.5 17.3 7.5 17.0 7.5H15.1C14.8 7.5 14.4 7.5 14.4 8.0 14.4 8.5 14.8 8.5 15.2 8.5L14.0 10.2 12.7 8.5C13.2 8.5 13.5 8.5 13.5 8.0 13.5 7.5 13.1 7.5 12.9 7.5H10.9C10.7 7.5 10.2 7.5 10.2 8.0 10.2 8.5 10.7 8.5 10.9 8.5H11.6L13.5 10.9 11.5 13.6H10.8C10.6 13.6 10.1 13.6 10.1 14.1 10.1 14.6 10.6 14.6 10.8 14.6H12.8C13.0 14.6 13.4 14.6 13.4 14.1 13.4 13.6 13.1 13.6 12.6 13.6L14.0 11.6 15.5 13.6C15.0 13.6 14.6 13.6 14.6 14.1 14.6 14.6 15.1 14.6 15.3 14.6H17.3C17.5 14.6 17.9 14.6 17.9 14.1 17.9 13.6 17.5 13.6 17.3 13.6H16.6Z\" /><path d=\"M26.5 5.0C26.5 4.7 26.5 4.3 26.0 4.3 25.7 4.3 25.6 4.5 25.5 4.6 25.5 4.6 25.5 4.7 25.3 5.1 24.7 4.6 23.9 4.3 23.1 4.3 20.9 4.3 19.1 6.5 19.1 9.5 19.1 12.6 20.9 14.8 23.1 14.8 25.0 14.8 26.5 13.3 26.5 11.8 26.5 11.3 26.1 11.3 25.9 11.3 25.7 11.3 25.4 11.3 25.3 11.8 25.2 13.4 23.9 13.8 23.2 13.8 21.7 13.8 20.2 12.0 20.2 9.5 20.2 7.1 21.7 5.3 23.2 5.3 24.2 5.3 25.1 6.1 25.3 7.4 25.4 7.7 25.4 8.0 25.9 8.0 26.5 8.0 26.5 7.6 26.5 7.3Z\" /></g>")
    ("Cc" . "<g><path d=\"M9.0 4.9C9.0 4.7 9.0 4.3 8.5 4.3 8.2 4.3 8.1 4.4 8.0 4.5 8.0 4.6 8.0 4.6 7.8 5.1 7.2 4.6 6.5 4.3 5.6 4.3 3.5 4.3 1.6 6.5 1.6 9.5 1.6 12.6 3.5 14.8 5.6 14.8 7.6 14.8 9.0 13.3 9.0 11.8 9.0 11.3 8.6 11.3 8.4 11.3 8.3 11.3 7.9 11.3 7.9 11.7 7.7 13.4 6.4 13.8 5.7 13.8 4.2 13.8 2.8 12.0 2.8 9.5 2.8 7.0 4.2 5.3 5.7 5.3 6.7 5.3 7.6 6.0 7.8 7.4 7.9 7.7 7.9 7.9 8.4 7.9 9.0 7.9 9.0 7.6 9.0 7.2Z\" /><path d=\"M17.4 12.8C17.4 12.3 17.0 12.3 16.9 12.3 16.6 12.3 16.4 12.4 16.3 12.7 16.2 12.9 15.9 13.7 14.7 13.7 13.2 13.7 12.0 12.5 12.0 11.0 12.0 10.2 12.5 8.3 14.8 8.3 15.1 8.3 15.8 8.3 15.8 8.4 15.8 9.0 16.1 9.3 16.5 9.3S17.2 9.0 17.2 8.5C17.2 7.3 15.5 7.3 14.8 7.3 11.9 7.3 10.9 9.5 10.9 11.0 10.9 13.0 12.5 14.7 14.6 14.7 16.9 14.7 17.4 13.1 17.4 12.8Z\" /></g>")
    ("CcC" . "<g><path d=\"M9.0 4.9C9.0 4.6 9.0 4.2 8.5 4.2 8.2 4.2 8.1 4.4 8.0 4.5 8.0 4.6 8.0 4.6 7.8 5.0 7.2 4.6 6.5 4.2 5.6 4.2 3.5 4.2 1.6 6.4 1.6 9.5 1.6 12.5 3.5 14.7 5.6 14.7 7.6 14.7 9.0 13.3 9.0 11.8 9.0 11.2 8.6 11.2 8.4 11.2 8.3 11.2 7.9 11.2 7.9 11.7 7.7 13.3 6.4 13.7 5.7 13.7 4.2 13.7 2.8 12.0 2.8 9.5 2.8 7.0 4.2 5.2 5.7 5.2 6.7 5.2 7.6 6.0 7.8 7.3 7.9 7.6 7.9 7.9 8.4 7.9 9.0 7.9 9.0 7.5 9.0 7.2Z\" /><path d=\"M17.4 12.8C17.4 12.3 17.0 12.3 16.9 12.3 16.6 12.3 16.4 12.3 16.3 12.7 16.2 12.9 15.9 13.7 14.7 13.7 13.2 13.7 12.0 12.5 12.0 11.0 12.0 10.2 12.5 8.3 14.8 8.3 15.1 8.3 15.8 8.3 15.8 8.4 15.8 9.0 16.1 9.2 16.5 9.2S17.2 8.9 17.2 8.5C17.2 7.2 15.5 7.2 14.8 7.2 11.9 7.2 10.9 9.5 10.9 11.0 10.9 13.0 12.5 14.7 14.6 14.7 16.9 14.7 17.4 13.0 17.4 12.8Z\" /><path d=\"M26.5 4.9C26.5 4.6 26.5 4.2 26.0 4.2 25.7 4.2 25.6 4.4 25.5 4.5 25.5 4.6 25.5 4.6 25.3 5.0 24.7 4.6 23.9 4.2 23.1 4.2 20.9 4.2 19.1 6.4 19.1 9.5 19.1 12.5 20.9 14.7 23.1 14.7 25.0 14.7 26.5 13.3 26.5 11.8 26.5 11.2 26.1 11.2 25.9 11.2 25.7 11.2 25.4 11.2 25.3 11.7 25.2 13.3 23.9 13.7 23.2 13.7 21.7 13.7 20.2 12.0 20.2 9.5 20.2 7.0 21.7 5.2 23.2 5.2 24.2 5.2 25.1 6.0 25.3 7.3 25.4 7.6 25.4 7.9 25.9 7.9 26.5 7.9 26.5 7.5 26.5 7.2Z\" /></g>")
    ("Cg" . "<g><path d=\"M9.0 4.9C9.0 4.6 9.0 4.2 8.5 4.2 8.2 4.2 8.1 4.3 8.0 4.5 8.0 4.5 8.0 4.6 7.8 5.0 7.2 4.5 6.5 4.2 5.6 4.2 3.5 4.2 1.6 6.4 1.6 9.4 1.6 12.5 3.5 14.7 5.6 14.7 7.6 14.7 9.0 13.2 9.0 11.7 9.0 11.2 8.6 11.2 8.4 11.2 8.3 11.2 7.9 11.2 7.9 11.7 7.7 13.3 6.4 13.7 5.7 13.7 4.2 13.7 2.8 11.9 2.8 9.4 2.8 7.0 4.2 5.2 5.7 5.2 6.7 5.2 7.6 6.0 7.8 7.3 7.9 7.6 7.9 7.9 8.4 7.9 9.0 7.9 9.0 7.5 9.0 7.2Z\" /><path d=\"M13.6 11.6C12.7 11.6 12.0 10.8 12.0 9.9 12.0 9.0 12.7 8.3 13.6 8.3 14.5 8.3 15.2 9.0 15.2 9.9 15.2 10.9 14.4 11.6 13.6 11.6ZM12.1 12.2C12.1 12.2 12.7 12.6 13.6 12.6 15.1 12.6 16.3 11.4 16.3 9.9 16.3 9.4 16.2 8.9 15.9 8.5 16.2 8.3 16.6 8.2 16.8 8.2 16.9 8.6 17.3 8.8 17.5 8.8 17.8 8.8 18.2 8.6 18.2 8.1 18.2 7.7 17.8 7.2 16.9 7.2 16.8 7.2 15.9 7.2 15.2 7.8 14.9 7.6 14.3 7.3 13.6 7.3 12.0 7.3 10.8 8.5 10.8 9.9 10.8 10.6 11.1 11.2 11.3 11.5 11.2 11.8 11.0 12.1 11.0 12.6 11.0 13.2 11.2 13.6 11.4 13.8 10.2 14.6 10.2 15.7 10.2 15.9 10.2 17.3 11.9 18.3 14.0 18.3 16.2 18.3 17.9 17.3 17.9 15.9 17.9 15.3 17.6 14.4 16.8 14.0 16.6 13.9 15.9 13.5 14.4 13.5H13.2C13.1 13.5 12.8 13.5 12.7 13.5 12.5 13.5 12.4 13.5 12.2 13.3 12.0 13.0 12.0 12.7 12.0 12.7 12.0 12.6 12.0 12.4 12.1 12.2ZM14.0 17.3C12.4 17.3 11.1 16.6 11.1 15.9 11.1 15.6 11.3 15.1 11.8 14.7 12.2 14.5 12.4 14.5 13.6 14.5 15.1 14.5 17.0 14.5 17.0 15.9 17.0 16.6 15.7 17.3 14.0 17.3Z\" /></g>")
    ("Up" . "<path d=\"M10 1 5.5 10H9V19H11V10H14.5Z\" />")
    ("Do" . "<path d=\"M10 19 5.5 10H9V1H11V10H14.5Z\" />")))

(defun my-tool-bar-image (text)
  "TEXTに対応する画像(Image Descriptor)を返す。"
  (when-let* ((svg (alist-get text my-tool-bar-svg-alist nil nil #'equal)))
    (list 'image
          :type 'svg :data
          (concat
           (format "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"%s\" height=\"%s\" fill=\"#000\" viewBox=\"0 0 %s %s\" >"
                   ;; 押しづらいのでサイズを調整
                   ;; 元のSVGは1文字が高さ20、幅が10弱くらいの枠内に収まるように書いてある
                   (* (length text) 16) 28
                   (* (length text) 10) 20)
           svg "</svg>")
          :scale 'default)))

(define-minor-mode my-tool-bar-mode
  "私だけの特別なツールバー。"
  :init-value nil
  :global t
  :group 'tool-bar
  (if my-tool-bar-mode
      (setq secondary-tool-bar-map
            (let ((km (make-sparse-keymap)))
              (dolist (key (reverse '("C-" "S-" "M-" ;; "A-" "s-" "H-"
                                      "Mx" "Cx" "CxC" "Cc" "CcC"
                                      "Cg" "Up" "Do")))
                (my-tool-bar-add-prefix-item km key))
              km))
    (setq secondary-tool-bar-map nil))
  ;; Update the mode line now.
  (force-mode-line-update t))

(defun my-tool-bar-decode-C- (_prompt)
  (modifier-bar-button '(control)))

(defun my-tool-bar-decode-S- (_prompt)
  (modifier-bar-button '(shift)))

(defun my-tool-bar-decode-M- (_prompt)
  (modifier-bar-button '(meta)))

(defun my-tool-bar-decode-A- (_prompt)
  (modifier-bar-button '(alt)))

(defun my-tool-bar-decode-s- (_prompt)
  (modifier-bar-button '(super)))

(defun my-tool-bar-decode-H- (_prompt)
  (modifier-bar-button '(hyper)))

(defun my-tool-bar-decode-Mx (_prompt)
  (kbd "M-x"))

(defun my-tool-bar-decode-Cx (_prompt)
  (kbd "C-x"))

(defun my-tool-bar-decode-CxC (_prompt)
  (vconcat
   (kbd "C-x")
   (modifier-bar-button '(control))))

(defun my-tool-bar-decode-Cc (_prompt)
  (kbd "C-c"))

(defun my-tool-bar-decode-CcC (_prompt)
  (vconcat
   (kbd "C-c")
   (modifier-bar-button '(control))))

(defun my-tool-bar-decode-Cg (_prompt)
  (kbd "C-g"))

(defun my-tool-bar-decode-Up (_prompt)
  (kbd "<up>"))

(defun my-tool-bar-decode-Do (_prompt)
  (kbd "<down>"))

(defun my-tool-bar-add-prefix-item (km key-str)
  (let* ((modifier (alist-get key-str
                              '(("C-" . control) ("S-" . shift)
                                ("M-" . meta) ("A-" . alt)
                                ("s-" . super) ("H-" . hyper))
                              nil nil #'equal))
         (key-sym (or modifier
                      (intern key-str)))
         (decode-fun (intern (format "my-tool-bar-decode-%s" key-str))))
    (define-key km (vector key-sym)
                (list
                 'menu-item key-str #'ignore
                 :image (my-tool-bar-image key-str)
                 :help (if modifier
                           (format "Add %s to the following event" key-str)
                         key-str)
                 :enable (if modifier
                             `(modifier-bar-available-p (quote ,modifier))
                           t)))
    (define-key input-decode-map (vector 'tool-bar key-sym) decode-fun)))

実装の一部は modifier-bar-mode のコードを呼び出しています。

入力イベントに修飾キーを付加するのにinput-decode-mapというのを利用しているみたいです。そこから呼び出される関数modifier-bar-buttonにread-eventのループがあって、そこで入力された後続のイベントに修飾キーを付加して返すと、それが代わりの入力結果になるという流れのようです。

ツールバーへのボタンの表示はsecondary-tool-bar-mapにメニュー項目を追加することで行います。

ただし、項目文字列が表示されることは無く、代わりに画像を指定してやる必要があります。modifier-bar-modeのCtrlやらShiftやらの文字がどうも汚いなと思っていましたが、これはテキストでは無く画像で用意されています。image-load-path (etc/images/) の下に ctrl.pbm や shift.pbm といったファイル名で存在しています。

わざわざ画像を用意するのは億劫だなと思いましたが、試してみたところSVG画像でも表示できるようでした(WindowsとAndroidの両方で確認)。

しかしここで残念なお知らせが。

現在のAndroid版EmacsはSVGでtext要素が表示できません。

最初はフォントが見つけられないだけかなとも思ったのですが、 java/INSTALL の中に次のような記述を見つけました。

LIBRSVG

Librsvg 2.40.21, the final release in the librsvg 2.40.x series, the
last to be implemented in C, is provided as:

  librsvg-2.40.21-emacs.tar.gz

and has been lightly edited for compatibility with environments where
Pango cannot provide fonts, with the obvious caveat that text cannot be
displayed with the resulting librsvg binary.  Among numerous
dependencies are PCRE, and:

  libiconv-1.17-emacs.tar.gz
  libffi-3.4.5-emacs.tar.gz
  pango-1.38.1-emacs.tar.gz
  glib-2.33.14-emacs.tar.gz
  libcroco-0.6.13-emacs.tar.gz
  pixman-0.38.4-emacs.tar.gz
  libxml2-2.12.4-emacs.tar.gz
  gdk-pixbuf-2.22.1-emacs.tar.gz
  giflib-5.2.1-emacs.tar.gz
  libjpeg-turbo-3.0.2-emacs.tar.gz
  libpng-1.6.41-emacs.tar.gz
  tiff-4.5.1-emacs.tar.gz
  cairo-1.16.0-emacs.tar.gz

which must be individually unpacked and their contents provided on the
command line, as with other dependencies.  They will introduce
approximately 8 MiB's worth of shared libraries into the finished
application package.  It is unlikely that later releases of librsvg will
ever be ported, as they have migrated to a different implementation
language.

No effort has been expended on providing the latest and greatest of
these dependencies either; rather, the versions chosen are often the
earliest versions required by their dependents, these being the smaller
of all available versions, and generally more straightforward to port.

よく分かりませんが、テキストが表示できないのは仕様ということなのでしょうか? librsvg-2.40自体もかなり古いものなのでサポートしていないSVGの機能やバグが沢山あるはずです。 残念ですがとりあえずSVGでtext要素を使うのは諦めるよりありません。

というわけで上のソースコードにはLaTeXで生成した文字のパスデータが長々と並んでいるわけです。

2025-03-07

Emacsからssh-agentを起動する

ssh-agentというのは基本的にはEmacsが起動する前に実行しておくものだと思います。しかしAndroid版の場合、(Termuxの(非GUIの)Emacsパッケージだったりrootを取って色々したりしない限り)普通はそんなことは不可能です。ダイレクトにEmacsが起動してしまうので。

Android版のEmacsはTermuxと連携させればsshが使用できます。Magitも動作します(なぜかWindowsバリに遅いですが)。でもssh-agentが起動していなければ毎回パスフレーズを入力しなければなりません(設定しているのなら。Trampに限ってはauthinfoに記録させる方法もありますが)。

となると、Emacsからssh-agentを起動するという方法が浮かんできます。

やることは簡単。ssh-agentを起動して出力される環境変数(SSH_AUTH_SOCKSSH_AGENT_PID)をEmacs自体に反映してやれば良いのです。後はssh-addを起動してパスフレーズを入力してもらいます。

ということでcall-process関数を使用して ssh-agent を起動しようとしたのですが、なぜが処理が返ってきません。 (call-process "ssh-agent" nil t) のように書いて評価すると何秒経っても終了しません。C-gで中断することはできます。Windows上で試すとちゃんとすぐに処理が帰ってきます。call-processが必ず帰ってこないわけではありません。 ls のようなものは大丈夫ですし、 ssh-agent -k のようなものもすぐに帰ってきます。

shell-commandもダメでした(&を付けて非同期にするのは試していません)。内部でcall-processを呼んでいるからでしょう。

EShellのeshell-commandはすぐに帰ってきました。とは言えその後 ssh-add するときにキーフレーズを入力する方法が分からず諦めました。

というわけで、非同期的にプロセスを実行するstart-process関数を使って書くことにしました。

(defun my-ssh-setup ()
  "ssh-agentとssh-addを起動します。"
  (interactive)
  (my-ssh-agent-start)
  (my-ssh-add))

(defun my-ssh-call-process (program &optional args body on-exit)
  "PROGRAMを実行して終了するまで待ちます。

ARGSはPROGRAMの引数です。

終了するまでの間にBODYに指定した関数を不定期的に呼び出します。
関数に引き渡される引数はプロセスオブジェクト一つです。

終了した後にON-EXITに指定した関数を呼び出します。
関数に引き渡される引数はPROGRAMの終了ステータスコードです。

BODYやON-EXITを呼び出す際のカレントバッファはプロセスの出力を保持するバッ
ファです。それぞれのタイミングでPROGRAMが出力したテキストを調べることが
出来ます。"
  (let ((buffer (get-buffer-create "*ssh-agent*")))
    (with-current-buffer buffer
      (erase-buffer)
      (let ((process (apply #'start-process
                            program buffer
                            program args)))
        (set-process-sentinel
         process
         (lambda (process _event)
           (when (memq (process-status process)
                       '(exit signal))
             (process-put process :my-ssh-finished t))))
        (while (not (process-get process :my-ssh-finished))
          (when (input-pending-p)
            (discard-input)
            ;; Androidではdiscard-inputしてもread-eventで少し待たない
            ;; とsit-forが即時リターンしてsentinelが呼び出されないこと
            ;; がある。
            (read-event nil nil 0.2))
          (sit-for 0.1) ;; ここでsentinelが呼び出されるかも
          (when body
            (funcall body process)))
        (when on-exit
          (funcall on-exit (process-exit-status process)))
        (process-exit-status process)))))

(defun my-ssh-agent-start ()
  "ssh-agentを開始します。"
  (interactive)
  (if (getenv "SSH_AGENT_PID")
      (message "SSH_AGENT_PID is already set")
    (my-ssh-call-process
     "ssh-agent" nil nil
     (lambda (status)
       (unless (zerop status)
         (error "ssh-agent failed"))
       (let ((vars
              (mapcar
               (lambda (var)
                 (goto-char (point-min))
                 ;; Error if not found
                 (re-search-forward (concat var "=\\([-_a-zA-Z0-9./]+\\)"))
                 (list var (match-string 1)))
               '("SSH_AUTH_SOCK" "SSH_AGENT_PID"))))
         (dolist (vv vars) (apply #'setenv vv))
         (message "ssh-agent vars=%s" vars))))))

(defun my-ssh-agent-stop ()
  "ssh-agentを停止します。"
  (interactive)
  (call-process "ssh-agent" nil nil nil "-k")
  (setenv "SSH_AGENT_PID"))

(defun my-ssh-add ()
  "ssh-addを起動してパスフレーズを入力・記録します。"
  (interactive)
  (let ((read-pos nil))
    (my-ssh-call-process
     "ssh-add" nil
     (lambda (process)
       (save-excursion
         (goto-char (or read-pos (point-min)))
         ;; Enter passphrase ... が現れたらパスフレーズを入力させてそ
         ;; れをプロセスへ送信する。
         (when (re-search-forward "^Enter .*: *" nil t)
           (let ((prompt (match-string 0)))
             (process-send-string
              process
              (concat (read-passwd prompt) "\n")))
           (setq read-pos (point))))))))


;; 以下おまけ

(defun my-ssh-ensure ()
  "環境変数SSH_AGENT_PIDが設定されていなければ`my-ssh-setup'を呼び出します。
設定されているなら何もしません。"
  (interactive)
  (unless (getenv "SSH_AGENT_PID")
    (my-ssh-setup)))

(defun my-ssh-init ()
  "適当なタイミングでパスフレーズを入力するよう準備します。
これをinit.elから呼び出しておけば、MagitやVC、Trampのパスフレーズが必要
そうなタイミングで自動的に`my-ssh-ensure'が呼び出されます。
あまり細かい条件は見ていないので必要に応じて修正してください。"
  ;; Magitのpushまたはpull
  (defun my-ssh-agent-init-on-magit-start-git (_input &rest args)
    (with-demoted-errors "Error my-ssh-agent: %s"
      (when (member (flatten-tree args) '("push" "pull"))
        (my-ssh-ensure))))
  (advice-add #'magit-start-git :before
              #'my-ssh-agent-init-on-magit-start-git)

  ;; VCのgitのpushまたはpull
  (defun my-ssh-agent-init-on-vc-git--pushpull (&rest _)
    (with-demoted-errors "Error my-ssh-agent: %s"
      (my-ssh-ensure)))
  (advice-add 'vc-git--pushpull :before
              #'my-ssh-agent-init-on-vc-git--pushpull)

  ;; Trampのssh等
  (defun my-ssh-agent-init-on-tramp-ssh-controlmaster-options (vec)
    (with-demoted-errors "Error my-ssh-agent: %s"
      (when (seq-find (lambda (hop)
                        (member (tramp-file-name-method hop)
                                '("ssh" "sshx" "scp" "scpx")))
                      (tramp-compute-multi-hops vec))
        (my-ssh-ensure))))
  (advice-add #'tramp-ssh-controlmaster-options :before
              #'my-ssh-agent-init-on-tramp-ssh-controlmaster-options))

少し試した限りうまく行っているようです。

他の方法について。

非同期プロセスの処理を書くのは面倒なのでeshellをうまく利用して何とかならないかなと思ったのですが、私にはあまりうまく出来ませんでした。ssh-agentが出力するスクリプトをそのまま評価できれば良かったのですが。環境変数(process-environment変数)はデフォルトだとバッファローカルになりますがeshell-modify-global-environment変数を変えれば行けそう。最終的にはssh-addの入力部分をどうしたらよいのか分かりませんでした。無理矢理実現するなら大人しく上のように書いてしまった方が良いのかなと。

Magitだけを通すならおそらく magit-process-password-prompt-regexpsmagit-process-find-password-functions をいじれば(そもそもssh-agentを起動しなくても)何とかなりそうな気もします。どこかにパスフレーズを保存しておく方法に限らず、Emacsがssh-agentのようにパスフレーズを一時的に覚えておくことも出来るかもしれません。とは言えsshを使うのがMagitに限らないのであれば、大人しくssh-agentを起動してしまった方が良いでしょう。

AndroidではOSが無駄なプロセスを自動的に削除してしまうことがあるらしいので、そこは注意が必要かもしれません。

2025-03-05

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

Emacsではモードラインをマウスでドラッグすると、分割されているウィンドウのサイズ(境界線)が調整できます(実際にはモードラインに限らずヘッダーライン等でもできるみたいです)。

しかし出来るのはそこまで。フレームの端までドラッグしてもウィンドウが閉じたりはしません。「これ以上リサイズできるウィンドウは無いよ!」といった悲鳴(エラー)が発せられるだけです。

まぁそんなものか……とC-x 1やC-x 0を押せば済む話。……本当に? その時そこにキーボードがあるとは限らないのです。そう、Androidなら。

もちろんHacker's Keyboardを使ってC-x 1と押すことは出来ます。メニューバーのFileを押しメニューを下にスクロールしてRemove Other Windowsを選択しても良いです。ツールバーか何かにウィンドウを閉じるボタンを配置しても良いかもしれません。ちなみにマウスではモードラインを右クリックすればウィンドウが閉じますが、デフォルトのタッチ操作では右クリックは再現できないようです。

でもやっぱり境界のドラッグやスワイプで閉じられた方が自然ではありませんか?

調査と実装

端までドラッグすると次のようなエラーメッセージがログに記録されます。

adjust-window-trailing-edge: No resizable window below this one

adjust-window-trailing-edge という関数が実際にウィンドウのサイズを調整しています。

(defun adjust-window-trailing-edge (window delta &optional horizontal pixelwise)

この関数はウィンドウの右辺あるいは下辺の位置を調整します。

引数windowにドラッグ中のモードラインを持つウィンドウが渡されるようです。deltaはサイズの変化量(正の時は右または下へ移動、負の時は左または上へ移動)。horizontalは非nilの時右辺を移動し、nilの時下辺を移動します。

コードの中身を見ると案外複雑なのですが、これは実際に沢山のウィンドウを並べてからモードラインをドラッグしてみれば納得できるでしょう。

例えば次図で黒い矢印の場所をドラッグしたら、実際には赤い矢印の場所を移動しなければなりません。その下にあるウィンドウがこれ以上小さく出来ない場合はさらに外側(青色)を移動する必要もあります。

このウィンドウのモードラインをドラッグするこのウィンドウの下辺を変更する必要がある

ちなみに、ウィンドウの構造は基本的に次のような状態にはならないようです(同じ向きで入れ子になる状態。垂直方向も同様)。

横並び親も横並び

これは次のような状態になります。おそらく分割や削除を行ったときに不要な包含ウィンドウを削除して中身を展開しているのだと思います。

一つの横並び

モードラインを持つwindowから実際にリサイズするウィンドウを求める処理はadjust-window-trailing-edgeの比較的最初の方にあります。

    ;; Find the edge we want to move.
    (while (and (or (not (window-combined-p right horizontal))
                    (not (window-right right)))
                (setq right (window-parent right))))

注:

  • window-combined-pは、window(ここではright)がhorizontalで指定した方向に並んだウィンドウのうちの一つかどうかを判定します。方向が合わないならnilを返します。
  • window-rightは次の兄弟を返します。rightとありますが下かもしれません。方向は先にwindow-combined-pによって確認しています。window-next-siblingとは違いwindowにはnilを指定出来ず、エコーエリアを返すこともありません。

この処理は後で使うことになるので関数化しておきます。

(defun my-window-right-edge (window horizontal)
  (while (and (or (not (window-combined-p window horizontal))
                  (not (window-right window)))
              (setq window (window-parent window))))
  window)

adjust-window-trailing-edgeはその後実際にリサイズできる余地があるのかを調べるのですが、そこで余地がなければ「これ以上リサイズできない!」と件のエラー(悲鳴)を発するわけです。

なので、この関数の外側でエラーを監視し、そのエラーを検出したらリサイズではなく削除を行ってみてはどうでしょうか。

(defun my-adjust-window-trailing-edge:around (old-fun
                                              window delta
                                              &optional horizontal pixelwise)
  (condition-case err
      ;; 元の関数を呼び出す
      (funcall old-fun window delta horizontal pixelwise)
    ;; user-errorをトラップ
    (user-error
     (pcase (error-message-string err) ;; エラーメッセージで分岐する
       ;; 左または上にリサイズ可能なウィンドウが無い場合
       ((or "No resizable window on the left of this one"
            "No resizable window above this one")
        (delete-window
         ;; 削除するのは境界を持つウィンドウ(WINDOWを含む!)
         (my-window-right-edge window horizontal)))
       ;; 右または下にリサイズ可能なウィンドウが無い場合
       ((or "No resizable window on the right of this one"
            "No resizable window below this one")
        (delete-window
         ;; 削除するのは下または右隣のウィンドウ(WINDOWは維持される)
         (window-right
          (my-window-right-edge window horizontal))))
       ;; その他のエラーは再送
       (_ (signal (car err) (cdr err)))))))

(advice-add #'adjust-window-trailing-edge :around #'my-adjust-window-trailing-edge:around)
;; ↓で解除
;; (advice-remove #'adjust-window-trailing-edge #'my-adjust-window-trailing-edge:around)

これだけでもちゃんとドラッグでウィンドウが消えてくれます。

しかし上や左にドラッグしてウィンドウを消すと、次のようなエラーメッセージが表示されます。

Wrong type argument: window-valid-p, #<window 349>

ウィンドウを削除してもドラッグ状態はまだ続いているので、動かすたびに削除したwindowにアクセスしてこのエラーが発生してしまいます。右や下にドラッグした場合はドラッグ中のwindowは消えないのでエラーは発生しません。

また、下や右へドラッグしたときは継続して複数のウィンドウを消せますが、左や上へドラッグしたときはそれ以上消せません。

この問題を真面目に修正することも可能ですが、元のソースコードを修正せずにadvice等を使って外から直すのは面倒です。煩雑なコードをinit.elに入れるほどの価値は無いと思ったので、適当にエラーを黙らせることにしました(実際には他のelファイルに入っていてinit.elにあるのはautoloadと最初のadvice-addだけですが)。

コード

というわけで最終的なコードは次のようになりました。

;; ドラッグ開始時の処理が呼ばれるようにする。
;; (autoload 'my-mouse-drag-line--begin "my-mouse-drag-line")
(advice-add #'mouse-drag-line :after #'my-mouse-drag-line--begin)

(defun my-mouse-drag-line--begin (&rest _)
  "ドラッグ開始時の処理。"
  ;; ドラッグ中、adjust-window-trailing-edgeの動作を変えて、リサイズで
  ;; きないときは削除する。
  (advice-add #'adjust-window-trailing-edge :around
              #'my-mouse-drag-line--adjust-window-trailing-edge)
  ;; 一時キーマップが終了するタイミングを検出する方法がここくらいしか
  ;; 見当たらなかったので。これがダメならタイマーやpost-command-hookを
  ;; 使うくらいしか?
  (advice-add #'internal-pop-keymap :after
              #'my-mouse-drag-line--end))

(defun my-mouse-drag-line--end (&rest _)
  "ドラッグ終了時の処理。"
  ;; 元に戻す。
  (advice-remove #'adjust-window-trailing-edge
                 #'my-mouse-drag-line--adjust-window-trailing-edge)
  (advice-remove #'internal-pop-keymap
                 #'my-mouse-drag-line--end))

(defun my-window-right-edge (window horizontal)
  "WINDOWと右辺または下辺を共有する一番上のウィンドウを返す。"
  (while (and (or (not (window-combined-p window horizontal))
                  (not (window-right window)))
              (setq window (window-parent window))))
  window)

(defun my-mouse-drag-line--adjust-window-trailing-edge
    (old-fun window delta &optional horizontal pixelwise)
  "ドラッグ中にadjust-window-trailing-edgeが呼ばれたときの処理。
リサイズできなかった場合はウィンドウを削除する。"
  (condition-case err
      (funcall old-fun window delta horizontal pixelwise)
    (user-error
     (pcase (error-message-string err)
       ;; 左または上にリサイズ可能なウィンドウが無い場合。
       ;; ドラッグしている(モード行がある)ウィンドウを削除する。
       ((or "No resizable window on the left of this one"
            "No resizable window above this one")
        (delete-window (my-window-right-edge window horizontal))
        ;; 無理矢理drag-mouse-1イベントを起こして終了させる。
        ;; ここは (funcall (lookup-key overriding-terminal-local-map
        ;; [drag-mouse-1])) とかでも良いのかもしれない。
        ;; mouse-drag-lineを参照。exitfunを呼ぶ方法が限られる。
        (push 'drag-mouse-1 unread-command-events)
        ;; よく分かってないけどnilにしておけばエラーを回避できる。
        ;; (Emacs 30.1以降でタッチスクリーンを使用した場合)
        (setq touch-screen-current-tool nil))

       ;; 右または下にリサイズ可能なウィンドウが無い場合。
       ;; ドラッグしている(モード行がある)ウィンドウの次を削除する。
       ((or "No resizable window on the right of this one"
            "No resizable window below this one")
        (delete-window (window-right
                        (my-window-right-edge window horizontal))))

       ;; その他のエラーは再送する。
       (_ (signal (car err) (cdr err)))))))

;; ドラッグが終了した後にエラーが発生するようなので握りつぶす。
;; あまり常時エラーを握りつぶしたくないけどmouse関連だからまあいいか。
(defun my-mouse-select-window:around (old-fun click)
  (when (window-live-p (posn-window (event-start click)))
    (funcall old-fun click)))
(advice-add #'mouse-select-window :around #'my-mouse-select-window:around)

ちょっと適当な所もありますが、とりあえずマウスによるドラッグでもタッチ操作でもウィンドウを閉じられるようになりました。

タッチ操作の場合は touch-screen.el の作用によって、タッチイベントがマウスイベントに変換されて動作します。ドラッグも再現してくれるおかげでマウス用に書かれたコードがそのままで動作してくれます。ただ、この変換部分にもタッチしたwindowを記録して保持する部分が存在しているため、ドラッグ中に削除するとエラーが発生します。それを抑制しているのが (setq touch-screen-current-tool nil) の部分です。この辺りはあまりちゃんとコードを読んでいないので、操作によっては正しく動かない場合もあるかもしれません。複雑なタッチ操作を行おうとした場合とか? モードラインのドラッグ程度ならおそらく大丈夫だと思いますが。

2025-02-28

Android版のEmacsを試す

Android版のEmacsがリリースされたので初めて使ってみました。

参考資料

apkのダウンロード

端末や使い方によってダウンロードするapkが異なる(今回私が使った端末はPixel7/Android 15なので -29-arm64-v8a を選ぶ)。

Emacsを単体で使う場合:

Emacsと一緒に配布されているtermux-appと連携させる場合:

GitHub版のtermux-appと連携させる場合(再署名が必要):

上記リンクはこの記事を書いた時点でのものなので、最新の状況はSourceForgeのfilesページ(Emacs)GitHubのリリースページ(termux-app)を確認すると良い。

再署名

EmacsとTermuxを連携させたいなら(お互いのファイルにアクセスできるようにしたいなら)両者のapkを同じ鍵で署名する必要がある。

Emacsと一緒に配布されているtermux-app(termux-app_apt-android-7-release_universal.apk) を使う場合はすでに同じ鍵で署名されているので再署名は不要。

それ以外から入手したtermux-app(GitHub版の最新版や自分でビルドしたもの)を使う場合は再署名するのが手っ取り早い(正直Android版Emacsのビルドはやりたくないので)。

手順:

  • 注1: 既に私のPCに入っていたAndroid Studio(とSDK)を使用。
  • 注2: 署名に使う鍵は既に作成済み。
  • 注3: 以下はMSYS2のBash上でのコマンドライン。環境毎に適宜読み替えること。
  1. 署名の削除

    zip -d emacs-30.1-29-arm64-v8a.apk "META-INF/*"
    

    (unzipしてからzipしてもよい?)

    (termux-app_v0.118.1+github-debug_arm64-v8a.apkの方は削除しなくてもよい?)

  2. zipalign

    c:/Users/****/AppData/Local/Android/Sdk/build-tools/35.0.1/zipalign.exe -v -p 4 emacs-30.1-29-arm64-v8a.apk emacs-30.1-29-arm64-v8a-aligned.apk
    c:/Users/****/AppData/Local/Android/Sdk/build-tools/35.0.1/zipalign.exe -v -p 4 termux-app_v0.118.1+github-debug_arm64-v8a.apk termux-app_v0.118.1+github-debug_arm64-v8a-aligned.apk
    
  3. 署名

    予め鍵を生成して <keystore-path><key-alias> の部分に情報を埋めること。

    export JAVA_HOME="c:/Program Files/Android/Android Studio/jbr"
    c:/Users/****/AppData/Local/Android/Sdk/build-tools/35.0.1/apksigner.bat sign --ks <keystore-path> --ks-key-alias <key-alias> --out emacs-30.1-29-arm64-v8a-signed.apk emacs-30.1-29-arm64-v8a-aligned.apk
    c:/Users/****/AppData/Local/Android/Sdk/build-tools/35.0.1/apksigner.bat sign --ks <keystore-path> --ks-key-alias <key-alias> --out termux-app_v0.118.1+github-debug_arm64-v8a-signed.apk termux-app_v0.118.1+github-debug_arm64-v8a-aligned.apk
    

インストール

既に署名が異なるTermux関連アプリがインストールされている場合は先に全てアンインストールする。もちろん必要に応じてバックアップを取ること。

Android上でダウンロードしたapkをブラウザから直接インストールするか、再署名したのであればPCからadbでインストールするかAndroid上で動くファイル管理ソフト(MiXplorer等)を使ってコピーしてインストールする。先に身元不明のアプリをインストールできるようにする設定が必要かも。

基本的にストアを介さない野良アプリ扱いなので色々警告が出るが自己責任で。気になるなら専用端末に入れた方が良いかもしれない。どちらかと言えばTermuxの方が不安かも。Termuxが使いたくなければネットワーク越しのやりとりは全部url-retrieveで書けば良いと思う。

仮想キーボードのインストール

既に入っていたHacker's Keyboardを使用したが、こういうCtrl+何かのキーが押せるキーボードが無いとつらい。

もしくは物理キーボードを繋げるか。それはそれで配列のカスタマイズがつらい。(追記: 実際に繋げてみたが日本語IMEの起動方法が分からない)

Termuxの設定

pkg update
pkg upgrade

SSH越しのGitリポジトリにアクセスするなら:

pkg install openssh
pkg install git
ssh-keygen -t ed25519 -C "your_name@example.com"
  • ~/.ssh/id_ed25519.pub をサーバにセット
  • .bashrcに . source-ssh-agent と書いておくと良いかも
  • GitリポジトリからEmacsの設定を取り出したり色々できる

Termuxから /sdcard にアクセスするなら:

termux-setup-storage

Emacsの設定

/sdcardへのアクセス

Emacsから /sdcard にアクセスできないようなら「設定→アプリ→特別なアプリアクセス→Emacs」が必要かも? 

もしくは M-x android-request-storage-access ? (何回か入れ直したのでよく分からなくなってる)

Android判定

Emacs LispからAndroid版のEmacsで動いているかを判定するには次の式を使う。

(eq system-type 'android)

この式を使って、普段使っているearly-init.elやinit.elにAndroid専用の設定を追加していく。

環境変数の設定

Termux側のbinへPATHを通す。

(when (eq system-type 'android) ;; Android版
  (let ((termux-bin "/data/data/com.termux/files/usr/bin"))
    (when (file-directory-p termux-bin) ;; 一応アクセスできるか確認する
      (setenv "PATH" (concat termux-bin ":" (getenv "PATH")))
      (push termux-bin exec-path))))

これによって M-x shell から様々なコマンドが使えるようになった。gitも実行できるのでMagitも使えた(やけに遅かったけど)。

ちなみにHOMEの位置は:

  • Termux側: /data/data/com.termux/files/home
  • Emacs側: /data/data/org.gnu.emacs/files

同じ鍵で署名してあれば双方どちらにもアクセスできるはず。

ツールバーの表示

押せるものは何でも欲しい。

default-frame-alisttool-bar-mode でツールバーを消していたので消さないようにした。

メニューバーは元々消さずに使っているが、Androidでは絶対あった方が良いと思う。「Buffers」を押すだけでバッファリストが出る。

default-frame-alist をいじるついでに left-fringe と right-fringe を大きくしても良いかも。なぜか滅茶苦茶細いので。

modifier-bar-modeなんてのもあるらしい。secondary-tool-bar? え、global-window-tool-bar-mode? 何それ! M-xやC-x、C-x C-、C-cなどがワンボタンで押せると良いんだけど。その辺りは追々。

フォント

~/fonts/ (/data/data/org.gnu.emacs/files/fonts/) に.ttfファイルを置くとそれを使えるようになる。認識していれば (font-family-list) の評価結果に現れる。そのフォント名で (set-frame-font "<family-name>-<size>") とするか、early-init.elあたりで default-frame-alist(font . "<family-name>-<size>") として指定してやるか。

~/fontsディレクトリを表示しているところ(dired-details-rとnerd-icons-dired)
図1: ~/fontsディレクトリを表示しているところ(dired-details-rとnerd-icons-dired)

nerd-icons も M-x nerd-icons-install-fonts でこのディレクトリにインストールすれば普通に使える。

ただしSVG内からは参照できない気がする。というかSVGテキストが表示されないような気が。

仮想キーボードの表示

デフォルトだと読み取り専用の場所で仮想キーボードが消えてしまう。Dired等でHacker's Keyboardによる操作ができなくなってしまう。

(when (eq system-type 'android)
  (setq touch-screen-display-keyboard t))

これでも戻るボタンを押せば消せるのであまり邪魔にはならないと思う。とは言えスクロールして読むだけのバッファなら非表示の方が良いと思うので、モードによって切り替えた方が良いのかもしれない。

通常の日本語IMEとの切り替えは画面右下に切り替えボタンが出るのでそれで十分かなと(端末に依って異なるかも)。

おしまい

子フレームで補完候補を表示しているところ(ちなみにこの補完候補はタッチで選択出来ない)
図2: 子フレームで補完候補を表示しているところ(ちなみにこの補完候補はタッチで選択出来ない)

というわけで普段PCで使っているのとほとんど同じ物がAndroid上に現れてしまいました。GUI版のEmacsがそのまま動いているのはすごいですね。設定もPCで使っている物からほとんど変えていません。

後はどうやって使いやすくするか、あるいは慣れるか。

私としては今は軽いノートPCを持っているのでわざわざキーボードを繋げて使うことはあまりないと思います。あくまでタッチ操作で色々出来るようにしたいところ。

とりあえずタッチイベントを調べてみようかな。

作図ツール(少し問題あり)
図3: 作図ツール(少し問題あり)
2025-02-24

Emacs 30.1の設定(MS-Windows)

リリースされたので入れ替え。と言っても最近は30.0.9xをずっと使っていたのでトラブルなど無く、例によってネイティブコンパイルまわりの対処くらい。

1. ダウンロード

https://ftp.gnu.org/gnu/emacs/

  • emacs-30.1.zip
  • emacs-30.1.tar.xz (展開してfind-function-C-source-directory変数に指定し、describe-functionからソースコードを追えるようにするため)

2. zipを展開して適当な場所に置く

3. 起動してみる

パッと見問題なし。

4. 補う必要のあるファイルを確認する

  • 相変わらず libgccjit 関連のファイルは含まれていないのでネイティブコンパイルはそのままでは出来ない((native-comp-available-p) はnilを返す)
  • gdk_pixbuf関連のファイルはlibrsvgが自前のデコーダを持つようになったので不要

5. MSYS2で必要なファイルを取り寄せる

pacman -S mingw-w64-x86_64-libgccjit

MSYS2自体もアップデートして最新にした。

(ちなみにMSYS2のEmacsパッケージを使っていないのは、以前MSYS2版だけCPU100%不具合があったから。その時ノートPCでだけMSYS2版を使っていたのだけど、その不具合でファンがきゅいーん!と鳴ってうるさかった。多分もう直っているので、そもそも楽に使いたければMSYS2版を使った方が良いと思う。あ、それとテストのために古いバージョンのEmacsをいつでも使えるようにしておきたいという理由もある)

6. ネイティブコンパイルできるようにする

次のファイルをコピー。

  • emacs-30.1/binへ
    • msys64/mingw64/binから
      • libgccjit-0.dll
  • emacs-30.1/lib/gccへ
    • msys64/mingw64/binから
      • as.exe
      • ld.exe
    • msys64/mingw64/libから
      • crtbegin.o
      • crtend.o
      • dllcrt2.o
      • libadvapi32.a
      • libgcc_s.a
      • libkernel32.a
      • libmingw32.a
      • libmingwex.a
      • libmoldname.a
      • libmsvcrt.a
      • libpthread.a
      • libshell32.a
      • libuser32.a
    • msys64/mingw64/lib/gcc/x86_64-w64-mingw32/14.2.0/から
      • libgcc.a

libgccjit-0.dll以外のdllはemacs-30.1.zipの中に既に含まれていた。

.aや.oは全部必要なのか、また、(何かの条件で)不足するものが無いのかは確認していない。

~/.emacs.d/early-init.el には次の設定を入れている。

(when (and (eq system-type 'windows-nt) ;; Windowsの場合
           (fboundp #'native-comp-available-p) ;; emacs-28以降
           (native-comp-available-p)) ;; libgccjitが使える
  ;; -B で emacs-??/lib/gcc/ ディレクトリを指定する。
  (let ((gcc-dir (expand-file-name
                  (file-name-concat invocation-directory "../lib/gcc"))))
    (when (file-directory-p gcc-dir)
      (setq native-comp-driver-options (list "-B" gcc-dir)))))

この設定が無くても PATH を最小限にして runemacs -Q で起動して試したらちゃんとネイティブコンパイルが成功していたので不要かと思ったが、普段使っている環境だと大量にエラーが出たのでこの設定を入れた。ucrt64環境のldやasが参照された?

パッケージディレクトリの切り替え

バージョンが変わるとバイトコンパイルされたEmacs Lispで色々問題が起きるので、最近は次のような設定を入れている。

;; Emacsのバージョン毎にパッケージ格納先を切り替える。
;; ~/.emacs.d/elpa/30 のようにする。
;; `package-enable-at-startup'が非nil(デフォルト)のとき、early-init.el
;; の後、init.elの前にパッケージの初期化が行われるので、early-init.el
;; で切り替えておく必要がある。
(when (boundp 'package-user-dir)
  (setq package-user-dir
        (locate-user-emacs-file (format "elpa/%d" emacs-major-version))))
2025-02-21

Emacsでメールファイル(.eml等)をそのまま読む

昔から疑問に思っていて未だによく分かっていないことなのですが、Emacsから1件のメールのデータが入ったファイルを開いてその内容を読むにはどうしたら良いのでしょうか。

Web検索でちょっと調べてみても、既存のメールを取り扱うEmacs用のシステムに取りこんで読めというようなことばかりが引っかかり、単純に1件のメールだけが入ったファイルを普通のファイルを開くように読む方法がなかなか出てこないんですよね。

ここで言うメールが入ったファイルというのは沢山のメールが一つに詰め込まれたmbox形式とかではなくて、一般的なメールクライアントがメッセージを外部ファイルとして保存するときに.emlという拡張子を付けて出力するような先頭にメールヘッダーが入っているようなファイルのことです(ええと何形式というんだっけ)。内容はMIMEでエンコードされていて普通はそのままでは読めません(……ひょっとしてASCII言語圏ではそのまま読めるのでしょうか? だからあまり問題になっていない?)。

私は普段Emacs上ではWanderlustを使っているのですが、それが依存しているsemiというライブラリにMIMEエンコードされたバッファをプレビューするコマンドが含まれています。

その名もmime-view-buffer。メールファイルを開いて M-x mime-view-buffer を実行すれば、メールの内容が 別バッファに 人間が読める状態で表示されます。素晴らしい。

ちなみに検索して見つかった別の方法としては次のものがあります。これはGnusの関数(gnus-article-prepare-display)を使います。

Any way to just render an email file on disk? : r/emacs

(defun my/render-mime-message ()
  "Render the current buffer as a Gnus article."
  (interactive)
  (gnus-article-prepare-display))

これで読めると言えば読めるのですが、どうにも釈然としません。普通にEmacsからファイルを開く要領で読めるようにならないのでしょうか。要するにfind-fileで画像ファイルやpdfファイルを開いたら中身のバイナリではなく人間が読めるようなものが出るのと同じようにしたいわけです。

と、今更そのようなことを思い出したのは、先日いくつかのメールへのリンクをorg-modeに書きたくなって久しぶりにol-wl.elを使ったのがきっかけでした。メールへのリンクをorg-mode文書に書きたくなることはほとんどなく、近年はorg-contribが別パッケージに分かれたこともあってインストールすらされていませんでした。ol-wl.elを使うと wl: リンクタイプが追加されるのですが、そのパスにはWanderlustが管理するフォルダ表記とメッセージIDを指定します。そのリンクをC-c C-oで開くとWanderlustが起動してその中でメールの内容が表示されます。それを見て、単純に file: リンクタイプでメールが入っているファイルへリンクしたらダメなのかな? と思ったわけです。

画像やPDFを開くノリでできないかと思ったのですが、あれはバッファ全体をdisplayテキストプロパティ(またはオーバーレイプロパティ)で画像に置き換えることで解決しています。今回やりたいのは画像では無くテキストで置き換えること。displayプロパティで別のテキストに置き換えるとそのテキストの中にポイントを置けないのであまり望ましくないでしょう。なので、どちらかと言えばhexl-modeがファイルの中身をHEXダンプに置き換える時の手法が近そうです。

ただ、これはちゃんと実装しないと置き換えた後のテキストで元のファイルを上書きしてしまうリスクがあります。

とりあえず今回必要だったのはorg-modeからリンクを張ることなので、必ずしも律儀にそのファイルに関連付けられたバッファで開く必要はありません。org-modeではリンクを開くときの動作を条件毎にカスタマイズできるようになっているので、そこでmime-view-bufferで開くような関数を指定してしまえば良いのです。

(defun my-mime-view-file (file _original-link-path)
  "mime-view-bufferを使ってメールFILEのプレビューを開く。"
  (with-temp-buffer
    ;; PreviewバッファのカレントディレクトリはFILEがある場所にする。
    (setq default-directory
          (file-name-directory (expand-file-name file)))
    (insert-file-contents file)
    (mime-view-buffer
     nil
     ;; バッファ名にファイル名を入れる。
     (format "*MIME View: %s*" (file-name-nondirectory file)))))

;; Wanderlust用のメールが格納されているディレクトリにあるファイルを開
;; くときは my-mime-view-file を使う(例)。
(add-to-list 'org-file-apps '("c:/my-wl-mail-dir/" . my-mime-view-file)))

後は [[file:c:/my-wl-mail-dir/inbox/123]] のようなリンクを書けば、それをC-c C-oで開こうとすると上の関数の働きによって人間が読めるものがすぐに開くというわけです。

とりあえずこれでお茶を濁しておきますが、そのうちhexl-mode的な手法で内容を表示するmajor-modeを作りたい所。

メールが入ったファイルを読むだけのことがなんでこんなに面倒なんだろう。

2025-02-20

nerd-icons-diredへ移行

新PCへの移行に伴いこれまで使っていたall-the-icons-dired(を少し修正したもの)からnerd-icons-diredへ移行したのですが、いくつか気になった点があったので修正。

と、ソースコードを見たら、あれ、これall-the-icons-diredとほとんど同じですね。all-the-icons-diredを元にnerd-iconsを使うように修正した物っぽい? なのでこれまでall-the-icons-dired向けに修正したのがほとんどのそのまま適用出来ます。

まずはhl-line-mode(現在の行をハイライトしてくれるマイナーモード)でアイコン部分がハイライトされない問題。アイコンの背景が黒いままになってしまいます。これは以前「before-stringに別のオーバーレイのfaceが適用されない」で書いた現象が原因で、そこにも書きましたが回避策はbefore-stringやafter-stringに直接テキスト(アイコン)を書くのではなく、before-stringやafter-stringにdisplayプロパティを指定した文字列を指定して、そのdisplayプロパティでテキスト(アイコン)を表示すると、その部分にはオーバーレイのfaceが適用されるようになります。つまり、次のようにします。

 (defun nerd-icons-dired--add-overlay (pos string)
   "Add overlay to display STRING at POS."
   (let ((ov (make-overlay (1- pos) pos)))
     (overlay-put ov 'nerd-icons-dired-overlay t)
-    (overlay-put ov 'after-string string)))
+    (overlay-put ov 'after-string (propertize string 'display string))))
 

次に気がついたのはファイルが消えたのにアイコンが消えない場合があるということです。手っ取り早い対処法の一つはオーバーレイのevaporateプロパティをtにすることです。こうするとファイルの行が削除されたときに一緒にオーバーレイも消えてくれます。上で修正した場所のすぐ下にevaporateプロパティの設定を追加。

 (defun nerd-icons-dired--add-overlay (pos string)
   "Add overlay to display STRING at POS."
   (let ((ov (make-overlay (1- pos) pos)))
     (overlay-put ov 'nerd-icons-dired-overlay t)
-    (overlay-put ov 'after-string (propertize string 'display string))))
+    (overlay-put ov 'after-string (propertize string 'display string))
+    (overlay-put ov 'evaporate t)))
 

そもそもDiredバッファの変化を検出するために沢山のadviceを追加していますが、おそらくdired-after-readin-hookでやった方が簡単だと思います。多分。注意点としては、このフックは変更箇所をnarrowingしてから呼び出されるということがあります。なのでこれを使うなら不用意にwidenして全体を処理してはいけません。次のように修正してみました。

   "Get nerd-icons-dired overlays at POS."
   (apply #'nerd-icons-dired--overlays-in `(,pos ,pos)))
 
-(defun nerd-icons-dired--remove-all-overlays ()
-  "Remove all `nerd-icons-dired' overlays."
+(defun nerd-icons-dired--remove-all-overlays-from-whole-buffer ()
+  "Remove all `nerd-icons-dired' overlays from the whole buffer."
   (save-restriction
     (widen)
-    (mapc #'delete-overlay
-          (nerd-icons-dired--overlays-in (point-min) (point-max)))))
+    (nerd-icons-dired--remove-all-overlays)))
+
+(defun nerd-icons-dired--remove-all-overlays ()
+  "Remove all `nerd-icons-dired' overlays within the narrowed region."
+  (mapc #'delete-overlay
+        (nerd-icons-dired--overlays-in (point-min) (point-max))))
 
 (defun nerd-icons-dired--refresh ()
-  "Display the icons of files in a Dired buffer."
+  "Display the icons of files within the narrowed region of the Dired buffer."
   (nerd-icons-dired--remove-all-overlays)
   (save-excursion
     (goto-char (point-min))
@@ -110,40 +114,22 @@
       (nerd-icons-dired--refresh))
     result)) ;; Return the result
 
+(defun nerd-icons-dired--after-readin-hook ()
+  (when nerd-icons-dired-mode
+    (nerd-icons-dired--refresh)))
+
 (defun nerd-icons-dired--setup ()
   "Setup `nerd-icons-dired'."
   (when (derived-mode-p 'dired-mode)
     (setq-local tab-width 1)
-    (advice-add 'dired-readin :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-revert :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-internal-do-deletions :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-insert-subdir :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-create-directory :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-do-redisplay :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-kill-subdir :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-do-kill-lines :around #'nerd-icons-dired--refresh-advice)
-    (with-eval-after-load 'dired-narrow
-      (advice-add 'dired-narrow--internal :around #'nerd-icons-dired--refresh-advice))
-    (with-eval-after-load 'dired-subtree
-      (advice-add 'dired-subtree-toggle :around #'nerd-icons-dired--refresh-advice))
-    (with-eval-after-load 'wdired
-      (advice-add 'wdired-abort-changes :around #'nerd-icons-dired--refresh-advice))
+    (add-hook 'dired-after-readin-hook #'nerd-icons-dired--after-readin-hook nil t)
     (nerd-icons-dired--refresh)))
 
 (defun nerd-icons-dired--teardown ()
   "Functions used as advice when redisplaying buffer."
-  (advice-remove 'dired-readin #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-revert #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-internal-do-deletions #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-narrow--internal #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-subtree-toggle #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-insert-subdir #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-do-kill-lines #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-create-directory #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-do-redisplay #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-kill-subdir #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'wdired-abort-changes #'nerd-icons-dired--refresh-advice)
-  (nerd-icons-dired--remove-all-overlays))
+  (kill-local-variable 'tab-width)
+  (remove-hook 'dired-after-readin-hook #'nerd-icons-dired--after-readin-hook t)
+  (nerd-icons-dired--remove-all-overlays-from-whole-buffer))
 
 ;;;###autoload
 (define-minor-mode nerd-icons-dired-mode

これで私の使い方では問題が無いのですが、使い方によってはおかしいこともあるかもしれません。

次にリモート(Tramp経由)のディレクトリで使ったときに遅かったので次のように修正(この辺りはall-the-icons-diredを使っているときに気がついて修正したのを移植したもので、nerd-iconsになってからはあまりテストしていません)。ファイル毎にリモートアクセスが必要な処理をしているのでそれを排除しました。

 (defun nerd-icons-dired--refresh ()
   "Display the icons of files within the narrowed region of the Dired buffer."
   (nerd-icons-dired--remove-all-overlays)
   (save-excursion
     (goto-char (point-min))
     (while (not (eobp))
       (when (dired-move-to-filename nil)
-        (let ((file (dired-get-filename 'relative 'noerror)))
+        (let ((file (dired-get-filename nil 'noerror))) ;; Full path
           (when file
-            (let ((icon (if (file-directory-p file)
-                            (nerd-icons-icon-for-dir file
-                                                     :face 'nerd-icons-dired-dir-face
-                                                     :v-adjust nerd-icons-dired-v-adjust)
+            (let ((icon (if ;; Avoid using `file-directory-p' as it will
+                            ;; cause remote access.
+                            (save-excursion (forward-line 0)
+                                            (looking-at-p dired-re-dir))
+                            (if (file-remote-p file)
+                                ;; Avoid file-*-p functions
+                                (nerd-icons-sucicon "nf-custom-folder_oct"
+                                                    :face 'nerd-icons-dired-dir-face
+                                                    :v-adjust nerd-icons-dired-v-adjust)
+                              (nerd-icons-icon-for-dir file
+                                                       :face 'nerd-icons-dired-dir-face
+                                                       :v-adjust nerd-icons-dired-v-adjust))
                           (nerd-icons-icon-for-file file :v-adjust nerd-icons-dired-v-adjust)))
                   (inhibit-read-only t))
-              (if (member file '("." ".."))
+              (if (string-match-p "\\(?:\\`\\|[/\\\\]\\)\\.\\.?\\'" file) ;; . or ..
                   (nerd-icons-dired--add-overlay (dired-move-to-filename) "  \t")
                 (nerd-icons-dired--add-overlay (dired-move-to-filename) (concat icon "\t")))))))
       (forward-line 1))))

最後にファイル数が多いときにアイコンを表示しないようにしてみました。

+(defcustom nerd-icons-dired-max-lines 1000
+  "The maximum number of lines in the buffer in which icons will be displayed.
+Performance can be improved by hiding icons when there are a large
+number of files."
+  :group 'nerd-icons
+  :type '(choice (integer)
+                 (const :tag "No limit" nil)))
+
 (defvar nerd-icons-dired-mode)
 
 (defun nerd-icons-dired--add-overlay (pos string)
@@ -88,6 +96,13 @@
         (nerd-icons-dired--overlays-in (point-min) (point-max))))
 
 (defun nerd-icons-dired--refresh ()
+  (if (and nerd-icons-dired-max-lines
+           (> (line-number-at-pos (point-max) t) nerd-icons-dired-max-lines))
+      ;; If there are many files, it will be very slow, so disable icons.
+      (nerd-icons-dired--remove-all-overlays-from-whole-buffer)
+    (nerd-icons-dired--refresh--internal)))
+
+(defun nerd-icons-dired--refresh--internal ()
   "Display the icons of files within the narrowed region of the Dired buffer."
   (nerd-icons-dired--remove-all-overlays)
   (save-excursion

これらを合わせてnerd-icons-dired/nerd-icons-dired.el at misohena · misohena/nerd-icons-diredに置いてあります。

ちなみにall-the-iconsからnerd-iconsへ移行した理由ですが、私はall-the-iconsが使用するフォントの幅をFont Forgeで無理矢理揃えたものを使用していて、Emacs以外の用途で使うときに困るよなぁ……と前々から心配していたからです。新PCに移行したときに、この書き替えたフォントをインストールするのに躊躇しました。フォント名もちゃんと変えてall-the-icons.elも変えれば良いのかもしれませんが、面倒なので今回はnerd-iconsを使ってみることにしたわけです。

2025-02-17 ,

Emacs用のカラーピッカーに対する最近の変更

最近はまた作図ツールのカラーピッカー部分を色々直していました。

misohena/el-easydraw: Embedded drawing tool for Emacs

先日久しぶりに使ったら変なところが見つかったので、この際溜まっていた改良点をいくつか潰そうと思ったわけです。

特に単独利用、つまり(作図エディタから使うのではなく)任意のバッファ内で色を表すテキストを置き換えたり挿入したりする使い方を中心に直しました。

カラーピッカーを使ってcss-mode内の色テキストを置き換えているところ
図1: カラーピッカーを使ってcss-mode内の色テキストを置き換えているところ

以下修正点:

一時キーマップの不具合を修正
まずはきっかけとなった不具合の修正。カスタマイズバッファ(Custom-mode)内で使ったらエラーが出たので何かなと思ったら、どうも色を決定した後でも一時キーマップが終了していないようでした。なので、C-c C-cやOKボタンで色を決定した後、その次のキー入力が一時キーマップに食われてしまいます。その時C-c C-cでカスタマイズの反映をしようとすると、既に閉じてしまったカラーピッカーのOKボタンが押されてカスタマイズバッファ内の色テキストを置き換えようとし、その場所がすでに編集可能な範囲を外れているとエラーが出るということでした。一時キーマップの使い方を色々見直しました。
子フレームが外に出て一部が見えなくなる問題を修正
表示する位置の計算を調整しました。
別ウィンドウをクリックしてカラーピッカーを出したときの問題を修正
これEmacsでマウスを使うコマンドを書くとよくやっちゃうんですよね。マウスだとカレントバッファや選択中ウィンドウ以外を操作対象に出来るので。
導入を簡単にするマイナーモードを作成
edraw-color-picker-modeedraw-color-picker-global-mode を追加しました。自分でフックか何かを書いてコマンドを好きなキーに割り当てる人には必要ないのですが、初期設定を簡単にするためのマイナーモードを作成しました。Emacs全体で使えるようにするにはグローバルマイナーモードである edraw-color-picker-global-mode を有効にするだけです。キー割り当てや有効にするメジャーモードは M-x customize-group edraw-color-picker-mode から設定できます。メジャーモード毎にキー割り当てを変えられるようにするため少々苦労しました(バッファーローカルマイナーモードキーマップ)。context-menu-modeが有効な場合はコンテキストメニューにも項目が追加されます。
CSS Color Module Level 4までの各種構文に対応
今のところcolor関数以外のhsl、hwb、lab、lch、oklab、oklchに対応しています。
元の書き方に合わせた置換
置き換える前の色テキストを解析して、使用している構文、単位、空白の入れ方等を置換後のテキストにも反映させました。
出力書式の設定の増強
出力する形式をカスタマイズするためのプロパティも沢山追加しました。が、この辺りはまだまだ整理が必要です。UIもありません。
メニューを追加

メニューから出力形式をある程度選べるようになりました。また、色成分の直接入力や矢印キーによる値の変更など、存在に気がつきにくいコマンドを載せてあります。

メニューでCSSの出力書式を選んでいるところ(日本語環境の場合)
図2: メニューでCSSの出力書式を選んでいるところ(日本語環境の場合)
バッファへの即時反映
デフォルトでは、カラーピッカー使用中に挿入・置換結果が逐一バッファへ反映されるようになりました。css-mode等でバッファ内のテキストに色を付けている場合にはそれも自動的に更新されるというわけです。……しかしテストしてみるとcss-modeやweb-modeだと対応している(色を付けてくれる)構文って結構限られているんですね。
固定パレットの追加
少々見た目が煩雑になってしまいますが、下部に順番固定のパレットを配置しました。エントリーを右クリックするとメニューが出るので、現在選択中の色をそこへ設定できます。パレット全体をファイルへ保存したり読み込んだりも出来ます。作図エディタから使うとパレットの状態は自動的に保存されるのですが、他から使う場合は明示的に保存する必要があります。
M-p/M-nで履歴選択
M-pやM-nで最近使った色を選べるようになりました。
最後に選択した色相を維持
これまでは初期色が無彩色の場合、赤(色相0度)がから始まっていましたが、最後に選択したときの色相から再開するようにしました。ちゃんとやるならもっと色々工夫をしなければならないのですが、とりあえず最低限。
現在の色の表示
エコーエリアに色の情報を表示するようにしました。
デフォルトの大きさを調整
小さいと使いづらいのでデフォルトの大きさ(edraw-color-picker-near-point-scale)を1.0にしました。この辺りはお好みで。

まだまだ直した方が良いところは尽きませんが、少しはマシになったかもしれません。

作図エディタの方も色々改良していますが、それはまたいずれ。

というわけでEmacsのcss-modeやcustomize-face等でカラーピッカーを使う設定の続きでした。その記事を書いてからもう大分経ちましたね。ボヤボヤしていると1年2年あっという間に過ぎてしまうのでホント嫌になってしまいます。