From 2390b4f69d07b465ded9cdab7f08583e61c2c3f6 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 26 Sep 2026 00:57:07 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(PLAN71):=20prefix=20S=20=E3=81=AE?= =?UTF-8?q?=E4=B8=80=E8=A6=A7=E3=81=A8=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=82=92=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89=20tmux-menu=20?= =?UTF-8?q?=E3=81=8B=E3=82=89=E3=82=82=E9=96=8B=E3=81=8F=20(#270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tmux-menu を tmux-session menu の短縮名にし、セッションも -c も受け取らない形で一覧を開く - tmux の中は TMUX_PANE の pane に、外は attach して一覧を出す。サーバが無ければ終了コード 1 - prefix S は run-shell "TMUX_PANE=#{pane_id} tmux-menu" を呼び、一覧の定義を tmux-session に集める - Dockerfile に symlink tmux-menu、利用者向け文書と CHANGELOG、回帰テスト Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 10 ++ containers/base/Dockerfile | 7 +- containers/base/tmux-session | 40 +++++- containers/base/tmux.conf | 9 +- docs/user/environment-variables.md | 16 ++- tests/containers/test_tmux_conf.py | 10 +- tests/containers/test_tmux_session.py | 184 +++++++++++++++++++++++++- 7 files changed, 258 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb0ff26..515e94f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,16 @@ 処理からも見えます。設定を足すツールはこの変数の指す先へ 1 ファイル置けば、`~/.bashrc` を 書き換えずに済みます。zsh と `lfm` は対象外です。 **反映には `devbase build base --no-cache` と、使っている派生イメージの建て直しが要ります。** +- **`prefix S` のセッションの一覧とメニューを、コマンド `tmux-menu` からも開けるようにしました + (PLAN71 / #270)。** tmux の中では今の pane に一覧を出し、tmux の外では attach して一覧を出します + (attach 先は tmux の既定。サーバが無ければ何も作らず終了コード 1)。本体は `tmux-session menu` + で、`tmux-session menu -c 端末 <セッション>` の形は変わりません。`prefix S` の割り当ては + `tmux-menu` を呼ぶ形になりました(開く一覧とメニューは同じ)。ホストの tmux で使う手順 + (symlink の 5 つ目の `tmux-menu` と `~/.tmux.conf` の新しい行)は + `docs/user/environment-variables.md` の「セッションを名指しで扱う」にあります。 + **反映には `devbase build base --no-cache` と、使っている派生イメージの建て直し、 + コンテナの作り直し(`devbase down` → `devbase up`)が要ります。`devbase up` だけでは + 反映されません。** ### Changed - **スナップショットの世代を、アカウントグループ(対象ボリュームの組)ごとの系列で持つように diff --git a/containers/base/Dockerfile b/containers/base/Dockerfile index 5386a61..e7cf2c7 100644 --- a/containers/base/Dockerfile +++ b/containers/base/Dockerfile @@ -260,7 +260,7 @@ COPY --chmod=0644 tmux.conf /etc/tmux.conf # tmux セッションの整理コマンド。tmux-first は居座りクライアントを切断して # 一番若い番号のセッションへ切り替え、tmux-clean は置き去りのセッションを削除する。 # tmux-session は 1 つのセッションを名指しで移る・調べる・落とす (#234)。prefix S の -# メニュー (tmux.conf) からも呼ばれる。 +# 一覧とメニュー (tmux.conf) からも呼ばれる。一覧は tmux-menu でも開く (#270)。 # 背景と使い方は各スクリプト冒頭のコメントと docs/user/environment-variables.md を参照。 COPY --chmod=0755 tmux-first /usr/local/bin/tmux-first COPY --chmod=0755 tmux-clean /usr/local/bin/tmux-clean @@ -268,12 +268,13 @@ COPY --chmod=0755 tmux-session /usr/local/bin/tmux-session # 短縮名は alias ではなく symlink で用意する。alias は bash の対話シェルにしか効かず、 # zsh などの他シェルや非対話実行 (docker exec など) では使えないため。 -# tmux-go / tmux-peek / tmux-kill は呼ばれた名前で tmux-session のサブコマンドになる。 +# tmux-go / tmux-peek / tmux-kill / tmux-menu は呼ばれた名前で tmux-session のサブコマンドになる。 RUN sudo ln -sf tmux-first /usr/local/bin/tmux1 \ && sudo ln -sf tmux-clean /usr/local/bin/tmuxc \ && sudo ln -sf tmux-session /usr/local/bin/tmux-go \ && sudo ln -sf tmux-session /usr/local/bin/tmux-peek \ - && sudo ln -sf tmux-session /usr/local/bin/tmux-kill + && sudo ln -sf tmux-session /usr/local/bin/tmux-kill \ + && sudo ln -sf tmux-session /usr/local/bin/tmux-menu # フォントの既定。素の fontconfig は sans-serif を中国語フェイス (WenQuanYi Zen Hei) へ # 向けるため、日本語を描くと中国語の字形で写る (#161)。/etc/fonts/local.conf は diff --git a/containers/base/tmux-session b/containers/base/tmux-session index 188cf88..24821be 100755 --- a/containers/base/tmux-session +++ b/containers/base/tmux-session @@ -8,9 +8,10 @@ # tmux-session go [-c 端末] <セッション> 移る。そのセッションの他の端末は外す # tmux-session peek [-n 行数] <セッション> attach せずに端末・プロセス・画面を見る # tmux-session kill [-n] [-f] [-c 端末] <セッション>... attach・実行中を問わず落とす +# tmux-session menu セッションの一覧を開く (prefix S と同じ, #270) # tmux-session menu -c 端末 <セッション> prefix S の一覧から呼ばれるメニュー # -# tmux-go / tmux-peek / tmux-kill という名前 (symlink) で呼ぶと、そのサブコマンドになる。 +# tmux-go / tmux-peek / tmux-kill / tmux-menu という名前 (symlink) で呼ぶと、そのサブコマンドになる。 # 短縮名を alias にしないのは、alias が bash の対話シェルにしか効かないため。 # # セッションの指し方: `$数字` は ID、それ以外は名前の完全一致 (devbase-1 は devbase-10 に @@ -25,6 +26,7 @@ case "$PROG" in tmux-go) SUB="go" ;; tmux-peek) SUB="peek" ;; tmux-kill) SUB="kill" ;; +tmux-menu) SUB="menu" ;; *) PROG=tmux-session SUB="" @@ -44,10 +46,13 @@ usage() { tmux-session kill [-n] [-f] [-c 端末] <セッション>... attach・実行中を問わず落とす。-n は落とさずに予定を出す。 自分の pane があるセッションは -f が無ければ落とさない。 + tmux-session menu + セッションの一覧を開く (prefix S と同じ)。選ぶと下のメニューが出る。 + tmux の外では attach して一覧を出す。 tmux-session menu -c 端末 <セッション> 移る・中身を見る・落とすのメニューを出す (prefix S の一覧から呼ばれる)。 - 短縮名: tmux-go = go / tmux-peek = peek / tmux-kill = kill + 短縮名: tmux-go = go / tmux-peek = peek / tmux-kill = kill / tmux-menu = menu <セッション>: `$数字` は ID、それ以外は名前の完全一致。 -c 端末: この端末の代わりに操作する。知らせは端末の状態行へ出す。 @@ -124,14 +129,23 @@ case "$LINES_N" in '' | *[!0-9]*) usage_error "-n は 0 以上の整数で指定してください: $LINES_N" ;; esac +# menu はセッションも -c も受け取らないときに限り一覧を開く (#270)。どちらか一方でも +# あればメニューを出す形として読む。 +LIST=0 +[ "$SUB" != menu ] || [ $# -ne 0 ] || [ "$HAVE_CLIENT" = 1 ] || LIST=1 + case "$SUB" in kill) [ $# -ge 1 ] || usage_error "セッションを指定してください" ;; +menu) [ "$LIST" = 1 ] || { + [ $# -ge 1 ] || usage_error "セッションを指定してください" + [ $# -eq 1 ] || usage_error "セッションは 1 つだけ指定してください" +} ;; *) [ $# -ge 1 ] || usage_error "セッションを指定してください" [ $# -eq 1 ] || usage_error "セッションは 1 つだけ指定してください" ;; esac -[ "$SUB" != menu ] || [ "$HAVE_CLIENT" = 1 ] || usage_error "menu には -c が要ります" +[ "$SUB" != menu ] || [ "$LIST" = 1 ] || [ "$HAVE_CLIENT" = 1 ] || usage_error "menu には -c が要ります" # 知らせの出し先。-c のときは端末の状態行へ出し、標準出力には何も出さない # (UI から背景の run-shell で呼ばれるため、出力は誰にも見えない)。 @@ -157,6 +171,26 @@ command -v tmux >/dev/null 2>&1 || fail "tmux が見つかりません" SESSIONS=$(tmux list-sessions -F '#{session_id} #{session_name}' 2>/dev/null) || fail "tmux サーバが起動していません" +# 一覧を開く (prefix S と tmux-menu の共通の定義。/etc/tmux.conf の prefix S はここを呼ぶ)。 +# "%%%" は選んだセッションの =名前: に、" \ $ ; ~ を \ で守って置き換わる。'%%' だと ' を +# 含む名前で壊れる。#{q:...} は選んだセッションの ID と、Enter を押した端末の名前をシェル +# 向けに引用する。template を run-shell の文字列へ直接書くと #{...} が先に展開されて壊れる +# ため、定義はこのスクリプトに置く。 +# 出す pane は TMUX_PANE で決める。prefix S は run-shell に TMUX_PANE=#{pane_id} を渡す +# (run-shell の中には TMUX_PANE が無く、-t が無いと直近に操作された別の端末に出ることがある)。 +if [ "$LIST" = 1 ]; then + TREE_TEMPLATE='run-shell -t "%%%" "tmux-session menu -c #{q:client_name} #{q:session_id}"' + if [ -n "${TMUX:-}" ]; then + if [ -n "${TMUX_PANE:-}" ]; then + exec tmux choose-tree -t "$TMUX_PANE" -Zs -O name "$TREE_TEMPLATE" + fi + exec tmux choose-tree -Zs -O name "$TREE_TEMPLATE" + fi + # tmux の外: attach 先は tmux の既定 (端末の繋がっていないセッションを優先し、 + # その中で直近のもの)。attach した画面に一覧を出す。 + exec tmux attach-session \; choose-tree -Zs -O name "$TREE_TEMPLATE" +fi + if [ "$HAVE_CLIENT" = 1 ]; then tmux list-clients -F '#{client_name}' | grep -Fqx -- "$CLIENT" || fail "端末がありません: $CLIENT" diff --git a/containers/base/tmux.conf b/containers/base/tmux.conf index 8760e69..539a164 100644 --- a/containers/base/tmux.conf +++ b/containers/base/tmux.conf @@ -46,7 +46,8 @@ set -g focus-events on # 含まれないため、宣言しないと Claude Code などが出す URL がクリックできない。 set -ga terminal-features ",xterm-256color:hyperlinks" -# prefix S: セッションを選び、移る・中身を見る・落とすのメニューを出す (#234)。 -# "%%%" は選んだセッションの =名前: に、" \ $ ; ~ を \ で守って置き換わる。'%%' だと ' を -# 含む名前で壊れる。#{q:...} は選んだセッションの ID と、押した端末の名前をシェル向けに引用する。 -bind-key S choose-tree -Zs -O name "run-shell -t \"%%%\" \"tmux-session menu -c #{q:client_name} #{q:session_id}\"" +# prefix S: セッションを選び、移る・中身を見る・落とすのメニューを出す (#234 / #270)。 +# 一覧の定義は tmux-session にある (tmux-menu と同じ)。run-shell は渡した文字列の #{…} を +# 先に展開するため、template をここへ書かずにコマンドを呼ぶ。run-shell の中には TMUX_PANE が +# 無いため、押した pane の ID を渡す (無いと直近に操作された別の端末に出ることがある)。 +bind-key S run-shell "TMUX_PANE=#{pane_id} tmux-menu" diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md index d00f93d..a73c499 100644 --- a/docs/user/environment-variables.md +++ b/docs/user/environment-variables.md @@ -611,22 +611,34 @@ tmux の中では **`prefix S`**(既定では `Ctrl+b` → `Shift+s`)で、 | 中身を見る | `p` | `tmux-peek` の出力を浮いた窓に出す。`Enter` で閉じる | | 落とす | `k` | 確認(`y/n`)を挟んで落とす。今いるセッションも、同意すれば落とす | +キーを覚えていなくても、**`tmux-menu`** を打てば `prefix S` と同じ一覧とメニューが開きます(`tmux-session menu` と同じ)。 + +```bash +tmux-menu # tmux の中: 今の pane に一覧を出す + # tmux の外: attach して、attach した画面に一覧を出す +``` + +- tmux の外での attach 先は tmux の既定に従います(端末の繋がっていないセッションを優先し、その中で直近に使ったもの)。一覧からどのセッションへも移れます。名前の決まったセッションへ直接行くなら `tmux-go` を使います +- tmux のサーバが動いていないときは、サーバもセッションも作らずに理由を出して終了コード 1 で終わります + `prefix S` の割り当ては `/etc/tmux.conf` にあります。`~/.tmux.conf` で `S` を別の操作に割り当てていれば、後から読むそちらが勝ちます(コマンドはそのまま使えます)。 **ホストの tmux で使う場合**は、devbase の checkout の中のファイルへ symlink を張り、`~/.tmux.conf` へ 1 行足します(devbase はホストのこれらのファイルへ書き込みません)。複写ではなく symlink にすると、devbase を `git pull` するだけで更新が届きます。`~/.local/bin` が `PATH` に入っている必要があります。 ```bash DEVBASE_DIR=~/devbase # devbase を clone した場所 -for n in tmux-session tmux-go tmux-peek tmux-kill; do +for n in tmux-session tmux-go tmux-peek tmux-kill tmux-menu; do ln -sf "$DEVBASE_DIR/containers/base/tmux-session" ~/.local/bin/$n done ``` ```tmux # ~/.tmux.conf に足す (反映は tmux source-file ~/.tmux.conf か tmux kill-server) -bind-key S choose-tree -Zs -O name "run-shell -t \"%%%\" \"tmux-session menu -c #{q:client_name} #{q:session_id}\"" +bind-key S run-shell "TMUX_PANE=#{pane_id} tmux-menu" ``` +以前の手順で `bind-key S choose-tree …` の行を足した人は、そのままでも `prefix S` は動きます。`tmux-menu` を使うには symlink の 5 つ目(`tmux-menu`)を足します。行は上の新しい形へ差し替えると、一覧の定義が `tmux-session` の 1 か所にそろいます。 + dev コンテナで使うには、base イメージの建て直し(`devbase build base --no-cache`)と、使っている派生イメージの建て直し、コンテナの作り直し(`devbase down` → `devbase up`)が要ります。 サブコマンドごとの引数・終了コード・`peek` の出力・`prefix S` のメニューの仕様は diff --git a/tests/containers/test_tmux_conf.py b/tests/containers/test_tmux_conf.py index 7f085d1..9b64e07 100644 --- a/tests/containers/test_tmux_conf.py +++ b/tests/containers/test_tmux_conf.py @@ -255,12 +255,14 @@ def _prefix_keys(config: Path) -> list[str]: @needs_tmux def test_prefix_s_opens_session_chooser(): - """PLAN69 条件 13: prefix S の割り当てがちょうど 1 つあり、choose-tree を呼ぶ。 + """PLAN69 条件 13 / PLAN71 条件 11: prefix S の割り当てがちょうど 1 つあり、 + 押した pane の ID を ``TMUX_PANE`` で渡して ``run-shell`` で ``tmux-menu`` を呼ぶ。 - 既定の tmux には prefix S の割り当てが無い (3.6・3.7b で確認)。 + 既定の tmux には prefix S の割り当てが無い (3.6・3.7b で確認)。一覧の定義は + ``tmux-session`` にだけあり、割り当ては template を持たない (PLAN71 決定 2・決定 4)。 """ bound = [line for line in _prefix_keys(TMUX_CONF) if re.match(r"bind-key\s+(-r\s+)?-T prefix\s+S\s", line)] assert len(bound) == 1, bound - assert "choose-tree" in bound[0] - assert "tmux-session menu" in bound[0] + assert re.search(r'\sS\s+run-shell\s+"TMUX_PANE=#\{pane_id\} tmux-menu"\s*$', + bound[0]), bound[0] diff --git a/tests/containers/test_tmux_session.py b/tests/containers/test_tmux_session.py index 8b2c98c..2c256c6 100644 --- a/tests/containers/test_tmux_session.py +++ b/tests/containers/test_tmux_session.py @@ -21,6 +21,7 @@ import os import pty import re +import shlex import shutil import subprocess import sys @@ -35,7 +36,7 @@ SCRIPT = BASE_DIR / "tmux-session" TMUX_CONF = BASE_DIR / "tmux.conf" DOCKERFILE = BASE_DIR / "Dockerfile" -SHORT_NAMES = ("tmux-go", "tmux-peek", "tmux-kill") +SHORT_NAMES = ("tmux-go", "tmux-peek", "tmux-kill", "tmux-menu") needs_tmux = pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux が無い環境") @@ -445,13 +446,20 @@ def tm(): @pytest.mark.parametrize("argv", [("tmux-session", "-h"), ("tmux-session", "go", "-h"), ("tmux-go", "-h"), ("tmux-peek", "--help"), - ("tmux-kill", "-h")]) + ("tmux-kill", "-h"), ("tmux-menu", "-h")]) def test_help_exits_zero(tm, argv): done = tm.run(*argv) assert done.returncode == 0, done.stderr assert "tmux-session" in done.stdout +def test_help_lists_tmux_menu(tm): + """PLAN71 条件 9: ``tmux-session -h`` の使い方に ``tmux-menu`` が載る。""" + done = tm.run("tmux-session", "-h") + assert done.returncode == 0, done.stderr + assert "tmux-menu" in done.stdout + + @pytest.mark.parametrize("argv", [ ("tmux-session",), # サブコマンドが無い ("tmux-session", "nope", "x"), # 知らないサブコマンド @@ -469,6 +477,10 @@ def test_help_exits_zero(tm, argv): ("tmux-kill",), # kill にセッションが無い ("tmux-peek", "-c", "/dev/pts/1", "x"), # peek は -c を受け取らない ("tmux-session", "menu", "-c", "/dev/pts/1", "a", "b"), # menu にセッションが複数 + ("tmux-menu", "-c", "/dev/pts/1"), # PLAN71 条件 9: -c だけでセッションが無い + ("tmux-menu", "a"), # -c が無い + ("tmux-menu", "-c", "/dev/pts/1", "a", "b"), # 余分な引数 + ("tmux-menu", "-x"), # 知らないオプション ]) def test_usage_errors_exit_two(tm, argv): done = tm.run(*argv) @@ -1072,6 +1084,174 @@ def test_menu_notifies_client_on_failure(ui_tm): _wait(lambda: "no-such-session" in me.output()) +# --- tmux-menu: 一覧を開く形 (PLAN71) --- +# +# 一覧は名前順 (``-O name``) で、カーソルは開いた pane のセッション ``zz-home`` にある。 +# 選ぶセッションの名前はどれも ``zz-home`` より前に並ぶため、1 つ上が選ぶセッションになる。 + +# 一覧を開く形を打つコマンド行 (``tm.bin`` からの相対)。条件 8 で両方の名前を走らせる。 +LIST_COMMANDS = ["tmux-menu", "tmux-session menu"] + + +def _pane_mode(tm: TmuxEnv, sid: str) -> str: + return tm.tmux("display-message", "-p", "-t", sid, "#{pane_mode}").stdout.strip() + + +def _type_list_command(tm: TmuxEnv, sid: str, command: str) -> None: + """``sid`` の pane のプロンプトへ一覧を開くコマンド行を打ち、一覧が出るまで待つ。 + + 繋いだ端末ではなく pane へ ``send-keys`` で直接送る (繋いだ直後の端末は入力を捨てる + ことがある)。コマンドは ``tm.bin`` の絶対パスで打つ (``fake_tm`` では名前の + ``tmux-session`` が ``fake/`` の偽物に解決されるため)。 + """ + tm.tmux("send-keys", "-t", sid, f"{tm.bin}/{command}", "Enter") + _wait(lambda: _pane_mode(tm, sid) == "tree-mode") + + +def _pick_above(tm: TmuxEnv, me: Client, sid: str, attempts: int = 5) -> None: + """``sid`` の pane の一覧で 1 つ上を選び、``me`` の端末から Enter を送る。 + + Up は ``send-keys`` で送る (端末を通らずに一覧を動かす)。Enter は ``me`` から送る。 + ``send-keys`` の Enter では template の ``#{client_name}`` が直近に操作された端末に + なり、押した端末を確かめられない。繋いだ直後の ``me`` は Enter を捨てることがあるため、 + 一覧が残っていれば 2 秒ごとに送り直す。一覧を抜けたら ``me`` は入力を受け付けている。 + """ + tm.tmux("send-keys", "-t", sid, "Up") + for _ in range(attempts - 1): + me.send("\r") + with contextlib.suppress(AssertionError): + _wait(lambda: _pane_mode(tm, sid) != "tree-mode", timeout=2.0) + return + me.send("\r") + _wait(lambda: _pane_mode(tm, sid) != "tree-mode") + + +@needs_tmux +@pytest.mark.parametrize("command", LIST_COMMANDS) +def test_list_inside_opens_tree_in_pane(ui_tm, command): + """PLAN71 条件 1・8: tmux の中で打つと、その pane に一覧が出る。""" + tm = ui_tm + home = tm.new("zz-home") + tm.attach(home) + + _type_list_command(tm, home, command) + + +@needs_tmux +@pytest.mark.parametrize("name", SPECIAL_NAMES) +def test_list_inside_passes_selected_id_and_client(fake_tm, name): + """PLAN71 条件 2: 一覧で選ぶと、prefix S と同じ引数で menu が呼ばれる。""" + tm = fake_tm + target = tm.new(name) + home = tm.new("zz-home") + me = tm.attach(home) + + _type_list_command(tm, home, "tmux-menu") + _pick_above(tm, me, home) + + _wait(lambda: tm.record.exists() and tm.record.read_text()) + assert tm.record.read_text().splitlines() == ["menu", "-c", me.tty, target] + + +@needs_tmux +@pytest.mark.parametrize("name", ["devbase-3", "it's"]) +def test_list_inside_menu_go_detaches_others_and_switches(ui_tm, name): + """PLAN71 条件 3: メニューの ``a`` で Enter を押した端末が移り、他の端末は外れる。""" + tm = ui_tm + target = tm.new(name) + home = tm.new("zz-home") + me = tm.attach(home) + stale = tm.attach(target) + + _type_list_command(tm, home, "tmux-menu") + _pick_above(tm, me, home) + _wait(lambda: "移る" in me.output()) + me.send("a") + + _wait(lambda: tm.clients_of(target) == {me.tty}) + assert stale.tty not in tm.all_clients() + + +def _spawn_outside(tm: TmuxEnv, home: str) -> Client: + """tmux の外で ``tmux-menu`` を起動し、``home`` に繋がって一覧が出るまで待つ。""" + me = tm.spawn([str(tm.bin / "tmux-menu")]) + _wait(lambda: tm.all_clients().get(me.tty) == home) + _wait(lambda: _pane_mode(tm, home) == "tree-mode") + return me + + +@needs_tmux +def test_list_outside_attaches_and_opens_tree(ui_tm): + """PLAN71 条件 4: tmux の外では、端末の無いセッションへ attach して一覧を出す。""" + tm = ui_tm + target = tm.new("devbase-3") + home = tm.new("zz-home") + tm.attach(target) + + _spawn_outside(tm, home) + + +@needs_tmux +@pytest.mark.parametrize("name", SPECIAL_NAMES) +def test_list_outside_passes_selected_id_and_client(fake_tm, name): + """PLAN71 条件 5: 外から開いた一覧で選ぶと、attach した端末と選んだ ID が渡る。""" + tm = fake_tm + target = tm.new(name) + home = tm.new("zz-home") + tm.attach(target) + + me = _spawn_outside(tm, home) + _pick_above(tm, me, home) + + _wait(lambda: tm.record.exists() and tm.record.read_text()) + assert tm.record.read_text().splitlines() == ["menu", "-c", me.tty, target] + + +@needs_tmux +@pytest.mark.parametrize("argv", [("tmux-menu",), ("tmux-session", "menu")]) +def test_list_no_server_exits_one(tm, argv): + """PLAN71 条件 6・8: サーバが無ければ、何も作らず 1 で終わる。""" + done = tm.run(*argv) + assert done.returncode == 1, (done.stdout, done.stderr) + assert "サーバ" in done.stderr + assert tm.sessions() == {} + + +@needs_tmux +@pytest.mark.parametrize("form", ["tmux-menu -c {tty} {sid}", + "tmux-session menu -c {tty} {sid}"]) +def test_menu_form_same_by_both_names(ui_tm, form): + """PLAN71 条件 8: メニューを出す形は、どちらの名前でも同じメニューを出す。 + + ``run-shell`` は文字列を ``sh -c`` へ渡すため、ID (``$0``) は引用して渡す。 + """ + tm = ui_tm + target = tm.new("devbase-3") + me = tm.attach(tm.new("zz-home")) + + tm.tmux("run-shell", "-b", form.format(tty=me.tty, sid=shlex.quote(target))) + + _wait(lambda: "中身を見る" in me.output()) + + +@needs_tmux +def test_list_opens_in_tmux_pane_not_latest_client(tm): + """PLAN71 条件 11: 一覧は ``TMUX_PANE`` の pane に出て、直近に操作された端末には出ない。 + + 後に繋いだ ``other`` の端末が直近になる (設計の実測 9a)。 + """ + home = tm.new("home") + other = tm.new("other") + tm.attach(home) + tm.attach(other) + + done = tm.run("tmux-menu", env=tm.inside_env(home)) + + assert done.returncode == 0, (done.stdout, done.stderr) + _wait(lambda: _pane_mode(tm, home) == "tree-mode") + assert _pane_mode(tm, other) == "" + + # --- 配布 (受け入れ条件 16 のうちビルドの前に分かる部分) --- From 8556fe6ba262cceb5c7327d8d43ede8c3399df74 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 26 Sep 2026 01:06:53 +0900 Subject: [PATCH 2/5] =?UTF-8?q?docs(PLAN71):=20=E7=A2=BA=E5=AE=9A=E4=BB=95?= =?UTF-8?q?=E6=A7=98=E3=81=AE=20prefix=20S=20=E3=81=A8=E7=9F=AD=E7=B8=AE?= =?UTF-8?q?=E5=90=8D=E3=82=92=20tmux-menu=20=E3=81=AE=E5=BD=A2=E3=81=AB?= =?UTF-8?q?=E5=90=88=E3=82=8F=E3=81=9B=E3=82=8B=20(#270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prefix S の割り当てを run-shell "TMUX_PANE=#{pane_id} tmux-menu" に、短縮名と ln -sf を 4 つに、一覧の定義が tmux-session にあることを書く。呼び出しの形・ 終了コード・確かめ方の旧定義も合わせる。 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/specifications/tmux-named-session.md | 54 ++++++++++++++++------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/docs/specifications/tmux-named-session.md b/docs/specifications/tmux-named-session.md index 0c67067..a3035b4 100644 --- a/docs/specifications/tmux-named-session.md +++ b/docs/specifications/tmux-named-session.md @@ -10,8 +10,9 @@ base イメージは、1 つの tmux セッションを名前か ID で指して - 落とす(`kill`): attach 中・実行中を問わずセッションを終わらせる tmux の中では `prefix S` でセッションの一覧(`choose-tree`)を出し、選んだセッションに同じ -3 つをメニューから行える。UI は tmux 組み込みの `choose-tree` / `display-menu` / -`display-popup` だけで組み、パッケージを足さない。 +3 つをメニューから行える。コマンド `tmux-menu`(`tmux-session menu`)からも同じ一覧とメニューを +開ける。tmux の外で打つと attach して、attach した画面に一覧を出す。UI は tmux 組み込みの +`choose-tree` / `display-menu` / `display-popup` だけで組み、パッケージを足さない。 `tmux-first` / `tmux-clean` は「同じベース名のセッション群」を、操作中の端末と実行中の セッションを守りながら整理する道具である。`tmux-session` は 1 つを狙って強制的に効かせる道具で、 @@ -24,14 +25,15 @@ tmux の中では `prefix S` でセッションの一覧(`choose-tree`)を ## 対象範囲 -- コマンド `tmux-session` と短縮名 `tmux-go` / `tmux-peek` / `tmux-kill` +- コマンド `tmux-session` と短縮名 `tmux-go` / `tmux-peek` / `tmux-kill` / `tmux-menu` - `/etc/tmux.conf` の `prefix S` の割り当て - base と、base を継ぐ派生イメージへの伝播の規則 - `containers/lfm` と `containers/snapshot` は base を継がないため対象に含まない - ホストの `~/.local/bin` と `~/.tmux.conf` へ配る仕組みは持たない。devbase はホストの利用者の ファイルへ書かず(`~/.tmux.conf` を書き換えると、消したつもりの行が戻るなどの食い違いを 生む)、手順を利用者向け文書に書いて利用者が置く -- tmux の外で一覧から選ぶ UI は持たない。名指しの 3 操作は tmux の外からもコマンドで使える +- tmux の外で、attach せずに一覧から選ぶ UI は持たない。`tmux-menu` は tmux の外では attach して + 一覧を出す。名指しの 3 操作は tmux の外からもコマンドで使える - 読み取り専用の attach(`tmux attach -r`)の操作は持たない。`choose-tree` のプレビューが 同じ用途を満たす @@ -40,9 +42,9 @@ tmux の中では `prefix S` でセッションの一覧(`choose-tree`)を | 要素 | 置き場所 | 責務 | | --- | --- | --- | | 本体 | `containers/base/tmux-session` → `/usr/local/bin/tmux-session` | サブコマンド `go` / `peek` / `kill` / `menu` を持つ POSIX sh(`#!/bin/sh`、`set -eu`)の 1 ファイル | -| 短縮名 | `/usr/local/bin/tmux-go` / `tmux-peek` / `tmux-kill` | `tmux-session` への symlink。呼ばれた名前でサブコマンドが決まる | -| 導入 | `containers/base/Dockerfile` の「tmux セッションの整理コマンド」の節 | `COPY --chmod=0755 tmux-session /usr/local/bin/tmux-session` と、`tmux1` / `tmuxc` と同じ `RUN` の `ln -sf` 3 つ | -| キーの割り当て | `containers/base/tmux.conf` の末尾 → `/etc/tmux.conf` | `bind-key S choose-tree … "tmux-session menu …"` の 1 行 | +| 短縮名 | `/usr/local/bin/tmux-go` / `tmux-peek` / `tmux-kill` / `tmux-menu` | `tmux-session` への symlink。呼ばれた名前でサブコマンドが決まる | +| 導入 | `containers/base/Dockerfile` の「tmux セッションの整理コマンド」の節 | `COPY --chmod=0755 tmux-session /usr/local/bin/tmux-session` と、`tmux1` / `tmuxc` と同じ `RUN` の `ln -sf` 4 つ | +| キーの割り当て | `containers/base/tmux.conf` の末尾 → `/etc/tmux.conf` | `bind-key S run-shell "TMUX_PANE=#{pane_id} tmux-menu"` の 1 行。一覧の定義(`choose-tree` と template)は `tmux-session` が持つ | | 静的検査 | `.github/workflows/ci.yml` の `shellcheck` ジョブ | `containers/base/tmux-first` / `tmux-clean` / `tmux-session` を既定の severity(style まで)で検査する | | 回帰テスト | `tests/containers/test_tmux_session.py`、`tests/containers/test_tmux_conf.py` | 専用の tmux サーバでの振る舞いと、Dockerfile・tmux.conf の形を固定する | @@ -62,19 +64,22 @@ tmux の中では `prefix S` でセッションの一覧(`choose-tree`)を tmux-session go [-c 端末] <セッション> tmux-session peek [-n 行数] <セッション> tmux-session kill [-n] [-f] [-c 端末] <セッション>... +tmux-session menu tmux-session menu -c 端末 <セッション> tmux-go … = tmux-session go … tmux-peek … = tmux-session peek … tmux-kill … = tmux-session kill … +tmux-menu … = tmux-session menu … ``` -- `basename "$0"` が `tmux-go` / `tmux-peek` / `tmux-kill` なら第 1 引数をサブコマンドとして - 読まない。それ以外の名前で呼ばれたときは第 1 引数をサブコマンドとして読む +- `basename "$0"` が `tmux-go` / `tmux-peek` / `tmux-kill` / `tmux-menu` なら第 1 引数を + サブコマンドとして読まない。それ以外の名前で呼ばれたときは第 1 引数をサブコマンドとして読む - `-h` / `--help` はどのサブコマンドでも使い方を標準出力へ出して終了コード 0 で終わる - `kill` の `-n` は `--dry-run`、`-f` は `--force` とも書ける。`--` でオプションの終わりを示せる - オプションはサブコマンドごとに受け付けるものが決まっている。`peek` は `-c` を、`go` は `-n` を 受け取らない(知らないオプションとして終了コード 2) -- `go` / `peek` / `menu` はセッションをちょうど 1 つ、`kill` は 1 つ以上取る +- `go` / `peek` / `menu` はセッションをちょうど 1 つ、`kill` は 1 つ以上取る。ただし `menu` は + セッションも `-c` も無いときに限り一覧を開く形になる。どちらか一方でもあればメニューを出す形として読む - `-n 行数` は 0 以上の整数だけを受け付ける。既定は 20 ### セッションの指し方 @@ -182,9 +187,25 @@ screen (0.0 の直近 20 行) `/etc/tmux.conf` の割り当ては次の 1 行である。 ```tmux -bind-key S choose-tree -Zs -O name "run-shell -t \"%%%\" \"tmux-session menu -c #{q:client_name} #{q:session_id}\"" +bind-key S run-shell "TMUX_PANE=#{pane_id} tmux-menu" ``` +一覧の定義は `tmux-session` の 1 か所にあり、`prefix S` とコマンド `tmux-menu`(`tmux-session menu`)が +共有する。`run-shell` は渡した文字列の `#{…}` を先に展開するため、template を割り当てへ直接書かずに +コマンドを呼ぶ。一覧を開く形の `tmux-session` は次を実行する。 + +```sh +TREE_TEMPLATE='run-shell -t "%%%" "tmux-session menu -c #{q:client_name} #{q:session_id}"' +tmux choose-tree -t "$TMUX_PANE" -Zs -O name "$TREE_TEMPLATE" # TMUX と TMUX_PANE がある +tmux choose-tree -Zs -O name "$TREE_TEMPLATE" # TMUX があり TMUX_PANE が空 +tmux attach-session \; choose-tree -Zs -O name "$TREE_TEMPLATE" # tmux の外 +``` + +- 一覧を出す pane は `TMUX_PANE` で決める。`run-shell` の中には `TMUX_PANE` が無く、`-t` が無いと + 直近に操作された別の端末に出ることがあるため、`prefix S` は押した pane の ID を `TMUX_PANE` で渡す +- tmux の外での attach 先は tmux の既定(端末の繋がっていないセッションを優先し、その中で直近のもの) + に従い、attach した画面に一覧を出す。tmux のサーバが動いていなければ、サーバもセッションも作らずに + 終了コード 1 で終わる - `choose-tree -Zs -O name` は、名前順のセッションの一覧を全画面で出す。tmux の既定の操作 (`v` でプレビューの切り替え、`f` で絞り込み、`x` で 1 つ落とす、`t` で印を付けて `X` で まとめて落とす)はそのまま使える @@ -235,9 +256,12 @@ tmux display-menu -c 端末 -t '$ID' -T '#[align=centre]#{session_name}' … sequenceDiagram participant U as 利用者の端末 participant T as tmux サーバ + participant L as tmux-menu(一覧) participant M as tmux-session menu participant G as tmux-session go U->>T: prefix S + T->>L: run-shell
TMUX_PANE=pane tmux-menu + L->>T: choose-tree -t pane T->>U: choose-tree(一覧とプレビュー) U->>T: セッションを選んで Enter T->>M: run-shell -t "%%%"
menu -c 端末 $ID @@ -260,7 +284,7 @@ sequenceDiagram | --- | --- | | 0 | 成功。`-h` / `--help` | | 1 | tmux が無い・サーバが無い・対象のセッションが無い・`-c` の端末が無い・実行元を特定できない・自分のセッションを `-f` なしで `kill` しようとした・tmux のコマンドが失敗した | -| 2 | 使い方の誤り(サブコマンドが無い・知らないサブコマンドとオプション・オプションの値が無い・セッションの数が合わない・`-n` が 0 以上の整数でない・`-c` の値の形が外れた・`menu` に `-c` が無い) | +| 2 | 使い方の誤り(サブコマンドが無い・知らないサブコマンドとオプション・オプションの値が無い・セッションの数が合わない・`-n` が 0 以上の整数でない・`-c` の値の形が外れた・メニューを出す形の `menu` に `-c` が無い) | 誤りは理由を標準エラーへ出す。終了コード 2 のときは `使い方は <呼ばれた名前> -h` を添える。 @@ -333,14 +357,14 @@ sequenceDiagram あること - `prefix S` - `containers/base/tmux.conf` を読んだ tmux の `list-keys -T prefix` に `S` の割り当てがちょうど - 1 つあり、`choose-tree` を呼ぶこと(`test_tmux_conf.py`) + 1 つあり、`run-shell "TMUX_PANE=#{pane_id} tmux-menu"` であること(`test_tmux_conf.py`) - 引数を書き出すだけの偽の `tmux-session` を `PATH` の先頭に置き、`pty` から `C-b S` → 選択 → Enter を送ると、選んだセッションの ID と押した端末の名前が渡ること。特殊文字の名前でも同じこと - 本物の `tmux-session` で、メニューの「移る」「中身を見る」「落とす」(確認の `y`)がそれぞれ 効き、今いるセッションも同意すれば落ちること。背景の `run-shell` からの知らせが端末へ届くこと - 配布 - Dockerfile に `COPY --chmod=0755 tmux-session /usr/local/bin/tmux-session` が 1 行あり、 - 短縮名 3 つの `ln -sf tmux-session /usr/local/bin/<名前>` があること + 短縮名 4 つの `ln -sf tmux-session /usr/local/bin/<名前>` があること - `containers/base/tmux-session` が `#!/bin/sh` で始まり実行権を持つこと `test_tmux_conf.py` の copy-mode の割り当ての比較は `-T copy-mode` / `-T copy-mode-vi` の行だけを @@ -349,7 +373,7 @@ sequenceDiagram CI はイメージを建てないため、次は建てたイメージで手で確かめる。 - `docker run --rm --entrypoint /bin/bash devbase-base:latest -c 'ls -l /usr/local/bin/tmux-*; tmux-go -h; echo exit=$?'` - で 4 つのコマンドがあり、`tmux-go -h` が終了コード 0 で終わること + で `tmux-session` と短縮名 4 つのコマンドがあり、`tmux-go -h` が終了コード 0 で終わること - 建てた base の `shellcheck` で `containers/base/tmux-session` を既定の severity で検査すると、 指摘が 0 件であること - 建て直した base のコンテナの tmux で、`prefix S` のメニューの 3 つの操作が効くこと From 74c93d4df7d67def41e16b6410cc4593a6f6ed9d Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 26 Sep 2026 01:06:53 +0900 Subject: [PATCH 3/5] =?UTF-8?q?test(PLAN71):=20TMUX=5FPANE=20=E3=81=8C?= =?UTF-8?q?=E7=A9=BA=E3=81=AE=E3=81=A8=E3=81=8D=20-t=20=E3=81=AA=E3=81=97?= =?UTF-8?q?=E3=81=AE=E4=B8=80=E8=A6=A7=E3=81=B8=E8=90=BD=E3=81=A1=E3=81=A6?= =?UTF-8?q?=E9=96=8B=E3=81=8F=E3=81=93=E3=81=A8=E3=82=92=E7=A2=BA=E3=81=8B?= =?UTF-8?q?=E3=82=81=E3=82=8B=20(#270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/containers/test_tmux_session.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/containers/test_tmux_session.py b/tests/containers/test_tmux_session.py index 2c256c6..f6136b5 100644 --- a/tests/containers/test_tmux_session.py +++ b/tests/containers/test_tmux_session.py @@ -1252,6 +1252,22 @@ def test_list_opens_in_tmux_pane_not_latest_client(tm): assert _pane_mode(tm, other) == "" +@needs_tmux +def test_list_inside_without_tmux_pane_opens_tree(tm): + """PLAN71: ``TMUX`` があり ``TMUX_PANE`` が空なら、``-t`` なしの一覧へ落ちて開く。 + + 端末が 1 つだけ繋がっていれば、その端末の pane に一覧が出る。 + """ + home = tm.new("home") + tm.attach(home) + env = {k: v for k, v in tm.inside_env(home).items() if k != "TMUX_PANE"} + + done = tm.run("tmux-menu", env=env) + + assert done.returncode == 0, (done.stdout, done.stderr) + _wait(lambda: _pane_mode(tm, home) == "tree-mode") + + # --- 配布 (受け入れ条件 16 のうちビルドの前に分かる部分) --- From 9371822c8ff3b62455c2c295d16ad904b56b6ba6 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 26 Sep 2026 01:15:36 +0900 Subject: [PATCH 4/5] =?UTF-8?q?docs(PLAN71):=20=E7=A2=BA=E5=AE=9A=E4=BB=95?= =?UTF-8?q?=E6=A7=98=E3=81=AE=E5=9B=9E=E5=B8=B0=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=A8=E6=89=8B=E3=81=AE=E7=A2=BA=E8=AA=8D=E3=81=AB=20tmux-m?= =?UTF-8?q?enu=20=E3=81=AE=E4=B8=80=E8=A6=A7=E3=82=92=E8=B6=B3=E3=81=99=20?= =?UTF-8?q?(#270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/specifications/tmux-named-session.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/specifications/tmux-named-session.md b/docs/specifications/tmux-named-session.md index a3035b4..0670bbd 100644 --- a/docs/specifications/tmux-named-session.md +++ b/docs/specifications/tmux-named-session.md @@ -362,6 +362,23 @@ sequenceDiagram Enter を送ると、選んだセッションの ID と押した端末の名前が渡ること。特殊文字の名前でも同じこと - 本物の `tmux-session` で、メニューの「移る」「中身を見る」「落とす」(確認の `y`)がそれぞれ 効き、今いるセッションも同意すれば落ちること。背景の `run-shell` からの知らせが端末へ届くこと +- `tmux-menu`(一覧を開く形) + - tmux の中で `tmux-menu` と `tmux-session menu` のどちらをプロンプトから打っても、その pane に + 一覧(`tree-mode`)が出ること + - `TMUX_PANE` の pane に一覧が出て、後から attach して直近に操作された端末の pane には出ないこと。 + `TMUX` があり `TMUX_PANE` を消した環境でも、端末が 1 つだけなら、その端末の pane に一覧が出て + 終了コード 0 であること + - tmux の外で打つと、端末の繋がっていないセッションへ attach して一覧を出すこと + - 偽の `tmux-session` で、中・外のどちらで開いた一覧でも、選んで Enter を押すと + `menu -c <選んだ ID>` が渡ること。特殊文字の名前でも同じこと + - 本物の `tmux-session` で、一覧から出したメニューの「移る」で Enter を押した端末が移り、対象に + 繋がっていた他の端末が外れること + - メニューを出す形(`-c 端末 `)は、`tmux-menu` と `tmux-session menu` のどちらでもメニューを + 出すこと + - サーバが無いと、`tmux-menu` と `tmux-session menu` のどちらも終了コード 1 で標準エラーへ理由を + 出し、セッションを作らないこと + - `tmux-menu -h` が終了コード 0 で、`tmux-session -h` の使い方に `tmux-menu` が載ること。`-c` だけで + セッションが無い・`-c` が無い・余分な引数・知らないオプションは終了コード 2 であること - 配布 - Dockerfile に `COPY --chmod=0755 tmux-session /usr/local/bin/tmux-session` が 1 行あり、 短縮名 4 つの `ln -sf tmux-session /usr/local/bin/<名前>` があること @@ -377,6 +394,7 @@ CI はイメージを建てないため、次は建てたイメージで手で - 建てた base の `shellcheck` で `containers/base/tmux-session` を既定の severity で検査すると、 指摘が 0 件であること - 建て直した base のコンテナの tmux で、`prefix S` のメニューの 3 つの操作が効くこと +- 建て直した base のコンテナで `tmux-menu` を打つと、セッションの一覧が開くこと CI の ShellCheck ジョブは runner の shellcheck で `tmux-first` / `tmux-clean` / `tmux-session` を 検査する。 From e847c9fc04ad2622d0d70a9335edd47f642780dc Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 26 Sep 2026 01:27:55 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Docs:=20tmux-named-session.md=20=E3=82=92?= =?UTF-8?q?=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/specifications/tmux-named-session.md | 29 +- issues/PLAN71_tmux-menu-design.md | 354 ---------------------- issues/PLAN71_tmux-menu-measure.py | 249 --------------- issues/PLAN71_tmux-menu.md | 137 --------- 4 files changed, 24 insertions(+), 745 deletions(-) delete mode 100644 issues/PLAN71_tmux-menu-design.md delete mode 100755 issues/PLAN71_tmux-menu-measure.py delete mode 100644 issues/PLAN71_tmux-menu.md diff --git a/docs/specifications/tmux-named-session.md b/docs/specifications/tmux-named-session.md index 0670bbd..15677e1 100644 --- a/docs/specifications/tmux-named-session.md +++ b/docs/specifications/tmux-named-session.md @@ -82,6 +82,11 @@ tmux-menu … = tmux-session menu … セッションも `-c` も無いときに限り一覧を開く形になる。どちらか一方でもあればメニューを出す形として読む - `-n 行数` は 0 以上の整数だけを受け付ける。既定は 20 +`tmux-menu` を `menu` の短縮名にし、一覧を開く動きを「セッションも `-c` も無い形」に割り当てるのは、 +`tmux-go` = `go` と同じ規則で名前から動きが読め、メニューを出す形(`menu -c 端末 <セッション>`)の +呼び出し元を何も変えずに済むためである。メニューを出す形は、`/etc/tmux.conf` の一覧の template と、 +利用者がホストの `~/.tmux.conf` へ写した以前の割り当ての行が呼ぶため、形を変えない。 + ### セッションの指し方 | 引数の形 | 解決 | @@ -203,9 +208,15 @@ tmux attach-session \; choose-tree -Zs -O name "$TREE_TEMPLATE" # tmux の外 - 一覧を出す pane は `TMUX_PANE` で決める。`run-shell` の中には `TMUX_PANE` が無く、`-t` が無いと 直近に操作された別の端末に出ることがあるため、`prefix S` は押した pane の ID を `TMUX_PANE` で渡す -- tmux の外での attach 先は tmux の既定(端末の繋がっていないセッションを優先し、その中で直近のもの) - に従い、attach した画面に一覧を出す。tmux のサーバが動いていなければ、サーバもセッションも作らずに - 終了コード 1 で終わる + (`run-shell` は `#{pane_id}` を先に展開し、`%3` の形の引用の要らない ID が渡る) +- tmux は、端末を持たないクライアントから来たコマンドの現在の pane を、その環境の `TMUX_PANE` で + 決める。そのため `-t` が無くても `TMUX_PANE` の pane に出る。それでも `-t "$TMUX_PANE"` を付けるのは、 + 出す pane をコマンドの行に書いて読めるようにし、tmux が環境から現在の pane を引く規則に頼らない + ためである。`-t` の有無で振る舞いは変わらない +- tmux の外での attach 先は tmux の既定(端末の繋がっていないセッションを優先し、その中で直近のもの。 + すべてに端末が繋がっていれば全体で直近のもの)に従い、attach した画面に一覧を出す。attach 先を + 引数で取らないのは、名指しで移るなら `tmux-go` があり、一覧からどのセッションへも移れるためである。 + tmux のサーバが動いていなければ、サーバもセッションも作らずに終了コード 1 で終わる - `choose-tree -Zs -O name` は、名前順のセッションの一覧を全画面で出す。tmux の既定の操作 (`v` でプレビューの切り替え、`f` で絞り込み、`x` で 1 つ落とす、`t` で印を付けて `X` で まとめて落とす)はそのまま使える @@ -316,8 +327,11 @@ sequenceDiagram (コマンドが `PATH` に無ければメニューは動かない) - `containers/lfm` と `containers/snapshot` は base を継がないため入らない - ホストの tmux で使うときは、利用者が devbase の checkout の `containers/base/tmux-session` を - 指す symlink を `~/.local/bin` へ 4 つ張り、`~/.tmux.conf` へ上の 1 行を足す。複写ではなく - symlink にすると `git pull` で更新が届く + 指す symlink を `~/.local/bin` へ 5 つ(`tmux-session` と短縮名 4 つ)張り、`~/.tmux.conf` へ上の + 1 行を足す。複写ではなく symlink にすると `git pull` で更新が届く +- ホストの `~/.tmux.conf` に以前の割り当て(`bind-key S choose-tree …` で template を直接書く行)を + 残していても、`prefix S` はメニューを出す形を呼ぶためそのまま動く。`tmux-menu` を使うには symlink の + `tmux-menu` を足す - 動作を確かめてある tmux は、コンテナの 3.6(Ubuntu 26.04)とホストの 3.7b。使う機能 (`choose-tree` の template・`display-menu`(3.0 以降)・`display-popup`(3.2 以降)・ 書式の `q:`)は両方にある。`/bin/sh` は Ubuntu の dash と macOS の bash 3.2(POSIX モード)で @@ -384,6 +398,11 @@ sequenceDiagram 短縮名 4 つの `ln -sf tmux-session /usr/local/bin/<名前>` があること - `containers/base/tmux-session` が `#!/bin/sh` で始まり実行権を持つこと +一覧を開いて選ぶテストは、コマンド行とカーソルの移動を `send-keys` で pane へ送り、Enter だけは +繋いだ端末から送る。`send-keys` で送った Enter では、template の `#{client_name}` が押した端末ではなく +直近に操作された端末に展開され、渡った端末名を確かめられないためである。後から attach した端末は、 +打たなくても直近に操作された端末になる。 + `test_tmux_conf.py` の copy-mode の割り当ての比較は `-T copy-mode` / `-T copy-mode-vi` の行だけを 対象にし、`prefix S` の行が混ざっても崩れない。 diff --git a/issues/PLAN71_tmux-menu-design.md b/issues/PLAN71_tmux-menu-design.md deleted file mode 100644 index 010a7cb..0000000 --- a/issues/PLAN71_tmux-menu-design.md +++ /dev/null @@ -1,354 +0,0 @@ -# PLAN71: prefix S のセッションの一覧とメニューをコマンド tmux-menu からも開く の設計 - -要求と受け入れ条件は [PLAN71_tmux-menu.md](PLAN71_tmux-menu.md) にある。この文書は -「どう作るか」だけを扱う。土台の `tmux-session` の設計は PLAN69 にある。確定仕様は -[docs/specifications/tmux-named-session.md](../docs/specifications/tmux-named-session.md) である。 - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | tmux の中で `tmux-menu` を打つと、今の pane にセッションの一覧を出す。選ぶと `prefix S` と同じメニューが出る | tmux の中にいる利用者 | -| F2 | tmux の外で `tmux-menu` を打つと、attach と同時に一覧を出す | tmux の外にいる利用者 | -| F3 | `prefix S` を F1 と同じ定義で開く(一覧を開く定義を 1 か所にする) | tmux の中にいる利用者 | -| F4 | F1〜F3 を base イメージへ入れ、ホストで使う手順を示す | base を建てる利用者と、ホストの tmux の利用者 | - -## 構成要素 - -| 要素 | 変更 | 責務 | -| --- | --- | --- | -| `containers/base/tmux-session` | 変える | 呼ばれた名前 `tmux-menu` を `menu` へ振り分ける。`menu` がセッションを受け取らないときは一覧を開く(決定 1)。一覧を開く `choose-tree` の定義をこのファイルだけに持つ(決定 2) | -| `containers/base/tmux.conf` | 変える | `prefix S` の行を、キーを押した pane の ID を渡して `run-shell` で `tmux-menu` を呼ぶ形に変える(決定 2・決定 4) | -| `containers/base/Dockerfile` の「tmux セッションの整理コマンド」の節 | 変える | symlink の `RUN` に `tmux-menu` を 1 つ足す | -| `tests/containers/test_tmux_session.py` | 変える | F1・F2・F3 と、`menu -c 端末 <セッション>` の形が変わらないことを確かめる。`SHORT_NAMES` に `tmux-menu` を足す(下の「テスト基盤の更新」) | -| `tests/containers/test_tmux_conf.py` | 変える | 既存の `test_prefix_s_opens_session_chooser` を書き換え、`prefix S` の割り当てが `TMUX_PANE=#{pane_id}` を付けて `tmux-menu` を呼ぶことを `list-keys` で確かめる | -| `docs/user/environment-variables.md` の「セッションを名指しで扱う」 | 変える | `tmux-menu` の使い方を足す。ホストの手順の symlink を 5 つにし、`~/.tmux.conf` の行を新しい割り当てへ差し替える | -| `docs/specifications/tmux-named-session.md` | 変える(確定仕様化で) | `menu` の 2 つの形と `tmux-menu`、`prefix S` の新しい行 | -| `issues/PLAN71_tmux-menu-measure.py` | 足す(この設計で) | 「実測」の再現用スクリプト。確定仕様化のときに消す | -| `CHANGELOG.md` | 変える | `[Unreleased]` の `### Added` に足す。反映に `devbase build base --no-cache` が要ることを書く | - -次のものは変えない。 - -- `tmux-session` の `go` / `peek` / `kill` と、`menu -c 端末 <セッション>` が出すメニューの中身 -- `containers/base/tmux-first` / `containers/base/tmux-clean` -- `prefix s`(tmux の既定の `choose-tree -Zs`) - -## 決定の記録 - -### 決定 1: `tmux-menu` は `menu` サブコマンドの短縮名にし、セッションを受け取らない形を一覧を開く動きにする - -利用者が決めた名前 `tmux-menu` と、サブコマンドの名前 `menu` がそろう。`tmux-go` = `go` と同じ -規則で読める。今の `menu -c 端末 <セッション>` はセッションを必ず受け取るため、受け取らない -形は空いている。空いた形に一覧を割り当てれば、今の形の呼び出し元(今の `/etc/tmux.conf` と -ホストへ写した行)は何も変えずに動く。 - -新しいサブコマンド(`ui` など)を足して `tmux-menu` をそちらへ振り分ける案は採らない。 -`tmux-menu` と `menu` が別の動きになり、名前から動きが読めない。今の `menu` を別名へ改名する -案も採らない。ホストへ写した `~/.tmux.conf` の行が壊れる。 - -### 決定 2: 一覧を開く定義は `tmux-session` だけに持ち、`prefix S` は `run-shell` で `tmux-menu` を呼ぶ - -`prefix S` と `tmux-menu` が同じ一覧を開くことを、定義を 1 つにして保つ。2 か所に同じ template を -持つと、片方だけを直したときに 2 つの入口の動きが分かれる。実測のとおり、template を -`run-shell` の文字列へ直接書く形は `#{…}` の先の展開で壊れる。スクリプトの中に置けば -`run-shell` の展開を通らない。ホストの手順も `~/.tmux.conf` の 1 行が短くなる。 - -`prefix S` の行を今のまま残し、`tmux-session` にも同じ template を書く案は採らない。一致を -テストで縛ることはできるが、同じ文字列を 2 か所で持つこと自体が残る。 - -### 決定 3: tmux の外では attach 先を引数で取らない - -名前の決まったセッションへ行くなら `tmux-go` がある。`tmux-menu` は「まず一覧を見る」入口で、 -attach 先は tmux の既定(**繋がっている端末の無いセッションを優先し、その中で直近に使ったもの**。 -すべてに端末が繋がっていれば全体で直近に使ったもの)で足りる。一覧からどのセッションへも移れる。 -セッションを引数で取ると、決定 1 の「セッションも `-c` も受け取らない形だけが一覧を開く」規則が崩れる。 - -### 決定 4: `prefix S` はキーを押した pane の ID を `TMUX_PANE` で渡し、`choose-tree` の `-t` に使う - -`run-shell` から起動したシェルには `TMUX_PANE` が無い。`-t` の無い `choose-tree` は、呼び出し元の -pane を継がず、全セッションから直近に操作されたセッションを選び直す。`prefix S` を押してから -`choose-tree` が走るまでの間に別のセッションの端末が操作されると、一覧がその端末に出る(実測の 5)。 -`run-shell` は渡された文字列の `#{…}` を先に展開するため、`TMUX_PANE=#{pane_id} tmux-menu` と -書けば押した pane の ID(`%3` の形。引用の要らない文字だけ)が渡る(実測の 6)。 - -一覧を出す pane を決めるのは `TMUX_PANE` そのものである。tmux は、端末を持たないクライアントから -来たコマンドの現在の pane を、そのクライアントの環境の `TMUX_PANE` で決める(tmux の `cmd-find.c` -の `cmd_find_inside_pane`)。`-t` の無い `choose-tree` でも、`TMUX_PANE` があればその pane に出る -(実測の 7a)。それでも `tmux-menu` は `TMUX_PANE` を `-t` にも渡す。どの pane に出すかをコマンドの -行に書いて意図を読めるようにし、tmux が環境から現在の pane を引く規則に頼らないためである。 -`-t` は保険であり、`-t` の有無で振る舞いは変わらない(実測の 7a・7b)。 - -`tmux-menu` に `-t` のオプションを足して ID を渡す案は採らない。`menu` の受け付ける形が増え、 -tmux の中でコマンドを打つ形(`TMUX_PANE` がシェルにある)と同じ入口に揃わない。 - -## 実測(2026-09-24、ホストの tmux 3.7b) - -再現用のスクリプトは [PLAN71_tmux-menu-measure.py](PLAN71_tmux-menu-measure.py) にある -(`python3 issues/PLAN71_tmux-menu-measure.py`。一時ディレクトリのソケットでサーバを立て、利用者の -サーバに触れない)。`python3` の `pty` で端末を繋ぎ、`tmux-session` の代わりに引数を書き出すだけの -偽物を置く。一覧を開くスクリプト `open-list` は `tmux-menu` の代わりで、`TMUX_PANE` があれば -`-t "$TMUX_PANE"` を付けて次の `