The Problem: Sluggish Workspace Switching

MacOS native workspaces include transition animations that create noticeable delays when switching between them. Adjusting system settings cannot fully remove this lag. However Aerospace, a macOS window manager inspired by i3, can be used to remove the animation delay.

Installing Aerospace and Setting Up the Config

Aerospace can be installed via Homebrew (brew install --cask nikitabobko/tap/aerospace) and is configured through ~/.config/aerospace/aerospace.toml.

# Default layout settings
default-root-container-layout = 'accordion'
default-root-container-orientation = 'auto'

# Zero out gaps for no wasted space
[gaps]
inner.horizontal = 0
inner.vertical = 0
outer.left = 0
outer.bottom = 0
outer.top = 0
outer.right = 0

# Keybindings for workspace switching (we'll enhance this with a script)
[mode.main.binding]
ctrl-h = 'exec-and-forget source ~/.config/aerospace/aerospace.sh && switch_workspace -1'
ctrl-l = 'exec-and-forget source ~/.config/aerospace/aerospace.sh && switch_workspace +1'
ctrl-shift-l = 'exec-and-forget source ~/.config/aerospace/aerospace.sh && move_to_workspace +1'
ctrl-shift-h = 'exec-and-forget source ~/.config/aerospace/aerospace.sh && move_to_workspace -1'

# Make all new windows floating by default
[[on-window-detected]]
run = ['layout floating']

Every app window floats freely now—no more tiling.

The Magic Trick: A Bash Script for Navigation

A bash script at ~/.config/aerospace/aerospace.sh calculates next/previous workspaces.

It uses two functions:

  • switch_workspace for changing workspaces
  • move_to_workspace for shifting the active window with the switch

These are bound to ctrl+h/l for navigation and ctrl+shift+h/l for moving windows.

switch_workspace() {
  offset=$1
  workspaces="1 2 3 4"
  current_workspace=$(aerospace list-workspaces --monitor focused --visible)
  workspaces_array=($workspaces)
  
  # Find current index
  for i in "${!workspaces_array[@]}"; do
    if [ "${workspaces_array[i]}" = "$current_workspace" ]; then
      current_index=$i
      break
    fi
  done
  
  # Calculate target without wrapping
  target_index=$((current_index + offset))
  if (( target_index < 0 )); then
    target_index=0
  elif (( target_index >= ${#workspaces_array[@]} )); then
    target_index=$(( ${#workspaces_array[@]} - 1 ))
  fi
  
  # Switch
  aerospace workspace "${workspaces_array[target_index]}"
}

move_to_workspace() {
  offset=$1
  workspaces="1 2 3 4"
  current_workspace=$(aerospace list-workspaces --monitor focused --visible)
  workspaces_array=($workspaces)
  
  # Find current index
  for i in "${!workspaces_array[@]}"; do
    if [ "${workspaces_array[i]}" = "$current_workspace" ]; then
      current_index=$i
      break
    fi
  done
  
  # Calculate target with wrapping for moving
  target_index=$(( (current_index + offset + ${#workspaces_array[@]}) % ${#workspaces_array[@]} ))
  target_workspace="${workspaces_array[target_index]}"
  
  # Move and switch
  aerospace move-node-to-workspace "$target_workspace"
  aerospace workspace "$target_workspace"
}

You can view my complete configs here: aerospace.toml and aerospace.sh.