/blog/

2025 0925 Idempotently creating windows with AeroSpace

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"]

Responses

Webmentions

Hosted on remote sites, and collected here via Webmention.io (thanks!).

Comments

Comments are hosted on this site and powered by Remark42 (thanks!).