aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xdotfiles/common/.local/bin/tmux-util39
-rwxr-xr-xtests/tmux-util/fake-fzf30
-rw-r--r--tests/tmux-util/test_tmux_util.py74
3 files changed, 139 insertions, 4 deletions
diff --git a/dotfiles/common/.local/bin/tmux-util b/dotfiles/common/.local/bin/tmux-util
index 5e0ce85..4669b03 100755
--- a/dotfiles/common/.local/bin/tmux-util
+++ b/dotfiles/common/.local/bin/tmux-util
@@ -36,6 +36,40 @@ EOF
}
# -----------------------------------------------------------------------------
+# pick
+# -----------------------------------------------------------------------------
+
+cmd_pick() {
+ local sessions
+ sessions=$(tmux list-sessions \
+ -F '#{session_attached} #{session_name}' 2>/dev/null || true)
+ if [ -z "$sessions" ]; then
+ echo "No tmux sessions to pick from."
+ return 0
+ fi
+
+ # Format for fzf: "<state> <name>" — name is the second field of the
+ # selection line, which we recover after fzf returns.
+ local choice
+ choice=$(echo "$sessions" | awk '{
+ state = ($1 == "1") ? "attached" : "detached"
+ printf "%-9s %s\n", state, $2
+ }' | fzf --header="Pick a tmux session" | awk '{print $2}')
+
+ if [ -z "$choice" ]; then
+ # User cancelled (Ctrl-C or Esc); fzf exits 130, the pipeline
+ # returns empty.
+ return 0
+ fi
+
+ if [ -n "${TMUX:-}" ]; then
+ tmux switch-client -t "$choice"
+ else
+ tmux attach-session -t "$choice"
+ fi
+}
+
+# -----------------------------------------------------------------------------
# find
# -----------------------------------------------------------------------------
@@ -204,7 +238,10 @@ main() {
find)
cmd_find "$@"
;;
- pick|rename)
+ pick)
+ cmd_pick "$@"
+ ;;
+ rename)
echo "tmux-util: subcommand '$sub' is not implemented yet" >&2
return 2
;;
diff --git a/tests/tmux-util/fake-fzf b/tests/tmux-util/fake-fzf
new file mode 100755
index 0000000..476bf7f
--- /dev/null
+++ b/tests/tmux-util/fake-fzf
@@ -0,0 +1,30 @@
+#!/bin/sh
+# Fake fzf for testing tmux-util.
+#
+# Control via env vars:
+# FAKE_FZF_CHOICE_LINE — 1-based line index into stdin; that line is
+# printed as the selection and exit 0.
+# FAKE_FZF_CHOICE — print this literal string as the selection and
+# exit 0 (ignored if FAKE_FZF_CHOICE_LINE is set).
+# (neither set) — exit 130 (user cancelled).
+#
+# Every invocation is logged to $FAKE_TMUX_DIR/calls.log as one line.
+
+: "${FAKE_TMUX_DIR:?FAKE_TMUX_DIR must be set}"
+printf 'fzf %s\n' "$*" >> "$FAKE_TMUX_DIR/calls.log"
+
+if [ -n "${FAKE_FZF_CHOICE_LINE:-}" ]; then
+ awk -v n="$FAKE_FZF_CHOICE_LINE" 'NR == n { print; exit }'
+ exit 0
+fi
+
+if [ -n "${FAKE_FZF_CHOICE:-}" ]; then
+ # Drain stdin so the pipe upstream doesn't SIGPIPE
+ cat >/dev/null
+ printf '%s\n' "$FAKE_FZF_CHOICE"
+ exit 0
+fi
+
+# Cancelled
+cat >/dev/null
+exit 130
diff --git a/tests/tmux-util/test_tmux_util.py b/tests/tmux-util/test_tmux_util.py
index e313af6..afc1ebc 100644
--- a/tests/tmux-util/test_tmux_util.py
+++ b/tests/tmux-util/test_tmux_util.py
@@ -33,7 +33,7 @@ class TmuxUtilHarness(unittest.TestCase):
# bin dir with the fakes symlinked under their canonical names
self.bin_dir = os.path.join(self.tmp, "bin")
os.makedirs(self.bin_dir)
- for name in ("tmux", "kill", "sleep"):
+ for name in ("tmux", "kill", "sleep", "fzf"):
fake = os.path.join(FAKES_DIR, f"fake-{name}")
os.chmod(fake, os.stat(fake).st_mode | stat.S_IEXEC)
os.symlink(fake, os.path.join(self.bin_dir, name))
@@ -128,8 +128,8 @@ class TestDispatch(TmuxUtilHarness):
self.assertIn("Usage: tmux-util", result.stderr)
def test_unimplemented_subcommand_exits_nonzero(self):
- # go/pick/find/rename stub out for now; pick one to confirm the stub path.
- result = self.run_script("pick")
+ # rename stubs out for now; pick one to confirm the stub path.
+ result = self.run_script("rename")
self.assertNotEqual(result.returncode, 0)
self.assertIn("not implemented yet", result.stderr)
@@ -549,5 +549,73 @@ class TestFindBoundary(TmuxUtilHarness):
self.assertIn("missing pattern", result.stderr)
+# -----------------------------------------------------------------------------
+# pick — Normal cases
+# -----------------------------------------------------------------------------
+
+class TestPickNormal(TmuxUtilHarness):
+
+ def test_pick_attaches_chosen_session_outside_tmux(self):
+ self.set_sessions([
+ ("foo", 1, ["101:zsh"], 950, 1, "/tmp/foo"),
+ ("bar", 0, ["201:zsh"], 900, 1, "/tmp/bar"),
+ ])
+ # Pick the second line (bar)
+ env = {k: v for k, v in os.environ.items() if k != "TMUX"}
+ result = subprocess.run(
+ [SCRIPT, "pick"],
+ env={**env, "PATH": self.bin_dir + os.pathsep + env.get("PATH", ""),
+ "FAKE_TMUX_DIR": self.tmp, "FAKE_FZF_CHOICE_LINE": "2"},
+ capture_output=True, text=True, timeout=10,
+ )
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
+ calls = self.tmux_calls()
+ self.assertTrue(any("attach-session -t bar" in c for c in calls),
+ f"expected attach-session -t bar in {calls!r}")
+
+ def test_pick_switches_client_inside_tmux(self):
+ self.set_sessions([
+ ("foo", 1, ["101:zsh"], 950, 1, "/tmp/foo"),
+ ("bar", 0, ["201:zsh"], 900, 1, "/tmp/bar"),
+ ])
+ result = self.run_script("pick", env_extra={
+ "TMUX": "/tmp/fake-tmux-socket,1234,0",
+ "FAKE_FZF_CHOICE_LINE": "1", # first line → foo
+ })
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
+ calls = self.tmux_calls()
+ self.assertTrue(any("switch-client -t foo" in c for c in calls),
+ f"expected switch-client -t foo in {calls!r}")
+ self.assertFalse(any("attach-session" in c for c in calls),
+ f"unexpected attach-session in {calls!r}")
+
+
+# -----------------------------------------------------------------------------
+# pick — Boundary cases
+# -----------------------------------------------------------------------------
+
+class TestPickBoundary(TmuxUtilHarness):
+
+ def test_pick_no_sessions_prints_message(self):
+ self.set_sessions([])
+ result = self.run_script("pick")
+ self.assertEqual(result.returncode, 0)
+ self.assertIn("No tmux sessions", result.stdout)
+ # fzf should never have been invoked
+ self.assertFalse(any(c.startswith("fzf ") for c in self.tmux_calls()),
+ f"unexpected fzf call in {self.tmux_calls()!r}")
+
+ def test_pick_user_cancels_fzf_returns_zero_no_attach(self):
+ self.set_sessions([
+ ("foo", 1, ["101:zsh"], 950, 1, "/tmp/foo"),
+ ])
+ # No FAKE_FZF_CHOICE / FAKE_FZF_CHOICE_LINE → fzf exits 130 (cancelled)
+ result = self.run_script("pick")
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
+ calls = self.tmux_calls()
+ self.assertFalse(any("attach-session" in c or "switch-client" in c for c in calls),
+ f"unexpected attach/switch in {calls!r}")
+
+
if __name__ == "__main__":
unittest.main()