Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions docs/shell-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions home/.chezmoiignore
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
206 changes: 206 additions & 0 deletions home/dot_aerospace.toml
Original file line number Diff line number Diff line change
@@ -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'
1 change: 1 addition & 0 deletions home/dot_config/mise/conf.d/cli-tools.toml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion home/dot_config/zsh/plugins.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 17 additions & 8 deletions home/dot_zshrc.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ----
Expand Down
30 changes: 30 additions & 0 deletions home/run_once_after_install-aerospace.sh.tmpl
Original file line number Diff line number Diff line change
@@ -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 -}}
44 changes: 43 additions & 1 deletion home/run_once_before_install-mise.sh.tmpl
Original file line number Diff line number Diff line change
@@ -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