Skip to content

Commit b378f6f

Browse files
authored
aerospace experimentation (#2)
1 parent dd55a12 commit b378f6f

29 files changed

Lines changed: 1882 additions & 16 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ claude/.claude/*
3636
!claude/.claude/settings.json
3737
!claude/.claude/statusline.sh
3838

39+
# SketchyBar: the mic helper is compiled from the tracked .swift source. Because
40+
# stow runs --no-folding, that build output lands in $HOME and can't reach the
41+
# repo anyway — this is belt and braces for anyone who builds in-tree.
42+
sketchybar/.config/sketchybar/helpers/mic
43+
3944

4045
# ============================================================
4146
# macOS noise

Brewfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ tap "azure/functions" # provides azure-functions-core-tools@4
1313
tap "azure/bicep" # provides bicep
1414
tap "azure/kubelogin" # provides Azure's kubelogin (see note below)
1515
tap "nikitabobko/tap" # provides aerospace
16+
tap "FelixKratz/formulae" # provides borders (not in homebrew-core)
1617

1718

1819
# ============================================================
@@ -103,6 +104,7 @@ cask "copilot-cli"
103104

104105
# --- Notes & productivity ---
105106
cask "obsidian"
107+
cask "raycast"
106108

107109
# --- Browsers & media ---
108110
cask "google-chrome"
@@ -114,11 +116,14 @@ cask "teamviewer"
114116

115117
# --- Window management ---
116118
cask "aerospace"
119+
brew "borders"
120+
brew "sketchybar"
121+
brew "ical-buddy"
117122

118123
# --- System & hardware ---
119124
cask "macs-fan-control"
125+
brew "macmon"
120126
cask "monitorcontrol"
121-
cask "thaw"
122127
cask "logi-options+"
123128
cask "focusrite-control-2"
124129
cask "onyx"

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ each declared tap trust as it adds it.
7979
| `kitty` | Kitty terminal (Catppuccin Frappé, Fira Code Nerd Font Mono) |
8080
| `lazygit` | lazygit config (Catppuccin Frappé theme, delta pager) |
8181
| `nvim` | Neovim (lazy.nvim, Treesitter, Telescope, Catppuccin Frappé) |
82+
| `raycast` | [Raycast](https://www.raycast.com) script commands. Raycast's own settings are not file-based, so only this directory is tracked — add it under Settings > Script Commands > Add Script Directory |
8283
| `ssh` | SSH config with macOS Keychain, hardened algorithms. Put machine-local hosts in `~/.ssh/config.local` (included first, untracked) |
8384
| `zsh` | `.zshrc` (Oh My Zsh + Powerlevel10k, fzf, zoxide, direnv, aliases), `.p10k.zsh`, `.zprofile` |
8485

@@ -101,6 +102,7 @@ The install script can configure:
101102
- **Finder** — show hidden files, extensions, path/status bar, list view, folders on top, search current folder, unhide `~/Library`, no `.DS_Store` on network/USB
102103
- **Dock** — minimize to app icon, hide recent apps, no space rearranging, icon size, faster animations (auto-hide timings are tuned, but auto-hide itself is left to you)
103104
- **Keyboard** — fast key repeat, below what System Settings can express (`KeyRepeat=1.5`, `InitialKeyRepeat=12`, in 15 ms ticks — the slider bottoms out at 2 and 15). Also disables auto-correct, smart quotes and smart dashes, and enables full keyboard access. Takes effect after logout/login
105+
- **Launcher** — disables the Spotlight (`⌘Space`) and Finder-search (`⌘⌥Space`) shortcuts so Raycast can take the key. `⌥Space` is deliberately *not* used: kitty runs `macos_option_as_alt left`, so it would swallow zsh's `expand-history`
104106
- **Screenshots** — save to `~/Pictures/Screenshots`, PNG format, no shadow
105107
- **Trackpad** — three-finger drag
106108
- **Misc** — expanded save/print panels by default

aerospace/.config/aerospace/aerospace.toml

Lines changed: 179 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,46 @@
1818
# ^[h run-help -> alt-h is 'focus left'
1919
# ^[l OMZ ls widget -> alt-l is 'focus right'
2020
# ^[1..^[9 digit-argument -> workspace switching
21+
#
22+
# alt-ctrl-shift-<letter> reaches the terminal as ^[^<letter> (Shift is dropped
23+
# for control characters), costing two more. Both keep an alternative:
24+
# ^[^H backward-kill-word -> alt-ctrl-shift-h throws a window one monitor left
25+
# (alt-backspace, ^[^?, still does it)
26+
# ^[^L clear-screen -> alt-ctrl-shift-l throws a window one monitor right
27+
# (ctrl-l still does it)
28+
# ^[F forward-word -> alt-shift-f is 'fullscreen' (^[f is the same widget)
29+
#
30+
# ON ANIMATIONS: there are none, and none are possible. AeroSpace repositions a
31+
# window with a single Accessibility API write — there is no tween and no config
32+
# option for one. (Upstream only discusses animation to explain why it replaced
33+
# native macOS Spaces, whose switching animation can't be disabled.) The focus
34+
# cue below is the substitute: JankyBorders draws a ring on the focused window,
35+
# and that ring pulses on every focus change.
2136

2237
config-version = 2
2338

2439

2540
# ============================================================
2641
# Startup
2742
# ============================================================
28-
# Left off deliberately while evaluating: a tiling WM rearranges every window on
29-
# the desktop, and "just don't launch it" is the cheapest escape hatch.
30-
start-at-login = false
43+
# The escape hatch is `aerospace enable toggle`, which suspends window management
44+
# without quitting — so there's no reason to keep launching it by hand.
45+
start-at-login = true
3146

3247
# Re-reads this file on save. Per upstream, reload once by hand after enabling
3348
# for the auto-reloading to start.
3449
auto-reload-config = true
3550

36-
after-startup-command = []
51+
# JankyBorders — draws the focus ring (see the focus cues under Behaviour).
52+
# Run with no arguments so it reads ~/.config/borders/bordersrc, which is the
53+
# stowed `borders` package. Started from here rather than `brew services` so the
54+
# ring's lifetime is tied to the window manager's: quit AeroSpace, no orphan.
55+
after-startup-command = [
56+
'exec-and-forget borders',
57+
# The status bar. Both run with no arguments so they read their own stowed
58+
# config (~/.config/borders/bordersrc, ~/.config/sketchybar/sketchybarrc).
59+
'exec-and-forget sketchybar',
60+
]
3761
on-mode-changed = []
3862

3963

@@ -53,14 +77,55 @@ gaps.inner.horizontal = 8
5377
gaps.inner.vertical = 8
5478
gaps.outer.left = 8
5579
gaps.outer.bottom = 8
56-
gaps.outer.top = 8
80+
# Per-monitor, because the built-in display has a notch and the externals don't.
81+
#
82+
# SketchyBar's islands are 26pt tall centred in a 32pt transparent bar, so their
83+
# bottom edge is at y=29; 37 = 29 + the usual 8pt gap. The bar draws inside the
84+
# area AeroSpace tiles into, so without this windows slide underneath it.
85+
#
86+
# The built-in gets 5, not 37. AeroSpace tiles inside the monitor's VISIBLE rect,
87+
# and macOS reserves that display's top 32pt as notch safe area even with the
88+
# menu bar auto-hidden (measured: safeAreaInsets.top = 32, visibleFrame 1512x950
89+
# of a 1512x982 frame). So the 32 is already excluded and only the remaining 5
90+
# has to be asked for. Measured to confirm: with a flat 34 here, a window on the
91+
# built-in landed at y=66 (= 32 + 34) while one on an external landed at y=34 —
92+
# a 32pt dead strip on the laptop only. 5 + 32 = 37 puts both at the same place.
93+
#
94+
# Also paired with the macOS menu bar being auto-hidden (see the install script):
95+
# with the menu bar visible this would sit on top of its 30, and the bar would
96+
# land below it rather than at the top of the screen.
97+
gaps.outer.top = [{ monitor.'built-in' = 5 }, 37]
5798
gaps.outer.right = 8
5899

59100

60101
# ============================================================
61102
# Behaviour
62103
# ============================================================
104+
# Focus cue. AeroSpace can't animate a window move (see the note at the top), so
105+
# the JankyBorders ring flashes lavender for ~120ms and settles back to mauve.
106+
# Re-invoking `borders` reconfigures the running instance rather than starting a
107+
# second one, which is what makes the two-frame pulse work.
108+
# Colours are Catppuccin Frappé: lavender #babbf1, mauve #ca9ee6 (the same mauve
109+
# kitty uses for active_tab_background). They must stay in sync with bordersrc.
110+
# If a held-down alt-h/alt-l ever makes the pulse look like flicker, drop this
111+
# callback — the static ring survives on its own.
112+
#
113+
# Deliberately NOT 'move-mouse window-lazy-center' here: the pointer stays put
114+
# on window focus changes. The monitor callback below is a different thing — it
115+
# only fires when the focused *monitor* changes.
116+
on-focus-changed = [
117+
'exec-and-forget /bin/sh -c "borders active_color=0xffbabbf1; sleep 0.12; borders active_color=0xffca9ee6"',
118+
]
63119
on-focused-monitor-changed = ['move-mouse monitor-lazy-center']
120+
121+
# Pushes workspace switches to SketchyBar, which has no way to learn about them
122+
# on its own. Inert while sketchybar isn't running — the command just fails into
123+
# the void — which is why it's safe to leave here while the bar is still being
124+
# trialled. $AEROSPACE_FOCUSED_WORKSPACE is set by AeroSpace for this callback.
125+
exec-on-workspace-change = [
126+
'/bin/bash', '-c',
127+
'sketchybar --trigger aerospace_workspace_change FOCUSED_WORKSPACE=$AEROSPACE_FOCUSED_WORKSPACE',
128+
]
64129
focus-follows-mouse.enabled = false
65130
automatically-unhide-macos-hidden-apps = false
66131
key-mapping.preset = 'qwerty'
@@ -69,22 +134,113 @@ key-mapping.preset = 'qwerty'
69134
persistent-workspaces = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
70135

71136

137+
# ============================================================
138+
# Window rules
139+
# ============================================================
140+
# Runs every time a new window is detected. Two syntaxes exist; this is the
141+
# current one (`if = 'test ...'`). The `if.app-id = '...'` form seen in most
142+
# blog posts is the deprecated legacy spelling.
143+
#
144+
# `check-further-callbacks` defaults to false, so the FIRST matching rule wins
145+
# and everything below it is skipped. The rules here are mutually exclusive, so
146+
# order doesn't bite yet — it will the moment two conditions can both match.
147+
#
148+
# Bundle IDs were read off each app's Info.plist, not guessed. To add one:
149+
# `aerospace list-apps` while it's running.
150+
#
151+
# Deliberately NOT here: browsers, kitty, VS Code. This fires for *every* new
152+
# window, so a placement rule on those would yank a window you just opened onto
153+
# another monitor mid-task.
154+
on-window-detected = [
155+
# Float — utilities and preference panes; a tile only distorts the layout.
156+
{ if = 'test %{app-bundle-id} = com.apple.systempreferences', run = 'layout floating' },
157+
{ if = 'test %{app-bundle-id} = com.apple.calculator', run = 'layout floating' },
158+
{ if = 'test %{app-bundle-id} = com.fasttracksoftware.adminbyrequest', run = 'layout floating' },
159+
{ if = 'test %{app-bundle-id} = app.monitorcontrol.MonitorControl', run = 'layout floating' },
160+
{ if = 'test %{app-bundle-id} = com.logi.optionsplus', run = 'layout floating' },
161+
{ if = 'test %{app-bundle-id} = com.crystalidea.macsfancontrol', run = 'layout floating' },
162+
{ if = 'test %{app-bundle-id} = com.focusrite.control', run = 'layout floating' },
163+
{ if = 'test %{app-bundle-id} = com.titanium.OnyX', run = 'layout floating' },
164+
{ if = 'test %{app-bundle-id} = com.jetbrains.toolbox', run = 'layout floating' },
165+
# Raycast — the launcher panel itself is a non-activating window AeroSpace
166+
# already ignores, but its Settings window tiles. Left COMMENTED because the
167+
# bundle ID is unverified: Raycast is in the Brewfile but not installed here,
168+
# and the Homebrew cask carries no zap/quit stanza to read it from. Per the
169+
# rule above, read it rather than guess — install Raycast, open its Settings
170+
# window, run `aerospace list-apps`, paste what it reports, and uncomment.
171+
# A wrong ID does not error; the rule just never fires.
172+
# { if = 'test %{app-bundle-id} = com.raycast.macos', run = 'layout floating' },
173+
174+
# Place — chat/mail/notes open on their home screens (see the map below).
175+
{ if = 'test %{app-bundle-id} = com.microsoft.teams2', run = 'move-node-to-workspace 6' },
176+
{ if = 'test %{app-bundle-id} = com.microsoft.Outlook', run = 'move-node-to-workspace 7' },
177+
{ if = 'test %{app-bundle-id} = md.obsidian', run = 'move-node-to-workspace 8' },
178+
]
179+
180+
181+
# ============================================================
182+
# Monitors
183+
# ============================================================
184+
# The desk, left to right — AeroSpace ordinals are ordered the same way, so its
185+
# numbering already matches the physical layout:
186+
# 1 = Built-in Retina Display (left, laptop)
187+
# 2 = P24h-2L (1) (middle, macOS main display — the work screen)
188+
# 3 = P24h-2L (2) (right, side monitor)
189+
#
190+
# Both externals report the same name ("P24h-2L"); macOS appends (1)/(2) and
191+
# those suffixes can swap on reconnect, so they are never matched by name.
192+
#
193+
# The low numbers go to the middle screen deliberately: easiest keys on the
194+
# busiest monitor, at the cost of alt-N no longer tracking desk position.
195+
#
196+
# LAPTOP MAIN (middle) SIDE
197+
# ┌───────┐ ┌─────────────────┐ ┌───────┐
198+
# │ 8 9 │ │ 1 2 3 4 5 │ │ 6 7 │
199+
# └───────┘ └─────────────────┘ └───────┘
200+
#
201+
# Every entry ends in a pattern that always resolves, so undocking or closing
202+
# the lid collapses workspaces onto a live screen instead of stranding them.
203+
#
204+
# NOTE: force-assignment makes `move-workspace-to-monitor` a no-op. Use
205+
# `move-node-to-monitor` (alt-ctrl-shift-h/l below) to shuffle windows instead.
206+
[workspace-to-monitor-force-assignment]
207+
1 = 'main' # middle — the work screen
208+
2 = 'main'
209+
3 = 'main'
210+
4 = 'main'
211+
5 = 'main'
212+
6 = ['3', 'secondary'] # right — ordinal with 3 screens, secondary with 2
213+
7 = ['3', 'secondary']
214+
8 = ['built-in', 'main'] # laptop — 'main' covers clamshell
215+
9 = ['built-in', 'main']
216+
217+
72218
# ============================================================
73219
# Main bindings
74220
# ============================================================
75221
[mode.main.binding]
76222

77-
# Focus
78-
alt-h = 'focus left'
223+
# Launch a terminal, i3/sway style. Deliberately alt-SHIFT-enter and not the
224+
# idiomatic alt-enter: AeroSpace binds system-wide, ahead of kitty and zsh,
225+
# so alt-enter would permanently swallow ^[^M — which is self-insert-unmeta
226+
# in zsh's emacs keymap and "insert newline without submitting" in Claude
227+
# Code. Neither can be won back per-app once AeroSpace has the key.
228+
# `open -na` for a new instance each time; kitty has no single-instance or
229+
# remote-control setup here for a new window to attach to.
230+
alt-shift-enter = 'exec-and-forget open -na kitty'
231+
232+
# Focus — the three monitors sit side by side, so h/l treat them as one
233+
# continuous frame and walk across the edges. j/k stay workspace-local.
234+
alt-h = 'focus --boundaries all-monitors-outer-frame left'
79235
alt-j = 'focus down'
80236
alt-k = 'focus up'
81-
alt-l = 'focus right'
237+
alt-l = 'focus --boundaries all-monitors-outer-frame right'
82238

83-
# Move the focused window
84-
alt-shift-h = 'move left'
239+
# Move the focused window — h/l carry it onto the neighbouring monitor
240+
alt-shift-h = 'move --boundaries all-monitors-outer-frame left'
85241
alt-shift-j = 'move down'
86242
alt-shift-k = 'move up'
87-
alt-shift-l = 'move right'
243+
alt-shift-l = 'move --boundaries all-monitors-outer-frame right'
88244

89245
# Resize
90246
alt-minus = 'resize smart -50'
@@ -93,6 +249,8 @@ persistent-workspaces = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
93249
# Layout
94250
alt-slash = 'layout tiles horizontal vertical'
95251
alt-comma = 'layout accordion horizontal vertical'
252+
# ^[F is bound to forward-word, but so is ^[f — this costs a duplicate.
253+
alt-shift-f = 'fullscreen'
96254

97255
# Workspaces
98256
alt-1 = 'workspace 1'
@@ -117,7 +275,14 @@ persistent-workspaces = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
117275
alt-shift-9 = 'move-node-to-workspace 9'
118276

119277
alt-tab = 'workspace-back-and-forth'
120-
alt-shift-tab = 'move-workspace-to-monitor --wrap-around next'
278+
# Window-level counterpart to alt-tab. ^[` is unbound in emacs mode.
279+
alt-backtick = 'focus-back-and-forth'
280+
# Was move-workspace-to-monitor, which force-assignment turns into a no-op.
281+
alt-shift-tab = 'focus-monitor --wrap-around next'
282+
283+
# Throw the focused window one screen over and follow it there
284+
alt-ctrl-shift-h = 'move-node-to-monitor --focus-follows-window left'
285+
alt-ctrl-shift-l = 'move-node-to-monitor --focus-follows-window right'
121286

122287
alt-shift-semicolon = 'mode service'
123288

@@ -130,6 +295,8 @@ persistent-workspaces = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
130295
esc = ['reload-config', 'mode main']
131296
r = ['flatten-workspace-tree', 'mode main'] # reset layout
132297
f = ['layout floating tiling', 'mode main'] # toggle float/tile
298+
b = ['balance-sizes', 'mode main'] # equalise all windows
299+
s = ['swap --swap-focus dfs-next', 'mode main'] # swap with next window
133300
backspace = ['close-all-windows-but-current', 'mode main']
134301

135302
alt-shift-h = ['join-with left', 'mode main']

borders/.config/borders/bordersrc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/sh
2+
# JankyBorders — draws a ring around the focused window.
3+
#
4+
# AeroSpace can't animate window moves, so this ring is the focus cue. It's
5+
# launched by aerospace.toml's after-startup-command with no arguments, which is
6+
# what makes `borders` read this file.
7+
#
8+
# Catppuccin Frappé, matching the rest of the setup:
9+
# mauve #ca9ee6 — focused; the same colour as kitty's active_tab_background
10+
# surface1 #51576d — unfocused
11+
# Colours are 0xAARRGGBB, so the leading ff is opacity.
12+
#
13+
# aerospace.toml's on-focus-changed pulses active_color to lavender (#babbf1)
14+
# and back to the mauve below. Change the mauve here and it must change there
15+
# too, or every focus change will "settle" on the old colour.
16+
exec borders \
17+
active_color=0xffca9ee6 \
18+
inactive_color=0xff51576d \
19+
width=4.0 \
20+
style=round \
21+
hidpi=on

claude/.claude/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
"effortLevel": "xhigh",
88
"tui": "fullscreen",
99
"theme": "auto",
10-
"agentPushNotifEnabled": true
10+
"agentPushNotifEnabled": true,
11+
"sandbox.enabled": true
1112
}

git/.config/git/ignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@
5151
# Deliberately NOT ignored globally: bin/ and obj/. They belong in a project's
5252
# own .gitignore, which `dotnet new gitignore` generates. A global rule would
5353
# also hit non-.NET repos that legitimately track a bin/ directory of scripts.
54+
55+
**/.claude/.cc-writes/

0 commit comments

Comments
 (0)