Skip to content

Repository files navigation

macOS development environment

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.

What it manages

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

How the repository is installed

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/

Prompt appearance

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 auto

auto removes the per-shell override and reads the macOS appearance again.

Set up a new Mac

1. Clone to the expected location

mkdir -p ~/dev
git clone <repository-url> ~/dev/dotfiles
cd ~/dev/dotfiles

If the checkout lives elsewhere, update the hard-coded repository and personal project paths before continuing.

2. Review machine-specific configuration

At minimum, inspect:

  • files_to_symlink/zshrc for Homebrew versions, aliases, PATH entries, SSH hosts, and local tools;
  • files_to_symlink/init_tmux_sessions.sh for project directories and startup commands;
  • files_to_symlink/init.lua for installed applications and keyboard shortcuts;
  • files_to_symlink/cloudflared/*.yml for local tunnels, hostnames, and credentials-file paths.

3. Check dependencies

bash check_dependencies.sh

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

4. Install the links

bash symlink.sh

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

5. Install VS Code marketplace extensions

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

Then run Developer: Reload Window in each open local or Remote SSH window.

VS Code environment

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.

Common workflows

Change a managed configuration

  1. Edit the source under files_to_symlink.
  2. Re-run bash symlink.sh only when adding or repairing links; existing symlinks expose normal file edits immediately.
  3. 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.

Switch CLI PHP

The shell defines switch as the interactive shortcut:

switch

A version can also be supplied directly:

bash ~/switch_php_ver.sh 8.4

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

Open or switch tmux projects

init

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

Colour themes

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

Press 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.sh

Verification

There 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 --check

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

Safety and maintenance notes

  • 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.pem under ~/.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/backups exists for recovery and comparison. Do not implement new behavior in a backup copy.
  • Repository guidance for coding agents lives in AGENTS.md.

About

dots

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages