This repository is the source of truth for a personal macOS development environment. It started as a dotfiles collection, but now also contains workstation automation, project-session bootstrapping, developer utilities, and a source-controlled VS Code setup with custom local extensions.
It is intentionally opinionated and machine-specific. The current scripts assume:
- macOS with Apple Silicon Homebrew under
/opt/homebrew; - this repository is checked out at
~/dev/dotfiles; - personal projects live under
/Users/mac/dev; - the installed applications and language versions match the paths in the configuration.
Review those assumptions before using the repository on another machine.
| Area | Repository source | Purpose |
|---|---|---|
| Shell | files_to_symlink/zshrc, files_to_symlink/starship*.toml |
PATH setup, aliases, shell history, adaptive dark/light prompt, fzf, mise, SDKMAN, and language tooling |
| Terminal | files_to_symlink/ghostty.config, files_to_symlink/ghostty/backgrounds/, files_to_symlink/ghostty/shaders/, files_to_symlink/ghostty/themes/, files_to_symlink/muxy/ghostty.conf, files_to_symlink/tmux*.sh, files_to_symlink/tmux.conf |
Ghostty and Muxy appearance, ~520 selectable colour themes, configurable dark-mode star background, shaders, and tmux behavior |
| Project sessions | files_to_symlink/init_tmux_sessions.sh |
Personal tmux sessions and pane layouts for active projects |
| Desktop automation | files_to_symlink/init.lua |
Hammerspoon application shortcuts, IDE selection, and quitting VS Code once its last window closes |
| Notes | files_to_symlink/obsidian/snippets/ |
Obsidian vault CSS snippets |
| Editor defaults | files_to_symlink/editorconfig |
Global EditorConfig rules |
| VS Code | files_to_symlink/vscode/ |
User settings, keybindings, marketplace extension list, installers, and repository-owned local extensions |
| PHP tooling | files_to_symlink/switch_php_ver.sh |
Switches the Homebrew CLI PHP link and persists the selected version |
| Local routing | files_to_symlink/cloudflared/*.yml |
Tracked Cloudflared tunnel routing; credentials remain outside the repository |
| Bootstrap | check_dependencies.sh, symlink.sh |
Checks the expected tools and installs the repository links |
Files are edited in this repository and symlinked into their live macOS locations. Do not edit a linked file in the home directory as though it were an independent copy.
The root installer currently manages:
| Source | Live destination |
|---|---|
files_to_symlink/zshrc |
~/.zshrc |
files_to_symlink/tmux.conf |
~/.tmux.conf |
files_to_symlink/editorconfig |
~/.editorconfig |
files_to_symlink/init.lua |
~/.hammerspoon/init.lua |
files_to_symlink/ghostty.config |
~/.config/ghostty/config |
files_to_symlink/ghostty/backgrounds/*.jpg |
~/.config/ghostty/backgrounds/*.jpg |
files_to_symlink/ghostty/shaders/*.glsl |
~/.config/ghostty/shaders/*.glsl |
files_to_symlink/ghostty/themes/* |
~/.config/ghostty/themes/* |
files_to_symlink/ghostty/stars-overlay.conf |
~/.config/ghostty/stars-overlay.conf |
files_to_symlink/muxy/ghostty.conf |
~/Library/Application Support/Muxy/ghostty.conf |
files_to_symlink/obsidian/snippets/file-explorer-font.css |
~/Documents/mac_obisidian_vault/.obsidian/snippets/file-explorer-font.css |
files_to_symlink/starship.toml |
~/.config/starship.toml |
files_to_symlink/starship-light.toml |
~/.config/starship-light.toml |
files_to_symlink/cloudflared/vanta.yml |
~/.cloudflared/vanta.yml |
files_to_symlink/cloudflared/growee.yml |
~/.cloudflared/growee.yml |
files_to_symlink/cloudflared/spro-marketing.yml |
~/.cloudflared/spro-marketing.yml |
files_to_symlink/init_tmux_sessions.sh |
~/init_tmux_sessions.sh |
files_to_symlink/tmux-notes.lua |
~/tmux-notes.lua |
files_to_symlink/tmux-notes-core.lua |
~/tmux-notes-core.lua |
files_to_symlink/tmux-notes.sh |
~/tmux-notes.sh |
files_to_symlink/nvim/tmux-notes.lua |
~/.config/nvim/lua/plugins/tmux-notes.lua |
files_to_symlink/tmux-project.sh |
~/tmux-project.sh |
files_to_symlink/tmux-session-ui.sh |
~/tmux-session-ui.sh |
files_to_symlink/tmux-status.sh |
~/tmux-status.sh |
files_to_symlink/tmux-agent-usage.sh |
~/tmux-agent-usage.sh |
files_to_symlink/tmux-background.sh |
~/tmux-background.sh |
files_to_symlink/tmux-theme.sh |
~/tmux-theme.sh |
files_to_symlink/tmux-ui.sh |
~/tmux-ui.sh |
files_to_symlink/tmux-open-url.sh |
~/tmux-open-url.sh |
files_to_symlink/tmux-repo.sh |
~/tmux-repo.sh |
files_to_symlink/switch_php_ver.sh |
~/switch_php_ver.sh |
files_to_symlink/vscode/User/* |
~/Library/Application Support/Code/User/ |
files_to_symlink/vscode/extensions/local.* |
~/.vscode/extensions/ |
New zsh sessions select the dark or light Starship configuration from the current macOS appearance. Existing shells can switch without restarting:
prompt-theme light
prompt-theme dark
prompt-theme autoauto removes the per-shell override and reads the macOS appearance again.
mkdir -p ~/dev
git clone <repository-url> ~/dev/dotfiles
cd ~/dev/dotfilesIf the checkout lives elsewhere, update the hard-coded repository and personal project paths before continuing.
At minimum, inspect:
files_to_symlink/zshrcfor Homebrew versions, aliases, PATH entries, SSH hosts, and local tools;files_to_symlink/init_tmux_sessions.shfor project directories and startup commands;files_to_symlink/init.luafor installed applications and keyboard shortcuts;files_to_symlink/cloudflared/*.ymlfor local tunnels, hostnames, and credentials-file paths.
bash check_dependencies.shThe checker reports missing tools; it does not install them. Homebrew is required. It currently classifies these commands as required:
git nvim tmux fzf fd bat php composer python3 node
Valet, tree, lsd, GPG, Ruby, Cargo, pipx, mise, Geometry, Ghostty, and SoftHSM are checked as optional integrations.
Treat this as a diagnostic inventory rather than a complete package manifest.
For example, the current .zshrc initializes Starship and calls mise from
~/.local/bin/mise directly, although Starship is not checked and mise is
classified as optional. Install those tools or adjust the shell configuration
before making it active.
bash symlink.shThis creates the expected configuration directories, installs Hammerspoon with
Homebrew when ~/.hammerspoon is absent, links the root configuration files,
and delegates VS Code setup to its installer.
The root links use ln -sf, so inspect any existing destination files first.
The VS Code installer is more defensive: it moves conflicting user files and
local extensions into timestamped backups before linking the repository
versions.
The root installer links the repository-owned extensions but does not install
Marketplace packages. With VS Code's code command available:
bash files_to_symlink/vscode/install_marketplace_extensions.shThen run Developer: Reload Window in each open local or Remote SSH window.
The VS Code directory is effectively a small editor-distribution project of its own. It contains:
- macOS user settings and keybindings;
- workbench CSS and scripts injected by the Custom CSS and JS Loader, with a checksum-repair script for the corruption warning that patching triggers;
- a reproducible Marketplace extension list;
- local extensions for PHP/Laravel navigation and editing, PHP DocBlocks,
project icons, preview-tab and Markdown behavior, status-bar control, and a
PhpStorm-style project chooser on
Cmd+O; - unit tests for the more involved local-extension behavior;
- installers that link and register those extensions.
See files_to_symlink/vscode/README.md for
installation details and the extension inventory.
Before changing anything under files_to_symlink/vscode, read
files_to_symlink/vscode/CUSTOMIZATION_HISTORY.md.
It is the canonical record of current behavior, architectural decisions, known
limitations, verification steps, and the append-only change history.
- Edit the source under
files_to_symlink. - Re-run
bash symlink.shonly when adding or repairing links; existing symlinks expose normal file edits immediately. - Reload the affected application or shell.
To add a new managed file, place it under files_to_symlink and add its source
and destination to symlink.sh. If it introduces a required command, also
update check_dependencies.sh.
The shell defines switch as the interactive shortcut:
switchA version can also be supplied directly:
bash ~/switch_php_ver.sh 8.4The script discovers installed php@* Homebrew formulae, updates the Homebrew
PHP link, and writes the selection to ~/.php-version. It deliberately does
not stop or restart PHP-FPM services.
initThis opens a fuzzy project picker containing Home and the immediate directories
under ~/dev. Selecting a project switches to its existing Tmux session or
creates a session with one shell in that project directory. Inside Tmux, init
opens the same centered popup as Cmd-P; outside Tmux it uses the current
terminal directly.
Selecting "Find any folder" opens a simple directory browser starting in
~/dev. It behaves like navigating with cd: select a child directory to
enter it, select .. to go up, or select . to open the current directory as
a Tmux project. Escape returns to the project list. Common dependency and
build directories are excluded. Set TMUX_PROJECT_BROWSE_ROOT to start in
another directory.
From inside Tmux, press Ctrl-A, then p to open the same picker. Option-.
and Option-, switch directly to the next or previous running project.
Running sessions use a persistent manual order. Drag their names in the status
bar to reorder them, or use Option-Up / Option-Down on a running project in
the picker. Click a status-bar session name to switch to it. In the picker,
Ctrl-X deletes the selected running session; deleting the current session
first switches the client to the next session in the manual order.
When the names outgrow the bar, a window slides over the list instead of its
tail being dropped: and at each end scroll it one session at a time,
each carrying the number of sessions it still hides. That count is set in plain
text: every enclosed numeral the font offers — , and their outlined
variants — draws its digit at around half the height of a text digit, because
the ring or box around it is what gets drawn at icon size. A dash joins the
count to the chevron as the shaft of an arrow, and both dash and chevron are
dimmed while the count is not, so the pair reads as one mark pointing at what is
out of sight. A chevron with nothing left to reach simply shows no count, and
both are drawn at the same weight: which way there is more to see is the count's
job to say, not something to infer from one chevron being brighter than the
other. They wear no brackets, because brackets are what separate one session
from the next and a chevron is not a session. Switching to a session outside the
window pulls it into view by the least the window can move, so it arrives as the
first or last name shown and the one at the far end drops out. A window scrolled
by hand is otherwise left where it was put, so the active project can be
scrolled off the bar, and each client scrolls its own.
Each clickable button carries a short lowercase label — vi, col, git, bg
— because five bare icons in a row said nothing about what they opened.
Read-only groups need none: their value is the label. Health is the exception
in both directions: its icon and colour already say the state, so the label is
cut to that state's initial — o, w, c, or s while a check runs or before
the first report — rather than spending four columns to repeat the icon.
The git link follows the active pane's current repository, so a pane opened in
a different checkout goes to that remote. If the pane has no repository with a
browser URL, it falls back to the tmux session's project repository.
The bar is balanced four groups a side: battery, the health state, the Vim
reference, the color picker and the pane counter on the left; usage, the
repository link and the moving background on the right. A tmux label and the
window name used to close each end and were dropped together: both were
constants — every window here is named shell — and because the centered
session block is budgeted against whichever side is wider, neither could buy the
session list any room on its own. Removing both freed twenty columns, which is
one more project name on the bar. The running command joins
them only when something is actually running: a pane sitting at a shell prompt
adds nothing by saying so, and Claude Code's binary is named by version, which
reads as a meaningless "2.1.222", so it is shown by name. The
two clickable pickers sit together at each edge rather than bunching on one
side. It uses single-cell Nerd Font glyphs for battery, usage,
pane, and command metadata, drawn from the Material Design range through
Ghostty's bundled symbol fallback. Drawing every icon from one family keeps them
at a consistent size and optical centre, which mixed Unicode blocks do not:
JetBrains Mono renders and at the same weight as the rest,
where the earlier ▪ and ✧ were drawn small and high respectively.
Sessions carry no marker glyph at all — the active project renders its name and
its brackets in orange, while inactive projects keep accent brackets around a
plain name. The bar sits at the top of the
client, with the manually ordered session group centered between the fixed
system details on the left and pane metadata on the right. A second,
one-cell status row draws the native double pane rule directly underneath,
including top-facing junctions where vertical pane borders begin. A matching
uninterrupted double rule sits above the content row, replacing the shorter
session markers and framing the entire status area. A native double rule also
runs along the bottom of the screen, using ╩ where vertical pane borders join
it.
HTTP and HTTPS addresses shown inside a pane open in the default macOS browser
on Option-click, including dotless development hosts such as localhost. A
plain click only selects the pane. Command cannot gate this: mouse reporting
encodes just Shift, Alt and Ctrl, so tmux never learns that Command was down,
and Ghostty's own Command-click link handling is disabled while tmux grabs the
mouse. Tmux also advertises OSC 8 hyperlink support to Ghostty for applications
that emit real terminal hyperlinks. Mouse selections made while viewing
scrollback are copied without leaving copy mode, so the selected text and scroll
position stay visible. Click once inside the pane to clear the highlight, leave
copy mode, and forward the click to the terminal application; q or Escape
also returns to the live bottom.
The item in the right side of the status bar shows the highest current
Codex or Claude usage window. Click it, press Option-U, or
press Ctrl-A, then u to open the detailed usage popup. The popup reads the
providers' existing local OAuth credentials, refreshes on demand, and caches
only normalized percentages and reset times for five minutes under
~/.cache/tmux-agent-usage/; it does not run a background process.
When the current session directory belongs to a Git repository with a web
remote, a button appears beside usage. Click it to open that repository in
the default browser; SSH-style Git remotes are converted to their HTTPS page.
The col button on the left of the bar opens a live accent selector. Its menu previews
the standard and bright Catppuccin palette colors plus Omarchy's default Tokyo
Night window-border accent (#7aa2f7), applies the choice across status and
pane UI immediately, and persists it under ~/.local/state/tmux-ui/accent.
The menu remains open after the launching mouse button is released, so a
separate click selects the desired color.
The ok button on the left runs periodic system health checks. It reports disk
space, memory pressure, the largest memory user, swap activity, CPU load,
temperature, the highest sustained power draw (with its PID), and orphaned
development processes. The icon is the status, and the label follows it: ok
when everything passes, warn in amber
when something needs attention, crit in red when a threshold is breached,
and scan while a check runs. Click it, press Ctrl-A then h, or Option-H, to open the report;
r re-runs the checks with per-check progress, q or Esc closes. That key row
and the rule above it are pinned to the bottom of the popup rather than trailing
the last check, so they stay in the same place whether or not a check has advice
to add. The AI usage popup does the same with its own key row.
The checks exist because every one of them caught something that had gone
unnoticed: a disk that reached 95% full, six orphaned Neovim processes holding
190 MB for five days, and a terminal shader repainting every frame. The whole
sweep costs about four seconds, mostly interval sampling, and runs at most
hourly, so it cannot become the problem it looks for. The status bar itself
reads only a one-line state file, so a redraw stays fork-free; a refresh is
spawned in the background only once the report is genuinely stale. Results are
cached under ~/.cache/tmux-health/.
The vi button on the left of the bar opens a
searchable Vim keybinding reference. Alongside the editor sections it carries a
command-line section for zsh's vi mode and a tmux scrollback section, so the
same window answers "how do I jump to the start of the line" wherever you are. Press Ctrl-A, then k, or Option-K,
for the same popup. Rows are grouped by task, starting
with whole recipes such as select-all-and-copy or move-to-the-bottom-and-paste,
and every description is phrased in the words you would type to look it up, so
searching select a few lines and copy finds the binding directly. Enter
copies the key sequence itself to the clipboard, and Esc closes. Descriptions
carry their own search vocabulary rather than a hidden synonym column because
fzf applies --with-nth before it matches, leaving only the visible column
searchable.
The item beside the color selector opens the moving-background popup.
Press Ctrl-A, then b for the same menu. Selecting a row cycles animation
on/off, speed, density, or brightness and applies the change immediately while
leaving the popup open. The choice persists under
~/.local/state/tmux-ui/moving-background. The generated Ghostty override and
active shader live under ~/.config/ghostty; the image and animation remain
dark-mode-only, and disabling them leaves the selected theme untouched.
Press Ctrl-A then t, or Option-T, to open the theme picker.
It lists every theme Ghostty can resolve: the ~463 bundled inside the app plus
the 59 in files_to_symlink/ghostty/themes/ that Ghostty does not ship. The
preview pane renders each one's sixteen palette colours over a sample of shell
output, so a theme can be judged before it is applied.
Enter applies the highlighted theme without closing the popup, so comparing
two of them is Enter, arrow, Enter rather than reopening the picker each
time — the real terminal recolours underneath while the list stays put. Esc
closes and leaves whichever theme was applied last, which is the one already on
screen; there is no separate confirm step and nothing to revert.
There are two themes, not one. The config uses Ghostty's
theme = dark:…,light:… form, so a dark slot and a light slot are stored
separately under ~/.local/state/tmux-ui/ghostty-theme and Ghostty draws
whichever one matches the current macOS appearance. The picker opens on the slot
that is on screen and Tab switches to the other; the prompt names the slot
being edited and the header names both, with an arrow on the live one:
dark theme ›
Enter apply · Tab switch slot · Esc close
→ dark: Aizen Light
light: Catppuccin Latte
That arrow is worth reading before concluding the picker is stuck. Ghostty
chooses the slot from the macOS appearance, not from how dark the palette looks,
so a pale theme parked in the dark slot leaves the window white while macOS is
still in dark mode — and edits then land on the dark slot even though the screen
looks light. ~/tmux-theme.sh show prints the same summary without opening the
popup, and ~/tmux-theme.sh picker light opens straight onto a given slot.
The choice is written out to the generated ~/.config/ghostty/theme.ghostty,
which the main config includes last. Ghostty has no runtime theme command, so the
switch works by rewriting that file and driving the app's own Reload
Configuration through Hammerspoon.
The moving background composes with whichever dark theme is selected rather than
being baked into one: when it is on, tmux-theme.sh writes a generated
starfield-active theme — the selected palette followed by
ghostty/stars-overlay.conf — and points the dark slot at that instead.
The extra themes come from terminalcolors.com.
files_to_symlink/ghostty/fetch-terminalcolors-themes.sh re-downloads them,
skipping any scheme Ghostty already bundles so Gruvbox Dark keeps meaning
Ghostty's own. The downloaded files are committed, so a fresh install needs no
network:
bash files_to_symlink/ghostty/fetch-terminalcolors-themes.shPress Ctrl-A, then n to open the two-pane notes workspace for the current
project. Project notes are shown alongside global notes that are available from
every project:
~/.local/share/tmux-project-notes/
├── global/
└── projects/
└── <tmux-session>/
The narrow left pane contains tag filters, project notes, and global notes; the
right pane is the real editable Markdown buffer. Moving with j / k, arrow
keys, or Cmd-Up / Cmd-Down previews the selected note immediately. Enter
or Tab focuses the editor in Normal mode, so press i when you want to type.
The bottom line changes with the active column and mode and shows only the most
useful keys; ? displays the complete key guide.
Filename filtering and full-text search are separate: / filters the visible
sidebar by title or tag, Space Space opens the all-project filename picker,
and s or Space / searches inside every note and jumps to the matching line.
Select a tag row and press Enter, or press t for the tag picker. A filled dot
marks the active tag. Add a tag anywhere in a note as #todo, #idea, or
another single-word hashtag.
Write [[Note name]] to connect notes, [[Note name|short label]] to display a
different label, or [[Note name#Heading]] to document a heading-level target.
Place the cursor inside a link and press gf to open the target. A ←N marker
beside a filename shows how many other notes link to it. Press R on a note to
rename it without overwriting an existing file; matching wiki links in every
project and global note are updated at the same time.
Markdown is rendered while the editor is in Normal mode: headings, lists,
checkboxes, links, code blocks, and tables are easier to scan. Press i and the
current editing area returns to raw Markdown. English and Romanian word
suggestions are available only in Notes, without spelling underlines. In Insert
mode, use Ctrl-L to show completion,
Ctrl-N / Ctrl-P to choose a suggestion, and Ctrl-Y to accept it. The spell
source begins suggesting after three characters and uses both en_us and ro.
Use Space p / Space g for a new project/global note, Ctrl-Y in Normal mode
to copy the whole note, and Ctrl-D in the sidebar to delete. Those two are
leader-prefixed because the editor pane is a real Vim buffer, where P pastes
before the cursor and G jumps to the last line; the scope letters still match
the [P] and [G] badges in the sidebar. Notes autosave while editing.
Escape changes Insert mode back to Normal mode; from Normal mode or the
sidebar it saves and closes the workspace. Option-N also saves and closes it
immediately. Selecting editor text with the mouse copies it to the macOS
clipboard. Cmd-V pastes normally; hold Shift while dragging for a
Ghostty-native selection. TMUX_NOTES_EDITOR may point to another Neovim
executable.
Ghostty forwards Cmd-P, Cmd-N, Cmd-,, Cmd-., Cmd-Left, Cmd-Right,
and Cmd-/ to tmux. They open the project menu, toggle notes, switch sessions,
and switch to the next window. Cmd-Left mirrors Cmd-, for the previous
session; Cmd-Right mirrors Cmd-. for the next session. Cmd-Shift-N opens a
new Ghostty window, preserving the action formerly assigned to Cmd-N.
Cmd-, replaces Ghostty's default Open Config shortcut, while Cmd-Left and
Cmd-Right replace its default shell line-beginning and line-end shortcuts.
The older fixed multi-pane bootstrap remains available directly as:
bash ~/init_tmux_sessions.shThere is no single repository-wide test suite. Use checks appropriate to the area changed:
nvim --headless -u NONE -l tests/tmux-notes-core-test.lua
nvim --headless "+luafile tests/tmux-notes-workspace-test.lua" +qa!
bash -n check_dependencies.sh symlink.sh
bash -n files_to_symlink/*.sh files_to_symlink/vscode/*.sh
node --test files_to_symlink/vscode/extensions/*/test/*.test.js
git diff --checkFor VS Code changes, also follow the runtime verification steps in
CUSTOMIZATION_HISTORY.md; tests alone do not prove that the active extension
host loaded the repository copy.
- This is a personal environment, not a portable package manager. Versioned Homebrew paths, app names, project directories, and keyboard shortcuts are expected to need maintenance.
- Keep Cloudflared credential JSON files and
cert.pemunder~/.cloudflared; only non-secret routing YAML files belong here and are symlinked individually. - Marketplace extensions and application binaries are external dependencies, not vendored artifacts.
files_to_symlink/vscode/backupsexists for recovery and comparison. Do not implement new behavior in a backup copy.- Repository guidance for coding agents lives in
AGENTS.md.