the best way to save/restore your tmux session

效果展示

状态是什么
准备工作总是麻烦的,在不同的task之间切换,想重新找回上下文更是让人混乱。就像计算机一样。我们需要某种机制,能去确切的保存住当前的状态。在以后的任意时间点能够切换回来
tmux的一些问题
对于我来说,一个任务的上下文基本分为
笔记,记录了当前的进度,想法,todolist等
终端,我一般会开一个tmux session。将日志,测试,程序启动等这个任务相关的所有东西放在同一个屏幕上
网页。firefox+tst 可以直接保存在书签中。
ide,vscode打开的各种文件夹
这里我们主要关注tmux的部分。
tmux很好用。但当想用他来保存状态的话有几个问题
tmux自己只专注于终端分屏的部分,没有自带的保存layout,恢复layout的功能
tmux直到2.3的时候(2016年。。好像也不是很晚)的时候才支持panel级别的title
默认的panel title是自动变化的,当你ssh的时候,会自动变成远端的主机名。这个特性看起来很好。但是当你同时连接多个相同的节点时,很难单从主机名上回想起这个pane到底是想干什么
- 与之相对的zellij在pane重命名的部分做的就很不错。但这次我们还是主要讲tmux。。
第一个点。save/load layout实际上有很多tmux的衍生项目在做。
tmuxinator tmuxp 这类的要义在于你先定义一个配置文件(yaml etc),由这些工具帮你构造出一个tmux的session。(不过tmuxp 提供了freeze 命令 能够将当前的layout保存起来)
tmux-resurrect 更是直接把自己做成了插件,让你能够通过快捷键,保存所有的session。
tmux-resurrect 只能同时保存和还原所有的session。不能单独的保存/还原某个session
在我们看来,以上这些工具的问题在于,他们想自己做的太全了。完全没有必要。在使用者自己提供了一些hint之后,体验就能做的更好好。
build block
如果我们想自己做一个类似的工具的话其实很简单。直接使用命令行就已经足够了。(tmux-resurrect就是纯bash写的)
下面介绍所需要用到的命令
- tmux list-pane和tmux list window 能够列举任意session的pane和window的状态。
tmux list-panes -a -F 'pane #{session_name} #{window_index} #{window_active} :#{window_flags} #{pane_index} #{@mytitle} #{@mybooter} #{pane_active} #{pane_current_path}'
pane ice 0 1 :* 0 mray-run @ . ./scripts/v2ray.actions.sh @ mray-init-and-test 0 /home/cong/sm/lab/v2ray-core
pane ice 0 1 :* 1 vps @@ to-baba @@ ssh root@172.245.72.75 @@ python3 -m http.server 0 /home/cong/sm/work-park/alive-kernel
pane ice 0 1 :* 2 local @ ls 1 /home/cong/sm/lab/v2ray-core
pane ice 0 1 :* 3 k-curl @ while true;do sleep 1s;date;curl -m 1 172.245.72.75:8000;done 0 /home/cong/sm/lab/v2ray-core
pane t-tmux 0 0 : 0 1 /home/cong/sm/work-park/alive-kernel
pane t-tmux 1 0 :- 0 0 /home/cong/sm/work-park/alive-kernel
pane t-tmux 1 0 :- 1 0 /home/cong/sm/work-park/alive-kernel
pane t-tmux 1 0 :- 2 f 0 /home/cong/sm/work-park/alive-kernel
pane t-tmux 1 0 :- 3 1 /home/cong/sm/work-park/alive-kernel
pane t-tmux 2 1 :* 0 1 /home/cong/sm/work-park/alive-kernel
tmux list-windows -a -F 'window #{session_name} #{window_index} :#{window_name} #{window_active} :#{window_flags} #{window_layout}'
window ice 0 ::zsh 1 :* b6f4,320x81,0,0[320x40,0,0{160x40,0,0,58,159x40,161,0,59},320x40,0,41{208x40,0,41,60,111x40,209,41,61}]
window t-tmux 0 :python 0 : dbd3,320x81,0,0,24
window t-tmux 1 :测 0 :- 2b24,320x81,0,0[320x40,0,0,23,320x20,0,41,26,320x19,0,62{160x19,0,62,27,159x19,161,62,28}]
window t-tmux 2 :f 1 :* dbd4,320x81,0,0,25
- 其中window_layout,指的就是tmux session中某个window的所有pane的具体位置。
具体介绍见what-is-tmux-layout-string-format
当window的pane数量一致时,我们可以直接用 tmux select-layout -t $session_name:$win_index $window\_layout 来恢复布局。
tmux set-option -p -t '0:0.0' @mytitle "xxx"
可以在panel级别设置变量。-t 后跟的是pane的index 具体格式为 $session_name:$win_index.$pane_index
拥有在pane级别设置变量的能力极其的重要。这样我们能够赋予这个pane的任意想要的元数据。
从实质上来说,pane就变成了一个我们可以操作的对象了。
- tmux config中可以自定义panel的border format。
也就是定义panel 分割线上显示的内容。而这里是可以用变量的 比如
set -g pane-border-format "i:#{pane_index} t:#{@mytitle} b:#{@mybooter}"
我一般会在panel上显示当前的index,自定义的title,booter(如何恢复这个pane的状态)
`tmux send-key -t $pane_target 'C-c' 'enter' 'xx' 'enter'`
- 我们可以在tmux的内部或者外部使用这个指令发送任意的命令到任意的pane
解决方案
这样的话,具体的解决方式就是
在每个panel使用mytitle这个变量来设置title,这样是为了不会自动重命名等特性干扰
在每个pane上使用booter这个变量记录如何恢复状态。
从具体的实现上,在恢复完layout之后,会使用tmux send-key 把boot指令重发一遍。
我个人认为
想单纯的通过遍历process的方式找一个pane的boot指令是不现实的(tmux-resurrect的方式)
通过定义配置文件,然后resync的方式,不够直观(tmuxp等的方式)
在创建panel的时候,想办法将boot指令记录下来,这种通过pane option的方式暴露,这样正好能够达到一个很好的平衡。
tips and hacks
tmux-set-panel
对于我来讲,一个pane的状态主要有两个,名字和恢复指令。简单的封装了一个tmux-set-panel的函数,可以在更新panel元数据的同时执行初始化操作。
function tmux-set-panel() {
local t=$1
local title=$2
local booter=$3
tmux set -p -t "$t" @mytitle "$title"
tmux-boot-pane "$t" "$booter"
}
function tmux-boot-pane() {
local t=$1
local booter="$2"
tmux set -p -t "$t" @mybooter "$booter"
local cmd=$(
cd $SHELL_ACTIONS_BASE/scripts
python3 tmux.py gen_tmux_send_keys "$booter"
)
if [[ -z $t ]]; then
cmd="tmux send-keys $cmd"
else
cmd="tmux send-keys -t $t $cmd"
fi
echo "$cmd"
eval "$cmd"
}
def gen_tmux_send_keys(self, booter: str):
delimeter = booter.split(" ")[0]
cmds = [f"'{x.strip()}'" for x in booter.removeprefix(
delimeter).split(delimeter)]
cmds.insert(0, "C-c")
full = " 'enter' ".join(cmds)
return f"{full} 'enter';"
tmux-load
如果想把配置load进当前的tmux session,有个尴尬的问题。执行load指令如果也是在pane中,就可能导致load指令本身恰好被这个pane的booter过程打断。。
一个hack是我们可以在popup的float window中做这件事。。。
function tmux-load() (
tmux display-popup "zsh -c \"source ~/.zshrc; cd $PWD;pwd;python $SHELL_ACTIONS_BASE/scripts/tmux.py tmux-load $1\"" &
)
wait pane ready
有件更尴尬的事情是我们如果你配置了挺多zsh/shell config,new panel的过程是需要时间的。。。
这里一个简单实用的小hack是在初始化layout之后,booter 之前。等上几秒。。
总之,我们可以很容易的通过几行代码恢复任意的tmux session
def load(self, p: str):
exp = Layout.from_json(Path(p).read_text())
if not self.cli.ami_intmux():
raise Exception("在tmux环境下执行")
session_name = self.cli.cur_session()
cur = Layout(session_name=self.cli.cur_session(), wins=self.cli.list_window(session_name),
panes=self.cli.list_pane(session_name))
for w in exp.wins:
cur_win_index = [x.window_index for x in cur.wins]
if w.window_index not in cur_win_index:
self.cli.run(
f"tmux new-window -d -t \"{session_name}:{w.window_index}\"", hide=True)
self.cli.run(
f"tmux rename-window -t \"{session_name}:{w.window_index}\" \"{w.window_name}\"", hide=True)
# 创建panel
for p in exp.panes:
cur_pane_index = [x.pane_index for x in cur.panes]
if p.pane_index not in cur_pane_index:
self.cli.run(
f"tmux split-window -t \"{session_name}:{p.window_index}\" -c \"{p.pane_current_path}\"", hide=True)
# 设置layout
for w in exp.wins:
self.cli.run(
f"tmux select-layout -t \"{session_name}:{w.window_index}\" \"{w.window_layout}\"", hide=True)
# 设置option
for p in exp.panes:
t = f"{session_name}:{p.window_index}.{p.pane_index}"
cmd = f"""tmux set-option -p -t "{t}" @mytitle "{p.mytitle}" """
self.cli.run(cmd, hide=True)
cmd = f"""tmux set-option -p -t "{t}" @mybooter "{p.mybooter}" """
self.cli.run(cmd, hide=True)
sleep(3)
# booter
for p in exp.panes:
t = f"{session_name}:{p.window_index}.{p.pane_index}"
cmd = self.gen_tmux_send_keys(p.mybooter)
self.cli.run(f""" tmux send-keys -t "{t}" {cmd} """, hide=True)
pass
# focus window and pane
for p in exp.panes:
if p.pane_active:
t = f"{session_name}:{p.window_index}.{p.pane_index}"
self.cli.run(
f"""tmux switch-client -t "{session_name}:{p.window_index}" """, hide=True)
self.cli.run(
f"""tmux select-pane -t "{p.pane_index}" """, hide=True)
for w in exp.wins:
if w.window_active:
self.cli.run(
f"""tmux select-window -t "{session_name}:{w.window_index}" """, hide=True)
pass
todolist
整个save的过程完全可以发生tmux session的外部。所以我们只要写一个cronjob就能做的自动的定时save了。
load也是同样。可以在创建一个tmux session,然后设置他的各种layout
实际上要保存状态,tmux-resurrect还做了挺多。比如zoom的状态,对pane内程序进行感知。保存vim layout之类的