🐡

Magit で活用されているメニュー表示ライブラリ transient.el を使いこなしたい

2022/06/28に公開約8,400字

はじめに

transient.el は Magit で使われているメニュー表示用ライブラリです
Magit は Emacs から Git を簡単に扱うパッケージです

本文に出てくる c1 c2 c3 は何かの関数です
次のようなのが定義されていることにします

(defun c1() (interactive) (message "c1"))
(defun c2() (interactive) (message "c2"))
(defun c3() (interactive) (message "c3"))

一式入ってるいちばん簡単な例

(require 'transient)

(transient-define-prefix f1 ()
  [("a" "item1" (lambda () (interactive) (message "c1")))])

(global-set-key (kbd "C-c RET") 'f1)
 a item1
  • M-x f1 または C-c RET でメニューが起動する
  • a で lambda ブロックを実行する

description を入れる

"コマンド" の部分のこと。なくてもいいけどあるとわかりやすい。

(transient-define-prefix f1 ()
  ["コマンド" ("a" "item1" (lambda () (interactive) (message "c1")))])
コマンド
 a item1

指定の関数を実行する

(transient-define-prefix f1 ()
  [("a" "item1" c1)])
  • lambda のかわりに関数を指定する
  • a item1 と表示され ac1 を実行する

関数を複数指定する

(transient-define-prefix f1 ()
  [
   ("a" "item1" c1)
   ("b" "item2" c2)
   ])

複数書くとにならぶ

a item1
b item2

実行してもメニューを閉じないようにする

(transient-define-prefix f1 ()
  [
   ("a" "item1" c1 :transient t)
   ])
  • :transient ta を押してもメニューが閉じなくなる
  • 何回か連続で実行するような関数に指定する
  • C-q C-g ESC ESC ESC のどれかで閉じる

関数の方にパラメータを書いてもいい

defun のかわりに transient-define-suffix を使うと関数側でパラメータを書ける

(transient-define-suffix c1 ()
  :key "a"
  :description "item1"
  :transient t
  (interactive)
  (message "c1"))

これでメニューの方には関数を並べるだけでよくなる

(transient-define-prefix f1 ()
  [(c1)])

設定は上書きできる

(transient-define-prefix f1 ()
  [(c1 :key "b")])

普通の関数だったかのように書いてもいい

(transient-define-prefix f1 ()
  [("b" "item2" c1)])

レイアウト

これを頭で理解するのは難しい

「配列」を並べると下方向に増える

(transient-define-prefix f1 ()
  ["上" (a) (b)]
  ["下" (c) (d)])
上
 a
 b
下
 c
 d

「配列の配列」なら右方向に増える

(transient-define-prefix f1 ()
  [["左" (a) (b)]["右" (c) (d)]])
左    右
 a     c
 b     d

「配列の配列」を並べるとそれが下方向に増える

(transient-define-prefix f1 ()
  ["上" ["左上" (a) (b)] ["右上" (c) (d)]]
  ["下" ["左下" (a) (b)] ["右下" (c) (d)]])
左上    右上
  a       c
  b       d
左下    右下
  a       c
  b       d

description は動的に生成できる

  • 次の2つは同じ
  • これが役立ったことはいまのところない
(transient-define-prefix f1 ()
  ["上" (c1)])
(transient-define-prefix f1 ()
  [:description (lambda () "上") (c1)])

引数・オプション

基本形

(transient-define-prefix f1 ()
  ["オプション" ("-x" "論理型" "--xxx")]
  ["コマンド" ("a" "item1" (lambda () (interactive) (prin1 (transient-args 'f1))))])
オプション
 -x 論理型 (--xxx)

コマンド
 a item1
  • すぐに a をタイプすると nil を表示する
  • -x をタイプすると --xxx が有効になる (色が少し変わる)
  • --xxx を有効にしてから a をタイプすると ("--xxx") を表示する
  • (transient-args 'f1) がオプションの配列を返している
    • 引数の f1 の部分は対象の関数名
    • オプションがないときは nil になる (空配列ではなく)
      • が、それが問題になることはとくにない
  • -x の部分はハイフンで始めなくてもいい
    • x とすれば x でトグルできる
    • しかしコマンド見間違うので - で始めた方がいい
  • "オプション""コマンド" は無くてもいいけどあるとわかりやすい
  • オプションとコマンドをどこに書くか決まりはない
    • 配列内要素の3つ目が関数かどうかで見分けているみたい
    • だからオプション類は下に配置してもいい

初期値を指定する

(transient-define-prefix f1 ()
  :value '("--xxx")
  ["オプション" ("-x" "論理型" "--xxx")]
  ["コマンド" ("a" "item1" (lambda () (interactive) (prin1 (transient-args 'f1))))])
  • :value で初期値を指定する
  • 上の例ではメニューを起動した時点で --xxx が有効になっている (色がついている)

オプションを永続化する

C-x を押すとこうなるので


C-x を押したところ

続けて C-s するとファイルに保存する

~/.emacs.d/transient/values.el
((f1 "--xxx"))

よく使うかもしれない操作まとめ

操作 意味 備考
C-x C-s ファイルに保存 永続化
C-x s メモリに保存 Emacsを閉じたら元に戻る
C-x C-k 初期値に戻す

文字列型のオプション

--xxx--xxx= に変更する

(transient-define-prefix f1 ()
  ["オプション" ("-x" "文字列型" "--xxx=")]
  ["コマンド" ("a" "item1" (lambda () (interactive) (prin1 (transient-args 'f1))))])
  • -x をタイプするとプロンプトが出る
  • foo と入力すると --xxx=foo と表示が変わる
  • その後で a をタイプすると ("--xxx=foo") を表示する
  • 論理型と同様に初期値を書ける。例: :value '("--xxx=foo")

オプションの値を取り出す

汎用の transient-arg-value で配列から特定の値だけをええ感じに取り出せる

それぞれ論理型と文字列型の例

(transient-arg-value "--xxx" '("--xxx"))      ; => t
(transient-arg-value "--xxx=" '("--xxx=foo")) ; => "foo"

となるので第二引数にはオプションの配列 (transient-args 'f1) を渡す

(transient-args 'f1)                                ; => ("--xxx=foo")
(transient-arg-value "--xxx=" (transient-args 'f1)) ; => "foo"

文字列型オプションの = の有無はかなり重要

  • --xxx=foo ではなく --xxx foo としてしまうと値を取り出せない
  • 素直に --xxx=foo となるようにする
(transient-arg-value "--xxx " '("--xxx foo")) ; => nil

オプションから外部コマンドの組み立て

オプションの捌き方はさまざまだけど外部コマンドの仕様と一致している場合は円滑に渡せる

そのまま git の引数とする例:

(require 's)
(shell-command (s-join " " (cons "git" (transient-args 'f1))))

(transient-args 'f1)("--version") だとすれば git --version を実行する

表示条件

(transient-define-prefix f1 ()
  [
    :if (lambda () t)
    ("a" "item1" c1 :if (lambda () t))
   ])
  • 配列の先頭に書くと配列要素全体に適用する
  • 個別に指定してもよい
  • いろんな条件構文が用意されている
    • if if-not if-non-nil if-nil if-mode if-not-mode if-derived if-not-derived
  • if-derivedderived-mode-p で判定する
  • :if-mode (ruby-mode rust-mode) のように複数指定してもよい

日本語の問題

列で表示するとき項目名が日本語だとエラーになる場合がある
そんなときは :variable-pitch t で回避できる

(transient-define-prefix f1 ()
  :variable-pitch t
  [
   [("a" "ダウンロード" c1)]
   [("b" "デスクトップ" c2)]
   ])

毎回指定するのは面倒なのでグローバルな設定とした方がよさそう

(setq transient-align-variable-pitch t)

次の固定幅フォント使うオプションを有効にしても回避できる

(setq transient-force-fixed-pitch t)

しかし文字に隙間が空いて古めかしい感じの見た目になってしまうので最初ので十分だと思う

ラベルが重複すると誤作動する問題

(transient-define-prefix f1 ()
  [
   ("a" "item1" (lambda () (interactive) (message "c1")))
   ("b" "item1" (lambda () (interactive) (message "c2")))
   ])
  • lambda で記述しているときに限り、ラベルが重複すると誤作動する (不具合?)
  • 上のように item1 が重複していると lambda の部分が後者で上書きされる
  • なので a を押すと c2 が表示されてしまう

実用編

ディレクトリ移動

(transient-define-prefix f1 ()
  "ディレクトリ移動"
  ["ディレクトリ移動"
   ("l" "ダウンロード" (lambda () (interactive) (dired "~/Downloads")))
   ("t" "デスクトップ" (lambda () (interactive) (dired "~/Desktop")))
   ("d" "書類"         (lambda () (interactive) (dired "~/Documents")))
   ("i" "画像"         (lambda () (interactive) (dired "~/Pictures")))
   ("x" "Dropbox"      (lambda () (interactive) (dired "~/Dropbox")))
   ("~" "Home"         (lambda () (interactive) (dired "~/")))
   ("s" "src"          (lambda () (interactive) (dired "~/src")))
   ("g" "gems"         (lambda () (interactive) (dired "/usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems")))
   ("z" "Zenn"         (lambda () (interactive) (dired "~/src/zenn-content/articles")))
   ("c" "Cargo"        (lambda () (interactive) (dired "~/.cargo/registry/src")))
   ("e" "Emacs"        (lambda () (interactive) (dired "~/.emacs.d")))
   ])

こうやって移動すればディレクトリ間の距離の感覚はなくなっていく

Railsアプリ内でディレクトリ移動

.git の親ディレクトリからの相対的な移動の例

(transient-define-prefix f1 ()
  "Railsアプリ内の規定ディレクトリに移動する"
  ["ディレクトリ移動"
   ("l" "log"        (lambda () (interactive) (my-chdir-from-git-root "log")))
   ("m" "model"      (lambda () (interactive) (my-chdir-from-git-root "app/models")))
   ("c" "controller" (lambda () (interactive) (my-chdir-from-git-root "app/controllers")))
   ("t" "test"       (lambda () (interactive) (my-chdir-from-git-root "spec")))
   ])

(defun my-chdir-from-git-root (dir)
  "ディレクトリ移動(.gitの親から相対的に)"
  (interactive)
  (dired
   (concat
    (locate-dominating-file default-directory ".git")
    dir)))

入力がバッティングしたらどうする?

仮に最初につくった方と混ぜたとすると l が両方にあるので干渉してしまう
そういう場合は片方を2文字にする手もある

(transient-define-prefix f1 ()
  ["ディレクトリ移動"
   ("l" "ダウンロード" (lambda () (interactive) (dired "~/Downloads")))
   ("rl" "log"         (lambda () (interactive) (my-chdir-from-git-root "log")))
   ])

ただ Magit でこのような定義は見たことがない
徹底して1文字にこだわっているし、その方がいいと思う

参照

https://github.com/magit/transient/wiki/Developer Quick Start Guide
https://magit.vc/manual/transient.html
https://github.com/magit/transient

Discussion

ログインするとコメントできます