# 第 6 章 工作流 就 tmux 自身来说,它只是一个添加了一些附加功能的另一个终端而已,只是显示了更多的终端会话。但是 tmux 让我们在这些会话里运行程序时更加方便,所以本章将会探讨一些常见、不常见的配置和命令,它们可能对你的日常工作有很大的帮助。你会学到管理面板和会话的高级方式,如何让 tmux 和 shell 一起工作,如何使用外部脚本扩展 tmux 命令,如何创建能执行数条命令的快捷键。我们先从管理窗口和面板开始。 ## 6.1 高效使用面板和窗口工作 在本书中,你已经见到数种方式把 tmux 会话分割为多个面板和窗口。在本节,我们会学习使用面板和窗口工作的几种高级方式。 ### 把面板变为窗口 面板很适合用来划分工作空间,但是有时我们需要把一个面板“弹出(pop out)”变为一个独立的窗口,这样看这部分内容就会更方便。tmux 有这样一个命令来实现这个功能。 在任意面板内,按下 `PREFIX !` 键,tmux 就会依据当前面板创建一个新的窗口。 ### 把窗口变为面板 有时候,我们需要合并一个工作空间,我们可以很简便地把窗口变为一个面板。为此,我们要提一下 `join-pane` 命令。 在“合并(join)”一个面板时,我们有可能把面板从一个会话移动到另一个会话里。此时需要指定源窗口和面板,后面跟随目标窗口和面板。如果不指定目标窗口,那么当前焦点窗口就会作为目标窗口。 下面我们通过创建一个带有两个窗口的 tmux 会话来演示: ``` $ tmux new-session -s panes -n first -d $ tmux new-window -t panes -n second $ tmux attach -t panes ``` 现在,要把第一个窗口移动,作为第二个窗口的一个面板,按下 `PREFIX :` 键进入命令模式,然后输入这些: ``` join-pane -s 1 ``` 这句话的意思是“取出窗口(当窗口中有多个面板时则指取出面板,译者注)1 并把它添加到当前窗口”,因为我们没有指定一个目标窗口。 也可以使用这种方法来移动面板。如果第一个窗口有两个面板,可以像下面这样指定源面板,注意我们设置窗口从 1 开始编号,而面板从 0 开始编号: ``` join-pane -s 1.0 ``` 在这里,我们取出了第一个窗口的第一个面板然后把它添加到当前窗口。 更进一步地,甚至可以指定一个源会话,使用格式 `-t [session_name]:[window].[pane]` 指定一个目标窗口。 ### 最大化和恢复面板 有时我们只是想让一个面板最大化显示一会,这样就可以细看它的内容,这时可以使用 `break-pane` 命令,然后再使用 `join-pane` 命令把它放回原处。这个操作做起来有些繁琐,因此我们编写一个脚本来实现这个功能。这是[链接](http://superuser.com/questions/238702/maximizing-a-pane-in-tmux)。 首先,我们释放 `UP` 箭头键,把它设置为最大化命令。然后,创建一个新的快捷键 `PREFIX UP` 来触发这个 tmux 命令串,配置如下: ``` unbind Up bind Up new-window -d -n tmp \; swap-pane -s tmp.1 \; select-window -t tmp ``` 在配置里,我们创建了一个名为 tmp 的新窗口。通过给它命名,可以在子序列命令里调用它。当使用 `-d` 参数创建窗口时,tmux 会在后台创建这个窗口而不是把焦点转到这个窗口上。然后使用 `swap-pane` 命令选取已经选择的面板和临时窗口的已有面板进行交换。 要恢复窗口,只需要使用 `swap-pane` 命令把面板从临时窗口交换到原来的窗口里,选择源面板,然后杀掉临时窗口。我们把这个命令序列绑定到 `PREFXI DOWN` 键,就像这样: ``` unbind Down bind Down last-window \; swap-pane -s tmp.1 \; kill-window -t tmp ``` 由于它使用了 `last-window` 命令来返回源窗口,因此这个过程看起来就像是把一个面板啪的一下最大化了,然后又啪的一下把它恢复原位置,这个简单的例子说明了 tmux 高度灵活性。我们可以通过一个简单的快捷键来自动化实现一系列命令。 ### 在面板里执行命令 在第 3 章,我们已经学习了如何使使用 shell 命令和 `send-keys` 在面板里启动程序,我们还可以让 tmux 在新建一个窗口或面板时自动执行命令。 假设有两台服务器,bums 和 smithers,分别是 web 服务器和数据库服务器。当启动 tmux 时我们想让 tmux 使用一个窗口的两个面板分别连接到这两台服务器上。 下面我们来创建一个新的名为 `servers.sh` 的脚本然后创建一个会话连接到两台服务器: ``` $ tmux new-session -s servers -d "ssh deploy@bums" $ tmux split-window -v "ssh dba@smithers" $ tmux attach -t servers ``` 新建一个会话时,可以把要执行的命令作为最后一个参数传入到 tmux 中。在这里我们先是新建了一个会话然后在第一个窗口连接到 bums 服务器,然后从会话中分离出来。之后我们使用垂直分割切分窗口然后连接到 smithers 服务器。 这个配置有个副作用:从远程服务器上退出登录时,面板或窗口会关闭。 ### 在 OS X 系统的同一目录下打开新面板 在 Linux 系统上创建 tmux 新的面板时,新面板使用的是当前面板的路径。但是在 OS X 系统上,新的面板会位于启动 tmux 会话时的那个目录。只需要做一点小小的工作,就可以在打开一个面板时捕捉它的工作路径然后自动地切换路径,就像 Linux 做的那样。 为此,我们使用 `send-keys` 命令来调用一个脚本把当前路径保存到环境变量中,然后这个脚本回调 `send-keys` 向拆分的窗口中发送命令,再把路径更改为环境变量中保存的那个路径。 首先,在主目录下创建一个名为 `~/tmux-panes` 的新文件,写入以下内容: ``` TMUX_CURRENT_DIR=`pwd` tmux split-window $1 tmux send-keys "cd $TMUX_CURRENT_DIR;clear" C-m ``` 然后编辑 `.tmux.conf` 文件来调用这个文件做垂直和水平分割。这里使用 `PREFXI v` 键和 `PREFXI n` 键,以防覆盖了当前已有的分割快捷键。代码如下: ``` unbind v unbind n bind v send-keys " ~/tmux-panes -h" C-m bind n send-keys " ~/tmux-panes -v" C-m ``` 就像在之前讨论过的,我们需要使用 `-v` 参数来水平分割窗口,使用 `-h` 参数来垂直分割窗口。 最后,为 `tmux-panes` 脚本添加执行权限: ``` $ chmod +x ~/tmux-panes ``` 在重新加载 `.tmux.conf` 配置文件后就可以分割面板了。 这种方法的弊端是它看起来有点 hack。它把命令输入到已有的 tmux 窗口并执行脚本。也就是说它只能在一个有命令提示符的窗口里才能被触发。所以,如果你的主窗口在运行 Vim,这个命令是不会有效的。即便是把 `send-keys` 命令换成 `run-shell` 命令也不会有效,因为新产生的 shell 也没有访问环境变的权限,因此它也就无法处理这个脚本了。但是这个脚本依然是一个比较方便的小技巧,通过自定义键盘快捷键,依然还保留了原始的命令。 ## 6.2 管理会话 随着使用 tmux 越来越顺手,你会发现你会同时使用多个 tmux 会话。例如,你可能会为每个程序都开启一个 tmux 会话,这样就可以保持开发环境的相对独立。tmux 提供了多种特性能让你在管理这些会话时不会感到痛苦。 ### 在会话间移动 单机上的所有 tmux 会话都通过一个服务器进行管理。也就是说我们能够在一台机器上就可以实现在会话之间来回移动。 下面来演示这个过程,我们会启动两个分离的 tmux 会话,一个名为 editor,它打开了 Vim,一个名为 processes 的会话则在运行 `top` 命令,命令如下所示: ``` $ tmux new -s editor -d vim $ tmux new -s processes -d top ``` 可以这样连接到 editor 会话: ``` $ tmux attach -t editor ``` 然后按下 `PREFIX (` 键进入前一个会话,按下 `PREFIX )` 则可以跳转到下一个会话。 还可以使用 `PREFIX s` 键显示一个会话列表,这样就可以快速地从一个会话跳转到另一个会话。 你可以添加自定义的快捷键到 `.tmux.conf` 文件里来绑定 `switch-client` 命令。默认的配置应该是像这样: ``` bind -r ( switch-client -p bind -r ) switch-client -n ``` 如果你已经配置了多个工作空间,这样操作会极大地提高你的效率,而且它不需要分离会话再重新连接。 ### 创建或连接到已有会话 到目前为止,我们学会了多种办法在任意时刻创建新的 tmux 会话。然而,事实上还可以判断一个 tmux 会话是否存在,如果存在的话就连接到它。 `has-session` 命令返回一个可以用在 shell 脚本里的布尔值。可以用它在 bash 脚本做一些类似这样的事情: ``` if ! tmux has-session -t remote; then exec tmux new-session -s development -d # other setup commands before attaching.... fi exec tmux attach -t development ``` 如果修改这个脚本让它通过参数读取会话名称,你就可以用它来连接或创建任意 tmux 会话。 ### 在会话之间移动窗口 可以把一个会话的窗口移动到另一个会话里。如果已经在一个开发环境里打开了一个进程,现在想把它移动到另一个环境中,或者想合并工作空间时会非常有用。 `move-window` 命令被映射到快捷键 `PREFIX .` 键(英文句号键,译者注),这样可以很方便地把要移动的窗口作为当前焦点窗口,按下快捷键,然后输入目标会话即可。 下面演示一下这个过程,创建一个会话,一个名为 editor,一个名为 processes 分别运行了 `vim` 和 `top` 命令: ``` $ tmux new -s editor -d vim $ tmux new -s processes -d top ``` 我们会把 processes 会话的窗口移动到 editor 会话里。 首先,连接到 processes 会话,就像这样: ``` $ tmux attach -t processes ``` 然后,按下 `PREFIX .` 键,然后在显示的命令行里输入 editor。 这会把 processes 会话里的唯一窗口移动出来,因此 processes 会话会自动关闭。如果连接到 editor 会话,就可以看到这两个窗口。 也可以使用 shell 命令来完成这个功能,因此不必在合并窗口时要连接会话。可以这样使用 `move-window` 命令: ``` $ tmux move-window -s processes:1 -t editor ``` 这个命令的意思就是,把 processes 会话的第 1 个窗口移动到 editor 会话中。 ## 6.3 tmux 和你的操作系统 既然 tmux 已经变成了你工作流的一部分,那么你肯定想让它和操作系统集成地越紧密越好。在本机,我们会向你展示多种方式,让你的 tmux 和操作系统一起工作。 ### 使用一个不同的 Shell 在本书中,我们使用的 shell 环境都是 bash,但是如果你更喜欢 zsh,你依然可以使用所有的 tmux 优良特性。 可以在 `.tmux.conf` 文件里明确地设置默认的 shell 环境,就像这样: ``` set -g default-command /bin/zsh set -g default-shell /bin/zsh ``` 由于 tmux 只是一个终端复用器而并没有拥有独立的 shell,因此可以精确地指定使用哪个 shell。 ### 默认启动 tmux 可以配置操作系统让它在打开一个终端时自动启动 tmux。通过使用会话名可以创建一个不存在的会话。 当 tmux 在运行时,它会把 `TERM` 变量设置为 `screen` 或者是 `default-terminal` 配置文件里的配置。可以在 `.bashrc` 文件(OS X 系统是 `.bash_profile` 文件)里使用这个变量来确定当前是否处于一个 tmux 会话中。我们在第 2 章配置了 tmux 终端为 screen-256color,因此可以使用这样一个脚本: ``` if [[ "$TERM" != "screen-256color" ]] then tmux attach-session -t "$USER" || tmux new-session -s "$USER" exit fi ``` 如果没有在 tmux 会话里,我们会尝试连接到一个名为 $USER 的会话里,也就是当前用户名。可以把这个值替换为任意你想要的值,在这里使用用户名能帮助我们避免冲突。 如果会话不存在,tmux 会抛出一个错误,shell 脚本会把这个错误解释为 `false` 值。然后脚本会继续执行右侧的命令,也就是创建一个以用户名作为名称的会话。然后退出脚本。 当 tmux 会话启动时,它会再次运行配置文件,但是这次它会看到我们处于一个 tmux 会话中,因此就会略过这部分的后续代码,然后继续执行配置文件的其他配置,确保所有环境变量都已被配置。 现在,创建一个新的终端会话时,我们就自动地连接到一个 tmux 会话中。但是请小心,因为你每次打开新的终端窗口时都会连接到相同的会话,在任意终端窗口里输入 `exit` 命令都会关闭所有连接到会话的终端窗口! ### 把程序输出记录到日志里 有时需要把一个终端会话的输出记录到日志文件里。我们在之前已经讨论过如何使用 `capture-pane` 和 `save-buffer` 命令来完成这些操作,但是实际上 tmux 可以通过 `pipe-pane` 命令把一个面板里的活动都记录到一个文本文件里。这很像许多 shell 里的 `script` 命令,使用 `pipe-pane` 命令可以选择打开或关闭这个功能,而且可以在一个程序已经运行之后再开始使用这个命令。 要激活这个功能,在命令模式输入命令 `pipe-pane -o "mylog.txt"`。 `-o` 参数让我们打开了输出功能,也就是说如果再次发送相同的命令就可以把这个功能关掉。为了更方便地执行这个命令,我们把它添加到配置文件里并绑定一个快捷键。像这样: ``` bind P pipe-pane -o "cat >>~/#W.log" \; display "Toggled logging to ~/#W.log" ``` 现在可以按下 `PREFIX P` 命令来控制打开日志功能了。多亏了 `display` 命令(`display-message` 命令的简写),我们可以在状态栏看见日志文件名。`display` 命令和状态栏一样能访问相同的变量。 ### 在状态栏添加电池电量显示 如果你在笔记本电脑上使用 tmux,你可能想在状态栏里显示电池剩余电量,尤其是终端在全屏状态下运行的时候。幸亏有 `#(shellcommand)` 变量能够让这个事情变得相当容易。 现在我们把电池状态添加到配置文件里。我们抓取了一个简单的 shell 脚本,它能够获取剩余电池电量并把它写入主目录下名为 `battary` 的文件里。要让 tmux 能够使用这个脚本,要赋予它执行权限。在终端里运行这些命令: ``` $ wget --no-check-certificate \ https://raw.github.com/richoH/dotfiles/master/bin/battery $ chmod +x ~/battery ``` 如果在终端里运行这个命令: ``` $ ~/battery Discharging ``` 我们就会看到电池剩余电量的百分比。可以让 tmux 通过 `#(<command>)` 命令把它显示在状态栏里。所以,要在时钟之前显示电池电量,需要这样修改配置文件里 `status-right` 这一行: ``` set -g status-right "#(~/battery Discharging) | #[fg=cyan]%d %b %R" ``` 重新加载 `.tmux.conf` 文件时,电池的剩余电量就会显示出来。 要想在电池充电时获取它的状态,需要执行这个命令: ``` $ ~/battery Charging ``` 然后根据上面的方法,可以把这个命令添加到状态栏里。这部分的工作留给你来完成。 使用这种方法更深度地定制状态栏。只需要编写自己的脚本然后返回你想要显示的任意值,然后把它扔到状态栏里。 ## 6.4 接下来做什么? 你已经学会了 tmux 的基础你就可以做很多事情,而且你现在已经对不同的配置有了一定的经验。tmux 的用户手册,可以在终端里获取,命令如下: ``` $ man tmux ``` 这里面有完整的配置选项列表和所有 tmux 可用命令。 别忘了 tmux 本身也是在快速进化之中。下一个版本将会带来新的配置选项,会为你带来更高的灵活性。 现在你已经把 tmux 集成到你的工作流之中了,你可以尝试发掘一些其他的常用技术。例如,可以一起使用 tmux 和 Vim 来创建更高效的开发环境。你还可以在 tmux 会话里使用 irssi(一个终端界面的 IRC 客户端)和 Alpine(一个基于终端的邮件应用),每个程序占用你的一个面板,和你的文本编辑器并排排列,或是让它们运行在后台窗口里。然后你可以从会话中分离出来,过段时间再连接到 tmux 会话中,一如既往。