posts
用 Rust 写了一个 dotfiles 管理工具
原来的脚本越来越难维护,所以写了一个支持 Plan、执行预览和历史记录的 TUI 工具。
我一直使用 Git 管理 Fish、Neovim、tmux、Ghostty 等配置。
最开始只需要写几个脚本,把仓库中的文件链接到 ~/.config。后来又陆续加入了软件安装、目录创建、插件更新、旧配置清理和不同系统的判断,脚本逐渐变得不好维护。
主要有几个问题:
- macOS 和 Linux 的安装方式不同;
- 不同机器不需要完全相同的配置;
- 执行前看不到完整的操作列表;
- 文件已经存在时,不容易判断应该备份、覆盖还是跳过;
- 执行失败后,只能从终端输出中寻找失败位置;
- 关闭终端后,无法再查看之前的执行结果。
所以我写了一个简单的 dotfiles 管理工具:dotman。
项目代码仍然放在我的 dotfiles 仓库中:
https://github.com/tabsp/dotfiles
基本流程
dotman 使用 Rust 编写,支持 macOS 和 Linux,默认提供一个基于 Ratatui 的终端界面。
执行流程如下:
Profile → Sync → Plan → Review → Run → History
Profile 用来记录 dotfiles 仓库地址、分支和本地路径。
运行时先同步仓库,再读取其中的 dotman.yaml,生成本次部署计划。确认没有问题后才会真正执行,最后保存运行记录。
日常使用直接运行:
dotman
无人值守环境可以使用:
dotman deploy --headless
配置
部署内容保存在仓库根目录的 dotman.yaml 中。
package_managers:
macos: brew
ubuntu: brew
arch: pacman
auto_install_pkg_manager: true
default_shell: fish
install:
- fish
- tmux
- neovim
- lazygit
- ripgrep
- fzf
links:
~/.config/fish: config/fish
~/.config/nvim: config/nvim
~/.tmux.conf: config/tmux.conf
create:
- ~/.config/fish/local.d
- ~/Workspace
shell:
- command: fish -lc 'fisher update'
description: Sync fish plugins
optional: true
if: command -v fish >/dev/null 2>&1
clean:
- target: ~/.config/old-tool
force: true
目前支持五类操作:
install:安装软件;links:创建配置文件软链接;create:创建目录;shell:执行额外命令;clean:清理旧配置。
软件安装命令不直接写在 YAML 中,而是从内置工具数据库中根据当前平台选择。
链接也可以单独设置冲突处理方式:
links:
- target: ~/.config/nvim
source: config/nvim
backup: true
relink: false
这样在目标目录已经存在时,可以明确选择备份或重新链接,不需要在 shell 中重复写判断逻辑。
Plan 和 Review
我的几台机器用途不同,并不需要安装完全相同的软件。
以前通常使用 hostname、环境变量或者多份脚本区分,现在 dotman 会先生成 Plan,可以在界面中选择本机需要的项目,并保存选择结果。
Plan 只表示准备执行什么,Review 会进一步检查当前状态,例如:
- 软件是否已经安装;
- 软链接是否正确;
- 目标路径是否存在冲突;
- shell 命令的执行条件是否满足;
- clean 会删除或备份什么;
- 是否需要 sudo。
这样可以在真正修改文件之前发现问题。
执行和历史记录
执行时,每个 action 都有独立状态:
Pending
Running
Changed
No Change
Skipped
Not Run
Aborted
Failed
stdout 和 stderr 会实时显示在界面中,并按照具体操作分组。
如果中途取消,已经完成的操作会保留原状态,当前操作标记为 Aborted,剩余操作标记为 Not Run。
每次执行都会生成一个 ULID,记录保存在:
~/.local/share/dotman/runs/<ulid>.json
可以通过下面的命令查看:
dotman history
dotman run <ulid>
记录中包含执行时间、主机信息、配置版本、每个操作的状态、错误和输出。
这主要解决了脚本运行结束后无法回头检查的问题。
为什么使用 Rust
选择 Rust 首先是因为它比较适合这类工具。
dotman 需要处理文件系统、子进程、并发输出、执行中断、状态转换和终端恢复。使用结构体和枚举表达 Plan、Action 和 Run,比使用大量字符串和条件判断更清晰。
另一个原因是 Rust 比较适合使用 Agent 开发。
Agent 修改代码后,可以立即运行:
cargo check
cargo clippy
cargo test
编译器会直接指出字段变化、类型不匹配和没有处理完整的枚举分支。
例如给 action 新增一个状态后,所有没有处理该状态的 match 都会产生编译错误。Agent 可以根据这些错误继续修改,而不是等到运行时才发现某个界面或历史记录漏掉了新状态。
Rust 的类型约束在这里相当于额外的检查机制,比较适合让 Agent 持续修改和重构代码。
另外,最终程序可以编译为单个二进制文件,新机器不需要先安装 Python 或 Node.js。
项目结构
主要代码大致分为以下几部分:
src/
├── config.rs # 读取 dotman.yaml
├── profile.rs # 管理仓库和 Profile
├── plan.rs # 生成 Plan
├── execute.rs # 执行操作
├── store.rs # 保存选择和运行记录
├── headless.rs # 无交互模式
├── ops/ # install、link、shell、clean
└── tui/ # Plan、Review、Run、History
执行器和 TUI 是分开的。
执行器产生结构化事件,TUI 将事件显示为进度和日志,Headless 模式则直接输出到终端。两种模式使用同一套执行逻辑。
目前 dotman 已经可以完成我自己的新机器初始化和日常配置同步,后续主要继续补充测试、跨平台行为和 TUI 细节。