From 4b210a24daf373c9a7699e7e0deef46a9d6f955c Mon Sep 17 00:00:00 2001 From: Nate Vick Date: Fri, 7 Aug 2026 15:18:29 -0700 Subject: [PATCH 1/4] feat(mise): pin zig 0.15.2 cmux builds GhosttyKit.xcframework and ghostty pins a minimum_zig_version, so the toolchain has to be present and pinned rather than tracking latest. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H2p4TH5SX7B3J8oAQaidc8 --- home/dot_config/mise/conf.d/cli-tools.toml.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/home/dot_config/mise/conf.d/cli-tools.toml.tmpl b/home/dot_config/mise/conf.d/cli-tools.toml.tmpl index 8c77511..afac29f 100644 --- a/home/dot_config/mise/conf.d/cli-tools.toml.tmpl +++ b/home/dot_config/mise/conf.d/cli-tools.toml.tmpl @@ -19,6 +19,7 @@ ripgrep = "latest" node = "22" helix = "latest" bottom = "latest" # `btm` — lighter, narrow-friendly; used by the cmux dock Host panel +zig = "0.15.2" # cmux builds GhosttyKit.xcframework; ghostty pins minimum_zig_version {{- if eq .chezmoi.os "linux" }} "npm:@endevco/aube" = "latest" # aube — our Node package manager (jdx, same author as mise); verified v1.33.1 "github:aristocratos/btop" = "latest" # btop via github backend (aqua asset-match is broken, ubi deprecated); Linux release binaries only From 74badbe3b3687c6734a016c73eafda8ba52bbad5 Mon Sep 17 00:00:00 2001 From: Nate Vick Date: Fri, 7 Aug 2026 15:18:43 -0700 Subject: [PATCH 2/4] fix(mise): require a minimum mise at bootstrap, not just any mise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh Mac carrying a stale `brew install mise` (2024.12.10) sailed past the presence-only `command -v mise` check, so mise.run never ran. cli-tools.toml uses `[settings.ruby] compile` and `idiomatic_version_file_enable_tools`; the 2024 parser rejects the former outright, which failed install-tools.sh and aborted the whole apply before any CLI tool was installed. Gate on a version floor and upgrade in place when it is not met. brew-managed installs take `brew upgrade` — self-update refuses to touch them, and dropping a second mise in ~/.local/bin would only shadow the old one inside scripts that reorder PATH. Version compare is field-wise numeric: `sort -V` is spotty under busybox, and string compare ranks 2026.8.10 below 2026.8.2. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H2p4TH5SX7B3J8oAQaidc8 --- home/run_once_before_install-mise.sh.tmpl | 44 ++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/home/run_once_before_install-mise.sh.tmpl b/home/run_once_before_install-mise.sh.tmpl index b66c334..d77449e 100644 --- a/home/run_once_before_install-mise.sh.tmpl +++ b/home/run_once_before_install-mise.sh.tmpl @@ -1,6 +1,48 @@ #!/bin/sh +# Installs mise, and — just as important — makes sure an *existing* mise is new +# enough. cli-tools.toml uses `[settings.ruby] compile` and +# `idiomatic_version_file_enable_tools`; mise from 2024 rejects the former with a +# hard TOML parse error, which fails install-tools.sh and aborts the whole apply. +# A presence-only check let that through: a Mac carrying a stale `brew install +# mise` never reached mise.run and then died at `mise install`. +# +# MISE_MIN is the oldest release verified to parse this repo's config, not a +# researched lower bound — the true minimum is somewhere below it. Erring high +# only costs an occasional no-op upgrade. set -eu -if ! command -v mise >/dev/null 2>&1 && [ ! -x "$HOME/.local/bin/mise" ]; then + +MISE_MIN="2026.8.2" + +MISE="" +if command -v mise >/dev/null 2>&1; then + MISE="$(command -v mise)" +elif [ -x "$HOME/.local/bin/mise" ]; then + MISE="$HOME/.local/bin/mise" +fi + +if [ -z "$MISE" ]; then echo "==> Installing mise" curl -fsSL https://mise.run | sh + exit 0 +fi + +# Field-wise numeric compare. Not `sort -V`: busybox sort (Alpine) is spotty on +# it. Not string compare: that ranks 2026.8.10 below 2026.8.2. +older_than() { + [ "$1" != "$2" ] && + [ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | head -1)" = "$1" ] +} + +# "2026.8.2 macos-arm64 (2026-08-04)" -> "2026.8.2" +CURRENT="$("$MISE" --version 2>/dev/null | awk '{print $1}')" + +if [ -n "$CURRENT" ] && older_than "$CURRENT" "$MISE_MIN"; then + echo "==> mise $CURRENT is older than $MISE_MIN; upgrading" + if brew list --versions mise >/dev/null 2>&1; then + # self-update refuses to touch a brew-managed install, and dropping a second + # mise in ~/.local/bin would only shadow it inside scripts that reorder PATH. + brew upgrade mise + elif ! "$MISE" self-update -y; then + curl -fsSL https://mise.run | sh + fi fi From de1e8b1464c19fb4e313f54f4ee579f4f60774e6 Mon Sep 17 00:00:00 2001 From: Nate Vick Date: Fri, 7 Aug 2026 15:27:18 -0700 Subject: [PATCH 3/4] perf(zsh): defer fzf/zoxide/op inits to turbo, drop openclaw wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Startup had drifted to 83ms against the 80ms budget, blocking commits. docs/shell-performance.md rule 1 says only mise (tools on PATH) and starship (prompt paints) earn a sync slot, but five _eval_cached calls ran sync. fzf's keybindings, zoxide's `z` and op's completion are all unreachable until a prompt exists, so they cost ~8ms before the prompt for nothing. Move them into _deferred_tool_inits, invoked from the atload of zsh-autosuggestions. Piggybacking an existing turbo plugin avoids cloning a `null` carrier on every bootstrap purely to schedule code. Also drop the openclaw completion sourcing entirely — 4418 lines and 3.3ms sync. ~/.openclaw itself is untouched. mise stays sync. Its hook is 22ms, but `mise --version` alone is 11ms, so no fork of it is cheap; shims-only would trade that one-time cost for ~21ms on every tool invocation (37ms vs 16ms for node), which is a bad deal with ls aliased to eza. A shims+turbo hybrid would only move the cost into the first prompt render — better benchmark, worse experience. 83ms -> 71ms median (min 66, max 80). Verified under zsh/zpty that z, fzf-history-widget, _op and autosuggestions all still load; `zsh -i -c exit` never fires turbo, so -c cannot see this and is not a valid check. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H2p4TH5SX7B3J8oAQaidc8 --- docs/shell-performance.md | 1 + home/dot_config/zsh/plugins.zsh | 2 +- home/dot_zshrc.tmpl | 25 +++++++++++++++++-------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/docs/shell-performance.md b/docs/shell-performance.md index 9728102..936677a 100644 --- a/docs/shell-performance.md +++ b/docs/shell-performance.md @@ -17,6 +17,7 @@ zsh -c 'zmodload zsh/zprof; source ~/.zshrc >/dev/null 2>&1; zprof | head -15' - Don't add `emulate -L zsh` inside `_eval_cached` — `LOCAL_OPTIONS` would revert `setopt`s made by the sourced init (this broke starship once). 3. **compinit is deferred.** `compdef` doesn't exist at rc-load time; the stub in `dot_zshrc.tmpl` queues calls and the zinit turbo block flushes them after `zicompinit` (`-C` flag: trusts the cached dump). Anything that emits `compdef` at rc time (like `op completion`) just works via the queue. 4. New plugins go in `plugins.zsh` under the existing `zinit wait lucid for` blocks, never eager-loaded in `.zshrc`. +5. **Tool inits that aren't mise or starship go in `_deferred_tool_inits`** (defined in `dot_zshrc.tmpl`, invoked from the `atload` of `zsh-autosuggestions` in `plugins.zsh`). fzf, zoxide and op used to run sync and cost ~8ms before the prompt for keybindings and completions nothing can reach until a prompt exists. Piggybacking on an existing turbo plugin's `atload` avoids cloning a `null` carrier just to schedule code. Note that `zsh -i -c exit` never fires turbo, so this work is invisible to the budget check — that's the point, but it also means functional changes here need verifying in a real interactive shell (`zsh/zpty`), not with `-c`. ## Enforcement diff --git a/home/dot_config/zsh/plugins.zsh b/home/dot_config/zsh/plugins.zsh index 974480b..2623592 100644 --- a/home/dot_config/zsh/plugins.zsh +++ b/home/dot_config/zsh/plugins.zsh @@ -4,7 +4,7 @@ zinit wait lucid for \ atinit"ZINIT[COMPINIT_OPTS]=-C; zicompinit; zicdreplay; _flush_deferred_compdefs" \ zdharma-continuum/fast-syntax-highlighting \ - atload"_zsh_autosuggest_start" \ + atload"_zsh_autosuggest_start; _deferred_tool_inits" \ zsh-users/zsh-autosuggestions \ blockf \ zsh-users/zsh-completions diff --git a/home/dot_zshrc.tmpl b/home/dot_zshrc.tmpl index fd294d2..25aad1d 100644 --- a/home/dot_zshrc.tmpl +++ b/home/dot_zshrc.tmpl @@ -69,14 +69,25 @@ _flush_deferred_compdefs() { # ---- starship (sync: prompt paints immediately) ---- _eval_cached starship starship init zsh +# ---- deferred tool inits (invoked from the turbo block in plugins.zsh) ---- +# Rule 1 in docs/shell-performance.md: only mise (tools on PATH) and starship +# (prompt must paint) earn a sync slot. fzf's keybindings, zoxide's `z`, and op's +# completion are all unreachable until there is a prompt to type at, so they load +# ~1ms after it appears rather than ~8ms before it. +_deferred_tool_inits() { + _eval_cached fzf fzf --zsh + _eval_cached zoxide zoxide init zsh + # `op completion zsh` emits `compdef _op op`. Running here rather than at rc + # time means compinit has already restored the real compdef, so it lands + # directly instead of going through the _DEFERRED_COMPDEFS queue. + _eval_cached op op completion zsh + [[ -f "$HOME/.config/op/plugins.sh" ]] && source "$HOME/.config/op/plugins.sh" +} + # ---- turbo plugins + aliases ---- [[ -f "$HOME/.config/zsh/plugins.zsh" ]] && source "$HOME/.config/zsh/plugins.zsh" [[ -f "$HOME/.config/zsh/aliases.zsh" ]] && source "$HOME/.config/zsh/aliases.zsh" -# ---- fzf + zoxide ---- -_eval_cached fzf fzf --zsh -_eval_cached zoxide zoxide init zsh - # ---- history prefix-search on Up/Down (OMZ-style) ---- # Type a prefix, then Up/Down cycles only through history entries starting with it. autoload -Uz up-line-or-beginning-search down-line-or-beginning-search @@ -88,11 +99,9 @@ bindkey '^[[B' down-line-or-beginning-search; bindkey '^[OB' down-line-or-beginn [[ -n "${terminfo[kcud1]}" ]] && bindkey "${terminfo[kcud1]}" down-line-or-beginning-search # ---- vendor integrations (guarded; no-op when tool absent) ---- -# `op completion zsh` already emits its own `compdef _op op` (queued by the stub above). -_eval_cached op op completion zsh -[[ -f "$HOME/.config/op/plugins.sh" ]] && source "$HOME/.config/op/plugins.sh" +# op's completion + shell plugins load from _deferred_tool_inits above; these are +# plain PATH/fpath edits with no subprocess, so they stay sync. [[ -d "$HOME/.grok/bin" ]] && { export PATH="$HOME/.grok/bin:$PATH"; fpath=("$HOME/.grok/completions/zsh" $fpath); } -[[ -f "$HOME/.openclaw/completions/openclaw.zsh" ]] && source "$HOME/.openclaw/completions/openclaw.zsh" [[ -d "$HOME/.composio" ]] && export PATH="$HOME/.composio:$PATH" # ---- chezmoi drift nudge ---- From 116332cf9930f9028e4a6d00faebd456434f648d Mon Sep 17 00:00:00 2001 From: Nate Vick Date: Fri, 7 Aug 2026 15:27:46 -0700 Subject: [PATCH 4/4] feat(aerospace): manage AeroSpace config and install it on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures ~/.aerospace.toml and installs AeroSpace on non-headless Macs, gated the same way as use-spark since a tiling WM is useless without a GUI. Swaps the cursor window rule for cmux (com.cmuxterm.app, verified from the bundle Info.plist) on workspace 1. This is the repo's first Homebrew tap and first cask — everything else brew installs (tmux, vim, mise) is homebrew-core, whose bottles carry checksums brew verifies. AeroSpace is not in homebrew-cask, so it comes from nikitabobko/tap, the upstream author's own tap for github.com/nikitabobko/AeroSpace: the same trust boundary as the app itself rather than a third party repackaging it. Installed fully qualified instead of via a bare `brew tap`, so provenance is explicit at the call site and no extra tap is left configured for unrelated formulae to resolve against. Documented in the README for the same reason. Never fails the apply, matching install-system-packages.sh. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H2p4TH5SX7B3J8oAQaidc8 --- README.md | 5 + home/.chezmoiignore | 6 + home/dot_aerospace.toml | 206 ++++++++++++++++++ home/run_once_after_install-aerospace.sh.tmpl | 30 +++ 4 files changed, 247 insertions(+) create mode 100644 home/dot_aerospace.toml create mode 100644 home/run_once_after_install-aerospace.sh.tmpl diff --git a/README.md b/README.md index 38a5a53..7ff9c7b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,11 @@ the only true prerequisites are `zsh`, `git`, and `curl`. All shell wiring lives `home/dot_config/zsh/aliases.zsh` and is `command -v`-guarded, so anything missing degrades to the native command. +On macOS (non-headless), the bootstrap also installs [AeroSpace](https://nikitabobko.github.io/AeroSpace), +an i3-like tiling WM, and manages `~/.aerospace.toml`. It is the repo's only Homebrew **cask** and its +only **tap** (`nikitabobko/tap`, the upstream author's own) — everything else brew installs is +homebrew-core. + ### Aliases | Alias | Expands to | Notes | diff --git a/home/.chezmoiignore b/home/.chezmoiignore index 0ac2613..181c189 100644 --- a/home/.chezmoiignore +++ b/home/.chezmoiignore @@ -46,3 +46,9 @@ Library/** .config/cmux .config/cmux/** {{ end }} + +# AeroSpace is a macOS tiling WM — useless without a GUI, so gate it the same way +# as use-spark. install-aerospace.sh carries the matching guard. +{{ if or (ne .chezmoi.os "darwin") .headless }} +.aerospace.toml +{{ end }} diff --git a/home/dot_aerospace.toml b/home/dot_aerospace.toml new file mode 100644 index 0000000..36f3856 --- /dev/null +++ b/home/dot_aerospace.toml @@ -0,0 +1,206 @@ +config-version = 2 +# Place a copy of this config to ~/.aerospace.toml +# After that, you can edit ~/.aerospace.toml to your liking + +# It's not necessary to copy all keys to your config. +# If the key is missing in your config, "default-config.toml" will serve as a fallback + +# You can use it to add commands that run after login to macOS user session. +# 'start-at-login' needs to be 'true' for 'after-login-command' to work +# Available commands: https://nikitabobko.github.io/AeroSpace/commands +after-login-command = [] + +# You can use it to add commands that run after AeroSpace startup. +# 'after-startup-command' is run after 'after-login-command' +# Available commands : https://nikitabobko.github.io/AeroSpace/commands +# after-startup-command = [ +# 'exec-and-forget borders active_color=0xff56b6c2 inactive_color=0xff494d64 width=3.0' +# ] + +# Start AeroSpace at login +start-at-login = true + +# Normalizations. See: https://nikitabobko.github.io/AeroSpace/guide#normalization +enable-normalization-flatten-containers = false +enable-normalization-opposite-orientation-for-nested-containers = false + +# See: https://nikitabobko.github.io/AeroSpace/guide#layouts +# The 'accordion-padding' specifies the size of accordion padding +# You can set 0 to disable the padding feature +accordion-padding = 30 + +# Possible values: tiles|accordion +default-root-container-layout = 'tiles' + +# Possible values: horizontal|vertical|auto +# 'auto' means: wide monitor (anything wider than high) gets horizontal orientation, +# tall monitor (anything higher than wide) gets vertical orientation +default-root-container-orientation = 'auto' + +# Possible values: (qwerty|dvorak) +# See https://nikitabobko.github.io/AeroSpace/guide#key-mapping +key-mapping.preset = 'qwerty' + +# Mouse follows focus when focused monitor changes +# Drop it from your config, if you don't like this behavior +# See https://nikitabobko.github.io/AeroSpace/guide#on-focus-changed-callbacks +# See https://nikitabobko.github.io/AeroSpace/commands#move-mouse +on-focused-monitor-changed = ['move-mouse monitor-lazy-center'] + +[[on-window-detected]] + if.app-id = 'com.tinyspeck.slackmacgap' + run = 'move-node-to-workspace 3' + +[[on-window-detected]] + if.app-id = 'company.thebrowser.Browser' + run = 'move-node-to-workspace 2' + +[[on-window-detected]] + if.app-id = 'com.exafunction.windsurf' + run = 'move-node-to-workspace 1' + +[[on-window-detected]] + if.app-id = 'com.cmuxterm.app' + run = 'move-node-to-workspace 1' + +[[on-window-detected]] + if.app-id = 'com.readdle.SparkDesktop' + run = 'move-node-to-workspace 0' + +[[on-window-detected]] + if.app-id = 'com.apple.MobileSMS' + run = 'move-node-to-workspace M' + +[[on-window-detected]] + if.app-id = 'com.apple.Music' + run = 'move-node-to-workspace M' + +# Gaps between windows (inner-*) and between monitor edges (outer-*). +# Possible values: +# - Constant: gaps.outer.top = 8 +# - Per monitor: gaps.outer.top = [{ monitor.main = 16 }, { monitor."some-pattern" = 32 }, 24] +# In this example, 24 is a default value when there is no match. +# Monitor pattern is the same as for 'workspace-to-monitor-force-assignment'. +# See: https://nikitabobko.github.io/AeroSpace/guide#assign-workspaces-to-monitors +[gaps] +inner.horizontal = 5 +inner.vertical = 5 +outer.left = 5 +outer.bottom = 5 +outer.top = 5 +outer.right = 5 + +# 'main' binding mode declaration +# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes +# 'main' binding mode must be always presented +[mode.main.binding] + +# All possible keys: +# - Letters. a, b, c, ..., z +# - Numbers. 0, 1, 2, ..., 9 +# - Keypad numbers. keypad0, keypad1, keypad2, ..., keypad9 +# - F-keys. f1, f2, ..., f20 +# - Special keys. minus, equal, period, comma, slash, backslash, quote, semicolon, backtick, +# leftSquareBracket, rightSquareBracket, space, enter, esc, backspace, tab +# - Keypad special. keypadClear, keypadDecimalMark, keypadDivide, keypadEnter, keypadEqual, +# keypadMinus, keypadMultiply, keypadPlus +# - Arrows. left, down, up, right + +# All possible modifiers: cmd, alt, ctrl, shift + +# All possible commands: https://nikitabobko.github.io/AeroSpace/commands + +# Disable Commands +cmd-h = [] +cmd-alt-h = [] + +# See: https://nikitabobko.github.io/AeroSpace/commands#exec-and-forget +# You can uncomment the following lines to open up terminal with alt + enter shortcut (like in i3) +alt-enter = 'exec-and-forget open -n /Applications/Ghostty.app' + +# See: https://nikitabobko.github.io/AeroSpace/commands#layout +alt-slash = 'layout tiles horizontal vertical' +alt-comma = 'layout accordion horizontal vertical' + +# See: https://nikitabobko.github.io/AeroSpace/commands#focus +alt-h = 'focus --boundaries-action wrap-around-the-workspace left' +alt-j = 'focus --boundaries-action wrap-around-the-workspace down' +alt-k = 'focus --boundaries-action wrap-around-the-workspace up' +alt-l = 'focus --boundaries-action wrap-around-the-workspace right' + +# Consider using 'join-with' command as a 'split' replacement if you want to enable +# normalizations +alt-shift-backslash = 'split horizontal' +alt-shift-minus = 'split vertical' + +alt-f = 'fullscreen' + +alt-s = 'layout v_accordion' # 'layout stacking' in i3 +alt-w = 'layout h_accordion' # 'layout tabbed' in i3 +alt-e = 'layout tiles horizontal vertical' # 'layout toggle split' in i3 + +alt-shift-space = 'layout floating tiling' # 'floating toggle' in i3 + +# See: https://nikitabobko.github.io/AeroSpace/commands#move +alt-shift-h = 'move left' +alt-shift-j = 'move down' +alt-shift-k = 'move up' +alt-shift-l = 'move right' + +# See: https://nikitabobko.github.io/AeroSpace/commands#resize +# alt-shift-minus = 'resize smart -50' +# alt-shift-equal = 'resize smart +50' + +# See: https://nikitabobko.github.io/AeroSpace/commands#workspace +alt-1 = 'workspace 1' +alt-2 = 'workspace 2' +alt-3 = 'workspace 3' +alt-4 = 'workspace 4' +alt-5 = 'workspace 5' +alt-6 = 'workspace 6' +alt-7 = 'workspace 7' +alt-8 = 'workspace 8' +alt-9 = 'workspace 9' +alt-0 = 'workspace 0' +alt-m = 'workspace M' + +# See: https://nikitabobko.github.io/AeroSpace/commands#move-node-to-workspace +alt-shift-1 = 'move-node-to-workspace 1' +alt-shift-2 = 'move-node-to-workspace 2' +alt-shift-3 = 'move-node-to-workspace 3' +alt-shift-4 = 'move-node-to-workspace 4' +alt-shift-5 = 'move-node-to-workspace 5' +alt-shift-6 = 'move-node-to-workspace 6' +alt-shift-7 = 'move-node-to-workspace 7' +alt-shift-8 = 'move-node-to-workspace 8' +alt-shift-9 = 'move-node-to-workspace 9' +alt-shift-0 = 'move-node-to-workspace 0' +alt-shift-m = 'move-node-to-workspace M' + +# See: https://nikitabobko.github.io/AeroSpace/commands#workspace-back-and-forth +alt-tab = 'workspace-back-and-forth' +# See: https://nikitabobko.github.io/AeroSpace/commands#move-workspace-to-monitor +alt-shift-tab = 'move-workspace-to-monitor --wrap-around next' + +# See: https://nikitabobko.github.io/AeroSpace/commands#mode +# alt-shift-semicolon = 'mode service' + +# 'service' binding mode declaration. +# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes +[mode.service.binding] +# esc = ['reload-config', 'mode main'] +# r = ['flatten-workspace-tree', 'mode main'] # reset layout +# s = ['layout sticky tiling', 'mode main'] # sticky is not yet supported https://github.com/nikitabobko/AeroSpace/issues/2 +# f = ['layout floating tiling', 'mode main'] # Toggle between floating and tiling layout +# backspace = ['close-all-windows-but-current', 'mode main'] +alt-shift-c = 'reload-config' + +alt-r = 'mode resize' + +[mode.resize.binding] +h = 'resize width -50' +j = 'resize height +50' +k = 'resize height -50' +l = 'resize width +50' +enter = 'mode main' +esc = 'mode main' diff --git a/home/run_once_after_install-aerospace.sh.tmpl b/home/run_once_after_install-aerospace.sh.tmpl new file mode 100644 index 0000000..1eae2dd --- /dev/null +++ b/home/run_once_after_install-aerospace.sh.tmpl @@ -0,0 +1,30 @@ +{{ if and (eq .chezmoi.os "darwin") (not .headless) (not .container) -}} +#!/bin/sh +# AeroSpace — i3-like tiling WM for macOS. GUI-only, so this is gated to +# non-headless Macs; ~/.aerospace.toml is gated the same way in .chezmoiignore. +# +# TRUST NOTE: this is the repo's only tap and only cask — everything else brew +# installs (tmux, vim, mise) is homebrew-core. AeroSpace is not in homebrew-cask, +# so it comes from nikitabobko/tap, which is the upstream author's own tap for +# github.com/nikitabobko/AeroSpace — same trust boundary as the app itself, not a +# third party repackaging it. Installed fully qualified rather than via a bare +# `brew tap` so the provenance is explicit at the call site and no extra tap is +# left configured for unrelated formulae to resolve against. +# +# Never fails the apply: same policy as install-system-packages.sh. +set -eu + +command -v brew >/dev/null 2>&1 || { + echo "==> no brew; skipping AeroSpace (install manually: https://nikitabobko.github.io/AeroSpace)" >&2 + exit 0 +} + +if brew list --cask aerospace >/dev/null 2>&1 || [ -d /Applications/AeroSpace.app ]; then + exit 0 +fi + +echo "==> Installing AeroSpace (cask: nikitabobko/tap)" +brew install --cask nikitabobko/tap/aerospace || + echo "==> AeroSpace install failed; install manually: brew install --cask nikitabobko/tap/aerospace" >&2 +exit 0 +{{ end -}}