Skip to content

Instantly share code, notes, and snippets.

@johnlindquist
Created September 11, 2025 15:36
Show Gist options
  • Save johnlindquist/8bb42014988f280eb0f736d9633639b2 to your computer and use it in GitHub Desktop.
Save johnlindquist/8bb42014988f280eb0f736d9633639b2 to your computer and use it in GitHub Desktop.
Yabai Window Management Script - Move Window Right to Next Space with Auto-Creation

Yabai Window Movement Script - Move Window Right

Overview

This bash script is part of a macOS window management system that leverages Yabai (a tiling window manager for macOS) to programmatically move windows between spaces. Specifically, this script moves the currently focused window to the next space to the right, automatically creating a new space if the window is already on the rightmost space.

Purpose

The script automates window organization by providing a keyboard-shortcuttable way to shift windows rightward through macOS spaces (virtual desktops), enhancing productivity for users who work with multiple spaces.

Key Features

  • Smart Space Detection: Automatically detects if the current window is on the rightmost space
  • Dynamic Space Creation: Creates a new space when moving from the rightmost position
  • Window Focus Preservation: Ensures the moved window retains focus after the move
  • Comprehensive Logging: Detailed logging for debugging and monitoring
  • Error Handling: Graceful handling of edge cases and errors

Technical Details

Dependencies

  • Yabai: macOS tiling window manager (/opt/homebrew/bin/yabai)
  • jq: Command-line JSON processor (/opt/homebrew/bin/jq)
  • log_helper.sh: Custom logging utility script (must be in same directory)
  • Bash: Shell script environment

How It Works

  1. Initial State Capture: Records the current window, space, and display configuration
  2. Window Identification: Gets the ID of the currently focused window
  3. Space Detection: Determines the next available space to the right
  4. Movement Logic:
    • If a space exists to the right: moves window to that space
    • If no space exists (rightmost position): creates new space, then moves
  5. Focus Management: Switches to the target space and refocuses the moved window
  6. State Verification: Logs the final state for verification

Script Architecture

┌─────────────────┐
│  Get Window ID  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Query Spaces    │
└────────┬────────┘
         │
         ▼
    ┌────────┐
    │ Next   │
    │ Space? │
    └───┬────┘
        │
   ┌────┴────┐
   │         │
   ▼         ▼
┌──────┐  ┌──────────┐
│ Move │  │  Create  │
│  to  │  │  Space   │
│ Next │  │    +     │
└──────┘  │   Move   │
          └──────────┘

Usage Instructions

Basic Usage

# Execute directly
~/.config/scripts/move_window_right.sh

# Or make executable and run
chmod +x move_window_right.sh
./move_window_right.sh

Integration with Keyboard Shortcuts

This script is designed to be triggered via keyboard shortcuts using tools like:

  • Karabiner-Elements: Complex keyboard modifications
  • skhd: Simple hotkey daemon for macOS
  • System Preferences: Native macOS keyboard shortcuts

Example Karabiner configuration snippet:

{
  "description": "Move window right",
  "manipulators": [{
    "type": "basic",
    "from": {"key_code": "right_arrow", "modifiers": {"mandatory": ["shift", "command"]}},
    "to": [{"shell_command": "~/.config/scripts/move_window_right.sh"}]
  }]
}

Code Structure

Functions

  • log_state(): Captures and logs current window/space/display state
  • focus_window(): Refocuses a specific window by ID with verification
  • Main Logic: Sequential execution of window movement operations

Logging

The script uses structured logging with the following levels:

  • START/END: Script lifecycle events
  • INFO: Informational messages
  • ACTION: Yabai commands being executed
  • DEBUG: Detailed debugging information
  • ERROR: Error conditions

Logs are written via log_helper.sh to a centralized location (typically ~/.config/logs/).

Error Handling

  • Validates window ID retrieval
  • Handles edge cases for rightmost space
  • Uses explicit string comparison for jq fallback values
  • Includes sleep delays for Yabai operation completion

Common Issues and Solutions

Issue: Window doesn't focus after moving

Solution: The script includes a 0.1 second delay and explicit refocus command to handle Yabai's asynchronous operations.

Issue: Script fails with "Could not get focused window ID"

Solution: Ensure a window is focused before running the script. Some system windows may not be queryable.

Issue: New space creation fails

Solution: Check Yabai permissions and ensure SIP is configured correctly for space management.

Related Scripts

This script is part of a larger window management system. Related scripts include:

  • move_window_left.sh: Move window to previous space
  • space_right.sh: Switch focus to next space (without moving window)
  • remove_empty_spaces.sh: Clean up unused spaces
  • log_helper.sh: Centralized logging utility

Notes

  • The script uses absolute paths for binaries to ensure consistent execution
  • String comparison for jq's empty fallback uses literal '""' to avoid bash interpretation issues
  • Window IDs are preserved throughout the operation for reliable refocusing
  • State logging before and after operations aids in debugging space management issues

License

Part of personal dotfiles configuration - adapt as needed for your setup.

#!/usr/bin/env bash
# ---------- move_window_right.sh ----------
# Moves the current window to the next space, creating one if necessary.
# --- Script Setup ---
SCRIPT_NAME="move_window_right"
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
LOGGER_SCRIPT_PATH="$SCRIPT_DIR/log_helper.sh"
# Log the start of the script
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "START" "Script execution started"
# --- Script Logic ---
y=/opt/homebrew/bin/yabai
jq=/opt/homebrew/bin/jq
# --- Helper Functions ---
log_state() {
local state_type=$1
local current_window=$($y -m query --windows --window 2>/dev/null | $jq -r '.id' 2>/dev/null || echo "null")
local current_space=$($y -m query --spaces --space 2>/dev/null | $jq -r '.index' 2>/dev/null || echo "null")
local current_display=$($y -m query --displays --display 2>/dev/null | $jq -r '.index' 2>/dev/null || echo "null")
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "${state_type}_STATE" \
"window:$current_window space:$current_space display:$current_display"
}
focus_window() {
local window_id="$1"
if [[ -n "$window_id" ]]; then
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "ACTION" "Refocusing window $window_id"
$y -m window --focus "$window_id"
# Check what window is actually focused after the command
local actual_focused=$($y -m query --windows --window | $jq '.id')
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "DEBUG" "Actually focused window after refocus attempt: $actual_focused"
fi
}
# Log before state
log_state "BEFORE"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Getting focused window ID"
window_id=$($y -m query --windows --window | $jq '.id')
if [[ -z "$window_id" || "$window_id" == "null" ]]; then
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "ERROR" "Could not get focused window ID"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "END" "Script execution finished with error"
exit 1
fi
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Focused window ID: $window_id"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Querying current space index"
cur=$($y -m query --spaces --space | $jq '.index')
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Current space index: $cur"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Querying next space index"
next=$($y -m query --spaces --display | $jq --argjson cur "$cur" '
map(select(.index > $cur)) # only spaces to the right
| sort_by(.index) | .[0].index // "" # pick the closest, or ""')
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Next space index: '$next'"
# Use literal string comparison for the jq fallback ""
if [[ "$next" == '""' ]]; then # Check if next IS literally ""
# Edge -> create space, then move window
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Edge detected (next is \"\"), creating new space"
$y -m space --create
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "ACTION" "Created new space"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Querying last space index"
last=$($y -m query --spaces --display | $jq '.[-1].index')
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Last space index: $last"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "ACTION" "Moving window $window_id to space $last"
$y -m window "$window_id" --space "$last"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "ACTION" "Focusing space $last"
$y -m space --focus "$last"
# Small delay to ensure yabai has completed the space switch
sleep 0.1
focus_window "$window_id"
else # Next is NOT ""
# Neighbour exists -> move window there
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "INFO" "Neighbour found ('$next'), moving window $window_id to space $next"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "ACTION" "Moving window $window_id to space $next"
$y -m window "$window_id" --space "$next"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "ACTION" "Focusing space $next"
$y -m space --focus "$next"
# Small delay to ensure yabai has completed the space switch
sleep 0.1
focus_window "$window_id"
fi
# Log after state
log_state "AFTER"
"$LOGGER_SCRIPT_PATH" "$SCRIPT_NAME" "END" "Script execution finished"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment