Skip to content

Commit bd3dcfb

Browse files
committed
feat(shell): 新增 fzf 驱动的 tmux/zellij 会话管理命令
在 shell/shared.d 新增三个纯新增文件,提供交互式会话管理: - fzf-helpers.sh: 通用底座 fzf_pick_action,通过全局变量回传 「条目+动作」,支持 attach 等需在当前 shell 执行的场景 - tmux.sh: tmux-sessions,列出会话后 Enter attach / Ctrl-x kill - zellij.sh: zellij-sessions,同范式管理 zellij 会话 设计要点: - bash/zsh 双 shell 兼容(shared.d 同时被两种 rc source) - 工具守护放在函数体内,工具缺失时友好提示 + exit 0, 而非函数未定义导致 command not found - 复用 fzf-history 的 --expect 多动作范式,交互统一
1 parent 7171320 commit bd3dcfb

3 files changed

Lines changed: 329 additions & 0 deletions

File tree

‎shell/shared.d/fzf-helpers.sh‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/bin/bash
2+
# ========================================================================
3+
# 文件: fzf-helpers.sh
4+
# 作用: 提供基于 fzf 的通用交互底座函数,供 tmux.sh / zellij.sh 等场景复用。
5+
# 本文件由 deploy.sh 软链接到 ~/.bashrc.d/,bash 与 zsh 都会 source,
6+
# 因此必须双 shell 兼容,禁用 zle / bind -x 等 shell 专属结构。
7+
#
8+
# 加载顺序说明:
9+
# bash/zsh 函数定义为惰性——本文件定义 fzf_pick_action 时,
10+
# 领域文件 tmux.sh/zellij.sh 只需「定义」自己的函数,
11+
# 函数体内对 fzf_pick_action 的引用在「调用时」才解析。
12+
# 故无需保证 *.sh 的 glob 加载顺序,只要全部 source 完即可。
13+
# ========================================================================
14+
15+
# ----------------------------------------------------------------------
16+
# 统一的颜色/提示输出(与 deploy.sh 风格一致,仅交互式场景使用)
17+
# 放在文件级,避免每次调用都重新定义。
18+
# ----------------------------------------------------------------------
19+
__fzf_helpers_color_setup() {
20+
# 仅当终端支持颜色时启用 ANSI 转义,否则置空避免乱码。
21+
if [ -t 1 ] && [ -n "$TERM" ] && [ "$TERM" != "dumb" ]; then
22+
_FZF_HLP_RED=$'\033[0;31m'
23+
_FZF_HLP_GREEN=$'\033[0;32m'
24+
_FZF_HLP_YELLOW=$'\033[1;33m'
25+
_FZF_HLP_CYAN=$'\033[0;36m'
26+
_FZF_HLP_NC=$'\033[0m'
27+
else
28+
_FZF_HLP_RED=''
29+
_FZF_HLP_GREEN=''
30+
_FZF_HLP_YELLOW=''
31+
_FZF_HLP_CYAN=''
32+
_FZF_HLP_NC=''
33+
fi
34+
}
35+
__fzf_helpers_color_setup
36+
37+
# ----------------------------------------------------------------------
38+
# fzf_pick_action — fzf 通用「选条目 + 选动作」底座
39+
#
40+
# 设计意图:
41+
# attach 这类操作必须在「当前 shell 进程」执行(要接管当前终端),
42+
# 若用 item=$(fzf_pick_action ...) 子 shell 回传,attach 会发生在子 shell、
43+
# 退出即丢终端。因此本函数通过「全局变量」回传两项结果,调用方在当前
44+
# shell 读取后直接执行目标命令。此语义与 fzf-history 用 BUFFER/READLINE_LINE
45+
# 在当前 shell 写命令行同源。
46+
#
47+
# 入参:
48+
# $1 - header: fzf 顶部提示文本(说明各按键含义)。
49+
# $2 - expect_keys: --expect 的按键,逗号分隔,如 "ctrl-x,ctrl-d"。
50+
# Enter 固定走 accept 语义(表示默认动作),不在 expect 列表中。
51+
#
52+
# 输入(stdin):
53+
# 候选条目,每行一个,已包含最终显示文本(调用方负责拼好显示格式)。
54+
# 调用方需自行从整行解析出真实值(如会话名),本函数保持中立。
55+
#
56+
# 回传(全局变量,调用方在函数返回后立即读取):
57+
# FZF_PICK_ITEM - 用户选中的整行原文;无选中时为空。
58+
# FZF_PICK_ACTION - 用户按下的 expect 键名(如 "ctrl-x");按 Enter 时为空串。
59+
#
60+
# 返回码:
61+
# 0 - 用户正常选中条目。
62+
# 1 - fzf 不可用、stdin 为空、或用户未选中(Esc/Ctrl-c)。
63+
#
64+
# 用法示例:
65+
# printf 'session-a\nsession-b\n' | fzf_pick_action "[Enter]:attach | [Ctrl-x]:kill" "ctrl-x"
66+
# if [ $? -eq 0 ]; then
67+
# case "$FZF_PICK_ACTION" in
68+
# ctrl-x) tmux kill-session -t "$FZF_PICK_ITEM" ;;
69+
# *) tmux attach-session -t "$FZF_PICK_ITEM" ;;
70+
# esac
71+
# fi
72+
# ----------------------------------------------------------------------
73+
fzf_pick_action() {
74+
# 每次调用前清空上一次的回传变量,避免脏数据。
75+
FZF_PICK_ITEM=''
76+
FZF_PICK_ACTION=''
77+
78+
# fzf 未安装则静默失败,交由调用方提示。
79+
if ! command -v fzf >/dev/null 2>&1; then
80+
return 1
81+
fi
82+
83+
local header="${1:-}"
84+
local expect_keys="${2:-}"
85+
local selection input
86+
87+
# 缓存 stdin: fzf 对空输入行为不保证(非 tty 下可能挂起等待),
88+
# 先用 cat 读入,空则直接返回 1,避免进入 fzf 卡住。
89+
# 调用方通常已自行过滤空列表,此处为底座自身的防御。
90+
input=$(cat)
91+
[ -z "$input" ] && return 1
92+
93+
# 组装 fzf 参数:--expect 为空时不传该 flag(避免 fzf 报错)。
94+
# --height=40% --reverse 与 fzf-history 保持一致观感。
95+
if [ -n "$expect_keys" ]; then
96+
selection=$(printf '%s\n' "$input" | fzf --height=40% --reverse \
97+
--header="$header" \
98+
--expect="$expect_keys")
99+
else
100+
selection=$(printf '%s\n' "$input" | fzf --height=40% --reverse \
101+
--header="$header")
102+
fi
103+
104+
# 用户按 Esc/Ctrl-c → fzf 返回非 0。
105+
if [ $? -ne 0 ] || [ -z "$selection" ]; then
106+
return 1
107+
fi
108+
109+
# 解析 fzf --expect 输出:
110+
# 第 1 行 = 按下的 expect 键(按 Enter 时为空行)
111+
# 第 2 行起 = 选中的条目
112+
# 用 read 配合「整体块」读取,避免管道子 shell 截断退出码。
113+
local key line
114+
{
115+
read -r key
116+
read -r line
117+
} <<EOF
118+
$selection
119+
EOF
120+
121+
# 未按 expect 键时 fzf 第一行是空,第二行才是条目。
122+
FZF_PICK_ACTION="$key"
123+
FZF_PICK_ITEM="$line"
124+
125+
# 极端情况:解析后条目仍空(不应发生),视为未选中。
126+
if [ -z "$FZF_PICK_ITEM" ]; then
127+
return 1
128+
fi
129+
return 0
130+
}

‎shell/shared.d/tmux.sh‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/bin/bash
2+
# ========================================================================
3+
# 文件: tmux.sh
4+
# 作用: 基于 fzf 的 tmux 会话交互管理。
5+
# 依赖 fzf-helpers.sh 提供的 fzf_pick_action 底座。
6+
# 本文件由 deploy.sh 软链接到 ~/.bashrc.d/,bash 与 zsh 都会 source。
7+
#
8+
# 加载顺序说明:
9+
# 函数定义为惰性,本文件只定义 tmux-sessions 函数;
10+
# 工具存在性检查放在「函数体内」运行时执行,而非文件顶层。
11+
# 这样即便 tmux 未安装,函数仍被定义,调用时得到友好提示而非
12+
# 'command not found'。详见下方函数注释。
13+
# ========================================================================
14+
15+
# ----------------------------------------------------------------------
16+
# tmux-sessions — 列出 tmux 会话,交互式 attach 或 kill
17+
#
18+
# 设计意图:
19+
# 用 fzf 列出所有 tmux 会话,用户选中后:
20+
# Enter → 在当前 shell attach(接管当前终端,退出后回原 shell)
21+
# Ctrl-x → kill 选中的会话
22+
# 复用 fzf_pick_action 底座,与 fzf-history 的 --expect 多动作范式一致。
23+
#
24+
# 入参: 无。
25+
#
26+
# 返回码:
27+
# 0 - 正常结束(含无会话/工具缺失/用户取消,均为友好退出)。
28+
#
29+
# 健壮性(守护放在函数体内,而非文件顶层):
30+
# - tmux/fzf/底座缺失 → 提示并返回 0,而非函数未定义导致 command not found。
31+
# - 无活动会话 → 提示并返回 0。
32+
# - attach 在当前 shell 执行,保证退出 tmux 后回到原 shell(不用 exec 替换进程)。
33+
# ----------------------------------------------------------------------
34+
tmux-sessions() {
35+
# 运行时检查 tmux(不放在文件顶层,确保函数恒被定义)。
36+
if ! command -v tmux >/dev/null 2>&1; then
37+
printf '%s[tmux]%s 未检测到 tmux,请先安装。\n' "$_FZF_HLP_RED" "$_FZF_HLP_NC"
38+
return 0
39+
fi
40+
# fzf 与底座缺失给出明确提示。
41+
if ! command -v fzf >/dev/null 2>&1; then
42+
printf '%s[tmux]%s 请先安装 fzf。\n' "$_FZF_HLP_RED" "$_FZF_HLP_NC"
43+
return 0
44+
fi
45+
if ! command -v fzf_pick_action >/dev/null 2>&1; then
46+
printf '%s[tmux]%s 底座 fzf-helpers.sh 未加载,请检查部署。\n' "$_FZF_HLP_RED" "$_FZF_HLP_NC"
47+
return 0
48+
fi
49+
50+
# 读取会话列表。
51+
# 无运行中的 server 时 tmux list-sessions 退出码非 0(输出 'no server running'),
52+
# 此时视为「无活动会话」而非错误。
53+
local sessions
54+
sessions=$(tmux list-sessions 2>/dev/null)
55+
if [ -z "$sessions" ]; then
56+
printf '%s[tmux]%s 当前没有活动会话。\n' "$_FZF_HLP_YELLOW" "$_FZF_HLP_NC"
57+
return 0
58+
fi
59+
60+
# list-sessions 默认输出形如:
61+
# dev: 3 windows (created ...) (attached)
62+
# main: 1 windows (created ...) (detached)
63+
# 整行作为显示行,按 : 前缀切出真实会话名。
64+
printf '%s\n' "$sessions" | fzf_pick_action \
65+
'[Enter]:attach | [Ctrl-x]:kill session'
66+
67+
if [ $? -ne 0 ]; then
68+
# 用户按 Esc/Ctrl-c 取消。
69+
return 0
70+
fi
71+
72+
# 从显示行提取真实会话名:取首个 ':' 之前的部分。
73+
# ${FZF_PICK_ITEM%%:*} 为参数扩展(非正则),bash/zsh 均支持。
74+
local session_name
75+
session_name="${FZF_PICK_ITEM%%:*}"
76+
77+
if [ -z "$session_name" ]; then
78+
printf '%s[tmux]%s 无法解析会话名。\n' "$_FZF_HLP_RED" "$_FZF_HLP_NC"
79+
return 0
80+
fi
81+
82+
case "$FZF_PICK_ACTION" in
83+
ctrl-x)
84+
# kill 会话:-t 指定目标。
85+
if tmux kill-session -t "$session_name" 2>/dev/null; then
86+
printf '%s[tmux]%s 已删除会话: %s\n' "$_FZF_HLP_GREEN" "$_FZF_HLP_NC" "$session_name"
87+
else
88+
printf '%s[tmux]%s 删除会话失败: %s\n' "$_FZF_HLP_RED" "$_FZF_HLP_NC" "$session_name"
89+
fi
90+
;;
91+
*)
92+
# 默认动作 = attach。在当前 shell 执行,不用 exec(退出 tmux 后回原 shell)。
93+
# 若当前已在 tmux 内($TMUX 非空),attach 会失败,改用 switch-client 切换。
94+
if [ -n "$TMUX" ]; then
95+
tmux switch-client -t "$session_name"
96+
else
97+
tmux attach-session -t "$session_name"
98+
fi
99+
;;
100+
esac
101+
}

‎shell/shared.d/zellij.sh‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/bin/bash
2+
# ========================================================================
3+
# 文件: zellij.sh
4+
# 作用: 基于 fzf 的 zellij 会话交互管理。
5+
# 依赖 fzf-helpers.sh 提供的 fzf_pick_action 底座。
6+
# 本文件由 deploy.sh 软链接到 ~/.bashrc.d/,bash 与 zsh 都会 source。
7+
#
8+
# 加载顺序说明:
9+
# 函数定义为惰性,本文件只定义 zellij-sessions 函数;
10+
# 工具存在性检查放在「函数体内」运行时执行,而非文件顶层,
11+
# 确保函数恒被定义(与 tmux.sh 同理)。
12+
# ========================================================================
13+
14+
# ----------------------------------------------------------------------
15+
# zellij-sessions — 列出 zellij 会话,交互式 attach 或 kill
16+
#
17+
# 设计意图:
18+
# 用 fzf 列出所有 zellij 会话,用户选中后:
19+
# Enter → attach 到选中会话
20+
# Ctrl-x → kill 选中的会话
21+
# 与 tmux-sessions 保持一致的交互范式(同一底座、同一按键语义)。
22+
#
23+
# 入参: 无。
24+
#
25+
# 返回码:
26+
# 0 - 正常结束(含无会话/工具缺失/用户取消,均为友好退出)。
27+
#
28+
# 健壮性:
29+
# - zellij/fzf/底座缺失 → 提示并返回 0。
30+
# - 无活动会话 → 提示并返回 0。
31+
# - zellij 子命令版本差异: kill-session 为 zellij ≥0.40 命名;
32+
# 旧版可能用 delete-session,这里优先 kill-session,失败时降级提示。
33+
# ----------------------------------------------------------------------
34+
zellij-sessions() {
35+
# 运行时检查 zellij(不放在文件顶层,确保函数恒被定义)。
36+
if ! command -v zellij >/dev/null 2>&1; then
37+
printf '%s[zellij]%s 未检测到 zellij,请先安装。\n' "$_FZF_HLP_RED" "$_FZF_HLP_NC"
38+
return 0
39+
fi
40+
if ! command -v fzf >/dev/null 2>&1; then
41+
printf '%s[zellij]%s 请先安装 fzf。\n' "$_FZF_HLP_RED" "$_FZF_HLP_NC"
42+
return 0
43+
fi
44+
if ! command -v fzf_pick_action >/dev/null 2>&1; then
45+
printf '%s[zellij]%s 底座 fzf-helpers.sh 未加载,请检查部署。\n' "$_FZF_HLP_RED" "$_FZF_HLP_NC"
46+
return 0
47+
fi
48+
49+
# 读取会话列表。
50+
# zellij list-sessions 输出形如:
51+
# dev [ExitStatus: 0]
52+
# main (EXITED)
53+
# tmp [ExitStatus: 0] (current)
54+
# 每行首个空白字段为真实会话名,后续为状态标记。
55+
local sessions
56+
sessions=$(zellij list-sessions 2>/dev/null)
57+
if [ -z "$sessions" ]; then
58+
printf '%s[zellij]%s 当前没有活动会话。\n' "$_FZF_HLP_YELLOW" "$_FZF_HLP_NC"
59+
return 0
60+
fi
61+
62+
# 整行作为显示行交给 fzf。
63+
printf '%s\n' "$sessions" | fzf_pick_action \
64+
'[Enter]:attach | [Ctrl-x]:kill session'
65+
66+
if [ $? -ne 0 ]; then
67+
# 用户取消。
68+
return 0
69+
fi
70+
71+
# 提取真实会话名:取首个空白之前的部分。
72+
# 用参数扩展 ${FZF_PICK_ITEM%% *} 切,兼容 bash/zsh。
73+
local session_name
74+
session_name="${FZF_PICK_ITEM%% *}"
75+
76+
if [ -z "$session_name" ]; then
77+
printf '%s[zellij]%s 无法解析会话名。\n' "$_FZF_HLP_RED" "$_FZF_HLP_NC"
78+
return 0
79+
fi
80+
81+
case "$FZF_PICK_ACTION" in
82+
ctrl-x)
83+
# kill 会话:zellij ≥0.40 用 kill-session。
84+
# 旧版可能无此子命令,失败时给出版本提示。
85+
if zellij kill-session "$session_name" 2>/dev/null; then
86+
printf '%s[zellij]%s 已删除会话: %s\n' "$_FZF_HLP_GREEN" "$_FZF_HLP_NC" "$session_name"
87+
else
88+
printf '%s[zellij]%s 删除失败,请检查 zellij 版本(需 ≥0.40 的 kill-session): %s\n' \
89+
"$_FZF_HLP_RED" "$_FZF_HLP_NC" "$session_name"
90+
fi
91+
;;
92+
*)
93+
# 默认动作 = attach。zellij attach 不允许在已有 zellij 内嵌套 attach,
94+
# 会自行报错;此处直接透传由 zellij 处理。
95+
zellij attach "$session_name"
96+
;;
97+
esac
98+
}

0 commit comments

Comments
 (0)