p-arndt/hop
GitHub: p-arndt/hop
hop 是一个终端内的多主机 SSH 会话管理 TUI,让用户在单个界面中无缝切换远程 shell、SFTP 浏览和远程文件编辑,无需反复认证或开新窗口。
Stars: 9 | Forks: 0
hop
**在服务器之间跳转,无需离开你的终端。**
按下一个键,你就进入了 shell。再按一个,你就在浏览它的文件。再按一个,你就
*在那台机器上*编辑文件,在一个标签页里,与其他所有页面并排。然后跳到下一个
主机,你留下的一切都原封不动地待在原地。
*没有新窗口。没有重新认证。无需费力寻找你之前打开的那个 shell。*
[](https://github.com/p-arndt/hop/actions/workflows/ci.yml)
[](https://github.com/p-arndt/hop/actions/workflows/release.yml)
[](go.mod)
[](#-install)
[](https://github.com/charmbracelet/bubbletea)
[安装](#-install) · [快速开始](#-quick-start) · [按键](#-keys) · [开发](#-development) · [路线图](#-roadmap)
## 🎬 运行界面
按键在按下时会显示在右下角。`●` 已连接 · `◐` 连接中 ·
`○` 空闲 · `×2` 打开了两个 shell · `▤` SFTP 浏览器已打开
顶部始终会告诉你**你的按键正发往何处**。这是嵌入他人程序的 TUI 最容易让人晕头转向的一点,
因此它占据了永久的屏幕空间。

The host list. Status dot, group, and what enter would do to the host under the cursor.
|

A shell. A real terminal in the pane; the footer shows the three ways back out.
|

The SFTP browser. f, over the connection that's already open.
|

A remote editor tab. enter on a file runs the editor on the server, so :w writes the real file.
|
更多截图:一个连接上的两个 shell、设置和按键说明卡

Two shells, one connection. S (or alt+0) opens another channel, no second handshake.
|

Settings. ,. The accent is a swatch strip that recolours hop as you walk it.
|

Every key hop binds. ?. It lists the keyboard you actually have, with vim motions included only if you turned them on.
|
|
录制中的每个主机、文件和命令都是虚构的。它运行在一个一次性的 SSH 服务器
(`tools/demoserver`)上,拥有自己的 HOME 目录,因此 `just demo`
可以在任何机器上重新录制它,而不会暴露任何信息。详见
[demo/hop.tape](demo/hop.tape)。
## ✨ 功能特性
| | |
| --- | --- |
| 🖥️ **内嵌 SSH shell** | 窗格中的真实终端:一个纯 Go 编写的 SSH 客户端(`x/crypto/ssh`)为一个真实的 VT 模拟器(`x/vt`)提供数据。支持 Agent *或* 私钥认证、调整大小、光标等一切功能。 |
| 🔑 **2FA 和密码** | 当主机要求验证码(`pam_google_authenticator`)或密码时,会在其发出请求的瞬间弹出一张卡片。拨号会在握手中等待,而不是重启——一次性验证码只能使用一次。不存储任何信息;每个主机只提示一次,因为每个额外的 shell 都共用同一个连接。 |
| 🗂️ **每个主机多个 shell** | `S`(或 `alt+0`)在你已连接的主机上打开另一个 shell。它是一个*新通道*,因此无需新握手也无需二次认证。顶部的标签页可通过 `alt+1…9` 切换。 |
| 📁 **SFTP 文件浏览器** | `f` 可通过你现有的连接浏览远程文件系统。用 `d` 下载,用 `o` 在本地打开。 |
| ✎ **远程编辑器标签页** | 在文件上按 `enter` 会**在服务器上**通过第二个通道运行 `$EDITOR`,并将其呈现在一个标签页中。无需下载,无需复制,`:w` 会直接写入真实文件。 |
| 📂 **在你所在的位置使用 VS Code** | 在 shell 中按 `ctrl+o` `ctrl+o`(或在列表中按 `o`)会根据 shell 当前所在目录打开 VS Code Remote,而不是你登录时的目录。hop 通过 OSC 7 追踪它,自动将 prompt hook 安装到 bash/zsh 中,并清除它输入的那一行,让窗格看起来毫无痕迹。 |
| ⇅ **回滚历史** | `shift+↑` 会将 shell 暂停到其历史记录中,并支持类似 vim 的翻页。当全屏程序占据屏幕时,它会自动礼貌地拒绝此操作。 |
| 🔁 **掉线后重连** | 断开的连接(挂起的笔记本电脑、断掉的 VPN、重启的机器)会被*察觉到*——通过 keepalive 探测而非死寂——并且窗格会保留主机绘制的最后一个画面,而不是悄悄卡死。`r` 会重新拨号,并恢复会话的原貌:相同的 shell 标签页,以及停留在原目录的浏览器。 |
| 🔐 **诚实的主机密钥** | 未知的密钥会**中止拨号**并显示指纹卡片。`y` 信任它并追加;`n` 不予信任。*不匹配*始终是严重的错误。 |
| 📥 **SSH 配置导入** | `i`(或 `hop import`)会更新来自 `~/.ssh/config` 的所有主机。这是一次*同步*,而不是一次性行为:重新导入会刷新,而你手动添加的主机会被保留。 |
| 🔎 **模糊查找** | `/` 会在你输入时进行过滤,并高亮匹配的字符,让意外的命中结果一目了然。 |
| 🖱️ **鼠标** | 支持在列表、浏览器和窗格中滚动和点击:滚轮可滚动 shell 的历史记录,双击可建立连接,点击标签页可切换到该页面。每一个手势都映射为你已有的按键,而请求使用鼠标的远程程序(vim 的 `set mouse=a`、`htop`)会原样获取指针操作。`,` → *Mouse* → 关闭,可将点击拖拽的选择权交还给终端。 |
| ⚙️ **实时设置** | `,` 会打开一个弹窗,用于设置编辑器、下载目录、强调色(你能*看到*的色板条,而不是需要查找的数字)、打开方式以及 vim 按键。即刻生效。 |
| 🎯 **频率+最近使用排序** | 你实际使用的主机会浮到列表顶部。 |
| 🪟 **跨平台** | 适用于 Windows、macOS 和 Linux(amd64 + arm64)的静态、无依赖二进制文件。没有 cgo、libc 或运行时。 |
| ⌨️ **可选的 vim 按键** | 默认关闭,因为 `h`/`l` 代表“退出”和“进入”主机,对于没有此需求的人来说会感到意外。只需拨动一个开关,相应的动作就会立刻出现在各个地方。 |
## 📦 安装说明
### 下载二进制文件
从 [最新发布版本](https://github.com/p-arndt/hop/releases/latest) 获取适用于你平台的压缩包,
并将 `hop` 放到你的 `PATH` 路径中。
```
# macOS / Linux
tar -xzf hop_*_darwin_arm64.tar.gz
sudo mv hop /usr/local/bin/
```
```
# Windows
Expand-Archive hop_*_windows_amd64.zip -DestinationPath .
# 然后将 hop.exe 移动到你的 PATH 上
```
每个版本都附带一个 `hop_
_checksums.txt`,因此你可以使用 `sha256sum -c` 进行校验。
### 保持最新
```
hop check-update # is there a newer release?
hop self-update # download it, verify its checksum, swap this binary
```
`self-update` 会从最新的 GitHub 发布版本中获取*你所用*平台的压缩包,
根据该版本的 `checksums.txt` 校验其 SHA-256,并原子性地替换正在运行的二进制文件。
在 Windows 上,旧的 `hop.exe` 会被重命名移至一旁,并在下次启动 hop 时被清理。
源码构建(`version = dev`)会被拒绝:因为没有东西可以用来与它们比对。
hop 还会在后台每天检查一次,并在页脚和 CLI 中提示有新版本。
设置 `HOP_NO_UPDATE_CHECK=1` 可关闭此功能;上述两条命令依然有效。
### 从源码构建
需要 [Go 1.26+](https://go.dev/dl/)(以及可选的 [`just`](https://github.com/casey/just) ≥ 1.39)。
```
git clone https://github.com/p-arndt/hop.git && cd hop
just build # -> ./hop (or: go build -o hop .)
just build-release # stripped + version-stamped
```
## 🚀 快速开始
```
hop # launch the TUI
```
首次运行且没有任何主机时,hop 会主动为你导入 `~/.ssh/config`:按一下
`enter`,你的列表就会被填满。当你已经在 shell 中时,也可以使用 CLI:
```
hop import # sync hosts from ~/.ssh/config
hop import path/to/cfg # …or from somewhere else
hop add web1 deploy@10.0.0.4:2222
hop list # alias user@host:port
hop check-update # is a newer release out?
hop self-update # upgrade this binary in place
hop version
```
然后,在 TUI 中:`↑`/`↓` 移动,`enter` 连接,`ctrl+o` 返回。
这就是整个操作模型。
### 文件存放位置
| | 路径 |
| --- | --- |
| 主机数据库 | `/hop/hop.db` (SQLite) |
| 设置 | `/hop/config.json` (纯 JSON,可手动编辑) |
| 更新检查缓存 | `/hop/update-check.json` (最后一次检查 + 看到的最新版本) |
| 已知主机 | 你常规的 `~/.ssh/known_hosts` |
`` 在 Windows 上是 `%AppData%\hop\`,在 macOS 上是
`~/Library/Application Support/hop/`,在 Linux 上是 `~/.config/hop/`。
配置文件缺失或格式错误时,hop 会使用默认配置启动,而不会拒绝运行。
## ⌨️ 按键
hop 有三种模式,页脚始终显示你当前所处模式的快捷键。
每种模式都可以通过 **`ctrl+o`** 返回到主机列表。
| 模式 | 何时处于此模式 | 谁在接收你的按键 |
| --- | --- | --- |
| **导航** | 主机列表获得焦点时(默认) | hop |
| **浏览** | 你用 `f` 打开了 SFTP 浏览器 | hop |
| **终端** | 你用 `enter` 或 `s` 进行了连接 | **远程 shell** |
主机列表
| 按键 | 动作 |
| --- | --- |
| `↑` `↓` / `pgup` `pgdn` | 移动 / 翻页 |
| `enter` `→` | 连接,或聚焦已打开的 shell |
| `s` / `S` | 聚焦此主机的会话 / 在其上打开**另一个** shell |
| `f` | SFTP 浏览器 |
| `o` | 在此主机 shell 所在的目录中打开 VS Code Remote |
| `d` | 断开连接 |
| `r` | 重新连接掉线的会话 |
| `a` `e` `x` | 添加 / 编辑 / 删除主机 |
| `i` | 从 OpenSSH 配置中导入 |
| `/` | 模糊过滤 |
| `,` `?` | 设置 / 按键说明卡 |
| `ctrl+b` | 隐藏 / 显示侧边栏 |
| `q` `ctrl+c` | 退出 |
在 shell 内部
| 按键 | 动作 |
| --- | --- |
| `ctrl+o` | 返回 hop |
| `esc` `esc` | 返回 hop(在 400 毫秒内) |
| `alt+0` | 在此主机上打开另一个 shell |
| `alt+←` `alt+→` / `alt+1…9` | 切换 shell |
| `ctrl+o` `ctrl+o` | 在 VS Code Remote 中打开**当前目录**(先退出窗格,然后打开) |
| `shift+↑` / `shift+pgup` | 进入回滚历史 |
| `ctrl+b` | 隐藏侧边栏 —— shell 将占据整个窗口 |
| *其他所有按键* | 发送到远程 shell |
`ctrl+o` `ctrl+o` 会在 shell 当前所在目录打开 VS Code Remote ——
`cd` 到某处,`ctrl+o` 退出窗格,再按一次 `ctrl+o`。(之所以使用组合键,是因为
远程 shell 接管了所有普通按键,而控制字节是所有终端都无需配置即可发送的唯一方式。)hop 通过终端用于此目的的转义序列
**OSC 7** 获知该目录,并为 **bash** 和
**zsh** 自动安装 prompt hook:在首次出现提示符时输入一行命令,然后从窗格中抹除,
让会话看起来毫无痕迹。如果 shell 已经在发送 OSC 7,或者全屏程序(vim、tmux、`ForceCommand`)占据了屏幕,则不会向其输入任何内容;如果除了 hop 自己的命令行之外还有其他内容,也会拒绝执行擦除操作。在 hop 无法获知目录的任何地方,该按键仍会在主机的默认目录中打开,并予以提示。详见
[KEYBINDINGS.md](KEYBINDINGS.md)。
文件浏览器和编辑器标签页
| 按键 | 动作 |
| --- | --- |
| `enter` `→` | 进入目录,或在远程编辑器标签页中打开文件 |
| `o` / `d` | 在本地桌面应用中打开 / 下载 |
| `←` `backspace` | 返回上一级目录 |
| `r` | 刷新 |
| `ctrl+b` | / 显示侧边栏 |
| `alt+←` `alt+→` / `alt+1…9` | 切换编辑器标签页 |
| `:q` | 关闭编辑器标签页 |
| `ctrl+o` | 返回上一级 |
当连接断开时
| 按键 | 动作 |
| --- | --- |
| `r` `enter` | 重新连接并重新打开之前打开的内容 |
| `d` `x` | 丢弃会话 —— 主机恢复为空闲状态 |
| `ctrl+o` `esc` `q` | 返回主机列表,窗格保留在屏幕上 |
窗格会在横幅下保留主机绘制的最后一个画面,横幅说明了发生的情况。
没有内容会被转发到远端,因为远端已经不存在了。
📖 **[KEYBINDINGS.md](KEYBINDINGS.md)** 是完整的参考文档,包括 vim 移动指令、设置弹窗,以及*为什么*保留每个特定组合键的原因。
## 🛠️ 开发说明
```
just # list recipes
just run list # go run . list
just build # dev binary
just test # go test ./...
just test-e2e # + the Docker 2FA end-to-end tests (needs Docker)
just vet
just fmt # gofmt -w .
just ci # fmt-check + vet + test (what CI runs)
just demo # re-record assets/demo.gif + the stills (needs vhs)
```
`justfile` 刻意保持通用性:指令体是可在 `sh` 和 PowerShell 下运行的纯命令,而需要真正 shell 逻辑的那两条命令(`fmt-check`、`clean`)通过 `[unix]` / `[windows]` 属性进行了拆分。
**演示。** `just demo` 录制了上面的 GIF 和截图。`scripts/demo.mjs`
构建 hop,将 `HOME` 指向一个带有预设主机数据库的一次性目录,并启动
`tools/demoserver`,这是一个仅限本地回环的 SSH 服务器,它虚构了屏幕上的所有
内容:一个带有预设命令输出表格的假 shell、一个通过 SFTP 挂载的内存文件系统,以及一个虚假的 vi。角落里的按键叠加显示是 hop 自身的功能,仅在 `-tags hopdemo`(`internal/tui/keycast.go`)下编译,因此发布的二进制文件不会包含它。
**测试。** 无头测试使用真实的按键驱动真实的 Bubble Tea 模型,针对进程内的 Go SSH/SFTP 服务器和临时文件存储进行测试。详见
`internal/tui/hostmgmt_test.go`、`TestEmbeddedRoundTrip`、`TestSFTPRoundTrip`。
**2FA 端到端测试。** 进程内的 Go SSH 服务器会按照你的设定进行响应,但这无法证明 hop 能否真正登录到开启了双因素认证的机器上。因此
`internal/dockerenv` 会启动一个运行着真实 `openssh-server` 和真实的 `libpam-google-authenticator` 的 Ubuntu 容器,按照官方指南进行配置,并在四种情况下进行监听:仅验证码、强化的
`publickey,keyboard-interactive`、密码后加验证码,以及将这两种方式作为备选提供。这些测试像手机一样计算 TOTP 验证码并进行登录 —— `internal/sshx` 通过 SSH 引擎登录,`internal/tui` 通过在实际的卡片中输入进行测试。阴性对照(错误的验证码、十分钟前的验证码)也是测试套件的一部分,因为如果容器接受任何输入,就会让这里的所有其他测试通过,却什么也证明不了。使用 `just test-e2e` 选择启用;如果没有
`HOP_DOCKER_E2E=1`,它们将被跳过,因此 CI 和没有 Docker 的笔记本电脑不受影响。
CI 会在 Windows / Linux / macOS 矩阵上运行审查 + 测试 + 构建,因为 agent 传输和本地打开处理器是针对特定平台的:单操作系统的运行无法判断其他系统是否还能编译。
**发布。**
```
just release # patch bump: stamps VERSION, commits, tags, pushes
just release minor # or major, or an explicit 1.0.0
```
标签的推送会触发发布工作流:它会受到三操作系统测试矩阵的检验,
然后从一台 Linux runner 交叉编译所有六个目标(windows/linux/darwin × amd64/arm64),并附带校验和及由 git-cliff 生成的更新日志。Windows 会获得一个 `.zip` 压缩包,其他所有系统则是 `.tar.gz`,以保留执行位。
## 🗺️ 路线图
接下来:
- 🔌 **隧道 / 端口转发**:通过 hop 已建立的连接,为每个主机提供本地和远程转发
- 💓 **健康面板**:针对每个主机的可达性、延迟、运行时间和磁盘状态
- ⬆️ 浏览器中的**上传与文件操作**(`u`、`x`、`R`、`m`),并带有异步传输进度
- 🏷️ 列表中的**分组与标签**:按组划分、按标签过滤、固定收藏夹
- 📐 窄屏终端布局,支持向窗格中复制/粘贴
包含每个项目更多细节的最新动态版本请参见 [TODO.md](TODO.md)。
## 🙏 构建技术
[Bubble Tea](https://github.com/charmbracelet/bubbletea) · [Lip Gloss](https://github.com/charmbracelet/lipgloss) ·
[x/vt](https://github.com/charmbracelet/x) · [x/crypto/ssh](https://pkg.go.dev/golang.org/x/crypto/ssh) ·
[pkg/sftp](https://github.com/pkg/sftp) · [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) ·
[sahilm/fuzzy](https://github.com/sahilm/fuzzy) · [skeema/knownhosts](https://github.com/skeema/knownhosts)
专为拥有太多服务器却没有足够终端的人打造。
标签:EVTX分析, Go, Ruby工具, SSH客户端, 日志审计, 服务器管理, 终端工具, 运维工具, 远程连接