dots is a fast, cross-platform dotfiles manager with an interactive TUI, written in Rust. It installs your tools, wires up symlinks GNU Stow–style, applies premade app configs, and keeps everything healthy — on macOS (Homebrew) and Linux (apt/dnf) alike.
This repository is the
dotstool itself. Your actual dotfiles/configs live in a separate repo (e.g.dotfiles-CUK);dotsmanages the symlinks between them and your$HOME.
- Interactive TUI — run
dotsfor a dashboard covering symlink health, installed tools, shell plugins, app configs, and updates. - Cross-platform installs — one dependency list, resolved per platform via Homebrew,
apt, ordnf. - Symlink management — declare your own links (
dots link add), then create/repair them idempotently. Adopts existing files with automatic backups. - App configs — every directory in your dotfiles repo (
nvim/,git/,bat/, …) shows up in the TUI's Configs screen with an install-status badge ([ installed ]/[ partial ]/[ not installed ]); view a config's files, preview their contents, and install/remove it by (un)linking into$HOME. - Premade configs — bundled starter configs for Ghostty, Neovim, and opencode, applied on demand (existing files are backed up).
- Portable profiles — export your setup to
personal.jsonand re-import it on another machine, locally or straight from GitHub. - Self-updating — built-in update checker and one-command upgrade; a boxed banner in the corner of the TUI flags when a newer release is found.
- Single static binary — no runtime dependencies (pure-Rust TLS, no OpenSSL/keychain), optimized for size.
Release-by-release history lives in CHANGELOG.md.
curl -fsSL https://ctrluserknown.github.io/dots/install.sh | shThe installer downloads a prebuilt binary for your OS/arch straight from GitHub Releases (or builds from source in a scratch dir with cargo if no release matches — no git required either way), verifies it against the release's published SHA-256 checksum, puts it in ~/.dots/bin and on your PATH, and initializes config. It never clones this tool's own repo — the only git repo you need is your own personal dotfiles repo, which dots manages separately (see dots get-config). Upgrading from an install made before this changed: the installer detects a leftover full-repo clone at ~/.dots and removes the stale tool-repo files (.git, Cargo.toml, crates/, …) on your next run, leaving your settings.toml/links.toml/plugins//src/ untouched.
install.sh [--version <tag>] [--dir <path>]
Prefer to build it yourself? See BUILD_MACOS.md, or from a clone:
cargo build --release # binary at target/release/dotsTo remove it: uninstall.sh.
Run dots with no arguments to open the TUI. The dashboard's panes — Symlinks, Tools, Configs, Plugins, Network — each drill into their own screen with enter.
Every screen carries the same nav strip, and you move between them from anywhere:
| Key | Does |
|---|---|
| 1–6 | Jump straight to symlinks / tools / configs / aliases / profile / theme |
| [ ] or tab | Previous / next screen, wrapping at both ends |
| esc | Back to the dashboard |
| space | Settings (a popup, including the update check) |
A screen holding a prompt open — a search box, a path entry, a confirmation, the settings popup — keeps every key for itself, so navigation never fires mid-edit. Lua plugins can add their own panes, and the whole thing is rearrangeable into zones you define.
Everything is also scriptable via subcommands:
| Command | What it does |
|---|---|
dots health [--fix] |
Check and repair all declared symlinks, tools, and plugins |
dots update |
Check for and apply updates |
dots install <name> |
Install a single dependency |
dots install --all |
Install all missing required dependencies |
dots install --optional |
Install all missing optional dependencies |
dots aliases list | add <name> <value> | remove <name> |
Manage shell aliases |
dots premade list | apply | remove <app> |
List/apply/remove bundled starter configs (ghostty, neovim, opencode) |
dots config list | view | install | remove <name> |
View and (un)install app configs discovered in your dotfiles repo |
dots link add <source> <target> |
Adopt a file/dir and symlink it (recorded in links.toml) |
dots link list | apply | remove <target> |
Inspect, create/repair, or remove declared links |
dots profile generate [path] |
Export your setup to personal.json |
dots profile import <path> |
Import a personal.json from a local file |
dots profile import-git <user/repo/path.json> |
Import a personal.json from GitHub |
dots plugins list | new <name> | dir |
Manage Lua plugins that add dashboard panes |
dots layout show | init [--force] | path |
Inspect or scaffold the dashboard layout |
dots init [--quiet] |
Initialize config (idempotent; run automatically by the installer) |
dots -V / dots --version |
Print the version; --version adds the commit and how the version was resolved |
The dependency list is defined in crates/dots/src/packages.rs in three tiers, each mapped to its brew / dnf / apt package name:
| Category | When installed | Examples |
|---|---|---|
| Required | dots install --all |
git, eza, bat, fd, fzf, fastfetch, zoxide |
| Optional | dots install --optional |
neovim, herdr, btop, lazygit, yazi, carapace |
| Dev | dots install <name> |
go, lua, cmake, gcc, ripgrep, gh, docker, ffmpeg, … |
-
~/.dots/— the tool's home (repo checkout +bin/dots). -
~/.dots/settings.toml— tool settings, under a[dots]table:Key Default Meaning update_checktrueCheck for updates periodically update_frequency1440Minutes between update checks greetingtrueShow the greeting banner developer_modefalseEnable developer features theme(empty) Selected theme -
~/.personal/— your personal, machine-local layer:aliases.zsh(sourced after the built-in aliases),apps/, and an optionalconfig.tomlthat overridessettings.toml.
The dashboard is a set of zones, each holding an ordered list of widgets. A widget is either a built-in tile — symlinks, tools, configs, plugins, network — or a pane registered by a Lua plugin. Zones are yours to arrange: dots layout init writes the current layout to ~/.dots/layout.toml, and dots layout show prints where every widget ended up (plugins included) without opening the TUI.
With no layout file you get the default — one untitled zone holding the five built-ins in two columns, exactly the dashboard dots has always drawn. Delete layout.toml to return to it.
Prefer not to hand-edit TOML? Settings → Dashboard blocks opens an in-TUI editor over the same layout.toml — swap, add, remove, and reorder the widgets in each zone (built-ins and live plugin panes alike), applied to disk as you go.
| Key | Scope | Meaning |
|---|---|---|
columns |
top level | Columns that zones are arranged in |
id |
zone | Stable name — what ui.pane{ zone = … } targets |
title |
zone | Draws a labelled border around the zone; omit for an invisible region |
span |
zone | Top-level columns the zone occupies |
weight |
zone | Zone height, relative to the other zones |
columns |
zone | Columns the zone's own widgets are arranged in |
widgets |
zone | Widget ids, in draw order |
catch_all |
zone | Plugin panes that name no zone land here |
columns = 2
[[zones]]
id = "system"
title = "System"
span = 2 # full width
weight = 2 # twice the height of the row below
columns = 3 # three tiles across
widgets = ["symlinks", "tools", "configs"]
[[zones]]
id = "cloud"
widgets = ["github"] # a plugin pane, pinned by its id
catch_all = true # …and anything else a plugin registers
[[zones]]
id = "misc"
widgets = ["network"]Listing a plugin pane in widgets pins it there and overrides the pane's own zone hint — your file always wins. Anything that can't be placed (a typo'd id, a pane naming a zone that doesn't exist) is reported by dots layout show and skipped rather than silently dropped.
Drop *.lua files in ~/.dots/plugins/ to add your own panes to the dashboard — GitHub, AWS, or anything a shell command can report. Plugins are loaded at startup, run on the TUI thread, and each pane refreshes on its own interval. A plugin that fails to load is reported by dots plugins list and skipped; it never crashes the TUI.
Scaffold one with dots plugins new <name>, then edit it. Two globals are available:
ui — register panes and shape the dashboard:
| Call | Purpose |
|---|---|
ui.pane{ … } |
Register a dashboard pane (see fields below) |
ui.zone{ … } |
Declare a zone for panes to live in — same keys as a layout.toml zone (id required, size for its weight) |
ui.layout{ columns = N } |
Set the tile column count, honoured while you have no layout.toml of your own |
ui.pane fields — render is the only one you always want:
| Field | Default | Meaning |
|---|---|---|
id |
auto | Stable identifier (shown by dots plugins list) |
title |
id |
Pane title |
render |
— | function() → lines returning a string (split on newlines) or a table of strings |
size |
1 |
Row-height weight — 2 makes the pane twice as tall |
span |
1 |
Columns spanned — 2 makes it full-width in a 2-col zone |
zone |
catch-all | Which zone the pane belongs in |
refresh |
30 |
Seconds between render() calls |
on_enter |
— | function() run when the pane is selected with enter / click |
A zone a plugin declares is a suggestion: if your layout.toml already defines that id, yours is used unchanged.
dots — helpers for integrations:
| Call | Returns |
|---|---|
dots.sh(cmd) |
Trimmed stdout of sh -c cmd (drives gh, aws, …; "" on failure) |
dots.env(name) |
An environment variable ("" if unset) |
dots.dir() |
The ~/.dots path |
A minimal example:
ui.pane{
id = "github", title = "GitHub", span = 2, refresh = 120,
render = function()
local prs = dots.sh("gh pr list --author @me --state open | wc -l | tr -d ' '")
if prs == "" then return { "gh not available" } end
return { prs .. " PR(s) open" }
end,
}Ready-to-use examples live in examples/plugins/ (github.lua, aws.lua) — copy one into ~/.dots/plugins/ and run dots.
A Cargo workspace with two crates:
crates/dots/— thedotsbinary: CLI, TUI screens, installer, symlink/link engine, config, the Lua plugin host, the zone layout, and bundled premadeassets/.crates/tui-core/— shared TUI chrome (header/footer/description bars, color theme, flash model) used by thedotsscreens.
cargo test --all # unit + integration tests
cargo clippy --all -- -D warnings # lints
cargo fmt --all -- --check # formatting
bash tests/integration/test_setup.sh # shell integration test (matches CI)An isolated container test environment lives in test-env/ (see test-env/manage.sh); the manual QA checklist is in docs/manual-test-checklist.md. CI runs on Linux and macOS via .github/workflows/ci.yml.
See LICENSE.