Skip to content

Instantly share code, notes, and snippets.

@fbrinker
Last active March 6, 2024 11:09
Show Gist options
  • Save fbrinker/df9cfbc84511d807f45041737ff3ea02 to your computer and use it in GitHub Desktop.
Save fbrinker/df9cfbc84511d807f45041737ff3ea02 to your computer and use it in GitHub Desktop.
Swap i3 displays / workspaces between displays
#!/usr/bin/env bash
# requires jq
DISPLAY_CONFIG=($(i3-msg -t get_outputs | jq -r '.[]|"\(.name):\(.current_workspace)"'))
for ROW in "${DISPLAY_CONFIG[@]}"
do
IFS=':'
read -ra CONFIG <<< "${ROW}"
if [ "${CONFIG[0]}" != "null" ] && [ "${CONFIG[1]}" != "null" ]; then
echo "moving ${CONFIG[1]} right..."
i3-msg -- workspace --no-auto-back-and-forth "${CONFIG[1]}"
i3-msg -- move workspace to output right
fi
done
@fbrinker
Copy link
Author

fbrinker commented Apr 6, 2018

Dependencies

  • jq (if not already available)

Usage

To use this script, I recommend binding it to a keyboard shortcut in your i3 config:
bindsym $mod+Shift+s exec /home/fbrinker/i3-display-swap.sh

More than 2 outputs

The script rotates the workspaces technically to the right, so you may need another solution, depending on your configuration

@giuseppe-dandrea
Copy link

giuseppe-dandrea commented Oct 14, 2018

This script is not working if there is a space in the workspace name or back-and-forth is set to true in the config.
The second problem is simply solved changing line 12 in:
i3-msg workspace --no-auto-back-and-forth "${CONFIG[1]}"

@fbrinker
Copy link
Author

Ty, added --no-auto-back-and-forth :)

@gabrielhidasy
Copy link

The website is not up-to-date with this code, https://i3wm.org/docs/user-contributed/swapping-workspaces.html

@gbei93
Copy link

gbei93 commented Feb 21, 2020

my script is a variation from the one in https://i3wm.org/docs/user-contributed/swapping-workspaces.html, but it also moves the focus back to the workspace that was in focus before swapping displays.

#!/usr/bin/env bash
# requires jq

IFS=:
INITIAL_WORKSPACE=$(i3-msg -t get_workspaces \
  | jq '.[] | select(.focused==true).name' \
  | cut -d"\"" -f2)

i3-msg -t get_outputs | jq -r '.[]|"\(.name):\(.current_workspace)"' | grep -v '^null:null$' | \
while read -r name current_workspace; do
    echo "moving ${current_workspace} right..."
    i3-msg workspace "${current_workspace}"
    i3-msg move workspace to output right   
done
i3-msg workspace $INITIAL_WORKSPACE

@TitouanT
Copy link

TitouanT commented Feb 25, 2020

Like @gbei93's version, my version makes the focus deterministic and let you choose between 2 strategies:

  • keep the focus on the same screen.
  • keep the focus on the same workspace.

And it supports rotation over multiple screen, also the order of the screens is the one given by i3-msg -t get-ouputs and I don't know if it is always the same. But for two screens it handle well the case where the screen are not arranged horizontally.

I had an issue with empty workspaces so I fixed that too by keeping track of the screen from which I remove a workspace to leave it at each step to the desired workspace.

link to my fork: https://gist.github.com/TitouanT/4dfb8c968f06965aaa875fcf6e837b3e

@thiswillbeyourgithub
Copy link

I spent the last months not understanding why the script didn't work until I realized it was due to the switch back option...

Can someone put the correct script there https://i3wm.org/docs/user-contributed/swapping-workspaces.html ?

I'm so sad to see my months of frustration could be solved by seeing this thread....

@fbrinker
Copy link
Author

fbrinker commented Jul 30, 2020

Sorry to hear that. I will create a PR to update the script immediately :)
// Edit: Done

@jrwrigh
Copy link

jrwrigh commented Oct 4, 2020

Every once in a while, the swapping fails to work correctly. Specifically, it will move the focused workspace to the new screen, but it won't move the workspace on the opposite screen. Here's the script I have (along with some debugging outputs):

#!/usr/bin/env/bash

INITIAL_WORKSPACE=$(i3-msg -t get_workspaces \
  | jq '.[] | select(.focused==true).name' \
  | cut -d"\"" -f2)

DISPLAY_CONFIG=($(i3-msg -t get_outputs | jq -r '.[]|"\(.name):\(.current_workspace)"'))

echo 'INITIAL_WORKSPACE: ' $INITIAL_WORKSPACE
echo 'DISPLAY_CONFIG: ' $DISPLAY_CONFIG

for ROW in "${DISPLAY_CONFIG[@]}"
do
	IFS=':'
	read -ra CONFIG <<< "${ROW}"
	if [ "${CONFIG[0]}" != "null" ] && [ "${CONFIG[1]}" != "null" ]; then
		echo "moving ${CONFIG[1]} right..."
		i3-msg -- workspace --no-auto-back-and-forth "${CONFIG[1]}"
		i3-msg -- move workspace to output right
	fi
done

# Focus on original workspace
i3-msg -- workspace --no-auto-back-and-forth $INITIAL_WORKSPACE

Just by running the script a bunch of times, I can get it to fail maybe 5% of the time. However, the output of the messages is the exact same:

INITIAL_WORKSPACE:  1
DISPLAY_CONFIG:  DP-1-4:3
moving 3 right...
[{"success":true}]
[{"success":true}]
moving 1 right...
[{"success":true}]
[{"success":true}]
[{"success":true}]

A) Has this happened to anyone else?
B) Any idea on a fix/cause?

Edit:

May have found a solution. If you put a sleep command between the i3-msg -- workspace --no-auto-back-and-forth "${CONFIG[1]}" and i3-msg -- move workspace to output right lines, it seems to fix the problem. If you're using a recent version of GNU sleep, you can do fractions of seconds, so I have:

...
		i3-msg -- workspace --no-auto-back-and-forth "${CONFIG[1]}"
                sleep 0.05
		i3-msg -- move workspace to output right
...

I've yet to reproduce the problem after several dozen tries, so this might be fixed. I'll update this message if it's not.

Edit 2:

Just ran into the same problem behavior with the sleep command in there, so I still don't have a solution to the problem, nor any idea why it's randomly happening.

@TitouanT
Copy link

TitouanT commented Oct 4, 2020

@jrwrigh, does this happen when one of the workspaces is empty ?
If yes then I had the same problem and it is fixed with the $workspace_to_restore in my version that I described above. (I removed the script from my previous comment to just show a link to my fork).

@jrwrigh
Copy link

jrwrigh commented Oct 4, 2020

@TitouanT Nope, this is with non-empty workspaces. Even with only two workspaces (where there will be a left over empty workspace between when the two workspaces are moved), I don't reproduce the issue (at least with the sleep solution).

@thiswillbeyourgithub
Copy link

Does this happen while swapping specific applications? I recall having troubles a while ago while switching terminal windows.

@jrwrigh
Copy link

jrwrigh commented Oct 5, 2020

Not that I can tell. Terminal windows are normally involved, but that's because I usually have a lot of terminals open.

@fatihkaan22
Copy link

The problem for me was that my workspace names include : character. Also the script uses : as IFS. I solved it by setting the IFS as ;, and change : to ; on DISPLAY_CONFIG.

@thiswillbeyourgithub
Copy link

Personally I decided to stick to this :

bindsym $mod+shift+S mode "$mode_move_ws"
mode "$mode_move_ws" {
            bindsym l move workspace to output left, focus output left, mode "default"
            bindsym r move workspace to output right, focus output right, mode "default"

            bindsym q move workspace to output left, focus output right, mode "default"
            bindsym d move workspace to output right, focus output right, mode "default"

            bindsym Return mode "default"
            bindsym Escape mode "default"
            bindsym $mod+Shift+S mode "default"
}

@rauldux
Copy link

rauldux commented Jun 30, 2021

The problem for me was that my workspace names include : character. Also the script uses : as IFS. I solved it by setting the IFS as ;, and change : to ; on DISPLAY_CONFIG.

This was helpful, thanks. Maybe : should not be the default.

@kenakofer
Copy link

My workspace names are of the form 2:<span font_desc='JetBrains Mono Medium 13'> 2 </span>. So both ":" and " " in there. This is probably because I'm using Regolith or something.

The spaces wreak havoc because they look like a new ROW to the script. My workaround is to add at the top of the script

IFS=$'\n'

So that the ROWs delineated by new lines. Kind of funny to funny to set IFS twice in this script, but it works.

@kenakofer
Copy link

My workspace names are of the form 2:<span font_desc='JetBrains Mono Medium 13'> 2 </span>. So both ":" and " " in there. This is probably because I'm using Regolith or something.

The spaces wreak havoc because they look like a new ROW to the script. My workaround is to add at the top of the script

IFS=$'\n'

So that the ROWs delineated by new lines. Kind of funny to funny to set IFS twice in this script, but it works.

@sbng
Copy link

sbng commented Sep 23, 2021

I notice the script does not take into account 2 things.

  1. workspace with name "1: SPACE1", "2:SPACE2" etc. Added ${CONFIG[2]} to resolve this.
  2. the initial position of the current workspace. Either left or right. seems like it always assume it's left. Check for workspace position to determine left or right workspace. If x position == 0, the current active workspace is left, else it's right.

#/bin/bash
IFS=$'\n'
POS=$(i3-msg -t get_workspaces | jq '.[] | select(.focused==true).rect.x')
[[ "$POS" == "0" ]] && LOC="left" || LOC="right"

INITIAL_WORKSPACE=$(i3-msg -t get_workspaces
| jq '.[] | select(.focused==true).name'
| cut -d""" -f2)

DISPLAY_CONFIG=($(i3-msg -t get_outputs | jq -r '.[]|"(.name):(.current_workspace)"'))

echo 'INITIAL_WORKSPACE: ' $INITIAL_WORKSPACE
echo 'DISPLAY_CONFIG: ' $DISPLAY_CONFIG

for ROW in "${DISPLAY_CONFIG[@]}"
do
IFS=':'
read -ra CONFIG <<< "${ROW}"
if [ "${CONFIG[0]}" != "null" ] && [ "${CONFIG[1]}" != "null" ]; then
echo "moving ${CONFIG[1]}:${CONFIG[2]} right..."
i3-msg -- workspace --no-auto-back-and-forth "${CONFIG[1]}:${CONFIG[2]}"
i3-msg -- move workspace to output $LOC
fi
done

@FredericoFavaro
Copy link

FredericoFavaro commented Feb 19, 2022

Hi! I really try to use this script and with the alternatives here but i get always the same problem, just one screen actually swaps. I learn o lot throw your codes and because that i achieve a solution that works here.

#!/usr/bin/env zsh

# Dependencies: jq

# The default settings always focus on primary display. To change that to
# maintain focus on the screen/window focused after run this script just
# uncomment the two comment lines below (10 and 18).

DISPLAY_CONFIG=($(i3-msg -t get_outputs | jq -r '.[]|select(.active == true) |"\(.current_workspace)"'))
#ACTIVE_WS=($(i3-msg -t get_workspaces | jq -r '.[]|select(.focused == true) | "\(.name)"'))
for ROW in "${DISPLAY_CONFIG[@]}"
do
read -r CONFIG <<< "${ROW}"
    i3-msg -- workspace --no-auto-back-and-forth "${CONFIG}"
    i3-msg -- move workspace to output right
done
i3-msg -- output primary
#i3-msg -- workspace "${ACTIVE_WS}"

I try my best to make it simple. As the comments in code says, you can change the behavior of where the focus will be at the end. Take in consideration that will only work on dual monitor setup (or monitor + projector and so on).

@mark2185
Copy link

i3 v4.20.1 throws an error for this:

i3-msg -- output primary
ERROR: Your command: output primary
ERROR:               ^^^^^^^^^^^^^^
ERROR: Expected one of these tokens: <end>, '[', 'move', 'exec', 'exit', 'restart', 'reload', 'shmlog', 'debuglog', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'sticky', 'split', 'floating', 'mark', 'unmark', 'resize', 'rename', 'nop', 'scratchpad', 'swap', 'title_format', 'title_window_icon', 'mode', 'bar', 'gaps'
[{"success":false,"parse_error":true,"error":"Expected one of these tokens: <end>, '[', 'move', 'exec', 'exit', 'restart', 'reload', 'shmlog', 'debuglog', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'sticky', 'split', 'floating', 'mark', 'unmark', 'resize', 'rename', 'nop', 'scratchpad', 'swap', 'title_format', 'title_window_icon', 'mode', 'bar', 'gaps'","input":"output primary","errorposition":"^^^^^^^^^^^^^^"}]

And I've found it's not needed at all, the i3-msg -- workspace "${ACTIVE_WS}" will switch to the output anyway.

But it also needs --no-auto-back-and-forth in order to work as expected.

@olorton
Copy link

olorton commented Mar 6, 2024

I have taken a stab at re-writing this based on all the examples above. The behaviour I expect is:

  • Works with two displays, not more
  • Swap the currently active workspaces on each display
  • Maintain focus on the display that had focus before the swap
#!/usr/bin/env bash

if ! command -v jq &>/dev/null; then
    echo "jq could not be found"
    exit 1
fi

# Swaps workspaces between two displays, focus stays on the current active display

DISPLAY_CONFIG="$(i3-msg -t get_outputs | jq -r '.[]|select(.active == true) |"\(.current_workspace)"')"
ACTIVE_DISPLAY="$(i3-msg -t get_workspaces | jq -r '.[]|select(.focused == true) | "\(.output)"')"
IFS=$'\n'
for ROW in ${DISPLAY_CONFIG}; do
    i3-msg -- workspace --no-auto-back-and-forth "$ROW"
    i3-msg -- move workspace to output next
done
sleep 0.15
i3-msg -- focus output "${ACTIVE_DISPLAY}"

Notable changes I've made:

  • The bash globbing was causing all sorts of problems with workspace names that had spaces in them. So I've removed that in favour of string splitting on new lines.
  • Fixed the move workspace command to use the next token which works with horizontally and vertically arranged monitors.
  • I do not like having a sleep in there, but the final msg is useless without it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment