前段时间为了把包括 Niri 在内的所有配置文件集中管理, 设计了一个 Dotfile 管理工具. 这个脚本作为框架存在, 是一个命令执行器, 本身不具备安装命令的功能. 这个脚本在设计之初还有跨设备和跨系统 (包括 Linux 和 MacOS)需求. 因此动用了不少手段来实现能够同时管理不同版本的 dotfiles , 并复用共有的部分.

我曾经在 neovim 的配置过程中尝试过每个设备一个分支进行管理, 但是效果并不好. 这次并没有选择git多分支管理, 因为多分支需要定期通过 patch 或者定期 rebase 或 merge 来同步, 共有复用代码处理的并不好.

Bootstrap 脚本

./bootstrap 负责驱动 dotfiles 的检测, 安装和卸载过程.

./bootstrap init --item=test 会在items目录下初始化名为test的项目, 初始的结构如下.

1
2
3
4
5
6
7
8
9
10
.
├── bootstrap
├── bootstrap.conf
├── items
│ └── test
│ ├── item.conf
│ └── src
├── README.md
└── utils
└── replace.awk

Item

这个脚本管理的对象是item, 除了普通的配置文件, 还包括字体, 应用程序启动补丁等. 我现在的一些配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ ./bootstrap -ls
[+] alacritty
[+] anyrun
[+] bash
[+] btop
[+] discord
[+] fastfetch
[+] fcitx5-theme
[+] fish
[+] fonts
[+] ghostty
[ ] gimp-patch
[+] gitignore
[+] gnome-setting
[+] gtk-theme
[ ] hyprland
[+] hyprlock
[+] imv
[ ] karabiner
[+] kitty
[+] linuxqq
[+] lsd
[+] matrix
[+] mpv
[+] neovide
[+] niri
[+] nvim
[ ] nvim.vscode
[ ] quickshell
[ ] rofi
[+] starship
[+] swaync
[+] tmux
[+] waybar
[+] wezterm
[+] wlogout
[+] xmake
[+] yazi
[+] yt-dlp
[+] zathura
[+] zsh

每个 item 都有自己的 item.conf, 用来管理这个项目相关的安装和卸载行为.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env bash

# Check the dependency return false if installed dependency
function check_dep() {
return 0
}

# Things to do to install the dependency
function install_dep() {
:;
}

# Things to do to install the item
function install() {
:;
}

# Check if the item is installed, return false if installed
function check() {
return 0
}

# Things to do to uninstall the item
function uninstall() {
:;
}

这个空的模板定义了相关的函数. 这里的依赖可以是配置文件对应的程序, 例如 nvim 的依赖是 nvim, 在 install_dep 函数中使用 系统包管理器安装或拉取源码编译.

注意, 在编写安装步骤的时候实际产生影响的安装命令都需要在前面使用run来包装. 这保证了在dry-run模式下命令不会被执行, 并能被监控. 有重定向或者管道的命令需要使用引号包裹. 这是实现上的缺陷导致的. 我没有找到完美的命令执行器(也许需要一个含有) 控制流的类似makefile的dsl出现才行吧(笑

相关变量

  • $rootdir

代表整个 dotfiles 仓库的根目录绝对路径, 在编写涉及链接或者移动文件时, 最好使用$rootdir为头生成绝对目录.

  • $sys_id

你可以给自己的安装对象起名字. 例如有两台安装 linux 系统的计算机需要安装不同的配置, 就可以起不同的sys_is 加以区分.

bootstrap.conf

同在项目根目录下的bootstrap.conf负责配置脚本相关的路径和基本信息, 这个配置文件是第一次运行脚本自动生成的, 其中rootdir是 由你的 dotfiles 项目根目录位置决定的, 不应该修改. 你需要修改 sys_id 字段, 并给当前系统起名字(这一步是可选的)

presets.conf

presets.conf 主要定义预设组合, 很多配置项目经常是组合安装的, 比如装机的时候需要恢复桌面环境和常用软件的配置. 也可以在 其中配置系统包管理器的选项.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env bash
presets["linux_basic"]="alacritty bash zsh fastfetch tmux yazi nvim nvim.vscode"
presets["macmini"]="bash btop fastfetch ghostty karabiner kitty lsd nvim nvim.vscode tmux wezterm yazi zsh"
presets["hypr"]="hyprland kitty rofi waybar"
presets["niri"]="alacritty anyrun bash btop fish fastfetch fcitx5-theme gnome-setting \
gtk-theme git imv hyprlock lsd mpv nvim niri waybar swaync tmux wlogout yazi zathura zsh"

paru_flags="--noconfirm --skipreview"

pacman_opt="--noconfirm"

brew_flags=""

function paru() {
command paru $paru_flags "$@"
}

function pacman() {
command pacman $pacman_opt "$@"
}

function brew() {
command brew $brew_flags "$@"
}

工具脚本

utils/replace.awk是用来辅助生成配置文件的文本处理脚本. 有些配置文件只有在特定的地方才有区别, 可以使用模板来进行替换.

1
awk -f replace.awk <variables file> <template file>

在 MacOS 下需要使用gawk

变量被按字段储存:

1
2
3
4
5
6
7
8
9
BEGIN {
shell = "/bin/zsh"
opacity = 0.95
font_family_normal = "ComicCode Nerd Font Medium"
font_family_bold = "ComicCode Nerd Font Medium"
font_family_italic = "ComicCode Nerd Font Medium"
font_family_bold_italic = "ComicCode Nerd Font Medium"
font_size = 9.5
}

模板文件中, 变量被${{name}}标记, 并被替换成前面设置的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# WARN: THIS IS A FILE GENERATED BY A TEMPLATE, DONNOT EDIT IT 

[env]
TERM = "xterm-256color"
[terminal.shell]
program = "${{shell}}"
[window]
opacity = ${{opacity}}
padding.x = 2
padding.y = 2
decorations = "Full"
decorations_theme_variant = "Dark" # "Dark"
dimensions.columns = 200
dimensions.lines = 50
dynamic_title = false
[font]
normal.family = "${{font_family_normal}}"
bold.family = "${{font_family_bold}}"
italic.family = "${{font_family_italic}}"
bold_italic.family = "${{font_family_bold_italic}}"
size = ${{font_size}}
[general]
import = [
"~/.config/alacritty/color-scheme/catppuccin-mocha.toml"
]
[terminal]
osc52 = 'CopyPaste'

具体的使用方法都可以在我的 dotfiles 仓库看到.

Misc && FAQ

  • 这个脚本安装的配置会不会覆盖原本的配置文件?

这个脚本只负责执行安装的一系列命令, 你应该在check函数里检查install函数要安装的位置, 如果check函数返回非0, 会被视为配置已经安装. 再 install 的时候会询问是否覆盖.

  • 对比常见的dotfiles管理方案?

    • GNU Stow

    这直接链接到对应位置, 我不是很喜欢在 git 仓库下还出现 .config 这样的点文件. 而且 GNU Stow 不会在安装某个软件配置的时候 附带安装它, 你可能需要一个 list 来记录有哪些软件包被安装. 而且它只是简单的链接, 没有办法做到同时维护多个系统的配置. 到 时候又得开新的分支.

    • 裸储存库

    我对裸储存库的印象主要来自这里. 把整个 $HOME 做 git 的工作树还是 风险比较大, 我觉得这不适合新手, 也不适合有跨系统, 跨设备移植需求.

    • chezmoi

    之前被朋友安利过, 这里有一篇文章介绍如何使用. 虽然能进行跨平台的管理, 但是 似乎不是很方便. *chezmoi自己的配置文件不能被管理*

    我写的这个脚本完全剥离了具体的安装管理 dotfiles 的步骤, 只是提供了工具. 可以自由的配置在什么系统下执行什么命令, 安装什么 软件的配置, 可以完全适配跨系统, 跨设备, 快速重装和部署 dotfiles 的任务, 并配合 git 方便的进行管理.