I started using AeroSpace on macOS a few months ago. It’s a neat paradigm that works pretty well, despite working against any UX goals Apple could be said to have. The keyboard-driven navigation has been worth putting up with the inconveniences and bugs so far to me.
I’ve recently been looking into idempotent window creation — meaning, creating a new window if and only if a window for that purpose doesn’t already exist. For instance, a given project might always have at least three windows: VS Code, a Terminal for normal interaction, and a Terminal running Claude Code. When I start working on a project, I will want to open all those windows, but if I was working on it before, I don’t want to open new copies of them. To do this, I have tricks for both Terminal and VS Code.
Finding an existing window by its title
AeroSpace can list window titles by their application ID; combine this functionality with grep to find any application window by its title regex.
# Find a window ID based on its application bundle ID and a title regex
find_app_window_id() {
app_bundle_id="$1"
title_regex="$2"
aerospace list-windows --app-bundle-id "$app_bundle_id" --monitor all --format '%{window-id} %{window-title}' |
while read -r line; do
wid=${line%% *}
title=${line#* }
if echo "$title" | grep -qE "$title_regex"; then
echo "$wid"
fi
done
}
The application bundle ID is a macOS concept.
AeroSpace will list all open applications with aerospace list-apps
.
> aerospace list-apps
679 | com.apple.Terminal | Terminal
44453 | com.microsoft.VSCode | Code
68472 | org.mozilla.firefox | Firefox
# ... etc
Finding existing Terminal windows
The trick is to use xterm control sequences to embed a unique string into your Terminal’s title bar, then find that a window with that unique string via the above trick.
I use Unicode glyphs to make sure that no normal Terminal window will contain the title sequence.
# The glyph for my primary shell terminal for a given project
termglyph="❯"
# The glyph for my Claude Code terminal for a given project
llmglyph="∑"
I combine those glyphs with a short project identifier,
ending up with Terminal windows that contain ❯ me.micahrl.com
or ∑ dhd
in them.
I also need a way to run commands on a new Terminal window, both to set this title, and to cd to the project dir, run Claude, etc. Fortunately this already exists via AppleScript.
# Open a new Terminal window running the given script.
open_terminal_with_script() {
script="$1"
osascript -e '
tell application "Terminal"
do script "'"$script"'"
activate
end tell'
}
Calling this directly like open_terminal_with_script 'echo Hi; cd /tmp'
works ok,
but as you can see there will be issues with quoting.
The best way to sidestap that is to use a temporary file, and dot-source it.
Putting that all together, we get:
# Move or open a Terminal window by a special title, and run a command in it.
# Rely on a title that should be unique in any possible Terminal title text.
# It is recommended to use a special glyph to make it unique.
# Search for an existing window with that title, and if found, move it to the given workspace.
# If not found, open a new Terminal window running the given command,
# and set its title to the given title with a terminal escape sequence.
# (Note that setting the title will only actually set part of the window title,
# depending on your Terminal settings.)
terminal_move_or_open() {
workspace="$1"
title="$2"
cmd="$3"
# Create a temp file to hold the script to run.
# This gets around quoting issues with osascript.
# The script will delete itself before doing any work ---
# it's better to do it that way than try to do it from the parent shell
# where we might delete it before Terminal has a chance to run it.
# TMPDIR on macOS is guaranteed to exist and be writable, and allow no other users.
tmpfile="$TMPDIR/terminal_move_or_open_${workspace}_${title}.sh"
cat > $tmpfile << ENDSCRIPT
#!/bin/sh
# Run with set -x so the cmd is shown to the user when the terminal opens.
set -x
rm -f $tmpfile
printf '\033]0;%s\007' '$title'
$cmd
set +x
ENDSCRIPT
existing_terminal=$(find_app_window_id 'com.apple.Terminal' "$title" | head -n1 || true)
if test "$existing_terminal"; then
aerospace move-node-to-workspace --window-id "$existing_terminal" "$workspace"
else
# Run by dot-sourcing so that commands like 'cd' will work
open_terminal_with_script ". '$tmpfile'"
fi
}
I call it like this:
# Local directories
terminal_move_or_open t \
"${termglyph} me.micahrl.com" \
"cd '${HOME}/mrldata/Repositories/me.micahrl.com'"
terminal_move_or_open t \
"${llmglyph} me.micahrl.com" \
"cd '${HOME}/mrldata/Repositories/me.micahrl.com'; /Users/mrled/.bun/bin/claude;"
# Remote directories
terminal_move_or_open w \
"${termglyph} understatement1" \
"ssh chineseroom.micahrl.com -t 'cd ~/work/understatement1 && exec \$SHELL -l'"
terminal_move_or_open w \
"${llmglyph} undersetatement1" \
"ssh chineseroom.micahrl.com -t 'cd ~/work/understatement1 && exec \$SHELL -l -i -c \"/home/callista/.local/bin/claude --dangerously-skip-permissions\"'"
Finding existing VS Code windows
This turns out to be really easy, because VS Code always puts its project directory name in the title bar, and we already have a function for finding an app’s window based on its title bar.
vscode_move_or_open() {
workspace="$1"
window_regex="$2"
uri="$3"
existing_vscode=$(find_app_window_id 'com.microsoft.VSCode' "$window_regex" | head -n1 || true)
if test "$existing_vscode"; then
aerospace move-node-to-workspace --window-id "$existing_vscode" "$workspace"
else
# ?windowId=_blank forces a new window.
open "$uri?windowId=_blank"
fi
}
We use VS Code’s support for opening URIs because this allows us to specify remote project directories too.
# A local project directory in my homedir
vscode_move_or_open t 'me\.micahrl\.com$' "vscode://file/${HOME}/mrldata/Repositories/me.micahrl.com"
# A remote project directory over SSH
vscode_move_or_open w \
'understatement1 \[SSH: chineseroom.micahrl.com' \
'vscode://vscode-remote/ssh-remote+chineseroom.micahrl.com/home/callista/work/understatement1'
How I use this
I have functions for all my project workspaces that launch the IDE and the terminals, like this:
workspace_me_micahrl_com() {
aerospace workspace t
vscode_move_or_open t 'me\.micahrl\.com$' "vscode://file/${HOME}/mrldata/Repositories/me.micahrl.com"
terminal_move_or_open t \
"${termglyph} me.micahrl.com" \
"cd '${HOME}/mrldata/Repositories/me.micahrl.com'"
terminal_move_or_open t \
"${llmglyph} me.micahrl.com" \
"cd '${HOME}/mrldata/Repositories/me.micahrl.com'; /Users/mrled/.bun/bin/claude;"
}
workspace_understatement_dev_1() {
aerospace workspace w
vscode_move_or_open w \
'understatement1 \[SSH: chineseroom.micahrl.com' \
'vscode://vscode-remote/ssh-remote+chineseroom.micahrl.com/home/callista/work/understatement1?windowId=_blank'
terminal_move_or_open w \
"${termglyph} understatement1" \
"ssh chineseroom.micahrl.com -t 'cd ~/work/understatement1 && exec \$SHELL -l'"
terminal_move_or_open w \
"${llmglyph} undersetatement1" \
"ssh chineseroom.micahrl.com -t 'cd ~/work/understatement1 && exec \$SHELL -l -i -c \"/home/callista/.local/bin/claude --dangerously-skip-permissions\"'"
}
They’re invoked from a script by key bindings in my AeroSpace configuration.
What’s missing
I don’t have a good way to do this for Firefox windows — what I’d like is a way to query a window for all its tabs’ URLs, but I don’t know how to do that.
Other windows
All the other app windows I use daily tend to be one window per app,
so AeroSpace’s on-window-detected
callback
works fine for them.
[[on-window-detected]]
if.app-id = "com.apple.ActivityMonitor"
run = ["move-node-to-workspace 10"]