Magit を支える transient.el の使い方
transient.el とは?
Magit で使われているメニューUIライブラリで単独で使うこともできる。通常のキーアサインでどこに何を定義したか一日で忘れてしまう自分には必須のライブラリである。
前提
事前に次が定義されているとする。
(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 ブロックを実行する
指定の関数を実行する
(transient-define-prefix f1 ()
[("a" "item1" c1)])
lambda のかわりに関数を指定する。a item1
と表示され a
で c1
を実行する。
関数を複数指定する
(transient-define-prefix f1 ()
[
("a" "item1" c1)
("b" "item2" c2)
])
複数書くと縦にならぶ。
a item1
b item2
実行してもメニューを閉じないようにする
(transient-define-prefix f1 ()
[
("a" "item1" c1 :transient t)
])
-
:transient t
でa
を押してもメニューが閉じなくなる - 何回か連続で実行するような関数に指定する
-
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)])
description を入れる
"コマンド"
の部分のこと。なくてもいいけどあるとわかりやすい。
(transient-define-prefix f1 ()
["コマンド" ("a" "item1" (lambda () (interactive) (message "c1")))])
コマンド
a item1
レイアウト
これを頭で理解するのは難しい。
「配列」を並べると下方向に増える
(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 を動的に変更するには?
(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
するとファイルに保存する
((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-derived
はderived-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 ()
"ディレクトリ移動"
["ディレクトリ移動"
("t" "デスクトップ" (lambda () (interactive) (dired "~/Desktop")))
("l" "ダウンロード" (lambda () (interactive) (dired "~/Downloads")))
("d" "書類" (lambda () (interactive) (dired "~/Documents")))
("i" "画像" (lambda () (interactive) (dired "~/Pictures")))
("x" "Dropbox" (lambda () (interactive) (dired "~/Dropbox")))
("e" "Emacs" (lambda () (interactive) (dired "~/.emacs.d")))
("z" "Zenn" (lambda () (interactive) (dired "~/src/zenn-content/articles")))
("s" "src" (lambda () (interactive) (dired "~/src")))
])
このようにすればディレクトリ間の距離の感覚はなくなっていく。
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文字にこだわっている。2文字になるぐらいなら1文字目で別のメニューを発動させた方がいいかもしれない。
参照
Discussion