Skip to content
roest1Public

About

Terminal / Editor config for any Bash Shell. One "make install" on Linux or WSL.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

351 Commits

Folders and files

Repository files navigation

dotfiles

Terminal, editor and shell as one versioned unit β€” wezterm, neovim and bash in a single repo, with every symlink and every dependency declared in one manifest. Cross-platform: macOS, Linux, WSL, RHEL, and the Windows host.

Install

One line on a fresh machine β€” installs git if needed, clones, and runs make install:

curl -fsSL https://raw.githubusercontent.com/roest1/dotfiles/main/bootstrap.sh | bash

Already cloned? cd ~/dotfiles && make install, then exec bash.

Windows

That line sets up a Unix shell β€” including WSL, run from inside your distro. The Windows host is a separate install, from PowerShell:

irm https://raw.githubusercontent.com/roest1/dotfiles/main/bootstrap.ps1 | iex

It installs wezterm and gives you CTRL+SHIFT+O β€” one fuzzy picker for WSL, PowerShell, cmd, and an admin shell.

Turn on Developer Mode first, or creating a symlink is denied and the installer falls back to copying the config β€” which then stops tracking the repo. In PowerShell:

start ms-settings:developers

That opens Settings β†’ System β†’ Advanced (Windows 10 filed the same page under Privacy & security, which is why the URI is the route given rather than a menu path). Turn on:

Toggle Section Why
Developer Mode For developers Required. Without it symlink creation is denied and the installer copies instead, which silently stops tracking the repo.
Enable sudo Terminal Optional. Only the PowerShell (Admin) entry in the shell picker needs it; it is omitted from the picker rather than shown broken.

windows/README.md β€” the picker, elevation, and why the Windows config can't be shared with the one inside WSL.

Just want a cool PS1?

The bash prompt: user@host, path, repo visibility, branch, branch protection, and GitHub Actions status

Take the prompt on its own. It is one self-contained file β€” no framework, no plugin manager, nothing else from this repo:

curl -fsSL https://raw.githubusercontent.com/roest1/dotfiles/main/bash/bash_theme -o ~/.bash_theme
echo 'source ~/.bash_theme' >> ~/.bashrc
exec bash
🌍 / πŸ”’ repo is public / private
πŸ›‘οΈ branch has protection rules
2 staged 1 unstaged 2 untracked working tree
πŸ”Ό2Β πŸ”½3 commits to push / to pull β€” both at once means diverged
2 conflicting that pull would conflict, in 2 files
[11/11 βœ… 22h] GitHub Actions for this commit, last run 22h ago

Bash 3.2+, so macOS's system bash is fine. git is the only hard requirement; gh and jq add the bracketed CI status and the 🌍/πŸ”’/πŸ›‘οΈ markers. Every tool is behind a command -v guard, so a missing one drops its segment rather than breaking the prompt.

It spends one process per prompt inside a repo and none outside one β€” anything needing the network, or a real merge, is read from a cache that a detached job refreshes. bash/bash_theme explains why.

Everything lives in deps.conf

One file declares every config symlink and every dependency. Toggling is commenting β€” there's no flag to remember and no profile vocabulary to learn.

[nvim]
link  nvim                    ~/.config/nvim
tool  mise||pkg    nvim       neovim     # mise first: apt's is too old
tool  mise||cargo  stylua
tool  uv||pkg      ruff
# tool  pkg        shfmt                 ← commented out = skipped
post  bun install --cwd nvim/lsp-servers --frozen-lockfile

Three line types, and that's the whole vocabulary:

Line What
link <repo-path> <dest> a symlink; ~ expands to your home
tool <provider> <command> [package] a dependency; package defaults to the command name
post <shell command> runs after that section's tools

There is no platform line type. The Windows host declares its own payload in windows/install.ps1 and reads no manifest, which retired the only section that ever needed one β€” CI rejects platform as an unknown line type.

Command What
make install everything enabled in deps.conf
make install nvim one section (repeatable: make install bash nvim)
make link symlinks only β€” no sudo, no network, nothing downloaded
make check verify what's enabled is actually present
make status sync status β€” is the machine what deps.conf says?
make test Lua unit tests (no plugins, no network)
make sections list sections

Sections are named after the program, so there's nothing to name: [bash], [nvim], [wezterm]. The Makefile reads them from the manifest β€” adding a program is a new section plus its config file, and no other edits.

Installing a subset

The real constraint at work usually isn't "no neovim," it's "not that dependency." So granularity goes down to the individual tool:

  • Skip one tool β€” comment its line.
  • Skip a whole program β€” make install bash instead of make install.
  • Install nothing at all β€” make link gives you every config with no sudo and no network.

Scope the one-liner the same way: DOTFILES_TARGET=bash curl ... | bash.

No node, npm or pip anywhere. The toolchain is bun and uv. The six JavaScript language servers are declared in nvim/lsp-servers/package.json, installed by bun install, and run as bun <path> β€” with no node shim, since aliasing node to bun would make any incompatibility surface as an error blaming the wrong tool. Mason was dropped because it installs npm packages by shelling out to the npm binary, which only exists if node does. CI asserts on every push that node, npm and pip stay out of the manifest, and that all six servers complete a real LSP handshake with node absent from $PATH.

Providers

Each tool line names how to install it. || declares a fallback chain:

Provider Use
pkg system package manager (brew / apt / dnf)
mise tools distros don't reliably carry β€” pinned versions, binary downloads
uv Python tools (replaces pip; same vendor as ruff)
cargo compiles from source β€” slow, prefer mise
manual not installable from here β€” declares that an official installer or extracted build is an acceptable source; the real install lives in the section's deps.sh

manual is usually a concession β€” pkg couldn't serve this one. [uv] is the exception, and it is worth knowing about because it reads like drift otherwise: there manual is a preference. The distro's uv has uv self update compiled out, so it can only ever be as current as the next package bump, and uv ships weekly. ensure_uv in lib/pkg.sh curls Astral's installer first and falls back to pkg only when the network is unreachable.

There is no winget provider either, and CI rejects it. Windows packages are declared in $WingetTools at the top of windows/install.ps1.

There is no npm provider, and adding one is a CI failure. See the node paragraph above.

tool  pkg||cargo  eza     # distro package if it exists, else compile

[bash] is near-pure pkg, so a locked-down machine can install the whole shell without mise, node, or a curl-piped installer. That invariant is why uv has a section of its own rather than a line in [bash]: the moment it stopped being a pkg line it stopped being something the shell section may depend on.

Drift

make check only asks "does this command exist," which turns out to be a weak question β€” it happily reports ok for a tool installed by a completely different provider than the manifest declares. make status asks the stronger one, in the shape ArgoCD uses: declared state vs. live machine.

[nvim] tools
  βœ“ tree-sitter    pkg
  βœ— stylua         declared mise   actual cargo   ~/.cargo/bin/stylua

It checks two things:

  • links β€” is ~/.bashrc a symlink into this repo, or a stale backup or a hand-edited file?
  • tools β€” was this installed by the provider the manifest declares? Provenance comes from asking rpm/dpkg/brew/mise/uv directly, not from guessing at the path β€” ~/.local/bin is genuinely ambiguous between a uv shim, an apt rename symlink, and a hand-dropped binary.

It deliberately does not report "installed but not declared." Nothing here can tell a dotfiles dependency from a project-scoped tool, so that list is mostly noise β€” and silencing the noise meant naming unrelated projects inside the file that defines every machine you own. This repo describes itself, nothing else.

Drift matters because a manifest that describes a machine you don't have produces a different machine when you clone it somewhere fresh β€” which is the one thing this repo exists to get right.

Renaming a config file

Rename it in the repo, update its link line, re-run. That's the whole procedure β€” and the reason it's that short is that install.sh prunes orphans.

A rename leaves the old symlink in $HOME pointing at a repo path that no longer exists. Nothing else would ever notice: it isn't in the manifest any more, so the link loop doesn't walk it, and it sits there looking exactly like a working dotfile. You'd end up with both names side by side, which is precisely what renaming was supposed to prevent.

So install.sh removes any symlink that points into this repo and whose target is gone. Not a list of retired names β€” a structural test, which stays correct for the next rename with nobody remembering to maintain it. It can't touch a link that still resolves, or one pointing anywhere outside the repo, and it only scans directories the manifest actually links into.

[orphaned links]
  pruned /home/roest/.bash_roest_theme (target gone: .../bash/bash_roest_theme)

Writing a manifest line

./tools/adopt.sh <command> figures out the parts that are easy to get wrong β€” which provider owns it, and the package name when it differs from the command:

$ ./tools/adopt.sh rg fd clangd stylua wezterm
tool  pkg          rg           ripgrep
tool  pkg          fd           fd-find
tool  pkg          clangd       clang-tools-extra
tool  cargo        stylua
tool  pkg||manual  wezterm      # installed outside any manager (~/.local/bin/wezterm)

You name the commands. There's no bulk mode on purpose: nothing can tell a dotfiles dependency from a project-scoped tool β€” a uv tool belonging to one project looks identical to one you want on every machine. It prints; it never edits deps.conf.

It's a script rather than a make target on purpose too: every make target is about operating this repo, and this is about editing it.

Layout

Path What
deps.conf the manifest β€” links and dependencies, one file
bootstrap.sh Curl-able entry point for a fresh machine
install.sh Symlinks only; walks the manifest's link lines
bootstrap.ps1 The same, for the Windows host β€” irm ... | iex
windows/install.ps1 Windows host: links + winget. Declares its own payload β€” it reads no manifest
lib/manifest.sh Parses deps.conf
lib/providers.sh Maps a provider name to an installer; handles || chains
lib/pkg.sh The single implementation of "install a tool"
lib/run.sh Installs a section's tools, runs post steps and platform fixups
bash/, nvim/, wezterm/ Config, plus an optional deps.sh for platform-conditional fixups

nvim/ is self-contained β€” its own Makefile and README β€” so it still stands alone despite living here.

Portability is tested, not claimed

.github/workflows/ci.yml runs on every push: make link on Ubuntu and macOS, full installs across apt / brew / dnf, shellcheck, manifest validation, and a job that comments out the node block and asserts nvim still starts clean.

This exists because the previous Makefile promised WSL support in this README while make deps hard-exited on apt. Nothing ever ran it on Ubuntu, so nobody noticed.

Contributing

Portability fixes and machinery bugs are welcome; personal-preference changes aren't (fork instead β€” the layout is built for it). See CONTRIBUTING.md.

GitHub Terminal Tools

Full walkthrough β†’ git/GITHUB_TOOLS.md β€” walkthroughs and demos for the interactive GitHub tools.

github is the way in. ctrl-/ in any list, ? in any menu, for that screen's help.

Command What
github The current repo on GitHub, live: Actions, Pull Requests, Branches, Secrets, Environments
gha Workflow status for HEAD as a table, names linked to their jobs β€” the one-shot

Every screen is a list with a live preview. Hover a workflow run and its jobs appear with only the failed steps expanded, log tail inline; a running one refreshes under the cursor by itself. Needs fzf β‰₯ 0.65 (installed via mise for that reason).

Layout

Directory tree
~/dotfiles/
β”œβ”€β”€ deps.conf                             THE MANIFEST β€” links + dependencies
β”œβ”€β”€ Makefile                              Reads sections from deps.conf
β”œβ”€β”€ bootstrap.sh                          Curl-able fresh-machine entry point
β”œβ”€β”€ bootstrap.ps1                         The same, for the Windows host
β”œβ”€β”€ install.sh                            Symlink engine (backs up existing files)
β”œβ”€β”€ CONTRIBUTING.md                       How to add a tool without breaking the manifest
β”œβ”€β”€ .githooks/
β”‚   β”œβ”€β”€ pre-commit                        Blocks committing secrets (core.hooksPath)
β”‚   └── secret-patterns                   Shared by the hook and the CI check
β”œβ”€β”€ .github/
β”‚   β”œβ”€β”€ workflows/ci.yml                  Portability + supply-chain checks
β”‚   β”œβ”€β”€ dependabot.yml                    bun + github-actions ecosystems
β”‚   └── CODEOWNERS                        Required for code-owner review
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ manifest.sh                       deps.conf parser
β”‚   β”œβ”€β”€ providers.sh                      provider dispatch + || chains
β”‚   β”œβ”€β”€ pkg.sh                            the installers
β”‚   β”œβ”€β”€ run.sh                            section runner
β”‚   └── status.sh                         declared-vs-actual drift detection
β”œβ”€β”€ tools/
β”‚   └── adopt.sh                          Generates a deps.conf line for a command
β”œβ”€β”€ nvim/                                 Neovim config (own Makefile + README)
β”‚   └── lsp-servers/                      JS language servers + prettier, run by bun
β”œβ”€β”€ wezterm/
β”‚   β”œβ”€β”€ wezterm.lua                     β†’ ~/.config/wezterm/wezterm.lua        (Linux/macOS)
β”‚   └── wezterm-windows.lua             β†’ %USERPROFILE%\.config\wezterm\wezterm.lua
β”œβ”€β”€ windows/
β”‚   β”œβ”€β”€ install.ps1                       Windows link + winget engine
β”‚   β”œβ”€β”€ deps.ps1                          Nerd Font + elevation-helper fixups
β”‚   └── README.md                         Developer Mode, the picker, elevation
β”œβ”€β”€ bash/
β”‚   β”œβ”€β”€ bashrc                          β†’ ~/.bashrc
β”‚   β”œβ”€β”€ bash_theme                β†’ ~/.bash_theme
β”‚   β”œβ”€β”€ bash_productivity         β†’ ~/.bash_productivity
β”‚   β”œβ”€β”€ bash_git                  β†’ ~/.bash_git
β”‚   β”œβ”€β”€ bash_github               β†’ ~/.bash_github
β”‚   β”œβ”€β”€ bash_local                β†’ ~/.bash_local               (untracked)
β”‚   └── bash_password_commands    β†’ ~/.bash_password_commands   (untracked)
β”œβ”€β”€ uv/
β”‚   β”œβ”€β”€ uv.toml                         β†’ ~/.config/uv/uv.toml
β”‚   └── deps.sh                           Astral's installer; flags a distro uv
β”œβ”€β”€ git/
β”‚   β”œβ”€β”€ gitconfig                       β†’ ~/.gitconfig
β”‚   β”œβ”€β”€ README.md                         GitHub tips and tricks
β”‚   β”œβ”€β”€ GITHUB_TOOLS.md                   Interactive tools walkthrough
β”‚   └── GITHUB_SETUP.md                   New-repo settings + ruleset reference
β”œβ”€β”€ podman/
β”‚   └── README.md                         podman container-engine reference (Linux)
└── systemd/
    └── README.md                         systemctl / journalctl reference (Linux)

Neovim config lives in nvim/ β€” merged in from the former roest1/nvim repo with its full history. It keeps its own Makefile and README.md so the directory still stands alone.

What goes where

File Controls
bash/bashrc Shell options, PATH, package managers, sources theme + productivity
bash/bash_theme Prompt, colors, LS_COLORS, man page colors
bash/bash_productivity Custom commands, aliases, h help system
bash/bash_git Git aliases and gprune
bash/bash_github Plain gh (gha) + the helpers shared with the TUI
bash/bash_github_tui github and its screens β€” the only file that may invoke fzf
bash/bash_local Machine-specific config β€” CUDA, nvim path, etc. (untracked)
bash/bash_password_commands Sensitive commands (untracked)
wezterm/wezterm.lua Terminal on Linux/macOS β€” declares the mux domain
wezterm/wezterm-windows.lua Terminal on the Windows host β€” the shell picker
podman/README.md podman container-engine reference (Linux/RHEL)
systemd/README.md systemctl / journalctl reference (Linux/RHEL)

Machine-local config

Two files hold host-specific setup, sourced by bashrc when they exist:

touch ~/.bash_local && chmod 600 ~/.bash_local
touch ~/.bash_password_commands && chmod 600 ~/.bash_password_commands

~/.bash_local is for per-machine paths and exports β€” CUDA, a distro-specific nvim location. It's sourced immediately after EDITOR is set unconditionally, which makes it the right place for a per-machine EDITOR override. ~/.bash_password_commands is for anything sensitive.

They live in $HOME, not in this repo, and that is the entire security design. Git cannot commit a file outside its work tree β€” it can't see one. No .gitignore entry to maintain, no rule to remember, nothing to review when someone edits a config.

This is worth being deliberate about, because the obvious alternative is worse. Keeping them in bash/ and gitignoring them makes protection a rule rather than a property: git add -f overrides it, rewriting .gitignore silently removes it, and you're left auditing a file forever to be sure your passwords aren't one careless commit from being public.

Two more layers sit behind that, for the secret file someone creates inside the repo later:

Layer Catches
$HOME, outside the work tree everything β€” git cannot see the file
.githooks/pre-commit a staged secret, before the commit exists
CI: No secret or machine-local files are tracked anything that got past the hook, before it reaches main

The hook and the CI step read the same list, .githooks/secret-patterns, so they can't drift apart β€” add a pattern once and both are armed. install.sh points git at the hooks with core.hooksPath, so they arrive with a clone instead of being per-machine setup you have to remember.

Order matters here: the hook is bypassable with --no-verify and doesn't exist until install.sh has run once, and CI can only tell you a secret has already been pushed. Neither is a substitute for the file simply not being there.

If you want defence in depth beyond the repo, turn on GitHub's push protection β€” free on public repos. It blocks pushes containing recognised credential formats, though not arbitrary passwords in a shell script.

License

MIT

About

Terminal / Editor config for any Bash Shell. One "make install" on Linux or WSL.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Used by

Contributors

Languages