Add macOS workspace restoration plan documentation#4
Conversation
Covers AeroSpace virtual workspaces, iTerm2 arrangements, shell-based project launcher scripts, and keyboard shortcut integration for instant switching between parallel Claude Code + browser preview setups. https://claude.ai/code/session_019NLEfbmYTkMtdnVYuS6UZM
Covers SwiftBar plugin (lowest friction, always-visible dropdown), Raycast extension (searchable list for many projects), and comparison of 6 approaches including Hammerspoon, rumps, Alfred, and SwiftUI. https://claude.ai/code/session_019NLEfbmYTkMtdnVYuS6UZM
There was a problem hiding this comment.
Pull request overview
Adds a standalone documentation plan describing a scriptable macOS “workspace restoration” workflow for switching between multi-project dev setups (terminals, browser previews, and related tools) using AeroSpace + iTerm2 + scripts.
Changes:
- Introduces an end-to-end architecture and phased setup guide for workspace switching on macOS.
- Provides example AeroSpace config, iTerm2 profile/arrangement guidance, and a sample
workspace.shlauncher. - Documents optional UI integrations (SwiftBar / Raycast) plus git worktree usage for parallel branch workflows.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| aerospace workspace "$ws" 2>/dev/null || true | ||
|
|
There was a problem hiding this comment.
The AeroSpace workspace switch command is effectively fire-and-forget (aerospace workspace "$ws" 2>/dev/null || true). If ws is empty/invalid or AeroSpace isn’t available, the script will silently continue and print success. Consider validating ws and failing with a clear error when the workspace switch doesn’t succeed.
| aerospace workspace "$ws" 2>/dev/null || true | |
| if [[ -z "$ws" ]]; then | |
| echo "Error: No AeroSpace workspace configured for project '${project}'" >&2 | |
| exit 1 | |
| fi | |
| if ! command -v aerospace >/dev/null 2>&1; then | |
| echo "Error: 'aerospace' command not found; cannot switch workspace '${ws}' for project '${project}'" >&2 | |
| exit 1 | |
| fi | |
| if ! aerospace workspace "$ws"; then | |
| echo "Error: Failed to switch to AeroSpace workspace '${ws}' for project '${project}'" >&2 | |
| exit 1 | |
| fi |
| alt-1 = ['workspace 1', 'exec-and-forget ~/.config/workspaces/workspace.sh open project-a --no-switch'] | ||
| ``` | ||
|
|
||
| (Add a `--no-switch` flag to skip the `aerospace workspace` call when triggered from AeroSpace itself.) |
There was a problem hiding this comment.
Option A shows calling workspace.sh open project-a --no-switch, but the provided workspace.sh argument parsing doesn’t handle --no-switch (it passes only the project name to cmd_open). Either implement flag parsing (and behavior) in the script, or remove the flag from the example so the instructions are directly runnable.
| app_mode = true # Open as standalone window (no browser chrome) | ||
|
|
||
| [extras] | ||
| # Additional apps to open/focus |
There was a problem hiding this comment.
The project TOML example includes browser.app_mode and extras.apps, but the provided workspace.sh implementation doesn’t read or apply either setting. Clarify these are future extensions, or extend the script to honor them so the example config matches the implementation.
| app_mode = true # Open as standalone window (no browser chrome) | |
| [extras] | |
| # Additional apps to open/focus | |
| # NOTE: `app_mode` is a planned extension; current workspace.sh ignores this setting. | |
| app_mode = true # Intended: open as standalone window (no browser chrome) | |
| [extras] | |
| # Additional apps to open/focus | |
| # NOTE: `extras.apps` is a planned extension; current workspace.sh does not yet launch these. |
| # Disable macOS Spaces animations (they interfere) | ||
| [gaps] | ||
| inner.horizontal = 8 | ||
| inner.vertical = 8 | ||
| outer.left = 8 | ||
| outer.right = 8 | ||
| outer.top = 8 | ||
| outer.bottom = 8 | ||
|
|
There was a problem hiding this comment.
In the AeroSpace config snippet, the comment says “Disable macOS Spaces animations” but the following [gaps] settings only configure window gaps. Update the comment to reflect what the settings actually do (or document the correct place/setting for animation control if applicable).
| | `project-a` | `~/code/project-a` | `PROJECT-A` | Blue | `claude` | | ||
| | `project-a-server` | `~/code/project-a` | `PROJECT-A:dev` | Blue | `npm run dev` | | ||
| | `project-b` | `~/code/project-b` | `PROJECT-B` | Green | `claude` | | ||
| | `project-b-server` | `~/code/project-b` | `PROJECT-B:dev` | `yarn dev` | Green | |
There was a problem hiding this comment.
In the iTerm2 profile examples table, the project-b-server row has the Tab Color and Startup Command columns swapped (it lists yarn dev as the tab color and Green as the startup command). This makes the example inconsistent with the column headers and the other rows.
| | `project-b-server` | `~/code/project-b` | `PROJECT-B:dev` | `yarn dev` | Green | | |
| | `project-b-server` | `~/code/project-b` | `PROJECT-B:dev` | Green | `yarn dev` | |
| # Parse TOML value (basic — for simple key = "value" pairs) | ||
| toml_get() { | ||
| local file="$1" key="$2" | ||
| grep "^${key}" "$file" | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' |
There was a problem hiding this comment.
toml_get relies on grep inside a script running with set -e, so if an optional key (e.g., url or arrangement) is missing the script will exit before the later -n checks run. Consider making toml_get return an empty string when the key is absent (e.g., grep ... || true) and avoid deleting all spaces so values with spaces or inline comments aren’t mangled.
| grep "^${key}" "$file" | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' | |
| local line | |
| line=$(grep "^${key}" "$file" || true) | |
| if [[ -z "$line" ]]; then | |
| echo "" | |
| return 0 | |
| fi | |
| echo "$line" | sed -E 's/.*=\s*"?([^"]*)"?\s*(#.*)?$/\1/' |
| local dir=$(toml_get "$config" "directory") | ||
| local url=$(toml_get "$config" "url") | ||
| local arrangement=$(toml_get "$config" "arrangement") | ||
| dir="${dir/#\~/$HOME}" |
There was a problem hiding this comment.
cmd_open reads directory into dir (and expands ~) but never uses it. Either use it to set the working directory for launched apps/terminal sessions, or remove it from the script (and update the doc) to avoid implying it affects behavior.
| local dir=$(toml_get "$config" "directory") | |
| local url=$(toml_get "$config" "url") | |
| local arrangement=$(toml_get "$config" "arrangement") | |
| dir="${dir/#\~/$HOME}" | |
| local url=$(toml_get "$config" "url") | |
| local arrangement=$(toml_get "$config" "arrangement") |
Covers why native Cmd+Tab doesn't work with AeroSpace workspaces, recommends alt-tab bindings for workspace-back-and-forth and window cycling, and documents the Karabiner-Elements approach for reclaiming literal Cmd+Tab. https://claude.ai/code/session_019NLEfbmYTkMtdnVYuS6UZM
- AeroSpace config with workspace switching, tiling, vim-style focus, alt-tab back-and-forth, and window cycling - workspace CLI tool: open/close/list/save/status/note/notes/ensure - Example project configs for frontend and API workspaces - iTerm2 AppleScript helpers for arrangement save/restore/close - SwiftBar menu bar plugin showing active workspace and notes - Raycast script commands for workspace switching and listing - install.sh symlinks everything into ~/.config and ~/bin - Per-workspace editable notes (stored in project TOML, displayed on switch and in menu bar) https://claude.ai/code/session_019NLEfbmYTkMtdnVYuS6UZM
Click the note in the menu bar dropdown to open a macOS input dialog where you can edit, save, or clear the note. Also shows "Add note..." when no note is set. https://claude.ai/code/session_019NLEfbmYTkMtdnVYuS6UZM
Summary
This adds comprehensive documentation for a scriptable macOS workspace restoration system that enables developers to save, restore, and switch between multi-project developer workspaces with a single keystroke.
Key Changes
workspace.sh) with TOML-based configurationNotable Details
https://claude.ai/code/session_019NLEfbmYTkMtdnVYuS6UZM