How can I lock an application (and all its new windows) into a specific workspace?
IMPORTANT EDIT
Below a rewritten version of the script from the first answer (below). The differences:
- The script now is extremely low on resources (like it should be with background scripts). The actions are now arranged to act if (and only if) they are needed. The loop does practically nothing but check for new windows to appear.
- Bot the
WM_CLASS
and the targeted workspace are now arguments to run the script. Only use either the first or the second (identifying) part of theWM_CLASS
(see further below: how to use) - The script now keeps focus on the currently active window (actually re- focusses in a split second)
-
When the script starts, it shows a notification (example
gedit
):
The script
#!/usr/bin/env python3
import subprocess
import sys
import time
import math
app_class = sys.argv[1]
ws_lock = [int(n)-1 for n in sys.argv[2].split(",")]
def check_wlist():
# get the current list of windows
try:
raw_list = [
l.split() for l in subprocess.check_output(
["wmctrl", "-lG"]
).decode("utf-8").splitlines()
]
ids = [l[0] for l in raw_list]
return (raw_list, ids)
except subprocess.CalledProcessError:
pass
def get_wssize():
# get workspace size
resdata = subprocess.check_output(["xrandr"]).decode("utf-8").split()
i = resdata.index("current")
return [int(n) for n in [resdata[i+1], resdata[i+3].replace(",", "")]]
def get_current(ws_size):
# vector of the current workspace to origin of the spanning desktop
dt_data = subprocess.check_output(
["wmctrl", "-d"]
).decode("utf-8").split()
curr = [int(n) for n in dt_data[5].split(",")]
return (int(curr[0]/ws_size[0]), int(curr[1]/ws_size[1]))
def get_relativewinpos(ws_size, w_data):
# vector to the application window, relative to the current workspace
xpos = int(w_data[2]); ypos = int(w_data[3])
xw = ws_size[0]; yw = ws_size[1]
return (math.ceil((xpos-xw)/xw), math.ceil((ypos-yw)/yw))
def get_abswindowpos(ws_size, w_data):
# vector from the origin to the current window's workspace (flipped y-axis)
curr_pos = get_current(ws_size)
w_pos = get_relativewinpos(ws_size, w_data)
return (curr_pos[0]+w_pos[0], curr_pos[1]+w_pos[1])
def wm_class(w_id):
# get the WM_CLASS of new windows
return subprocess.check_output(
["xprop", "-id", w_id.strip(), "WM_CLASS"]
).decode("utf-8").split("=")[-1].strip()
ws_size = get_wssize()
wlist1 = []
subprocess.Popen(["notify-send", 'workspace lock is running for '+app_class])
while True:
# check focussed window ('except' for errors during "wild" workspace change)
try:
focus = subprocess.check_output(
["xdotool", "getwindowfocus"]
).decode("utf-8")
except subprocess.CalledProcessError:
pass
time.sleep(1)
wdata = check_wlist()
if wdata != None:
# compare existing window- ids, checking for new ones
wlist2 = wdata[1]
if wlist2 != wlist1:
# if so, check the new window's class
newlist = [[w, wm_class(w)] for w in wlist2 if not w in wlist1]
valids = sum([[l for l in wdata[0] if l[0] == w[0]] \
for w in newlist if app_class in w[1]], [])
# for matching windows, check if they need to be moved (check workspace)
for w in valids:
abspos = list(get_abswindowpos(ws_size, w))
if not abspos == ws_lock:
current = get_current(ws_size)
move = (
(ws_lock[0]-current[0])*ws_size[0],
(ws_lock[1]-current[1])*ws_size[1]-56
)
new_w = "wmctrl -ir "+w[0]+" -e "+(",").join(
["0", str(int(w[2])+move[0]),
str(int(w[2])+move[1]), w[4], w[5]]
)
subprocess.call(["/bin/bash", "-c", new_w])
# re- focus on the window that was focussed
if not app_class in wm_class(focus):
subprocess.Popen(["wmctrl", "-ia", focus])
wlist1 = wlist2
How to use
-
The script needs both
wmctrl
andxdotool
:sudo apt-get install wmctrl xdotool
Copy the script above into an empty file, save it as
lock_towspace.py
-
Of your specific application, find out the
WM_CLASS
: open your application, run in a terminal:xprop WM_CLASS and click on the window of the application
The output will look like (in your case):
WM_CLASS: WM_CLASS(STRING) = "sun-awt-X11-XFramePeer", "MATLAB R2015a - academic use"
Either use the first or the second part in the command to run the script.
-
The command to run the script then is:
python3 /path/to/lock_towspace.py "sun-awt-X11-XFramePeer" 2,2
In the command, the last section;
2,2
is the workspace where you want to lock the application to (without spaces: (!) column, row), in "human" format; the first column/row is1,1
- Test the script by running it. While running, open your application and let it produce windows as usual. All windows should appear on the targeted workspace, as set in the command.
OUTDATED ANSWER:
(second) TEST VERSION
The script below locks a specific application to its initial workspace. If the script is started, it determines on which workspace the application resides. All additional windows the application produces will be moved to the same workspace in a split second.
The focus issue is solved by automatically re- focussing on the window that was focussed before the additional window was produced.
The script
#!/usr/bin/env python3
import subprocess
import time
import math
app_class = '"gedit", "Gedit"'
def get_wssize():
# get workspace size
resdata = subprocess.check_output(["xrandr"]).decode("utf-8").split()
i = resdata.index("current")
return [int(n) for n in [resdata[i+1], resdata[i+3].replace(",", "")]]
def get_current(ws_size):
# get vector of the current workspace to the origin of the spanning desktop (flipped y-axis)
dt_data = subprocess.check_output(["wmctrl", "-d"]).decode("utf-8").split(); curr = [int(n) for n in dt_data[5].split(",")]
return (int(curr[0]/ws_size[0]), int(curr[1]/ws_size[1]))
def get_relativewinpos(ws_size, w_data):
# vector to the application window, relative to the current workspace
xw = ws_size[0]; yw = ws_size[1]
return (math.ceil((w_data[1]-xw)/xw), math.ceil((w_data[2]-yw)/yw))
def get_abswindowpos(ws_size, w_data):
curr_pos = get_current(ws_size)
w_pos = get_relativewinpos(ws_size, w_data)
return (curr_pos[0]+w_pos[0], curr_pos[1]+w_pos[1])
def wm_class(w_id):
return subprocess.check_output(["xprop", "-id", w_id, "WM_CLASS"]).decode("utf-8").split("=")[-1].strip()
def filter_windows(app_class):
# find windows (id, x_pos, y_pos) of app_class
try:
raw_list = [l.split() for l in subprocess.check_output(["wmctrl", "-lG"]).decode("utf-8").splitlines()]
return [(l[0], int(l[2]), int(l[3]), l[4], l[5]) for l in raw_list if wm_class(l[0]) == app_class]
except subprocess.CalledProcessError:
pass
ws_size = get_wssize()
init_window = get_abswindowpos(ws_size, filter_windows(app_class)[0])
valid_windows1 = filter_windows(app_class)
while True:
focus = subprocess.check_output(["xdotool", "getwindowfocus"]).decode("utf-8")
time.sleep(1)
valid_windows2 = filter_windows(app_class)
if all([valid_windows2 != None, valid_windows2 != valid_windows1]):
for t in [t for t in valid_windows2 if not t[0] in [w[0] for w in valid_windows1]]:
absolute = get_abswindowpos(ws_size, t)
if not absolute == init_window:
current = get_current(ws_size)
move = ((init_window[0]-current[0])*ws_size[0], (init_window[1]-current[1])*ws_size[1]-56)
new_w = "wmctrl -ir "+t[0]+" -e "+(",").join(["0", str(t[1]+move[0]), str(t[2]+move[1]), t[3], t[4]])
subprocess.call(["/bin/bash", "-c", new_w])
focus = str(hex(int(focus)))
z = 10-len(focus); focus = focus[:2]+z*"0"+focus[2:]
if not wm_class(focus) == app_class:
subprocess.Popen(["wmctrl", "-ia", focus])
valid_windows1 = valid_windows2
How to use
-
The script needs both
wmctrl
andxdotool
sudo apt-get install wmctrl xdotool
Copy the script into an empty file, save it as
keep_workspace.py
-
determine your application's `WM_CLASS' by opening the application, then open a terminal and run the command:
xprop WM_CLASS
Then click on your application's window. Copy the output, looking like
"sun-awt-X11-XFramePeer", "MATLAB R2015a - academic use"
in your case, and place it between single quotes in the head section of the script, as indicated. -
Run the script with the command:
python3 /path/to/keep_workspace.py
If it works as you like, I'll add a toggle function. Although it works already for a few hours on my system, bu it might need some tweaking first however.
Notes
Although you should not notice it, the script does add some processor load to the system. On my elderly system I noticed an increase of 3-10%. If you like how it works, I will probably further tweak it to reduce the load.
The script, as it is, assumes the secundary windows are of the same class as the main window, like you indicated in a comment. With a (very) simple change, the secondary windows can be of another class however.
Explanation
Although probably not very interesting for an average reader, the script works by calculating in vectors. On startup, the script calculates:
- the vector from the origin to the current workspace with the output of
wmctrl -d
- the vector to the application's window, relative to the current workspace, by the output of
wmctrl -lG
- From these two, the script calculates the absolute position of the application's window on the spanning desktop (all workspaces in one matrix)
From then on, the script looks for new windows of the same application, with the output of xprop WM_CLASS
, looks up their position in the same way as above and moves them to the "original" workspace.
Since the newly created window "stole" the focus from the last used window the user was working on, the focus is subsequently set to the window that had focus before.