zeno.zsh のメモ
前置き
ターミナルでメモ管理 (Neovim, nb, zeno.zsh) を見て、zeno.zsh の補完機能に魅力を感じて導入を決めました。これまで abbrev については zsh-abbr で対応していたのですが、1つのプラグインで abbrev と補完の両方を実現できる点も良いと思いました。
導入してみたところ、最初は設定でちょっとつまづいたのですが、使えるようになると非常に便利でしたので、備忘録として設定のポイントをまとめます。
zeno.zsh とは?
Deno を用いて開発された ZSH/Fish のプラグインで、主な機能は以下のとおりです。
- abbrev を用いた snippet の展開
- ファジーファインダー (fzf) を用いた補完
- fzf を用いた snippet の挿入
- fzf を用いたコマンド履歴検査と実行
環境
1> zsh --version
2zsh 5.9 (x86_64-pc-linux-gnu)
3
4> deno --version
5deno 2.5.6 (stable, release, x86_64-unknown-linux-gnu)
6v8 14.0.365.5-rusty
7typescript 5.9.2
8
9> fzf --version
100.67.0 (v0.67.0)
インストール
公式リポジトリでは zdharma-continuum/zinit: 🌻 Flexible and fast ZSH plugin manager や git clone でインストールする方法が紹介されていますが、私は Nix の Home Manager を使って rossmacarthur/sheldon: :bowtie: Fast, configurable, shell plugin manager をインストールしたうえで、sheldon 経由でインストールしました。
1# ~/.config/sheldon/plugins.toml
2shell = "zsh"
3
4[plugins]
5
6[plugins.zeno]
7 github = "yuki-yano/zeno.zsh"
8
9[plugins.fast-syntax-highlighting]
10 github = "zdharma-continuum/fast-syntax-highlighting"
また、zeno.zsh は Deno, the next-generation JavaScript runtime と junegunn/fzf: :cherry_blossom: A command-line fuzzy finder が必要なので、これらもインストールする必要があります。
zeno.zsh の基本設定
公式リポジトリに従って ~/.zshrc に以下のコードを追加しました。ZENO_HOME については、自分の環境に合わせて設定しています。キーバインドについては、ひとまず公式と同じキーバインドに設定しています。
1export ZENO_HOME=~/.config/zsh/zeno
2
3# git file preview with color
4export ZENO_GIT_CAT="bat --color=always"
5
6# git folder preview with color
7# export ZENO_GIT_TREE="eza --tree"
8
9if [[ -n $ZENO_LOADED ]]; then
10 bindkey ' ' zeno-auto-snippet
11
12 # if you use zsh's incremental search
13 # bindkey -M isearch ' ' self-insert
14 bindkey '^m' zeno-auto-snippet-and-accept-line
15 bindkey '^i' zeno-completion
16 # open snippet picker (fzf) and insert at cursor
17 bindkey '^xx' zeno-insert-snippet
18 bindkey '^x ' zeno-insert-space
19 bindkey '^x^m' accept-line
20 bindkey '^x^z' zeno-toggle-auto-snippet
21 # preprompt bindings
22 bindkey '^xp' zeno-preprompt
23 bindkey '^xs' zeno-preprompt-snippet
24 # Outside ZLE you can run `zeno-preprompt git {{cmd}}` or `zeno-preprompt-snippet foo`
25 # to set the next prompt prefix; invoking them with an empty argument resets the state.
26 bindkey '^r' zeno-smart-history-selection # smart history widget
27
28 # fallback if completion not matched
29 # (default: fzf-completion if exists; otherwise expand-or-complete)
30 # export ZENO_COMPLETION_FALLBACK=expand-or-complete
31fi
これでセッションを再起動すれば zeno.zsh が読み込まれますので、次はスニペットや補完の設定を始めていきます。
スニペットや補完の設定の保存場所など
起動時に読み込むディレクトリ
zeno.zsh は起動時に以下の順番でディレクトリを読み込んで設定を反映させます。また、設定ファイルは YAML と TypeScript の両方が使えます。
(公式リポジトリより引用)
- zeno loads configuration files from the project and user config directories and merges them in priority order.
- If the current workspace has a
.zeno/directory, its contents are loaded first, followed by the user config directory ($ZENO_HOMEor~/.config/zeno/), and finally any XDG config directories.- Within each location, files are merged alphabetically.
- Both YAML (
*.yml,*.yaml) and TypeScript (*.ts) files are supported, so you can pick the format that suits your workflow.- TypeScript configs can import
defineConfigand types fromjsr:@yuki-yano/zeno, giving you access to the fullConfigContextfor dynamic setups.
【拙訳】
- zeno は設定ファイルをプロジェクトおよびユーザー設定ファイルのディレクトリから読み込み、それらを優先順位に沿って統合する。
- カレントワークスペースに
.zeno/ディレクトリがあるならば、そこの設定ファイルが最初に読み込まれる。次にユーザー設定ファイルのディレクトリ ($ZENO_HOMEor~/.config/zeno/) が対象になり、最後に任意の XDG ディレクトリが対象になる。- 各場所において、ファイルはアルファベット順に統合される。
- YAML (
*.yml,*.yaml) と TypeScript (*.ts) ファイルの両方がサポートされるので、ワークフローに適したフォーマットを選択できる。
このような動作になっているため、例えば、特定のディレクトリだけで有効なスニペットや補完を設定することも可能です。その場合、そのディレクトリに .zeno/ ディレクトリを作成し、そのディレクトリに設定ファイルを作成すれば OK です。
ユーザー設定ファイルの読み込み
zeno.zsh は以下のとおり設定ファイルを読み込んで設定を反映させる。
(公式リポジトリより引用)
- If the detected project root contains a
.zeno/directory, load all.zeno/*.yml/*.yaml/*.ts(A→Z).- If
$ZENO_HOMEis a directory, merge all*.yml/*.yaml/*.tsdirectly under it.- For each path in
$XDG_CONFIG_DIRS, ifzeno/exists, merge allzeno/*.yml/*.yaml/*.ts(directories are processed in the order provided by XDG).- Fallbacks for backward compatibility (used only when no files were found in the locations above):
$ZENO_HOME/config.yml$XDG_CONFIG_HOME/zeno/config.ymlor~/.config/zeno/config.yml- Find
.../zeno/config.ymlfrom each in$XDG_CONFIG_DIRS
【拙訳】
- プロジェクトのルートディレクトリに
.zeno/ディレクトリがあれば、その中にある.zeno/*.yml/*.yaml/*.tsを全てロードする。$ZENO_HOMEがディレクトリを指しているならば、その配下にある*.yml/*.yaml/*.tsを全てマージする。$XDG_CONFIG_DIRSの各パスにzeno/ディレクトリが存在するならば、.zeno/*.yml/*.yaml/*.tsを全てマージする(ディレクトリの順番は、XDG の順番どおりに進む)- 後方互換性のためのフォールバックは以下のとおり(上記の場所でファイルが見つからないときだけ使われる)
$ZENO_HOME/config.yml$XDG_CONFIG_HOME/zeno/config.ymlまたは~/.config/zeno/config.yml$XDG_CONFIG_DIRSの各パスから見た.../zeno/config.yml
コマンド履歴
<ctrl-r> をタイプするとコマンド履歴が fzf で開くので、適宜絞り込みをかけて履歴からコマンドを実行できます。
fzf には純正のコマンド履歴機能がありますが、zeno.zsh のコマンド履歴はコマンドの実行時間などがプレビューで表示されるので、より高機能になっています。
補完機能の設定
補完設定は .yaml と .ts の両方で記述できますので、ここでは TypeScript の場合の書き方をメモしていきます。
設定では defineConfig() 関数を使うので、"jsr:@yuki-yano/zeno" からインポートします。
1import { defineConfig } from "jsr:@yuki-yano/zeno";
それから、defineConfig() 関数のコールバック関数に補完設定の配列を記述していきます。
コールバック関数に引数として渡される projectRoot と currentDirectory にはプロジェクトのルートディレクトリと現在のディレクトリの絶対パスが入っていますので、これらを利用することもできます。
補完設定のオプション
自分の設定で使っているオプションは以下のとおりです。
name: 補完設定の名前。任意の名前を指定するpatterns: 補完を発動させる場合の条件を指定するexcludePatterns: 補完を発動させない場合の条件を指定するsourceCommand: 補完候補を取得するためのコマンドを指定するsourceFunction:sourceCommandでは対応できない複雑な補完候補を取得する場合に指定するoptions: fzf に渡されるオプションを指定する--prompt: クエリ入力欄の前に表示される文字列を指定する。補完候補を選択した後はこの文字列がターミナルに挿入される。--multi: 補完候補を複数選択する場合にtrueを指定する--read0: fzf に渡される文字列を NULL 文字区切りとして扱う--preview: 選択した補完候補をプレビューするためのコマンドを指定する--ansi: ANSI カラーコードを有効化するときに指定する
callback: 補完候補を選択した後、その選択した補完文字列に対して実行する処理を指定する。callbackZero: 選択した補完文字列をcallbackのコマンドに渡す際に NULL 文字区切りとして渡す。--read0: trueとセットで指定する。
補完設定の例
1import { defineConfig } from "jsr:@yuki-yano/zeno";
2
3export default defineConfig(({ projectRoot, currentDirectory }) => ({
4 completions: [
5 {
6 name: "kill pid",
7 patterns: [
8 "^kill( .*)? $",
9 ],
10 excludePatterns: [
11 " -[lns] $", // kill -l, kill -n, kill -s では補完が発動しない
12 ],
13 sourceCommand: "LANG=C ps -ef | sed 1d",
14 options: {
15 "--multi": true,
16 "--prompt": "'Kill Process> '",
17 },
18 callback: "awk '{print $2}'",
19 },
20 {
21 name: "cd",
22 patterns: ["^cd $"],
23 sourceCommand:
24 "find . -path '*/.git' -prune -o -maxdepth 5 -type d -print0",
25 options: {
26 "--read0": true,
27 "--prompt": "'Chdir> '",
28 "--preview": "cd {} && ls -a | sed '/^[.]*$/d'",
29 },
30 callback: "cut -z -c 3-",
31 callbackZero: true,
32 },
33 {
34 name: "nb edit",
35 patterns: [
36 "^nb e( .*)? $",
37 "^nb edit( .*)? $",
38 ],
39 sourceCommand: "nb ls --no-color | rg '^\\[[0-9]+\\]'",
40 options: {
41 "--ansi": true,
42 "--prompt": "'nb edit > '",
43 "--preview": "echo {} | sed -E 's/^\\[([0-9]+)\\].*/\\1/' | xargs nb show"
44 },
45 callback: "sed -E 's/^\\[([0-9]+)\\].*/\\1/'"
46 },
47 {
48 name: "npm scripts",
49 patterns: [
50 "^pnpm $",
51 ],
52 sourceFunction: async ({ projectRoot }) => {
53 try {
54 const pkgPath = join(projectRoot, "package.json");
55 const pkg = JSON.parse(
56 await Deno.readTextFile(pkgPath),
57 ) as { scripts?: Record<string, unknown> };
58 return Object.keys(pkg.scripts ?? {});
59 } catch {
60 return [];
61 }
62 },
63 options: {
64 "--prompt": "'pnpm scripts> '",
65 },
66 callback: "pnpm {{}}",
67 },
68 ],
69}));
補完の呼び出し
patterns で指定した文字列を入力してから、ctrl-i または tab をタイプすると fzf で補完候補が表示されます。
スニペット機能の設定
どちらかというと fish の abbreviations(略語展開)に近い機能です。補完と同様に .yaml と .ts の両方で設定できます。
設定では defineConfig() 関数を使うので、"jsr:@yuki-yano/zeno" からインポートします。
1import { defineConfig } from "jsr:@yuki-yano/zeno";
そして、補完と同様に defineConfig() 関数のコールバック関数に設定の配列を記述していきます。
スニペット設定のオプション
自分の設定で使っているオプションとそれ以外のオプションは以下のとおりです。なお、文字だけでは挙動が掴みにくいものもあると思いますので、設定例に簡単な動作もコメントで記載しています。
name: スニペットの名前。任意の名前を指定するkeyword: スニペットの略語を指定するsnippet: 展開後のコマンド文字列を指定する。展開後にカーソルを移動させたい場所がある場合、{{hoge}}の形で指定する。hogeの部分は任意の文字列でOK。context: スニペット展開の条件を指定する。デフォルトの展開条件は「keywordが行頭にある場合」である。lbuffer: カーソルの左側に指定した文字列がある場合のみ展開したい場合に指定するrbuffer: カーソルの右側に指定した文字列がある場合のみ展開したい場合に指定するglobal:keywordがどこにあっても展開したい場合にtrueを指定するbuffer: 指定した正規表現に該当する文字列があれば、keywordがどこにあっても展開される
evaluate:keywordで指定した文字列をsnippetで指定したコマンドの結果で置き換える場合に指定する
スニペット設定の例
1import { defineConfig } from "jsr:@yuki-yano/zeno";
2
3export default defineConfig(({ projectRoot, currentDirectory }) => ({
4 snippets: [
5 {
6 name: "git commit",
7 keyword: "gm"
8 snippet: "git commit -m {{commit message}}",
9 },
10 {
11 name: "QMK compile",
12 keyword: "compile"
13 snippet: "qmk compile -kb {{keyboard}} -km {{keymap}}",
14 // 展開すると `{{keyboard}}` のところにカーソルが移動する。それから `ctrl-x p` と入力して
15 // `zeno-preprompt` コマンドを呼び出すと `{{keymap}}` のところにカーソルが移動する。
16 },
17 {
18 name: "branch",
19 keyword: "B",
20 snippet: "git symbolic-ref --short HEAD",
21 context: {
22 lbuffer: "^git\\s+checkout\\s+",
23 },
24 evaluate: true,
25 // `git checkout B<space>` が `git checkout {現在のブランチ名}` に展開される
26 },
27 {
28 name: "test",
29 keyword: "full",
30 snippet: "echo 'hogehoge'",
31 context: {
32 buffer: "^git.*commit.*$",
33 },
34 // `^git.*commit.*$` を満たしていれば、`full` が `echo 'hogehoge'` に展開される
35 // ex) `git commit` -> `git full<space> commit` -> `git echo 'hogehoge' commit`
36 },
37 {
38 name: "ls",
39 keyword: "ls",
40 snippet: "eza --icons always --long --git {{foo_bar}}",
41 },
42 {
43 name: "test2",
44 keyword: "G",
45 snippet: "echo 'global!'",
46 context: {
47 global: true,
48 },
49 // `G` をどこに入力しても `echo 'global!'` に展開される
50 // ex) `git commit` -> `git G<space> commit` -> `git echo 'global!' commit`
51 },
52 {
53 name: "test3",
54 keyword: "cdp",
55 snippet: `exa ${projectRoot}`,
56 // `${projectRoot}` が現在のプロジェクトのルートディレクトリに置き換えされる
57 // ex) `cdp<space>` -> `exa /path/to/project_root_directory`
58 },
59 ],
60}));
スニペットの展開
keyword で設定した文字列を入力してから <space> を入力すると展開されます。また、keyword で設定した文字列を入力してから <enter> を入力すると展開してから実行されます。
また、keyword を入力しなくても、ctrl-x x で zeno-insert-snippet を呼び出すと設定したスニペットが fzf で表示されるので、そこから選択することも可能です。
さらに、ctrl-x s で zeno-preprompt-snippet を呼び出すと、設定したスニペットが fzf で表示されるので、そこから選択することも可能です。
zeno-insert-snippet と zeno-preprompt-snippet の違いは、前者は選択したスニペットを挿入する処理なので、入力済みコマンドはそのまま残っているのに対し、後者は選択したスニペットで入力済みコマンドを置き換えてしまう点です。
スニペットのキーワードをキーワードのまま入力したい場合の方法
上記の設定で ls を実行する場合、ls<ctrl-x><ctrl-m> とタイプすれば ls が eza --icons always --long --git {{foo_bar}} に展開されることなく実行できます。
また、ls -la を実行する場合、ls<ctrl-x><space> とタイプすれば eza --icons always --long --git {{foo_bar}} に展開されることなくスペースを入力できます。
さらに、<ctrl-x><ctrl-z> をタイプして zeno-toggle-auto-snippet コマンドを実行すると、展開機能をオフにできます。もう一度 <ctrl-x><ctrl-z> をタイプすれば展開機能をオンにできます。
まとめ
zeno.zsh は設定に少し時間がかかりましたが、一度設定してしまえば非常に便利に使えるツールです。特に、補完機能とスニペット展開を1つのプラグインで実現できる点が気に入っています。
本記事がどなたかの参考になれば幸いです。