Personal dev-environment bootstrap for macOS and Linux: install scripts, dotfiles and window-management config, all plain Bash + Lua. No build step, no package manager. There are two small test suites, both for
things that can't be checked by reading: configs/wm-linux-config/test/ (Linux-only code, run from
a Mac) and test/run-interactive-test.py (the -i menu, driven through a real pty).
Inspired by Frontend Masters β Developer Productivity 2.
git clone https://github.com/<your-username>/last-dotfiles.git ~/last-dotfiles
cd ~/last-dotfiles
./run --dry # see the bootstrap order, execute nothing
./run # then run the whole bootstrap
β οΈ Clone it at~/last-dotfiles. The shell aliases in.zshrchardcode that path (the scripts themselves don't β they derive the repo root from their own location).β οΈ A bare./runrewrites~/.zshrc,~/.config/nvimand~/.gitconfig*, backing up whatever it replaces to<file>.backup-<timestamp>.
run finds executable files under runs/ and executes them.
./run # the whole bootstrap, in order
./run -i # interactive picker over everything under runs/
./run tmux # only scripts whose path matches "tmux"
./run --dry # print what would run, execute nothing
./run -i --dry # pick in the menu, then only print
./run --helpLike --dry in that it shows you the list first, except you choose from it. Unlike the default
run, it covers every folder under runs/, not just bootstrap, and it only lists what
applies to the machine you're on β install-hammerspoon doesn't show up on Linux, install-wm-linux
doesn't show up on macOS, and a footer tells you what was hidden.
βββ ββββββ βββββββββββββββββ
βββ βββββββββββββββββββββββββ
βββ ββββββββββββββββ βββ
βββ ββββββββββββββββ βββ
βββββββββββ βββββββββββ βββ
βββββββββββ βββββββββββ βββ
Β· d o t f i l e s Β· π macOS
runs/bootstrap/
β 2) install-docker π³ Docker Desktop (macOS) / Engine (Linux)
β 3) install-git-config π§ ~/.gitconfig* con identidad por directorio
β 4) install-hammerspoon π― Hammerspoon β gestiΓ³n de ventanas y hotkeys
...
βοΈ Ocultos por no ser de macOS: install-wm-linux
1-14 alternar a todos n ninguno b solo bootstrap
β΅ ejecutar q salir
βΆ Seleccionados: 2 βΊ
Type numbers (3, or 1 3 5, or 1,3,5) to toggle, a/n for all/none, b for just the
bootstrap set, Enter to run, q to quit. Whatever you pick still runs in BOOTSTRAP_ORDER, so
selecting Homebrew and Neovim together installs Homebrew first regardless of the order you ticked
them.
How a script declares itself. run doesn't hardcode any of this β it reads two headers from
each script, so adding a new one never means editing run:
#!/usr/bin/env bash
# run-os: darwin # darwin | linux | any (default: any)
# run-desc: π― Hammerspoon β gestiΓ³n de ventanas y hotkeysWith no filter it runs runs/bootstrap/ only, in this order β which is a contract, not
whatever order the filesystem happens to return:
install-home-brewβ everything downstream needsbrewonPATHinstall-zshinstall-node-version-managerinstall-nviminstall-tmuxinstall-dockerinstall-kubernetesinstall-git-configinstall-ghosttyinstall-cli-toolsinstall-chromiuminstall-chrome-canaryinstall-freelensinstall-hammerspoon(macOS; guard-skips on Linux)install-wm-linux(Linux; guard-skips on macOS)
runs/infra/, runs/access/ and runs/utils/ need an explicit filter, because they're imperative
or interactive rather than idempotent installers: install-k3s creates a cluster, tmux-sessionizer
opens an fzf picker, switch-cluster wants an argument, install-git-hooks writes into the current
directory's repo.
- The filter is a plain substring match against the script path, and widens the search to all of
runs/. - Scripts that don't match are listed at the end under
π Scripts filtrados. - A script must be executable (
chmod +x) to be picked up β otherwise it's silently skipped. That's howruns/lib/stays out: it holds sourced helpers, not runnable scripts. --dryis only understood byrunitself. Everything else runs for real.- A failing script no longer stops the rest; failures are summarised at the end and
runexits 1. -irefuses to run without a terminal, rather than hanging on a menu nobody can answer.
Sourced by the installers, never executed on its own.
| File | What it gives you |
|---|---|
brew-env.sh |
ensure_brew β puts Homebrew on PATH by discovering its prefix (Apple Silicon, Intel Mac, system Linuxbrew, user Linuxbrew). Needed because within a single ./run the installer that just installed brew only added it to ~/.zshrc. |
apt.sh |
require_apt / apt_install β runs apt-get update once per ./run and fails with a readable message on distros without apt. |
| Script | What it does | Notes |
|---|---|---|
install-home-brew |
Installs Homebrew if brew isn't on PATH. Works on macOS and Linux. |
Idempotent. Run this first. |
install-zsh |
Symlinks ~/.zshrc and ~/.p10k.zsh to configs/zsh-config/ first, then installs zsh + fzf, thefuck, autojump, the MesloLGS Nerd Font, Antigen and kube-ps1, and sets zsh as the default shell. |
Backs up any real (non-symlink) ~/.zshrc it would replace. Editing configs/zsh-config/.zshrc edits the live shell config directly. The symlink goes first on purpose: it's the one step that must not be left half-done. chsh registers the shell in /etc/shells if it isn't there (Homebrew's zsh never is), and degrades to a warning instead of aborting. |
install-nvim |
brew install neovim, then symlinks ~/.config/nvim to configs/nvim-config/. |
Backs up any real (non-symlink) ~/.config/nvim it would replace. |
install-tmux |
brew install tmux, then symlinks ~/.config/tmux/{tmux.conf,clipboard.sh} and ~/.ready-tmux. On Linux also installs xclip + wl-clipboard. |
Prefix remapped to C-a, vi copy-mode, hjkl pane navigation, prefix + r reloads. The clipboard command is chosen at yank time by clipboard.sh, so the same conf works on macOS, X11 and Wayland. |
install-docker |
macOS: brew install --cask docker-desktop and opens Docker Desktop. Linux: sets up Docker's official apt repo by hand and installs via apt.sh, then enables the systemd service (if systemctl exists) and adds you to the docker group. |
Skips if docker is already on PATH. Doesn't use the get.docker.com convenience script β it misdetects Ubuntu derivatives like Mint (falls back to the wrong Debian codename), so the repo/codename are resolved explicitly, preferring /etc/upstream-release/lsb-release when present. On Linux you need to log out/in for the group change to apply. |
install-obsidian |
macOS: brew install --cask obsidian. Linux: installs from Flathub (md.obsidian.Obsidian) via Flatpak, adding the flathub remote if it's missing. |
Skips if already installed. Flatpak was chosen over snap because Linux Mint blocks snapd by default but ships Flatpak/Flathub out of the box. |
install-spotify |
macOS: brew install --cask spotify. Linux: installs from Flathub (com.spotify.Client) via Flatpak, adding the flathub remote if it's missing. |
Skips if already installed. Same Flatpak rationale as install-obsidian. |
install-node-version-manager |
brew install nvm, creates ~/.nvm, then installs the latest Node and sets it as default. |
Writes nothing into ~/.zshrc β that file is a symlink into this repo, so appending machine-specific brew paths to it put the wrong /opt/homebrew vs /home/linuxbrew path in version control. .zshrc loads nvm via $HOMEBREW_PREFIX instead. |
install-hammerspoon |
macOS only (guard-skips on Linux β see install-wm-linux below for its Linux sibling). Installs Hammerspoon if missing, asks which profile you want, and symlinks ~/.hammerspoon/init.lua and ~/.hammerspoon/app_config.lua into this repo. |
Interactive. Backs up any real (non-symlink) file it would replace. See Hammerspoon below. |
install-wm-linux |
Linux only (guard-skips on macOS). Installs lua-wm, a custom Lua window-manager daemon that's the Hammerspoon equivalent for Linux: apt packages (lua5.3, lgi, keybinder/wnck/notify GTK bindings), symlinks ~/.config/lua-wm/{init.lua,app_config.lua} from configs/wm-linux-config/, and registers a systemd --user service plus an XDG autostart entry. |
Interactive profile picker, same pattern as install-hammerspoon. Shift+F12-equivalent reload is systemctl --user restart lua-wm. The Lua interpreter and the multiarch triplet for lgi are discovered, not hardcoded, so it also comes up on arm64. Works without a systemd user session (the XDG autostart entry is the primary boot path anyway). |
install-git-config |
Symlinks ~/.gitconfig, ~/.gitconfig_kaizen, ~/.gitconfig_zooplus and ~/.gitignore_global to configs/git-config/. |
Backs up any real file it replaces. |
install-ghostty |
Installs the Ghostty cask on macOS, then symlinks ~/.config/ghostty/config. |
Also moves macOS's ~/Library/Application Support/com.mitchellh.ghostty/config aside, because it takes priority over the XDG path. On Linux there's no official apt package, so it points you at the download page and links the config anyway. |
install-gnome-terminal |
Linux only. dconf loads configs/gnome-terminal-config/catppuccin-latte.dconf into a fixed-UUID profile (the same one the upstream catppuccin/gnome-terminal installer uses), then sets it as the default profile. |
Not in BOOTSTRAP_ORDER (same as install-obsidian/install-spotify) β run it explicitly with ./run gnome-terminal. GNOME Terminal keeps profiles in dconf, not a plain file, so this can't be a symlink; the dark profile you already had is left alone and still selectable from Preferences. |
install-calibre |
Calibre: cask on macOS, apt on Linux. | On Linux, also fixes a missing StartupWMClass in Debian/Ubuntu's calibre-gui.desktop via a user-level .desktop override β without it lua-wm can't refocus an already-open Calibre window and would launch a duplicate every time. |
install-nas-mount |
Linux only. Mounts the NAS books share over CIFS (/mnt/bibliotecas, x-systemd.automount) instead of the GVFS mount Nautilus uses β GVFS doesn't implement flock correctly, which makes Calibre think its library database is corrupted. |
Not in BOOTSTRAP_ORDER β run it explicitly with ./run nas-mount. Prompts for SMB credentials interactively on first run and writes them to ~/.smbcredentials (chmod 600), never into the repo. x-systemd.automount means the share mounts itself the first time something (e.g. Calibre) touches the mount point β nothing needs to run it eagerly at login. |
install-cli-tools |
Installs k9s, htop, glow, bat, lsd, vivid, lazygit, direnv and tree-sitter-cli with Homebrew if missing, then symlinks the configs that exist in the repo (k9s, htop, glow, bat, lsd). |
Checks with command -v, so an apt-installed or hand-compiled copy is respected instead of installing a second one. Entries are formula:binary:config-dir, because the binary isn't always named after the formula (tree-sitter-cli β tree-sitter) and not every tool has a versioned config β vivid is one of these, it's invoked from .zshrc, not configured through a file. |
install-kubernetes |
kubectl, kubectx/kubens and helm. |
Nothing installed kubectl before, even though .zshrc has five aliases using it and kube_ps1 in the prompt. kubectx moved here from install-zsh, where it had ended up only because its aliases live in the shell config. |
install-chromium |
Chromium: cask on macOS, apt on Linux (tries chromium, then chromium-browser). |
The window profiles bind a key to it and open a tab set in it, but nothing installed it. On Linux, also fixes chromium-browser.desktop's StartupWMClass (Debian/Ubuntu ship it as chromium, but the window actually reports Chromium-browser) via the same user-level .desktop override pattern as install-calibre β otherwise lua-wm can't refocus it. |
install-chrome-canary |
Google Chrome Canary via cask on macOS. | Canary doesn't exist on Linux; there the equivalent channel is google-chrome-unstable, which needs Google's apt repo. The script prints the commands rather than adding a third-party repo behind your back. |
install-freelens |
Freelens (the maintained successor to OpenLens): cask on macOS, official .deb from GitHub releases on Linux (arch-matched, sha256 verified before sudo apt install). |
On macOS it needs your password to copy into /Applications. |
| Script | What it does | Notes |
|---|---|---|
install-k3s |
brew install k3d, checks Docker is running (docker info, works whether Docker is a macOS app or a Linux systemd service), then creates a local cluster peter-cluster with 2 agents and 8080:80 on the load balancer. |
Idempotent: skips k3d and the cluster if they already exist. Verify with kubectl get nodes. |
switch-cluster <arn> |
kubectl config use-context <arn>. |
Takes one argument; prints usage if you omit it. Wrapped by the kdev / kprod aliases. |
| Script | What it does | Notes |
|---|---|---|
repokeys |
ssh-adds every private key in ~/.ssh (skipping *.pub, known_hosts*, config, certs) into the agent you already have, reporting each one. Uses --apple-use-keychain on macOS. |
Won't start an agent itself: this script is executed, not sourced, so an agent started here would die with it β which is exactly why it used to be a silent no-op on Linux. If there's no agent it tells you to run eval "$(ssh-agent -s)" in your shell. |
| Script | What it does | Notes |
|---|---|---|
tmux-sessionizer |
fzf-picks a directory under ~/kaizen or ~/zooplus, creates or attaches a tmux session named after it, and runs ready-tmux inside. |
Aliased to session. Works both inside and outside tmux. |
ready-tmux |
Runs ./.ready-tmux from the current directory if it's executable, else ~/.ready-tmux. |
The per-project session-layout hook. Templates in configs/tmux-examples/. |
install-git-hooks |
Copies everything in hooks/ into the current repo's hooks dir and makes it executable. |
cd into the target project first. It lives here rather than in bootstrap/ precisely so a bare ./run doesn't install the zooplus hooks onto whatever repo you're standing in. |
Everything under configs/ is symlinked to where the tool expects it, so editing a file here
edits the live config β no reinstall. Where a file lands, and who links it:
configs/ |
Symlinked to | Installer |
|---|---|---|
zsh-config/.zshrc |
~/.zshrc |
install-zsh |
zsh-config/.p10k.zsh |
~/.p10k.zsh |
install-zsh |
nvim-config/ |
~/.config/nvim |
install-nvim |
tmux-config/tmux.conf |
~/.config/tmux/tmux.conf |
install-tmux |
tmux-config/clipboard.sh |
~/.config/tmux/clipboard.sh |
install-tmux |
tmux-examples/.ready-tmux-default |
~/.ready-tmux |
install-tmux |
ghostty-config/config |
~/.config/ghostty/config |
install-ghostty |
git-config/.gitconfig + the two identities + .gitignore_global |
~/ |
install-git-config |
cli-config/{k9s,htop,glow,bat,lsd}/ |
~/.config/<tool>/ |
install-cli-tools |
hammerspoon-config/{init.lua,profiles/<one>.lua} |
~/.hammerspoon/{init.lua,app_config.lua} |
install-hammerspoon |
wm-linux-config/{init.lua,profiles/<one>.lua} |
~/.config/lua-wm/{init.lua,app_config.lua} |
install-wm-linux |
Every installer backs up a real (non-symlink) file it would replace to <dest>.backup-<timestamp>
before linking, so the first run on an already-configured machine never destroys anything.
Three things worth knowing about this model:
- Some files are written back by their tool.
nvimupdateslazy-lock.json,htoprewriteshtoprcwhen you change settings in its UI,k9smay rewriteconfig.yamlon exit. Through the symlink those land in the repo as normal changes. For the lockfile that's the point; for the other two it's just occasional noise to commit or discard. - Nothing else is generated any more.
install-tmuxused to writetmux.confwith a heredoc, which made it the one config you couldn't edit from the repo. It links now. tmux-examples/is still templates you copy into a project as.ready-tmux, except.ready-tmux-default, which doubles as the symlinked global fallback.
init.lua is the whole system. On load it runs Work mode; the hotkeys are:
| Key | Action |
|---|---|
F1βF10 |
Launch or focus the app bound to that key, at its side, 50% |
FΒ· Γ2 |
Expand it to 2/3, centred. Again, back to its half |
F11 |
Work mode |
F12 |
Reset layout β re-place the windows that are already open |
Shift+F10 |
Emoji picker (Ctrl+Cmd+Space) |
Shift+F11 |
Kaizen mode |
Shift+F12 |
Reload Hammerspoon |
These are mac-work.lua's bindings; each profile assigns its own keys, so check the profile you
actually have linked.
mac-work.lua splits the screen 50/50 and gives every app a fixed side, the same in both
modes. The side isn't cosmetic: two apps on the same side always cover each other, so it
encodes which pairs you can see at once.
- Left β where you write: IntelliJ, VS Code, Kiro, DBeaver, Obsidian. Genuinely mutually exclusive; you don't edit in two of them at once, so covering each other costs nothing.
- Right β what accompanies it: Ghostty, the browsers, the AI chats, Slack/Teams/Outlook, OpenLens, Docker, Finder, WebPomodoro.
That makes editor+terminal, editor+browser, editor+AI and notes+browser all work. It also puts macOS notifications (which land top-right) over a browser or a chat rather than over your editor.
A double tap expands a window to 2/3 centred and a second one returns it to its half; going to
another app collapses it on the way. expandFull overrides the expanded size to full screen β
only Chrome Canary, for demos. Nothing is remembered: the state is read back from the window's
real width, so moving things by hand or with Rectangle can't desync it.
At 09:30, 11:30, 13:30 and 15:30 on weekdays, work mode brings up Slack plus mail or Teams at
50/50 with a π¬ Tiempo de ComunicaciΓ³n alert, and after 10 minutes resetLayout() puts
everything back. They're postponed while the camera is in use, because a window landing
on top of a shared screen is a disaster. The mic isn't read: a USB headset with Teams running
keeps it "in use" all day, which used to postpone every window until it was discarded. Kaizen
doesn't schedule them. hs -c "wm.comms()" says how each slot ended.
Work mode and Kaizen mode both: adapt the layout to the current screen (laptop display β everything fullscreen; external display β the multi-window layout), close every app except Hammerspoon, relaunch the configured apps, tile them, open the configured Chrome/Chromium tab sets, and bring the foreground apps up.
All of it is driven by app_config.lua. That file isn't in the repo either β it's a symlink to one
of the per-machine profiles in profiles/, created by the installer:
./run hammerspoonπ₯ ΒΏQuΓ© perfil de Hammerspoon quieres instalar?
1) mac-personal
2) mac-work
#? 2
π ~/.hammerspoon/init.lua -> configs/hammerspoon-config/init.lua
π ~/.hammerspoon/app_config.lua -> configs/hammerspoon-config/profiles/mac-work.lua
Because they're symlinks, editing a profile in this repo edits the live config β Shift+F12 to
reload. Adding a third machine is just dropping a .lua into profiles/; it shows up in the menu.
Re-running the installer switches profiles. The first run backs up any real file it replaces to
~/.hammerspoon/<file>.backup-<timestamp>.
The installer only prompts when it has a terminal. Inside an unattended ./run, it keeps the
profile that's already linked, or skips with a notice if the machine was never configured.
Two profile schemas coexist, picked by what the profile declares β there's no flag to set:
| Schema | Declares | Used by |
|---|---|---|
| Sides | leftApps / rightApps |
mac-work.lua, mac-personal.lua |
| Grid | workAppLayout / kaizenAppLayout |
nobody β kept working, exercised by test/profile-grid.lua |
Shared by both:
functionKeysβ key + modifiers + action (an app name, orEMOJI/WORK_MODE/KAIZEN_MODE/RESET_LAYOUT/RELOAD_HAMMERSPOON)appPathsβ explicit.apppaths for apps outside/Applications(launchOrFocuscan't find those)appIdsβ bundle IDs. Needed when the running name differs from the.appname (Visual Studio Coderuns asCode) and when one name is a prefix of another:hs.application.getmatches by substring, so without itGoogle Chromecan resolve toGoogle Chrome CanaryminWidthForTilingβ below this primary-screen width, everything goes centred fullscreen instead (default 2000)appLaunchDelay,debugMode
Sides schema:
leftApps/rightAppsβ two lists of names. That's the whole geometry;init.luaderives the restexpandFullβ apps whose double tap goes to full screen instead of 2/3doubleTapMsβ window for the double tap (default 400)modes.work/modes.kaizenβ each withlaunch(what this mode opens),foreground,chrome,chromium;modes.work.commsholds the timed windows. Separatinglaunchfrom the side map is what fixed Kaizen leaving most keys unplaced
Grid schema:
workAppLayout/kaizenAppLayoutβ per-appposition(left|center|right),vertical(top|center|bottom) andwidth/heightas fractions ("1/3","2/3","3/4","4/4"β¦)workChromeConfig/workChromiumConfig/kaizenChromeConfig/kaizenChromiumConfigβ the tab sets each mode opensforegroundAppsβ what ends up on top per modeonDemandAppLayoutβ apps that no mode launches, but that still get a position when you press their key
profiles/mac-work.lua and profiles/mac-personal.lua are self-contained per-machine profiles β
keep new options in sync across all of them. They don't have to use the same schema, though
both use Sides today; the appβkey vocabulary is kept the same across machines for the apps that
exist on both, so the muscle memory doesn't depend on which one you're sitting at.
./configs/hammerspoon-config/test/suite.shStubs hs and runs the real init.lua under luajit, so it covers what you can't verify by
reading: which pixels each window lands on, the 50% β 2/3 β 50% cycle, the comms windows
(camera-in-use postpone, the discard after the cap, and the timers surviving a garbage
collection), the laptop-only fallback, the grid schema, the eventtap fallback for a hotkey macOS
refuses to register, and mac-personal.lua on Sides. Hammerspoon isn't needed to run it.
If you touch init.lua, run the suite against the previous version too and check that it
fails β with one exception: scenario E must pass against both, because that's what proves the
grid schema still behaves.
The Hammerspoon equivalent for Linux: init.lua runs as a lua5.3 daemon (via lgi/GTK, Wnck,
Keybinder, libnotify) instead of relying on a macOS-only accessibility API. Same shape as
hammerspoon-config/: app_config.lua is a symlink into profiles/, installed and reloaded via
./run wm-linuxReload after editing a profile with systemctl --user restart lua-wm (bound the same way
Shift+F12 reloads Hammerspoon on macOS). Logs: journalctl --user -u lua-wm -f. Currently only
profiles/linux-personal.lua exists (no linux-work.lua counterpart yet).
The functionKeys schema is identical to Hammerspoon's, but the action vocabulary is
smaller: KAIZEN_MODE, RESET_LAYOUT, EMOJI, plus RELOAD_WM as a synonym of
RELOAD_HAMMERSPOON. No WORK_MODE on Linux β linux-personal.lua is the only profile
so far, so there's nothing to switch between. Geometry β the fraction table and the
position table β is the same code.
The app names are not portable, though, and can't be: on macOS they're .app names resolved
through hs.application, on Linux they're the Name= field of a .desktop file matched against
WM_CLASS via Wnck. They coincide for vendor apps (Slack, Obsidian, Google Chrome) and
diverge whenever the packaging differs β IntelliJ IDEA Ultimate on Linux vs IntelliJ IDEA on
macOS. To find the right string:
grep -r '^Name=' /usr/share/applications/ | grep -i <app>
xprop WM_CLASS # then click the windowTwo things are macOS-only by nature and the Linux side ignores them: appIds (CFBundleIdentifiers)
and appPaths (.app paths). Multi-monitor placement (screen = "secondary") is also
macOS-only for now β lua-wm always uses the primary monitor.
Tests for all this live in configs/wm-linux-config/test/ β see its README.
.zshrc and .p10k.zsh (the Powerlevel10k prompt theme, 89 KB of generated settings β .zshrc
sources it, so without versioning it a new machine started with p10k's setup wizard instead of your
prompt).
.p10k.zsh sets POWERLEVEL9K_MODE=nerdfont-v3, so it needs a Nerd Font to render its icons β
otherwise the prompt is full of tofu boxes. install-zsh installs MesloLGS NF for you: the cask on
macOS, and a download into ~/.local/share/fonts + fc-cache on Linux, where casks don't exist.
You still have to select it as your terminal's font (in Ghostty: font-family = MesloLGS NF).
The current shell config: Antigen + oh-my-zsh, Powerlevel10k, autosuggestions/completions/syntax-highlighting, z, fzf-tab, kube-ps1 in the prompt, nvm, SDKMAN, and the aliases below.
If vivid is installed (install-cli-tools), .zshrc exports LS_COLORS from
vivid generate catppuccin-latte on every shell start β that's what makes ls --color and
lsd use Catppuccin Latte for file/directory names instead of the system default. Guarded by
command -v vivid, so it's silently a no-op if you haven't run that installer yet.
| Alias | Expands to |
|---|---|
run |
the task runner in this repo |
session |
tmux-sessionizer |
ready-tmux |
ready-tmux |
repokeys |
repokeys |
k, kgp, kaf, kn, kc |
kubectl / kubens / kubectx shorthands |
kdev, kprod |
switch-cluster against the zoobrain EKS clusters |
kpfbd |
background port-forward to the zoobrain DB |
gst, gl |
git status, pretty git log |
dpsp |
docker ps as a compact table |
tf, cls |
terraform, clear |
tmux.conf is a single static file that works on both OSes. The only thing that differs between
systems β the clipboard command β isn't decided here: it's delegated to clipboard.sh, which picks
pbcopy / wl-copy / xclip at yank time.
That timing is the whole point. On Linux install-tmux installs xclip and wl-clipboard, so
command -v finds both and only $WAYLAND_DISPLAY can tell them apart β and that depends on the
graphical session you're in, not on the machine. Doing it with if-shell inside tmux.conf would
resolve it when the tmux server starts, so entering a session of the other type would silently yank
to the wrong clipboard until you reloaded. A wrapper script gets it right every time.
config is linked to ~/.config/ghostty/config, which Ghostty honours on macOS and Linux, so one
file covers both.
~/Library/Application Support/com.mitchellh.ghostty/config, and
that one wins for any key present in both β verified: a font-size set only in the XDG file
had no effect. install-ghostty therefore moves it aside to .backup-<timestamp>. If it ever
reappears, something recreated it and it's silently overriding this repo. Check what's actually in
effect with:
ghostty +show-configSmall configs for terminal tools, one directory each, linked to ~/.config/<tool>/: k9s
(+ its aliases.yaml), htop, glow, bat, lsd β lazygit, direnv and tree-sitter-cli are
installed but have no versioned config yet. install-cli-tools installs the tools themselves too β the
config and the thing it configures arrive together, so you can't end up with a perfectly linked
~/.config/glow and no glow.
k9s's screenDumpDir was removed β it hardcoded /Users/<user>/Library/..., which is wrong on
Linux; k9s falls back to its own per-platform default.
k9s, glow, bat and lsd are all set to a light theme, to match the light theme used
everywhere else in the terminal (Ghostty, GNOME Terminal): glow.yml's style: light, bat's
--theme="Catppuccin Latte", k9s's config.yaml ui.skin: catppuccin-latte, and lsd's
config.yaml color.theme: custom pointing at colors.yaml. glow's default style: auto
depends on detecting the terminal's background, which doesn't work reliably and was rendering
dark-on-light; bat, k9s and lsd have no auto at all β without an explicit theme/skin,
all three just default to something dark. bat ships a real Catppuccin Latte theme built in;
k9s and lsd don't, so k9s/skins/catppuccin-latte.yaml and lsd/colors.yaml are vendored
straight from catppuccin/k9s and catppuccin/lsd; glow doesn't have one either and has no
skins mechanism to vendor one into, so it uses the closest generic built-in (light) instead.
lsd's colors.yaml only covers metadata columns (permissions, size, date, user/group, git
status) β it can't touch file/directory name colors at all, those come from $LS_COLORS
(shared with plain ls --color). .zshrc generates that from vivid (vivid generate catppuccin-latte) if it's installed, so directory names land on the same palette instead of
the system default (dark blue, illegible on a light background).
Not included, deliberately: lazygit (empty config), neofetch / zellij / bpytop (untouched
default templates), and direnv β its direnv.toml isn't a personal preference at all, it's a
warn_timeout line written by PostHog's Flox activation hook that references a path inside that
project. Versioning a file another tool regenerates only invites conflicts.
Just catppuccin-latte.dconf, installed by ./run gnome-terminal (not in BOOTSTRAP_ORDER,
same as install-obsidian/install-spotify). GNOME Terminal keeps its profiles in dconf, not
a plain config file, so this can't be symlinked like everything else in this repo β the
installer dconf loads it into a fixed-UUID profile (the same UUID the upstream
catppuccin/gnome-terminal installer uses) and sets it as the default, leaving whatever dark
profile you already had alone and still selectable from Preferences.
Installed by ./run git-config. .gitconfig uses includeIf "gitdir:β¦" to switch identity per
directory: ~/zooplus/ pulls in .gitconfig_zooplus (work email), ~/kaizen/ and
~/last-dotfiles/ pull in .gitconfig_kaizen (personal email). Both set pull.rebase = true and
share .gitignore_global, which is in this repo too.
The Sourcetree diff/merge tools are macOS-only paths, kept in the shared file because they're inert
elsewhere β git only touches them if you ask for -t sourcetree explicitly.
A kickstart.nvim config, single file, heavily
commented, plus the lazy-lock.json that pins the plugin versions. ./run nvim symlinks the whole
directory to ~/.config/nvim β editing it here edits the live config, and Lazy writes the lockfile
straight into this repo.
Two consequences of keeping only those two files, rather than a full kickstart clone:
- There's no
git pullfrom upstream. Porting upstream changes is a manual diff, which is why the file keeps upstream's formatting (2 spaces, single quotes) instead of being run through stylua. - The
require 'kickstart.plugins.*'lines must stay commented out. They're upstream scaffolding that lives in alua/directory this repo doesn't carry, so uncommenting one breaks startup.
Templates for the per-project .ready-tmux hook β copy one into a project as .ready-tmux (and chmod +x), or to ~/.ready-tmux as the global default:
.ready-tmux-defaultβ editor / frontend / backend / infra / git windows with documented splits.ready-tmux-example-1β minimal editor / shell / logs / git.ready-tmux-example-2β project-specific: runs the UI dev server andkdev && kgp
hooks/ is not for this repo β install-git-hooks copies these into a target project. They encode zooplus conventions:
| Hook | What it enforces |
|---|---|
prepare-commit-msg |
Extracts a ZOOB-<n> id from the branch name and pre-fills the message as REVISION | ZOOB-<n>:. |
commit-msg |
Requires <MAJOR|MINOR|REVISION> | [<STORY>:] <MESSAGE>. |
pre-commit |
For staged .vue/.ts files under ui/ (excluding __tests__/): runs npm run format and npm run lint:fix, re-stages them, and blocks the commit on lint errors. |
Commits in this repo instead follow Conventional Commits with an emoji:
feat(hammerspoon): π― update config layout
fix(zsh): π correct PATH export
Documented rather than silently patched:
- The
~/last-dotfilesclone path is hardcoded in the.zshrcaliases. Every script derives the repo root from its own location instead, so only the aliases care where you cloned it. wm-linux-config/only has alinux-personal.luaprofile; there's nolinux-work.luacounterpart tomac-work.luayet.- lua-wm ignores
screen = "secondary": multi-monitor placement is macOS-only so far. install-zshinstallsautojump, but nothing ever sources it β therupa/zantigen bundle already covers that, so it's a dead dependency rather than a broken one.- The
unixorn/fzf-zsh-pluginantigen bundle has never installed:~/.antigen/bundles/unixorn/is empty and antigen reportsInstalling... Error!whenever its cache is invalidated (then stays quiet until the next invalidation, which is why it goes unnoticed). The upstream repo is fine βgit cloneof it works by hand β so it's something in antigen's fetch.Aloxaf/fzf-tabis installed and covers fzf completion; what's missing is the extra fzf keybindings/aliases. Predates the cross-platform work and is unrelated to it. - The
.ready-tmuxtemplates send this repo's zsh aliases (gl,kdev) into panes, so they assume this.zshrcis in effect. - Non-Debian Linux isn't supported: the scripts that need system packages check for
apt-getand exit with a message rather than pretending.
Things that used to be listed here and no longer apply: the generated tmux.conf baking in the
clipboard command (it's a symlinked static file now), ./run silently doing nothing on macOS
(find -perm /111 is GNU-only), the bootstrap running in filesystem order, hooks/commit-msg never
failing a commit, hooks/pre-commit never blocking one, switch-cluster dying before its own error
message, install-node-version-manager writing brew paths into the versioned .zshrc, repokeys
being a no-op on Linux, and lua-wm mutating its own profile table.