neurosnap/zmx
GitHub: neurosnap/zmx
一款专注于终端会话持久化与附加/分离功能的轻量级工具,旨在替代 tmux 的会话管理而非窗口管理。
Stars: 1894 | Forks: 116
zmx
Session attach/detach for the terminal.
Docs
·
You might not need tmux
·
Sponsored by pico.sh
## 功能
- 持久化终端 shell 会话
- 能够附加到和分离 shell 会话,而不会终止该会话
- 原生终端回滚 (scrollback)
- 多个客户端可以连接到同一个会话
- 重新附加到会话时会恢复之前的终端状态和输出
- 无需附加到会话即可向其发送命令
- 以纯文本形式打印终端会话的回滚历史
- 在 mac 和 linux 上运行
- 本项目**不**提供窗口、标签页或分屏功能
## 演示
- [zmx - 介绍](https://youtu.be/UIXj0_rhPgI)
- [zmx - AI 门户](https://youtu.be/CV3skPYHP4Q)
## 安装
### 预编译二进制文件
- https://zmx.sh/a/zmx-0.6.0-linux-aarch64.tar.gz
- https://zmx.sh/a/zmx-0.6.0-linux-x86_64.tar.gz
- https://zmx.sh/a/zmx-0.6.0-macos-aarch64.tar.gz
- https://zmx.sh/a/zmx-0.6.0-macos-x86_64.tar.gz
### homebrew
```
brew install neurosnap/tap/zmx
```
### 软件包 (非官方)
- [Alpine Linux](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/zmx)
- [Arch AUR 追踪稳定版](https://aur.archlinux.org/packages/zmx)
- [Arch AUR 追踪 Git 版](https://aur.archlinux.org/packages/zmx-git)
- [openSUSE Tumbleweed](https://software.opensuse.org/package/zmx)
- [Gentoo (overlay)](https://codeberg.org/samuelhautamaki/samuelhautamaki-gentoo)
- [zmx-rpm 打包](https://github.com/engie/zmx-rpm/)
### 源码
- 需要 zig `v0.15`
- 克隆仓库
- 运行构建命令
请务必将 `~/.local/bin` 添加到你的 `PATH` 中:
```
zig build -Doptimize=ReleaseSafe --prefix ~/.local
```
## 用法
运行 `zmx help` 获取更多包含示例的用法信息。
```
Usage: zmx
[args...]
Commands:
[a]ttach [command...] Attach to session, creating if needed
[r]un [-d] [command...] Send command without attaching
[s]end Send raw input to session PTY
[p]rint Inject text into session display
[wr]ite Write stdin to file_path through the session
[d]etach Detach all clients (ctrl+\\ for current client)
[l]ist|ls [--short] List active sessions
[k]ill ... [--force] Kill session and all attached clients
[hi]story [--vt|--html] Output session scrollback
[w]ait ... Wait for session tasks to complete
[t]ail ... Follow session output
[c]ompletions Shell completions (bash, zsh, fish, nu)
[v]ersion Show version and metadata (socket dir, log dir)
[h]elp Show this help
```
## shell 提示符
当你附加到 `zmx` 会话时,我们不会提供任何指示来表明你正处于 `zmx` 中。我们确实提供了一个包含会话名称的环境变量 `ZMX_SESSION`。
我们建议在提示符中检查该环境变量,并在那里显示一些提示信息。
### fish
将此文件放在 `~/.config/fish/config.fish` 中:
```
functions -c fish_prompt _original_fish_prompt 2>/dev/null
function fish_prompt --description 'Write out the prompt'
if set -q ZMX_SESSION
echo -n "[$ZMX_SESSION] "
end
_original_fish_prompt
end
```
### bash 和 zsh
根据你使用的 shell,将其放入 `.bashrc` 或 `.zshrc` 中:
```
if [[ -n $ZMX_SESSION ]]; then
export PS1="[$ZMX_SESSION] ${PS1}"
fi
```
### powerlevel10k zsh 主题
[powerlevel10k](https://github.com/romkatv/powerlevel10k) 是一个 zsh 主题,它会覆盖默认的提示符状态栏。
将此内容放入 `.zshrc`:
```
function prompt_my_zmx_session() {
if [[ -n $ZMX_SESSION ]]; then
p10k segment -b '%k' -f '%f' -t "[$ZMX_SESSION]"
fi
}
POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS+=my_zmx_session
```
### oh-my-posh
[oh-my-posh](https://ohmyposh.dev) 是一个流行的 shell 主题和提示符引擎。当且仅当你激活了 zmx 时,以下代码将在提示符中显示图标和会话名称:
```
[[blocks.segments]]
template = '{{ if .Env.ZMX_SESSION }} {{ .Env.ZMX_SESSION }}{{ end }}'
foreground = 'p:orange'
background = 'p:black'
type = 'text'
style = 'plain'
```
### Starship
[Starship](https://starship.rs) 是一个流行的 shell 主题和提示符引擎。当且仅当你激活了 zmx 时,以下代码将在提示符中显示图标和会话名称:
```
format = """
${env_var.ZMX_SESSION}\
...
"""
[env_var.ZMX_SESSION]
symbol = " "
format = "[$symbol$env_value]($style) "
description = "zmx session name"
style = "bold magenta"
```
## shell 自动补全
可以使用 `completions` 子命令来启用对 `zmx` 命令和会话名称的 shell 自动补全。配置完成后,你将获得本地 `zmx` 命令和会话的自动补全功能:
```
ssh remote-server zmx attach session-na
# <- 自动补全建议出现在此处
```
### bash
将此内容添加到你的 `.bashrc` 文件中:
```
if command -v zmx &> /dev/null; then
eval "$(zmx completions bash)"
fi
```
### zsh
将此内容添加到你的 `.zshrc` 文件中:
```
if command -v zmx &> /dev/null; then
eval "$(zmx completions zsh)"
fi
```
### fish
将此内容添加到 `~/.config/fish/completions/zmx.fish` 中:
```
if type -q zmx
zmx completions fish | source
end
```
## 会话选择器
你可以在 shell 中添加一个交互式的会话选择器,让你通过模糊查找现有的会话、预览它们的回滚历史或创建新会话——所有这些都在一个提示符下完成。这对于远程 SSH 工作流尤其有用:将其添加到你的 shell 启动项中,这样连接到机器时就会立即弹出选择器。
需要 [fzf](https://github.com/junegunn/fzf)。
- **Enter** 选择匹配的会话(如果没有会话存在,则创建一个)
- **Ctrl-N** 使用输入的查询创建新会话,即使已高亮显示模糊匹配项
bash 和 zsh
```
zmx-select() {
local display
display=$(zmx list 2>/dev/null | while IFS=$'\t' read -r name pid clients created dir; do
name=${name#*name=}
pid=${pid#*pid=}
clients=${clients#*clients=}
dir=${dir#*start_dir=}
printf "%-20s pid:%-8s clients:%-2s %s\n" "$name" "$pid" "$clients" "$dir"
done)
local output query key selected session_name
output=$({ [[ -n "$display" ]] && echo "$display"; } | fzf \
--print-query \
--expect=ctrl-n \
--height=80% \
--reverse \
--prompt="zmx> " \
--header="Enter: select | Ctrl-N: create new" \
--preview='zmx history {1}' \
--preview-window=right:60%:follow \
)
local rc=$?
query=$(echo "$output" | sed -n '1p')
key=$(echo "$output" | sed -n '2p')
selected=$(echo "$output" | sed -n '3p')
if [[ "$key" == "ctrl-n" && -n "$query" ]]; then
session_name="$query"
elif [[ $rc -eq 0 && -n "$selected" ]]; then
session_name=$(echo "$selected" | awk '{print $1}')
elif [[ -n "$query" ]]; then
session_name="$query"
else
return 130
fi
zmx attach "$session_name"
}
```
你可以手动调用 `zmx-select`,将其绑定到某个快捷键,或者在非 zmx 会话的 shell 启动时自动运行它。配合 `&& exit`,常规流程将变为:通过 SSH 连接 -> 选择一个会话 -> 工作 -> 分离或退出会话 -> SSH 自动断开连接。使用 **Ctrl-C** 取消选择器将让你进入常规 shell 作为备用方案。
```
if command -v zmx &> /dev/null && command -v fzf &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
zmx-select && exit
fi
```
## 会话前缀
我们允许用户设置环境变量 `ZMX_SESSION_PREFIX`,它将为所有命令的会话名称添加前缀。这意味着如果设置了该变量,每个接受会话的命令都会自动加上该前缀。
```
export ZMX_SESSION_PREFIX="d."
zmx a runner # ZMX_SESSION=d.runner
zmx a tests # ZMX_SESSION=d.tests
zmx k tests # kills d.tests
zmx wait # suspends until all tasks prefixed with "d." are complete
```
## 设计理念
关于使用 `zmx` 而不是使用像 `tmux` 这样具有窗口、窗格、分屏等功能的工具,其全部理由在于,这些工作应该由你的操作系统窗口管理器来处理。如果使用像 `tmux` 这样的工具,你的开发技术栈中就会出现功能冗余:一个用于操作系统的窗口管理器,和一个用于终端的窗口管理器。此外,为了使用现代终端功能,你的终端模拟器**和** `tmux` 都需要支持这些功能。这阻碍了终端爱好者社区的发展和功能迭代。
相反,这个工具特别专注于会话持久化,并将窗口管理交由操作系统的窗口管理器处理。
## ssh 工作流
### 快速尝试
如果你想在不修改 `ssh` 配置的情况下尝试 `zmx` 和 `ssh`,请务必向 `ssh` 传递 `-t` 选项。以下是一个示例:
```
ssh -t dev-box zmx attach default
```
如果不加 `-t`,远程 shell 将不知道它正在与终端通信,显示画面很可能会变得混乱。
如果你按照下面的配置步骤操作,则不需要此选项,因为 `RequestTTY yes` 实现了相同的效果。
### 配置以供日常使用
将 `zmx` 与 `ssh` 结合使用是一等公民支持的功能。与其使用 `ssh` 通过一个终端和 `n` 个 tmux 窗格远程登录你的系统,不如打开 `n` 个终端并分别为它们运行 `ssh`。这听起来可能很繁琐,但有一些工具可以让这成为一个非常愉快的工作流程。
首先,为你的远程开发服务器创建一个 `ssh` 配置条目:
```
Host = d.*
HostName 192.168.1.xxx
RemoteCommand zmx attach %k
RequestTTY yes
ControlPath ~/.ssh/cm-%r@%h:%p
ControlMaster auto
ControlPersist 10m
```
从架构上讲,`ssh` 支持在与服务器的单个连接中多路复用多个通信通道。`ControlMaster` 这个设置就是告诉 `ssh` 通过一个 tcp 连接将多个 PTY 会话多路复用到单个服务器。非常巧妙!
现在你可以生成任意数量的终端会话:
```
ssh d.term
ssh d.irc
ssh d.pico
ssh d.dotfiles
```
因为 `attach` 命令本质上是一个“更新或插入 (upsert)”操作,这将会创建或附加到每个会话。
现在你可以使用 [`autossh`](https://linux.die.net/man/1/autossh) 工具让你的 ssh 连接自动重连。例如,如果你有一台笔记本电脑,合上再打开盖子时,它会自动重新连接你所有的 ssh 连接:
```
autossh -M 0 -q d.term
```
或者创建一个 `alias`/`abbr`:
```
abbr -a ash "autossh -M 0 -q"
```
```
ash d.term
ash d.irc
ash d.pico
ash d.dotifles
```
哇!现在你可以按照自己喜欢的方式为项目设置所有的操作系统平铺式窗口,并且可以拥有任意数量的窗口,这几乎完全复制了 `tmux` 的功能,而且是采用原生的窗口、标签页、分屏和回滚!它还有一个额外的好处,那就是支持你的模拟器支持的所有终端功能,不再受限于 `tmux` 所支持的功能。
这里的终极目标是利用你的窗口管理器的能力,通过单个命令自动为每个项目排列你的窗口。
## socket 文件位置
每个会话都有自己专属的 unix socket 文件。默认位置取决于你的环境变量(按优先级顺序检查):
1. `ZMX_DIR` => 使用确切路径(例如 `/custom/path`)
2. `XDG_RUNTIME_DIR` => 使用 `{XDG_RUNTIME_DIR}/zmx`(在 Linux 上推荐使用,通常会解析为 `/run/user/{uid}/zmx`)
3. `TMPDIR` => 使用 `{TMPDIR}/zmx-{uid}`(附加 uid 以确保多用户安全)
4. `/tmp` => 使用 `/tmp/zmx-{uid}`(默认后备方案,附加 uid 以确保多用户安全)
## 权限
你可以使用以下环境变量来配置 socket 目录和日志文件的权限:
- `ZMX_DIR_MODE` => 设置 socket 和日志目录的权限模式(八进制,默认为 `0750`)
- `ZMX_LOG_MODE` => 设置日志文件的权限模式(八进制,默认为 `0640`)
当以系统服务运行 `zmx` 并使用共享用户组时,这尤其有用。例如,设置 `ZMX_DIR_MODE=0770` 和 `ZMX_LOG_MODE=0660` 可以允许组成员附加到该会话。
## 调试
我们将 CLI 命令的全局日志存储在 `{socket_dir}/logs/zmx.log` 中。我们将会话特定的日志存储在 `{socket_dir}/logs/{session_name}.log` 中。目前它们默认是启用的,并且无法被禁用。这样做的目的是在我们达到稳定状态之前协助初期开发。
## 一份微小的契约
- 编写能够解决明确定义的问题的程序。
- 编写行为符合大多数用户期望的程序。
- 编写单人即可维护的程序。
- 编写能与其他微小型工具组合的程序。
- 编写能够被最终完成的程序。
## 已知问题
- 当升级 `zmx` 版本且我们修改了底层的 IPC 通信方式时,由于无法通过守护进程的 socket 正常通信,这会终止你所有的会话
- 通过 SSH 嵌套 `zmx` 会话时的终端状态恢复:主机 A `zmx` -> SSH -> 主机 B `zmx`
- 具体表现为光标位置会发生错乱
- 这本质上属于未定义且不支持的行为
- 当重新附加且之前启用了 kitty keyboard mode 时,我们会尝试重新发送该 CSI 查询以重新启用它
- 一些程序不知道如何处理该 CSI 查询(例如 `psql`),因此当你键入内容时,它会错误地回显 kitty 的转义序列
## 实现
- `daemon` 和客户端进程通过 unix socket 进行通信
- `daemon` 和 `client` 循环都利用了 `poll(2)`
- 每个会话都会创建自己的 unix socket 文件
- 我们使用 `libghostty-vt` 来恢复终端状态和输出
### libghostty-vt
当客户端重新附加到会话时,我们使用 `libghostty-vt` 来恢复终端之前的状态。
其工作原理如下:
- 用户创建会话 `zmx attach term`
- 用户与终端 stdin 交互
- stdin 通过 daemon 发送到 pty
- daemon 将 pty 输出发送给客户端*以及* `ghostty-vt`
- `ghostty-vt` 保存终端状态和回滚
- 用户断开连接
- 用户重新附加到会话
- `ghostty-vt` 将终端快照发送到客户端 stdout
这样一来,`ghostty-vt` 并不会介入活跃终端会话的中间环节,它只是接收与客户端接收到的完全相同的数据,以便它能够重新填充连接到该会话的客户端。这使得用户能够从他们上次中断的地方继续,就好像他们根本没有从终端会话中断开一样。它还有一个好处是非常快,位于你和你的 PTY 之间的唯一东西只有一个 unix socket。
## 前人作品
以下列出了启发我构建此项目的项目列表。在架构上,`zmx` 借鉴了这两个项目的特性。例如,`shpool` 启发了在重新附加时使用 libghostty 恢复终端状态的想法。Abduco 启发了每个会话对应一个 daemon(和 unix socket)的想法。
### shpool
https://github.com/shell-pool/shpool
`shpool` 是一项通过允许创建由 `shpool` 拥有的命名 shell 会话来实现会话持久化的服务,这样即使连接断开,会话也不会丢失。
### abduco
https://github.com/martanne/abduco
abduco 提供了会话管理功能(即它允许程序独立于其控制终端运行)。它与 dvtm 结合使用,提供了一个比 tmux 或 screen 更简单的替代方案。
## 横向对比
| 功能 | zmx | shpool | abduco | dtach | tmux |
| ------------------------------ | --- | ------ | ------ | ----- | ---- |
| 1:1 终端模拟器特性 | ✓ | ✓ | ✓ | ✓ | ✗ |
| 终端状态恢复 | ✓ | ✓ | ✗ | ✗ | ✓ |
| 窗口管理 | ✗ | ✗ | ✗ | ✗ | ✓ |
| 每个会话多个客户端 | ✓ | ✗ | ✓ | ✓ | ✓ |
| 原生回滚 | ✓ | ✓ | ✓ | ✓ | ✗ |
| 可自定义分离按键 | ✗ | ✓ | ✓ | ✓ | ✓ |
| 自动后台运行 (daemonize) | ✓ | ✓ | ✓ | ✓ | ✓ |
| 每个会话独立 daemon | ✓ | ✗ | ✓ | ✓ | ✗ |
| 会话列表 | ✓ | ✓ | ✓ | ✗ | ✓ |
## 社区工具
- [emacs-term-sessions](https://github.com/ArthurHeymans/emacs-term-sessions) 在 Emacs 中实现本地和远程的持久化终端会话。
- [pi-zmx](https://github.com/deevus/pi-zmx) -- 适用于 zmx 的 [pi](https://pi.dev) 扩展。
- [zsm](https://github.com/mdsakalu/zmx-session-manager) -- zmx 的 TUI 会话管理器。通过交互式终端 UI 列出、预览、过滤和终止会话。
- [zmosh](https://github.com/mmonad/zmosh) -- zmx 的一个分支,为远程会话增加了加密的 UDP 自动重连功能(类似于 mosh)。标签:SOC Prime, Zig, 开发工具, 终端会话管理