Ubuntu – Move semi- maximized windows across multiple (2 or more) monitors

displaymultiple-monitorsshortcut-keysunitywindow-manager

I frequently use Ctrl+Super+Arrow -Keys to move windows around. However, with a multiple monitor set up, the shortcuts only move the windows on the current screen.

**Example of my situation:

I have 3 monitors and I have an instance of Chrome open in my center monitor.

  1. When I hit Ctrl+Super+Arrow (left)the window splits to the left half of my center monitor.
  2. When I repeat, the window does not move.

What I'd like to see instead is that in [2.], the window moves to the right half of the left most monitor, and so on

How do I achieve this behaviour?

Best Answer

Move your semi- maximized windows across multiple screens ("aero- snap" alike)

The script below does exactly as you describe. It can be used on any number of screens. The script is optimized for Unity, but can easily be edited to fit other window managers.

It turned out to be a bit more complicated than I thought, since the script should take into account that the user possibly set the launcher to only appear on the leftmost screen (or not). The targeted positions (and targeted window sizes) should be calculated accordingly.


enter image description here

Notes

  • The script moves the windows through all connected screens, subsequently switching through the "halves" of the screens, from left to right and vice versa.
  • The script reads the sizes of the screens and sets the window size, to half the screen size accordingly.
  • The script reads the settings on the launcher (if the launcher appears on only the first screen or on all), and calculates the targeted window sizes accordingly.
  • The script does not take into account the possibly set autohide option for the launcher. The reason is that updating the windowsize once the launcher hides or shows up would take a background script, and quite an other story to code.
  • The windows are moved and resized by a combination of xdotool and wmctrl - commands. Since these commands can have some "characteristic behaviour" in combination with Unity, there is a possiblility minor changes can be useful on your system. I tested it on two (quite different) systems however, working fine on both.

The script

#!/usr/bin/env python3
import subprocess
import sys

# --- set possibly wanted marge (on the right side of the window) below
marge = 0
# --- set set launcher width below (default = 65)
default_launcherwidth = 65
# ---

move = sys.argv[1]
get = lambda cmd: subprocess.check_output(
    ["/bin/bash", "-c", cmd]).decode("utf-8")

screendata = [l for l in get("xrandr").splitlines() if " connected" in l]
# get the primary screen's position
pr = [l.split() for l in screendata if "primary" in l][0]
i = pr.index("primary"); s = pr[i+1]
primary= [int(s.split("x")[0]), int(s.split("+")[-2])]
# general screen list
screendata = [
    [s for s in l.split() if s.count("+") == 2][0] for l in screendata
    ]
screendata = [
    [int(s.split("x")[0]),int(s.split("+")[-2])] for s in screendata
    ]
screendata.sort(key=lambda x: x[1]); primary = screendata.index(primary)

def launcher_size():
    launcherset = get(
        "dconf read /org/compiz/profiles/unity/plugins/unityshell/num-launchers"
        ).strip()
    l_size_0 = 65; l_size_1 = l_size_0 if launcherset == "0" else 0
    return [l_size_0, l_size_1]

def magnets(index):
    l_size = l_sizes[0] if index == primary else l_sizes[1]
    screen = screendata[index]
    screenwidth = screen[0]; shift = screen[1]
    parts = (screenwidth - l_size)/2; virtual_mid = screenwidth - parts + shift
    left_trigger = shift + l_size
    return [left_trigger, int(virtual_mid), int(parts)]

def find_screen(x_loc):
    scr_index = len([scr for scr in screendata if x_loc >= scr[1]])-1
    scr_index = scr_index if scr_index >= 0 else 0
    screen = screendata[scr_index]
    return [scr_index, screen]

def get_active():
    active = hex(int(get("xdotool getactivewindow").strip()))
    active = active[:2]+(10-len(active))*"0"+active[2:] 
    x_pos = int([l.split()[2] for l in get("wmctrl -lG").splitlines() \
                 if l.startswith(active)][0])
    return [active, x_pos]

def decide(x_loc, active):
    current_scr = find_screen(x_loc)
    triggers = magnets(current_scr[0])
    width = triggers[2]
    index = current_scr[0]
    if move == "left":
        if x_loc <= triggers[1]:
            if x_loc == triggers[0]:
                if index != 0:
                    alter = magnets(index-1) 
                    width = alter[-1]
                    target = alter[1]
                else:
                    target = triggers[0]
            else:
                target = triggers[0]
        elif x_loc > triggers[1]:
            target = triggers[1]        
    elif move == "right":
        if x_loc >= triggers[1]:
            if index != len(screendata)-1:
                alter = magnets(index+1)
                width = alter[-1]
                target = alter[0]
            else:
                target = triggers[1]
        elif x_loc < triggers[1]:
            target = triggers[1]
    subprocess.call([
        "wmctrl", "-r", ":ACTIVE:","-b", "remove,maximized_vert,maximized_horz"
        ])
    subprocess.call(["wmctrl", "-ir", active, "-e", "0,"+str(target)+",400,"+\
                      str(int(width)-marge)+",200"])
    subprocess.call([
        "wmctrl", "-ir", active, "-b", "toggle,maximized_vert"
        ])

l_sizes = launcher_size()
w_data = get_active()
active = w_data[0]
x_loc = w_data[1]

decide(x_loc, active)

How to use

  • The scrpt needs both wmctrl and xdotool:

    sudo apt-get instal xdotool wmctrl
    
  • Copy the script into an empty file, save it as move_window.py

  • Test- run the script from a terminal window by (repeatedly) the command:

    python3 /path/to/move_window.py right
    

    which should move the window through your screens to the rignt, and:

    python3 /path/to/move_window.py left
    

    which should move the window through your screens to the left.

  • If all works fine, add both commands to shortcut keys: choose: System Settings > "Keyboard" > "Shortcuts" > "Custom Shortcuts". Click the "+" and add the commands to two shortcut keys of your choice.

Important notes

  1. The launcher width is set to 65px (corresponding to icon size 48px + 17px borders, which is the default). If the set width is different, it should be set correctly in the head of the script, as indicated in the section:

    # --- set set launcher width below (default = 65)
    default_launcherwidth = 65
    # ---
    
  2. In the headsection of the script, there is furthermore a line:

    marge = 0
    

    which sets the empty marge at the right side of the moved screen. Change it if you like to another value.

Related Question