Skip to main content

Command Palette

Search for a command to run...

the best way to save/restore your tmux session

Updated
3 min read
the best way to save/restore your tmux session

效果展示

状态是什么

准备工作总是麻烦的,在不同的task之间切换,想重新找回上下文更是让人混乱。就像计算机一样。我们需要某种机制,能去确切的保存住当前的状态。在以后的任意时间点能够切换回来

tmux的一些问题

对于我来说,一个任务的上下文基本分为

  1. 笔记,记录了当前的进度,想法,todolist等

  2. 终端,我一般会开一个tmux session。将日志,测试,程序启动等这个任务相关的所有东西放在同一个屏幕上

  3. 网页。firefox+tst 可以直接保存在书签中。

  4. ide,vscode打开的各种文件夹

这里我们主要关注tmux的部分。

tmux很好用。但当想用他来保存状态的话有几个问题

  1. tmux自己只专注于终端分屏的部分,没有自带的保存layout,恢复layout的功能

  2. tmux直到2.3的时候(2016年。。好像也不是很晚)的时候才支持panel级别的title

    1. 默认的panel title是自动变化的,当你ssh的时候,会自动变成远端的主机名。这个特性看起来很好。但是当你同时连接多个相同的节点时,很难单从主机名上回想起这个pane到底是想干什么

      1. 与之相对的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写的)

下面介绍所需要用到的命令

  1. 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
  1. 其中window_layout,指的就是tmux session中某个window的所有pane的具体位置。

具体介绍见what-is-tmux-layout-string-format

当window的pane数量一致时,我们可以直接用 tmux select-layout -t $session_name:$win_index $window\_layout 来恢复布局。

  1. tmux set-option -p -t '0:0.0' @mytitle "xxx"

可以在panel级别设置变量。-t 后跟的是pane的index 具体格式为 $session_name:$win_index.$pane_index

拥有在pane级别设置变量的能力极其的重要。这样我们能够赋予这个pane的任意想要的元数据。

从实质上来说,pane就变成了一个我们可以操作的对象了。

  1. tmux config中可以自定义panel的border format。

也就是定义panel 分割线上显示的内容。而这里是可以用变量的 比如

set -g pane-border-format "i:#{pane_index} t:#{@mytitle} b:#{@mybooter}"

我一般会在panel上显示当前的index,自定义的title,booter(如何恢复这个pane的状态)

  1. `tmux send-key -t $pane_target 'C-c' 'enter' 'xx' 'enter'`

    1. 我们可以在tmux的内部或者外部使用这个指令发送任意的命令到任意的pane

解决方案

这样的话,具体的解决方式就是

  1. 在每个panel使用mytitle这个变量来设置title,这样是为了不会自动重命名等特性干扰

  2. 在每个pane上使用booter这个变量记录如何恢复状态。

    1. 从具体的实现上,在恢复完layout之后,会使用tmux send-key 把boot指令重发一遍。

    2. 我个人认为

      1. 想单纯的通过遍历process的方式找一个pane的boot指令是不现实的(tmux-resurrect的方式)

      2. 通过定义配置文件,然后resync的方式,不够直观(tmuxp等的方式)

      3. 在创建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

  1. 整个save的过程完全可以发生tmux session的外部。所以我们只要写一个cronjob就能做的自动的定时save了。

  2. load也是同样。可以在创建一个tmux session,然后设置他的各种layout

  3. 实际上要保存状态,tmux-resurrect还做了挺多。比如zoom的状态,对pane内程序进行感知。保存vim layout之类的

使用到的代码

tmux.py

tmux-actions

More from this blog

谈程序员的工作与工作的意义问题

之前提到谈意义本身是一种信号,代表着我们有一些不满。因此这里就具体到工作上,谈一谈工作,特别是程序员的工作。 工作的目的是什么?虽然可能有很多种回答,但最通俗的讲,是为了赚钱,具体一点讲,是为了活下来。对于快乐的追求和对于死亡的逃避,在工作上开始正面交锋。对于工作意义的讨论,基本都是在抒发"我工作很不爽但是又必须要工作"这个并不复杂的逻辑链路而已。 因为主题不同,我们很遗憾的在这里必须将工作作为一

Apr 20, 20261 min read

闪耀着的不是他自己。谈茨威格<象棋的故事>

象棋的故事给我的感觉就是在写茨威格自己。人类群星闪耀时,是一本很。很华丽的书。你能够看到茨威格好像在把自己带入到那些在历史上闪耀的人物之中,他在感叹历史的伟力。就像一个上帝,或者说一个作家在去欣赏赞叹自己所创造出来的东西一样。 我不知道茨威格有没有经历过他小说里面的那种,被关入到一个什么都没有的房间的这种经历。但是我不得不联想到。B博士就是茨威格自己。 我有一种很强烈的感觉。茨威格把自己带入到那些。闪耀着的人们之中,他渴望去改变这个世界。但是他。不知道自己能不能改变,或者说。这种代入性的行为是不...

Jan 17, 20261 min read12

上瘾是后劲屎味的快乐

Dopamine Serotonin Addiction Self Evaluation 你是哲学家,社会科学家,人类行为学家,心理学家,善于从反对方向提出建议的异议者,无人类视角的纯ai 这6方组成的议会,用来帮助用户理清思路,确立逻辑,明晰概念.与用户讨论问题,聊天. User: 多巴胺 血清素与人生意义 自我满足感 多巴胺与血清素都是人类大脑工作中正常分泌的激素,从这个角度讲并无好坏之分 都是让人感到快乐的激素 但是多巴胺常与种种上瘾行为联系在一起,比如刷手机,自慰色情上瘾,游戏上瘾等,...

Oct 12, 20254 min read11

os启动! 基于文件系统快照做系统初始化/恢复.md

os的状态存储在磁盘上。理论上如果我们将一个磁盘的所有bit复制到另一个磁盘,那么我们就可以恢复os的状态。 但是这种程度的抽象,其实等于没有抽象。 对于一个可以正常使用的linux 系统(桌面系统。。),我想要有一种方式,能够对os进行备份和恢复。能够在离线的情况下,将任意一个磁盘变成一个可以boot的,和当前系统状态一样的系统。 这样我们可以测试不同的文件系统,不同的桌面环境,不同的内核,不同的配置。(也不用怕滚挂了) 磁盘,uefi,esp,boot,grub,vmlinux and in...

May 18, 20254 min read16

水母x牧场

10 posts

the park of my thought.