Skip to content
qxukenPublic

About

Single-binary dotfiles manager with Lua config, age-encrypted secrets and git history.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

qd

Dotfiles manager: one static binary, modules configured in Lua 5.5, secrets encrypted with age, history in git.

qd status              # what push would do
qd push [module...]    # repo → machine, runs setup hooks on first run
qd pull [module...]    # machine → repo
qd pull -s "message"   # …then commit and push
qd undo                # revert the last apply from the trash
qd init --url ssh://…  # new machine
qd add NAME PATH       # start keeping a directory as module NAME

Every module is a directory in the dotfiles repo with a qd.lua that returns a table; see contrib/qd.d.lua for the schema and DESIGN.md for how it works. A root qd.lua holds shared ignore, encrypt and include entries, plus whatever the plugins read from it.

local qd = require("qd")
return {
  path    = qd.host.windows and qd.path.local_appdata("nvim") or qd.path.config("nvim"),
  brew    = { "neovim", "ripgrep" },
  encrypt = { "**/*.p12" },
  ignore  = { "**/lazy-lock.json" },
  nushell = { source = { "config.nu" }, env_source = { "env.nu" } },
  setup   = { version = 1, after = function(m) qd.run("fnm", "install", "--lts") end },
}

brew, scoop and nushell are not core fields: they belong to plugins. The core syncs files and runs setup hooks; a plugin owns one key in every qd.lua and turns it into files to write (compile) or commands to run (packages), both pure, so --dry-run and undo cover them. The three built-ins load unless the root file declares its own list:

-- root qd.lua
return {
  plugins = { require("qd.nushell"), require("qd.brew"), require("plugins.mine") },
}

Plugins get qd.fail for errors, qd.check with qd.schema for validating their key, and qd.warn / qd.debug for output. All plugin and config output goes to stderr, print included, so qd show --format json stays parseable. See the Plugins section of DESIGN.md for the contract and contrib/apt.lua for a worked example.

setup.push runs after every push of its module, not only the first, for anything derived from the machine rather than the repo, such as a cache of brew --prefix.

Files a setup hook generates into path belong in ignore; otherwise the next pull copies them into the repo and the next push on another machine treats them as strays.

Machine state

qd state path shows the directory holding state.toml (tags, repo and identity paths, per-module setup versions), journal.jsonl, and trash/. Override it with QD_STATE, the home that destinations hang off with QD_HOME, and where self-update looks with QD_UPDATE_BASE_URL. Removed and overwritten files go to the trash; qd trash prune --older 30d cleans up.

Tags gate enabled in a config. qd tag add <name> records one in state.toml, which is what you want on a machine you own; QD_TAGS=a,b adds tags for one command, which is handy for trying a config out (QD_TAGS=work qd show version-control). DOTFILES_TAGS is read too, for the Nushell tool this replaces. Detected facts are not tags — a config asks qd.host.wsl or qd.host.ubuntu for those.

Keys

master.rec (recipients) is committed in the repo root; master.key is not and lives next to it or wherever qd state set-identity points.

As a library

The crate is qdot (qd is another crate's name on crates.io); the library and the binary are qd. Without its default cli feature it is the library alone — no clap, ureq or self-replace — and qd::Session does what the binary does, returning data and printing nothing:

qdot = { version = "0.3", default-features = false }
let mut s = qd::Session::open(None)?;            // QD_STATE, the recorded repo
let plans = s.status(&[], qd::plan::Direction::Push)?;
let done = s.sync(&["helix".into()], qd::plan::Direction::Pull, qd::SyncOpts::default())?;
s.add_module("kawoosh", &home.join(".config/kawoosh"), &["fonts/**".into()])?;

A program linking it shares state.toml, the journal and the trash with the qd on the PATH, so it should hold to qd::VERSION matching that binary's, and run the binary when they differ. kawoosh's dotfiles pane does.

Build

cargo build --release

.github/workflows/ci.yml runs fmt, clippy and the tests on Linux, macOS and Windows for every push and pull request.

Releases are cut by pushing a vX.Y.Z tag matching Cargo.toml; .github/workflows/release.yml builds each target on a runner of its own kind, attaches the five binaries plus checksums.sha256, version.txt and install.nu to the GitHub release, and publishes the crate to crates.io.

Updating

qd self-update [--dry-run]

It reads three files from a base URL — version.txt, checksums.sha256 and the binary for this platform — verifies the SHA-256 and swaps the executable. No forge API is involved, so a mirror, or a new home after a move, needs only to serve those three files:

qd self-update --base-url https://somewhere/else

Override the default permanently with QD_UPDATE_BASE_URL, or bake it into a build with the same variable. If even the base URL has gone, install one named binary directly:

qd self-update --url https://host/qd-macos-aarch64 --sha256 <hash>

which is never installed unverified. Binaries at 0.3.0 and earlier read the old Forgejo package registry and cannot update across the move to GitHub — those need one manual install, after which self-update works again.

Installing

contrib/install.nu picks the right binary for the machine, verifies its SHA-256 against the published checksums, and installs it into ~/.local/bin (or %LOCALAPPDATA%\qd\bin):

nu contrib/install.nu
nu contrib/install.nu --version 0.3.0 --dest ~/bin
nu contrib/install.nu --base-url https://somewhere/else
nu contrib/install.nu --init-url ssh://git@drydock9-port1.qxuken.dev/qxuken/dotfiles.git

It reads the same three files qd self-update does.

It is published beside the binaries, so a machine with nushell but no clone can fetch it first:

curl -fLO https://github.com/qxuken/qdot/releases/latest/download/install.nu

Without nushell, download the asset for the platform directly (qd-linux-x86_64, qd-macos-aarch64, qd-windows-x86_64.exe, …):

curl -fLO https://github.com/qxuken/qdot/releases/latest/download/qd-macos-aarch64

and chmod +x it.

About

Single-binary dotfiles manager with Lua config, age-encrypted secrets and git history.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages