Ubuntu – Rotate focus through monitors

multiple-monitorsshortcut-keysunity

I have 3 monitors on my development machine, which runs Ubuntu + Unity. I usually dedicate one monitor to web browsing and IM, and the other two to terminals where I run vim or enter commands. I'm looking for a single-key-sequence to switch between them at-will.

I would like to know how I can switch focus to (top-) windows on each monitor. The behavior I'm looking or is sort-of like ALT+TAB, except instead of rotating between applications (which only makes the most-recently-used instance of application available in the rotation), I can rotate focus between monitors.

As a compromise, I could stand to use the ALT+TAB mechanism if I could have each and every window in the list. I still imagine this getting annoying, though.

Best Answer

Script(s) to rotate focus through monitors

In the setup below, two scripts are involved: one background script to keep track of the history of focussed windows (see the explanation at the bottom to read why that is needed), and one "action" script to place under a shortcut key, to set focus on the next screen. If the next screen currently has no window to set focus on, a message is displayed:

enter image description here

The scripts

Script 1; the background script, save it (exactly) as focus_track.py

#!/usr/bin/env python3
import subprocess
import time
import os

rootdata = os.environ["HOME"]+"/.focus_history"

def get_screendata():
    return sorted([int(s.split("+")[-2]) for s in subprocess.check_output(["xrandr"]).decode("utf-8").split() if s.count("+") == 2])

def current_windows():
    try:
        return subprocess.check_output(["wmctrl", "-lG"]).decode("utf-8")
    except subprocess.CalledProcessError:
        pass

def convert_format(w_id):
    return w_id[:2]+(10-len(w_id))*"0"+w_id[2:]

def read_data():
    return open(rootdata).read().splitlines()

def get_top(wlist):
    top = convert_format([l.split("#")[-1].strip() for l in \
           subprocess.check_output(["xprop", "-root"]).decode("utf-8").splitlines() \
           if "_NET_ACTIVE_WINDOW(WINDOW)" in l][0])       
    return [l for l in wlist if top in l][0]

if __name__ == "__main__":
    open(rootdata, "wt").write("This is an empty line")
    while True:
        time.sleep(1)
        wdata = current_windows()
        if wdata != None:
            wlist = wdata.splitlines()
            # get frontmost window (as in wmctrl -lG)
            top = get_top(wlist)
            oldlist = read_data()
            if not top == oldlist[0]:
                # clean up closed windows
                [oldlist.remove(l) for l in oldlist if not l.split()[0] in wdata]
                # remove possible other mentions of the active window
                [oldlist.remove(l) for l in oldlist if l.startswith(top.split()[0])]
                open(rootdata, "wt").write(("\n").join([top]+oldlist))

Script 2; the action script, save it as next_focus.py In one and the same directory as script 1.

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

# read existing windows and their order (x-wise) from file
windows = [w.split() for w in focus_track.read_data()]
w_data = [[w[0], int(w[2])] for w in windows]
# get position of currently focussed window
currfocus = focus_track.get_top(focus_track.current_windows().splitlines()).split()[2]
# get screendata
screens = focus_track.get_screendata()

def screen_pos(x):
    return [(int(x) > n) for n in screens].count(True)

scr_position = screen_pos(currfocus)
next_screen = 1 if scr_position == len(screens) else scr_position+1
try:
    next_focus = [w for w in w_data if screen_pos(w[1]) == next_screen][0]
    subprocess.Popen(["wmctrl", "-ia", next_focus[0]])
except IndexError:
    subprocess.Popen(["notify-send", "No window to focus on next screen"])

How to use

  1. The scripts needs wmctrl to be installed

    sudo apt-get install wmctrl
    
  2. Copy script 1 into an empty file, save it as (exactly) focus_track.py. The name is important, since both scripts share functions; script 1 is imported into script 2.
  3. Copy script 2 into an empty file, save it as (exactly) next_focus.py in one and the same directory as script 1.
  4. Test- run the setup: N.B. Start the background script before opening (and thus focussing) windows. Windows, opened before the background script starts are not "recorded" untill they are focussed

    • Start the background script (from e.g. a terminal) with the command:

      python3 /path/to/focus_track.py
      
    • On your different screens, open windows.

    • Run script 2 with the command:

      python3 /path/to/next_focus.py
      

    The focus should switch to the next screen. If the current screen is the last in the row, the focus switches to the first screen.

  5. If all works fine, add script 1 to Startup Applications: Dash > Startup Applications > Add the command:

    python3 /path/to/focus_track.py
    

    and add script 2 to a keyboard shortcut: choose: System Settings > "Keyboard" > "Shortcuts" > "Custom Shortcuts". Click the "+" and add the command:

    python3 /path/to/next_focus.py
    

    to a shortcut key of your liking.

Notes

  • Since the background script keeps track of focus history, for immediate proper functioning, it should preferably start before you start working on your computer, e.g. on log in (using Startup Applications).
  • The script assumes the screens are set up from left to right, non- overlapping. Their vertical alignment is irrelevant however.
  • To "save fuel", and make the script's load negligibly small, the script updates the focus history (only) once per second. A window is therefore defined as focussed if it has focus for at least between 0.5 and 1 second.
  • Although I tested it on a two- screen setup, it should (and I am pretty sure it does) work properly on a setup with 3(+) screens.




Explanation:

What it takes

To switch focus from one screen to another, it is necessary to determine which is the front most window per screen. The major issue is however that windows, spread over multiple screens are actually all in one and the same stack, and thus ordered in one and the same succession (z-wise). The tools we have (wmctrl, xdotool, xprop etc.) in the best case can only determine the currently active window. They give us no information whatsoever about the window order on the other screens, since the windows are below the active window.

enter image description here

Therefore, at first sight, it seems pretty impossible to switch focus from one screen to another.
However:

How to get the information

With a workaround however, there is an escape: if we make a background script keep track of the currently focussed window, and maintain a history record of changes (for as long as the window exists), we actually can conclude what is the z-order of the currently opened windows. If we also keep track of their geometry and position, we have all information we need.

An example:
We have currently five windows: A, B, C, D, E. If their focus changes through D, E, A, C, B, we know the z-order of the windows is: B, C, A, E, D (front to back)

Together with their positions (x-wise) and the screen data (the x-resolution of the screens) we have all information we need. To switch focus to the next screen, we then simply have to look up the first window, located on the next screen.

Related Question