Skip to content

About

πŸ’» My personal dotfiles, setup scripts, and productivity tools β€” inspired by Frontend Masters: Developer Productivity 2.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

Β 

History

89 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🧰 last-dotfiles

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.


⚑ Getting started

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 .zshrc hardcode that path (the scripts themselves don't β€” they derive the repo root from their own location). ⚠️ A bare ./run rewrites ~/.zshrc, ~/.config/nvim and ~/.gitconfig*, backing up whatever it replaces to <file>.backup-<timestamp>.


πŸƒ The task runner

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 --help

-i β€” interactive mode

Like --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 hotkeys

With no filter it runs runs/bootstrap/ only, in this order β€” which is a contract, not whatever order the filesystem happens to return:

  1. install-home-brew β€” everything downstream needs brew on PATH
  2. install-zsh
  3. install-node-version-manager
  4. install-nvim
  5. install-tmux
  6. install-docker
  7. install-kubernetes
  8. install-git-config
  9. install-ghostty
  10. install-cli-tools
  11. install-chromium
  12. install-chrome-canary
  13. install-freelens
  14. install-hammerspoon (macOS; guard-skips on Linux)
  15. 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 how runs/lib/ stays out: it holds sourced helpers, not runnable scripts.
  • --dry is only understood by run itself. Everything else runs for real.
  • A failing script no longer stops the rest; failures are summarised at the end and run exits 1.
  • -i refuses to run without a terminal, rather than hanging on a menu nobody can answer.

runs/lib/

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.

πŸ“œ Scripts

runs/bootstrap/ β€” installers

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.

runs/infra/

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.

runs/access/

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.

runs/utils/

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. ⚠️ Acts on the current directory's repo β€” 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.

πŸ—‚οΈ Configs

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. nvim updates lazy-lock.json, htop rewrites htoprc when you change settings in its UI, k9s may rewrite config.yaml on 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-tmux used to write tmux.conf with 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.

hammerspoon-config/ β€” 🎯 window management

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.

Sides, not columns

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.

Communication windows

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, or EMOJI / WORK_MODE / KAIZEN_MODE / RESET_LAYOUT / RELOAD_HAMMERSPOON)
  • appPaths β€” explicit .app paths for apps outside /Applications (launchOrFocus can't find those)
  • appIds β€” bundle IDs. Needed when the running name differs from the .app name (Visual Studio Code runs as Code) and when one name is a prefix of another: hs.application.get matches by substring, so without it Google Chrome can resolve to Google Chrome Canary
  • minWidthForTiling β€” 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.lua derives the rest
  • expandFull β€” apps whose double tap goes to full screen instead of 2/3
  • doubleTapMs β€” window for the double tap (default 400)
  • modes.work / modes.kaizen β€” each with launch (what this mode opens), foreground, chrome, chromium; modes.work.comms holds the timed windows. Separating launch from the side map is what fixed Kaizen leaving most keys unplaced

Grid schema:

  • workAppLayout / kaizenAppLayout β€” per-app position (left|center|right), vertical (top|center|bottom) and width/height as fractions ("1/3", "2/3", "3/4", "4/4"…)
  • workChromeConfig / workChromiumConfig / kaizenChromeConfig / kaizenChromiumConfig β€” the tab sets each mode opens
  • foregroundApps β€” what ends up on top per mode
  • onDemandAppLayout β€” 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.

Tests

./configs/hammerspoon-config/test/suite.sh

Stubs 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.

wm-linux-config/ β€” 🐧 window management (Linux)

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-linux

Reload 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 window

Two 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.

zsh-config/

.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-config/

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.

ghostty-config/

config is linked to ~/.config/ghostty/config, which Ghostty honours on macOS and Linux, so one file covers both.

⚠️ On macOS Ghostty also reads ~/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-config

cli-config/

Small 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.

gnome-terminal-config/

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.

git-config/

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.

nvim-config/init.lua

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 pull from 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 a lua/ directory this repo doesn't carry, so uncommenting one breaks startup.

tmux-examples/

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 and kdev && kgp

πŸͺ Git hooks

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

πŸ› Known rough edges

Documented rather than silently patched:

  • The ~/last-dotfiles clone path is hardcoded in the .zshrc aliases. 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 a linux-personal.lua profile; there's no linux-work.lua counterpart to mac-work.lua yet.
  • lua-wm ignores screen = "secondary": multi-monitor placement is macOS-only so far.
  • install-zsh installs autojump, but nothing ever sources it β€” the rupa/z antigen bundle already covers that, so it's a dead dependency rather than a broken one.
  • The unixorn/fzf-zsh-plugin antigen bundle has never installed: ~/.antigen/bundles/unixorn/ is empty and antigen reports Installing... 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 clone of it works by hand β€” so it's something in antigen's fetch. Aloxaf/fzf-tab is 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-tmux templates send this repo's zsh aliases (gl, kdev) into panes, so they assume this .zshrc is in effect.
  • Non-Debian Linux isn't supported: the scripts that need system packages check for apt-get and exit with a message rather than pretending.

Fixed, for the record

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.

About

πŸ’» My personal dotfiles, setup scripts, and productivity tools β€” inspired by Frontend Masters: Developer Productivity 2.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages