Skip to content

Repository files navigation

Kenny's dotfiles

My personal dotfiles, for macOS and Arch/Manjaro. Take anything you want, but at your own risk.

You should first review the code and remove things you don't want or need. Don't blindly use my settings unless you know what that entails.

Installation

git clone https://github.com/kdbaustert/dotfiles.git ~/dotfiles
cd ~/dotfiles
./install.sh          # macOS (Apple silicon)
./install-linux.sh    # Arch / Manjaro

Both are idempotent — safe to re-run, and re-running is how you update. Existing real files are backed up (.backup-<timestamp>) before being replaced by symlinks; existing symlinks are simply re-pointed. Both ask for your sudo password up front, and each refuses to run on the other's platform.

install.sh is interactive. Run bare, it asks a yes/no for every section (drawn with gum, fetched pinned into ~/.local/bin on first run), all up front, before anything runs — Enter means yes. Sections always run in their usual order, and sudo is only requested when one you kept needs it.

./install.sh                   # a yes/no for every section
./install.sh brew              # just the Homebrew + Brewfile step, no questions
./install.sh symlinks zsh      # relink, then redo the zsh setup and theme caches
./install.sh --skip nix,hosts  # the questions, without these two
./install.sh -y                # everything, no questions
./install.sh --help            # list the section names

Without a terminal (piped, over ssh without -t) it behaves as -y.

They are siblings rather than forks: everything that is the same on both — the symlinks, zinit, the theme caches, LS_COLORS, the setup-script runner — lives in setup/lib.sh and is called from each. What is left in the two scripts is only what Homebrew, PAM and CoreText do differently from pacman, systemd and fontconfig.

install.sh expects Apple silicon (the pay-respects install in the zsh step is pinned to arm64 and skips itself elsewhere). install-linux.sh expects pacman, and its pay-respects step is disabled until you fill in the checksum — the Linux release asset could not be hashed from the Mac this was ported on, so the step warns and skips rather than installing something unverified. See the comment above that block.

Two things on Linux are worth knowing before the first run:

  • The package lists in arch/ were transcribed, not verified. The installer checks every name against the sync database and reports the ones pacman does not recognise instead of aborting, so the first run tells you what to fix.
  • .config/ghostty/config is not deployed here at all. Ghostty is macOS-only in this setup, so it is the one .config entry the symlink step skips on Linux; a re-run also removes a link an earlier one left. .config/rio/config.toml is deployed and is still macOS-shaped — Rio needs navigation.mode off NativeTab and renderer.backend off Metal by hand.

What it does

  • Installs the Xcode command line tools and Homebrew, then everything in homebrew/Brewfile
  • Installs Nix (multi-user, daemon mode) beside Homebrew — for per-project shells, not for tools the Brewfile already has; the shell hook is sourced from .zprofile, not /etc/zshrc
  • Symlinks the root dotfiles and every .config/* entry into $HOME
  • Installs pay-respects (pinned + checksummed) into ~/.local/bin
  • Clones the trapd00r LS_COLORS database as an alternative to the default vivid/Voltage one — see themes/voltage.md
  • Bootstraps zinit and pre-installs/compiles the zsh plugins
  • Builds the Voltage theme caches for bat and fast-syntax-highlighting
  • Splices an ad, tracker and porn blocklist into /etc/hosts (setup/hosts.sh) and installs a root LaunchDaemon that refreshes it every Monday
  • Enables Touch ID for sudo via /etc/pam.d/sudo_local
  • Points iTerm2 at iterm/ for its preferences

Optional setup scripts

The scripts in setup/ are opt-in, because they install a lot of global packages. Select them by name:

SETUP_SCRIPTS="pnpm composer" ./install.sh
SETUP_SCRIPTS=all ./install.sh          # pnpm composer mas gh-extensions
Script Installs
pnpm.sh Global JS packages (pnpm, not npm — see the file)
composer.sh Global Composer packages
mas.sh Mac App Store apps (sign in to the App Store first)
gh-extensions.sh GitHub CLI extensions

The macOS system defaults in setup/macos.sh are not one of these — they are the macos section of install.sh (./install.sh macos), last in the run. Review that script before saying yes to it.

Layout

Path Contents
.config/ Everything symlinked to ~/.config
zsh/ Aliases, functions, zinit config, extra/ snippets
homebrew/ Brewfile
setup/ The optional scripts above
themes/ Voltage — the shared palette; see themes/voltage.md
iterm/ iTerm2 preferences folder
obsidian/ "Amethyst Night" theme

Notes

There are two Neovim configs, and they are not variants of each other. .config/nvim is hand-rolled: lazy.nvim, 23 plugins, no vendored runtime, and a ~50ms start (~80ms opening a file, once treesitter and the LSP attach). .config/lvim is LunarVim, an IDE layer with its own runtime directory. The split is the point — nvim is the editor, lvim is there when the batteries-included version is wanted, and neither can break the other.

Roughly 35ms of that start is nvim-tree, which is open on every launch and so cannot be lazy-loaded. That is the price of the sidebar being always-on, and it is the single biggest item in the startup profile — nvim --startuptime if it ever needs revisiting.

They share exactly one thing: .config/voltage.nvim, the Voltage colorscheme as a standalone plugin-shaped directory that both put on their runtimepath. It lives outside both because two transcriptions of the same palette is the drift themes/voltage.md exists to prevent.

.config/nvim splits into lua/configs (settings needing no plugin) and lua/plugins (one file per concern, each returning a lazy spec). Language servers come from Mason; formatters and linters deliberately do not — they are the ones the Brewfile and setup/pnpm.sh already install, so the shell and the editor run the same binaries. lazy-lock.json is tracked, so :Lazy restore reproduces the plugin set exactly.

LunarVim's runtime is not installed by install.sh, only its config. .config/lvim is symlinked like every other .config entry, but the editor itself lives in ~/.local/share/lunarvim and comes from upstream's installer, which is not vendored here:

LV_BRANCH=master bash <(curl -s https://raw.githubusercontent.com/LunarVim/LunarVim/master/utils/installer/install.sh)

Run that once on a new box, then lvim.

It is lvim, not nvim. LunarVim isolates itself: its shim sets NVIM_APPNAME=lvim and starts Neovim against its own init.lua, so nothing in .config/lvim is on the path of a bare nvim, and nothing in .config/nvim is on the path of lvim. They are two separate editors that happen to share a binary — see the next note. $EDITOR stays nvim.

The config LunarVim writes on first run is replaced by the symlink, and the plugin set is pinned by the tracked .config/lvim/lazy-lock.json — :Lazy restore reproduces it exactly. The formatters and linters it wires up are the ones already installed by the Brewfile and setup/pnpm.sh, not by Mason. Upstream targets Neovim 0.10 while the Brewfile tracks current neovim, so startup logs one harmless vim.tbl_flatten is deprecated warning from LunarVim's own code.

obsidian/ is not deployed by install.sh. Obsidian themes live at <vault>/.obsidian/themes/ and the vault path is per-machine; copy them in by hand.

Part of .claude/ is deployed. CLAUDE.md — Claude Code's global instructions, applied to every project — plus hooks/notify.sh, statusline.sh and every directory under skills/ are symlinked into ~/.claude/. The rest of ~/.claude is state Claude writes itself (settings.json, settings.local.json, projects/, todos/), so the directory is created and linked into rather than linked over. .claude/themes/ is kept here for reference only; nothing links it.

Two settings blocks are manual. Both deployed scripts are named by ~/.claude/settings.json, which is untracked, so on a new machine add this under hooks:

"Notification": [{ "hooks": [{ "type": "command",
  "command": "$HOME/.claude/hooks/notify.sh" }] }]

and this at the top level:

"statusLine": { "type": "command",
  "command": "$HOME/.claude/statusline.sh", "padding": 0 }

hooks/notify.sh fires a terminal-notifier banner when Claude is waiting on you. Only the Notification event is hooked — permission prompts and idle questions. Hooking Stop as well would banner every single turn.

statusline.sh keeps the plan's usage on screen as four bars, one per window, each coloured green → yellow → orange → red as it fills:

Current session  ███████░░░░░░░  52%  2h 14m left
Current week     ███████████░░░  81%  4d 9h left
Fable week       █████░░░░░░░░░  38%  4d 9h left
Context window   ████░░░░░░░░░░  31%  312K of 1M · Opus 5

Current session and Current week are Claude Code's own names for the two rate-limit windows, so the status line and /usage never disagree about which bar is which. /usage and /context answer the same questions on demand; the point of the status line is that you see the 5-hour number before it becomes the reason you went looking — hence a countdown rather than a reset time. Windows Claude Code does not report (API-key auth has no rate limits) drop their row rather than showing an empty bar. The Fable weekly window is one Claude Code tracks and draws in /usage yet never hands the script — 2.1.266 still drops it — so the script fetches it itself: a background refresher pulls the same usage endpoint /usage calls, at most every five minutes, into ~/.cache/claude-usage.json, and the render reads that file alongside stdin. The cache also fills in the session and week rows when the session has not seen them yet. The script's header has the detail.

That refresher only runs while something is rendering, so a scheduled job keeps the cache warm when nothing is — a LaunchAgent on macOS (launchd/dev.kennyb.claude-usage-refresh.plist), a user timer on Arch (systemd/claude-usage-refresh.timer), both calling statusline.sh --refresh every five minutes. The installers deploy and enable them. Without it the cache ages past its one-hour cutoff overnight and the first render of the morning has nothing to draw those rows from.

There is no AGENTS.md at either scope any more. .claude/ used to carry one alongside CLAUDE.md — the vendor-neutral name other agent tools read directly, holding the "which binary to reach for" rules (rg, fd, jq, the g-prefixed GNU tools). Claude Code has no AGENTS.md discovery path, though — verified against 2.1.231, neither ~/.claude/AGENTS.md nor <project>/AGENTS.md is picked up, only CLAUDE.md is — so an @AGENTS.md import line was the only thing loading it, and deleting that line lost the rules quietly rather than erroring. Those rules are now inlined in .claude/CLAUDE.md. install.sh removes the retired ~/.claude/AGENTS.md symlink on its next run.

The root of this repo carried the same pair and was merged first, for the same reasons: the rules for editing this repo are inlined directly in the top-level CLAUDE.md, since Claude Code is the only agent that edits it and one file can't drift from its copy.

iris autostarts, and IRIS_AUTOSTART=0 opts one shell out. It wraps the shell rather than plugging into zle, so it arbitrates keys against atuin, fzf-tab and zsh-autosuggestions before they see them — the iris block in .zshrc records each collision and where it is settled. It ran on demand only for a month because it swallowed the empty-prompt Up arrow; iris 0.7.0's core.navigate-closed = "shell" gives that key back to atuin, which is what made always-on reasonable again. IRIS_AUTOSTART=0 zsh gets you a plain shell without editing anything.

Don't run iris setup or iris uninstall. Both rewrite your shell RC file in place, and ~/.zshrc is a symlink into this repo — they would edit the tracked .zshrc rather than a copy. To remove iris, delete that block, brew uninstall versenilvis/iris/iris, and drop .config/iris.

No System Integrity Protection changes are needed. Earlier versions of this README opened with instructions to disable SIP; that was only ever required by yabai's scripting addition. yabai and skhd have been removed, so nothing here touches SIP any more — leave it on.

Credits

Thanks to the dotfiles community.

About

There's lots of configurations out there, but this one is mine.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages