3

I use 15.04 with Unity and 4 virtual desktops.

When I have opened a window on desktop 1 and am viewing desktop 2 (for example), is there a way to easily get that window from desktop 1 to show up on desktop 2 without switching the view to desktop 1?

So I want to get a window from a currently invisible desktop to my active desktop without seeing the invisible desktop (and eventually other opened windows on that one).

Is there an easy way to achieve this?

2
  • Can very wel be done. If noone answered before, I will when I get home, but it might take a while :) Jul 19, 2015 at 9:28
  • Hi Byte Commander, posted my answer. Please let me know if anything is unclear. Jul 20, 2015 at 21:15

1 Answer 1

4

List windows, choose one to move to the current workspace

When the script below is called, it will list all windows on all workspaces. Pick one and press OK to move the window to the current workspace and raise it. By default, it moves the window to position 100 (x), 100 (y)

The script is relatively simple as a result of the use of both wmctrl and xdotool. While wmctrl is used to list all windows, xdotool simply moves them to a predefined position on the current workspace "without asking questions" on the window's size (unlike wmctrl) and relative positions of both workspaces to each other.
A more precise positioning of the window, according to the position on its original workspace, would very well be possible, but would also multiply the needed code (like e.g. here). I assume in most situations, this will do.

An example:

I am on workspace 8, while I have a gedit window on workspace 1. Calling the script lists the windows:

enter image description here

picking the gedit window will move it to the current workspace:

enter image description here

The script

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

def get(command):
    return subprocess.check_output(["/bin/bash", "-c", command]).decode("utf-8")

def check_window(w_id):
    w_type = get("xprop -id "+w_id)
    if " _NET_WM_WINDOW_TYPE_NORMAL" in w_type:
        return True
    else:
        return False

# split wmctrl output by machine name
wlist = [l.split(socket.gethostname()) for l in get("wmctrl -l").splitlines()]
# extract window -id from first section
wlist = [[wlist[i][0].split()[0], wlist[i][-1].strip()] for i, l in enumerate(wlist)]
# filter only "real, normal" windows
wlist = [w for w in wlist if check_window(w[0]) == True]
# create columns for zenity list
cols = (" ").join(['"'+w[1]+'" '+'"'+w[0]+'"' for w in wlist])
# calculate height and width for the zenity window, according to window names and list length
h = str(140+(len(wlist)*23))
w = str((max([len(w[-1]) for w in wlist])*8))
# define the zenity window
cmd = "zenity --list --hide-column=2 --print-column=2 "+\
      "--title='Window list' --column='Current windowlist' "+\
      "--column='wid' --height="+h+" --width="+w+" "+cols

try:
    # call the window
    w_id = get(cmd).split("|")[-1].strip()
    # move the selected window to the current workspace
    subprocess.Popen(["xdotool", "windowmove", "--sync", w_id, "100", "100"])
    # raise it (the command below alone should do the job, but sometimes fails
    # on firefox windows without first moving the window).
    subprocess.Popen(["wmctrl", "-iR", w_id])
except subprocess.CalledProcessError:
    pass

How to use

  1. The script needs both wmctrl and xdotool

    sudo apt-get install wmctrl xdotool
    
  2. copy the script into an empty file, svae it as move_windows.py

  3. Test-run it by the command:

    python3 /path/to/move_windows.py
    
  4. a window should appear, listing currently opened windows:

    enter image description here

    pick one to see if it is moved to the current workspace and raised correctly.

  5. If all works fine, ad it to a shortcut key: choose: System Settings > "Keyboard" > "Shortcuts" > "Custom Shortcuts". Click the "+" and add the command:

    python3 /path/to/move_windows.py
    

Note

The size of the zenity window, listing current windows, is set automatically. The script looks for the longest window name and the number of rows (windows) and sets the size accordingly.


EDIT

As requested in a comment, below a version in which the zenity list- window includes more information: de current workspace of the targeted window(s) and the application it belongs to.

enter image description here

As mentioned above, the information on the relative/absolute workspace positions leads to a more "substantial" amount of code, but luckily I could use this earlier answer as a base.

How to use

The use is pretty much the same as the first version of the script (above), but the command needs to include the preferred sorting option. Run it by one of the commands:

python3 /path/to/move_windows.py -app

to sort the list by application,

python3 /path/to/move_windows.py -ws

to sort the list by workspace, and

python3 /path/to/move_windows.py -win

to sort the list by window name.

The script:

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

arg = sys.argv[1]
# list (column) header titles and their (data) position in the produced window data list
cols = [["Workspace", -1], ["Application name", -2] , ["Window name", -3]]
# rearrange columns, depending on the chosen option
if arg == "-app":
    cols = [cols[1], cols[2], cols[0]]
elif arg == "-ws":
    cols = [cols[0], cols[2], cols[1]]
elif arg == "-win":
    cols = [cols[2], cols[1], cols[0]]
# extract headers, list positions, to be used in the zenity list
col1 = cols[0][0]; i1 = cols[0][1]
col2 = cols[1][0]; i2 = cols[1][1]
col3 = cols[2][0]; i3 = cols[2][1]
# just a helper function
get = lambda cmd: subprocess.check_output([
    "/bin/bash", "-c", cmd
    ]).decode("utf-8")
# analyse viewport data, to be able to calculate relative/absolute position of windows
# and current viewport
def get_spandata():
    xr = get("xrandr").split(); pos = xr.index("current")
    res = [int(xr[pos+1]), int(xr[pos+3].replace(",", "") )]
    spandata = get("wmctrl -d").split()
    span = [int(n) for n in spandata[3].split("x")]
    cols = int(span[0]/res[0]); rows = int(span[1]/res[1])
    curr_vector = [int(n) for n in spandata[5].split(",")]
    curr_viewport = int((curr_vector[1]/res[1])*cols + (curr_vector[0]/res[0])+1)
    return {"resolution": res, "n_columns": cols, "vector": curr_vector, "current_viewport": curr_viewport}

posdata = get_spandata()
vector = posdata["vector"]; cols = posdata["n_columns"]
res = posdata["resolution"]; currvp = posdata["current_viewport"]
# function to distinguish "normal" windows from other types (like the desktop etc)
def check_window(w_id):
    w_type = get("xprop -id "+w_id)
    if " _NET_WM_WINDOW_TYPE_NORMAL" in w_type:
        return True
    else:
        return False
# split windowdata by machine name
mach_name = socket.gethostname()
wlist = [[l.strip() for l in w.split(mach_name)] for w in get("wmctrl -lpG").splitlines()]
# split first section of window data
for i, w in enumerate(wlist):
    wlist[i][0] = wlist[i][0].split()
# filter only "real" windows
real_wlist = [w for w in wlist if check_window(w[0][0]) == True]
# adding the viewport to the window's data
for w in real_wlist:
    w.append(get("ps -p "+w[0][2]+" -o comm=").strip())
    loc_rel = [int(n) for n in w[0][3:5]]
    loc_abs = [loc_rel[0]+vector[0], loc_rel[1]+vector[1]]
    abs_viewport = int((loc_abs[1]/res[1])*cols + (loc_abs[0]/res[0])+1)
    abs_viewport = str(abs_viewport)+"*" if abs_viewport == currvp else str(abs_viewport)
    w.append(abs_viewport)
# set sorting rules
if arg == "-app":
    real_wlist.sort(key=lambda x: x[-2])
elif arg == "-ws":
    real_wlist.sort(key=lambda x: x[-1])
elif arg == "-win":
    real_wlist.sort(key=lambda x: x[-3])
# calculate width and height of the zenity window:
# height = 140px + 23px per line
h = str(140+(len(real_wlist)*23))
# width = 250px + 8px per character (of the longest window title)
w = str(250+(max([len(w[-3]) for w in real_wlist])*8))
# define the zenity window's content
cmd = "zenity --list --hide-column=4 --print-column=4 --title='Window list' "\
      "--width="+w+" --height="+h+" --column='"+col1+"' --column='"+col2+"' --column='"+col3+\
      "' --column='w_id' "+(" ").join([(" ").join([
          '"'+w[i1]+'"','"'+w[i2]+'"','"'+w[i3]+'"','"'+w[0][0]+'"'
          ]) for w in real_wlist])
# finally, call the window list
try:
    w_id = subprocess.check_output(["/bin/bash", "-c", cmd]).decode("utf-8").split("|")[0]
    subprocess.Popen(["xdotool", "windowmove", "--sync", w_id, "100", "100"])
    subprocess.Popen(["wmctrl", "-iR", w_id])
except subprocess.CalledProcessError:
    pass

EDIT 2: 15.04 specific

The output of the used ps command seems to have changed for gnome-terminal in 15.04. Therefore, in 15.04, the application name of gnome-terminal was not displayed correctly in the script above. The version below derives the application name from the WM_CLASS, as in the output of the xprop command:

enter image description here

The usage is exactly the same as in the (second) script above:

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

arg = sys.argv[1]
# list (column) header titles and their (data) position in the produced window data list
cols = [["Workspace", -1], ["Application name", -2] , ["Window name", -3]]
# rearrange columns, depending on the chosen option
if arg == "-app":
    cols = [cols[1], cols[2], cols[0]]
elif arg == "-ws":
    cols = [cols[0], cols[2], cols[1]]
elif arg == "-win":
    cols = [cols[2], cols[1], cols[0]]
# extract headers, list positions, to be used in the zenity list
col1 = cols[0][0]; i1 = cols[0][1]
col2 = cols[1][0]; i2 = cols[1][1]
col3 = cols[2][0]; i3 = cols[2][1]
# just a helper function
get = lambda cmd: subprocess.check_output([
    "/bin/bash", "-c", cmd
    ]).decode("utf-8")
# analyse viewport data, to be able to calculate relative/absolute position of windows
# and current viewport
def get_spandata():
    xr = get("xrandr").split(); pos = xr.index("current")
    res = [int(xr[pos+1]), int(xr[pos+3].replace(",", "") )]
    spandata = get("wmctrl -d").split()
    span = [int(n) for n in spandata[3].split("x")]
    cols = int(span[0]/res[0]); rows = int(span[1]/res[1])
    curr_vector = [int(n) for n in spandata[5].split(",")]
    curr_viewport = int((curr_vector[1]/res[1])*cols + (curr_vector[0]/res[0])+1)
    return {"resolution": res, "n_columns": cols, "vector": curr_vector, "current_viewport": curr_viewport}

posdata = get_spandata()
vector = posdata["vector"]; cols = posdata["n_columns"]
res = posdata["resolution"]; currvp = posdata["current_viewport"]
# function to distinguish "normal" windows from other types (like the desktop etc)
def check_window(w_id):
    w_type = get("xprop -id "+w_id)
    if " _NET_WM_WINDOW_TYPE_NORMAL" in w_type:
        cl = [l.replace('"', '').split(",")[-1].strip()\
              for l in w_type.splitlines() if "WM_CLASS(STRING)" in l][0]
        return (True, cl)
    else:
        return (False, "")
# split windowdata by machine name
mach_name = socket.gethostname()
wlist = [[l.strip() for l in w.split(mach_name)] for w in get("wmctrl -lpG").splitlines()]
# split first section of window data
for i, w in enumerate(wlist):
    wlist[i][0] = wlist[i][0].split()
# filter only "real" windows   
real_wlist = [w+[check_window(w[0][0])[1]] for w in wlist if check_window(w[0][0])[0] == True]
# adding the viewport to the window's data
for w in real_wlist:
    loc_rel = [int(n) for n in w[0][3:5]]
    loc_abs = [loc_rel[0]+vector[0], loc_rel[1]+vector[1]]
    abs_viewport = int((loc_abs[1]/res[1])*cols + (loc_abs[0]/res[0])+1)
    abs_viewport = str(abs_viewport)+"*" if abs_viewport == currvp else str(abs_viewport)
    w.append(abs_viewport)
# set sorting rules
if arg == "-app":
    real_wlist.sort(key=lambda x: x[-2])
elif arg == "-ws":
    real_wlist.sort(key=lambda x: x[-1])
elif arg == "-win":
    real_wlist.sort(key=lambda x: x[-3])
# calculate width and height of the zenity window:
# height = 140px + 23px per line
h = str(140+(len(real_wlist)*23))
# width = 250px + 8px per character (of the longest window title)
w = str(250+(max([len(w[-3]) for w in real_wlist])*8))
# define the zenity window's content
cmd = "zenity --list --hide-column=4 --print-column=4 --title='Window list' "\
      "--width="+w+" --height="+h+" --column='"+col1+"' --column='"+col2+"' --column='"+col3+\
      "' --column='w_id' "+(" ").join([(" ").join([
          '"'+w[i1]+'"','"'+w[i2]+'"','"'+w[i3]+'"','"'+w[0][0]+'"'
          ]) for w in real_wlist])
# finally, call the window list
try:
    w_id = subprocess.check_output(["/bin/bash", "-c", cmd]).decode("utf-8").split("|")[0]
    subprocess.Popen(["xdotool", "windowmove", "--sync", w_id, "100", "100"])
    subprocess.Popen(["wmctrl", "-iR", w_id])
except subprocess.CalledProcessError:
    pass
8
  • Wonderful what you always hack together! :D I am impressed. One question: The outputted window titles - could they be reformatted to e.g. show also the desktop number they're on and the application name (e.g. for the Terminal)?
    – Byte Commander
    Jul 21, 2015 at 11:20
  • @ByteCommander I had the feeling you would ask :). Luckily, I could use the linked answer (script) as a base, but change its functionality. Jul 21, 2015 at 12:02
  • I modified elif arg == "-ws": cols = [cols[0], cols[2], cols[1]] to elif arg == "-ws": cols = [cols[0], cols[1], cols[2]] to change the column order. One thing I noticed though: i.imgur.com/wBSTi8f.png shows that it somehow cuts long application names off. It seems not to do that on your screen shot though. Any idea what might be different there? It was already a problem before I modified the column order.
    – Byte Commander
    Jul 21, 2015 at 12:14
  • I see no change after replacing the line. And another thing: my monitor flashes every time I launch that second version of your script, but it does that with many applications, e.g. unity-control-center, qtox or inxi too. I guess you can't fix that except by going back to the first script version...
    – Byte Commander
    Jul 21, 2015 at 12:26
  • @ByteCommander I have no explanation for that, only that it doesn't on my system. Probably some kind of minor incompatibility of the graphics driver. Jul 21, 2015 at 12:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .