Moving workspaces between outputs in i3

May 29, 2023

A feature sorely missing from i3 is the ability to switch workspaces between displays. In i3, workspaces are tied to a single display, while in window managers like xmonad, workspaces can freely move between displays. Having recently migrated to i3 from xmonad, this was a feature I sorely needed. Fortunately, this behavior can be implemented with a small hack.

Before beginning, make sure the following packages are installed:

xrandr
xdotool

Put the following script somewhere and make it executable. This only works on horizontally stacked displays, and there's a small delay when moving between workspaces:

#!/bin/bash

workspace="$1"
displayinfo="$(xrandr --listmonitors | cut -d' ' -f4,6 | grep -v '^$')"
displays="$(echo "$displayinfo" | awk '{print $2}')"
maximums="$(echo "$displayinfo" | awk -F '/' '{sum += $1; print sum}')"
X="$(xdotool getmouselocation --shell | awk -F '=' '/X=/{ print $2 }')"
i3_output=$(i3-msg -t get_workspaces)

readarray -t d_arr <<< "$displays"
readarray -t m_arr <<< "$maximums"

for index in "${!d_arr[@]}"; do
    concatenated="${d_arr[index]} ${m_arr[index]}"
    maximum=${m_arr[index]}

    if [ "$X" -le "$maximum" ]; then
        workspaces=$(echo "$i3_output" | jq -r --arg output "${d_arr[index]}" '.[] | select(.output == $output) | .name')
        readarray -t workspace_array <<< "$workspaces"

        for workspace in "${workspace_array[@]}"; do
            if [ "$workspace" -eq "$w"]; then
                i3-msg workspace number $1
                exit
            fi
        done

        i3-msg "[workspace=\"$1\"]" move workspace to output ${d_arr[index]}
        i3-msg workspace number $1
        exit
    fi
done

Then add the following lines to ~/.config/i3/config. This assumes there are workspaces 1-10 and may vary depending on your individual configuration:

set $ws1 "1"
set $ws2 "2"
set $ws3 "3"
set $ws4 "4"
set $ws5 "5"
set $ws6 "6"
set $ws7 "7"
set $ws8 "8"
set $ws9 "9"
set $ws10 "10"

bindsym $mod+1 exec <path to script> $ws1
bindsym $mod+2 exec <path to script> $ws2
bindsym $mod+3 exec <path to script> $ws3
bindsym $mod+4 exec <path to script> $ws4
bindsym $mod+5 exec <path to script> $ws5
bindsym $mod+6 exec <path to script> $ws6
bindsym $mod+7 exec <path to script> $ws7
bindsym $mod+8 exec <path to script> $ws8
bindsym $mod+9 exec <path to script> $ws9
bindsym $mod+0 exec <path to script> $ws10