Is there a script (or software) to open an application window on a specific viewport and position?
Solution 1:
It can be done very well, but you need some understanding on Unity/viewports. I hope the story below is understandable, if not, please leave a comment.
The script below can be used to open a window of any application on any of your viewports, on any position, if you run it with the right arguments. The script is an edited version of this one, but now prepared to place windows on the spanning virtual desktop.
1. Understanding viewports and window coordinates
Workspaces in Unity
In Unity, unlike other window managers, you actually only have one spanning workspace, which is divided into viewports. In your case, your workspace is divided into eight viewports.
How the position of the windows is defined
The window position, as the output of the command:
wmctrl -lG
(you need to have wmctrl installed to run the command)
is described as the position, relative to the upper left corner of the current viewport:
So if you are on viewport 1
:
a window on viewport 2 to could be positioned on e.g. 1700 (x-wise) x 500 (y-wise)
(my screen is 1680x1050)
However, if you are on viewport 6:
the same window would be positioned on 20 (x), -550 (y)
Using these coordinates correctly is important to run the script with the right arguments, as described below:
2. How to use the script
The script below can be used to place a new window of an application on your virtual (spanning) workspace.
-
Make sure
wmctrl
is installed:sudo apt-get install wmctrl
Copy the script below into an empty file, save it as
setwindow
(no extension) in~/bin
. Create the directory if it doesn't exist yet. Make the script executable.-
If you just created
~/bin
, either run the commandsource ~/.profile
or log out/in to make the directory available in$PATH
. -
Test run the command:
setwindow <application> <x_position> <y_position> <horizontal_size> <vertical_size>
e.g.
setwindow gedit 100 100 200 200
A gedit window should show up on the current viewport.
Notes:
- Keep in mind that not all applications allow window sizes below a certain width or height. The minimum width of a
gedit
window on my system is e.g. appr. 470 px. - The script only works fine if the whole window fits on the targeted viewport, choose your coordinates/sizes accordingly. Also mind that the Unity Launcher and the panel use some space (!) which can influence the position of the window.
- Use negative
<x_position>
to place windows on the left of the current viewport(s) - Use negative
<y_position>
to place windows above the current viewport(s) -
To open new windows on different viewports at once, you can simply chain commands. Looking at the viewport setup in the "Long story" example, If I am on viewport 1, I can open gedit windows on viewport 1, 2, 3 and 4 with the command:
setwindow gedit 100 100 200 200&&setwindow gedit 1780 100 200 200&&setwindow gedit 3460 100 200 200&&setwindow gedit 5140 100 200 200
The script
#!/usr/bin/env python3
import subprocess
import time
import sys
app = sys.argv[1]
get = lambda x: subprocess.check_output(["/bin/bash", "-c", x]).decode("utf-8")
ws1 = get("wmctrl -lp"); t = 0
subprocess.Popen(["/bin/bash", "-c", app])
# fix exception for Chrome (command = google-chrome-stable, but processname = chrome)
app = "chrome" if "chrome" in app else app
while t < 30:
ws2 = [w.split()[0:3] for w in get("wmctrl -lp").splitlines() if not w in ws1]
procs = [[(p, w[0]) for p in get("ps -e ww").splitlines() \
if app in p and w[2] in p] for w in ws2]
if len(procs) > 0:
w_id = procs[0][0][1]
cmd1 = "wmctrl -ir "+w_id+" -b remove,maximized_horz"
cmd2 = "wmctrl -ir "+w_id+" -b remove,maximized_vert"
cmd3 = "wmctrl -ir "+procs[0][0][1]+" -e 0,"+sys.argv[2]+","+sys.argv[3]+","+sys.argv[4]+","+sys.argv[5]
for cmd in [cmd1, cmd2, cmd3]:
subprocess.call(["/bin/bash", "-c", cmd])
break
time.sleep(0.5)
t = t+1
EDIT: the lazy version
In case you'd prefer to just enter coordinates and size, simply as if you would open a window on the current viewport, and give the targeted viewport as an argument (without having to calculate anything), then use the version below...
If you set it up like the first version of the script, you can run it with the command:
setwindow <application> <x_position> <y_position> <horizontal_size> <vertical_size> <targeted_viewport>
An example: to open a Google-Chrome
window positioned on 20, 20
, size 300x300
, on viewport 5
:
setwindow google-chrome 20 20 300 300 5
The setup is pretty much the same as the first version of the script.
Note that also this script only works correctly if the defined window (position/size) fits completely within the targeted viewport.
The script:
#!/usr/bin/env python3
import subprocess
import time
import sys
app = sys.argv[1]
target_vp = int(sys.argv[6])
def get_res():
# get resolution
xr = subprocess.check_output(["xrandr"]).decode("utf-8").split()
pos = xr.index("current")
return [int(xr[pos+1]), int(xr[pos+3].replace(",", "") )]
res = get_res()
def current(set_vp):
# get the current viewport
vp_data = subprocess.check_output(
["wmctrl", "-d"]
).decode("utf-8").split()
dt = [int(n) for n in vp_data[3].split("x")]
cols = int(dt[0]/res[0])
rows = int(dt[1]/res[1])
curr_vpdata = [int(n) for n in vp_data[5].split(",")]
curr_col = int(curr_vpdata[0]/res[0])
curr_row = int(curr_vpdata[1]/res[1])
curr_vp = curr_col+curr_row*cols+1
# calculate the vector to the origin from the current viewport (in resolution units)
vec_curr = vector(curr_vp, cols)
# calculate the vector to the origin from the targeted viewport
vec_set = vector(set_vp, cols)
# calculate the vector between current and targeted viewport
vec_relative = [vec_set[0] - vec_curr[0],
vec_set[1] - vec_curr[1]]
# calculate needed correction (absolute)
relative = [vec_relative[0]*res[0],
vec_relative[1]*res[1]]
return relative
def vector(vp, cols):
rem = vp%cols
vec_x = rem-1 if rem != 0 else cols-1
vec_y = int((vp-1)/cols)
return [vec_x, vec_y]
res = get_res() # nieuw
get = lambda x: subprocess.check_output(["/bin/bash", "-c", x]).decode("utf-8")
ws1 = get("wmctrl -lp"); t = 0
# check for additional arguments to run the application
try:
subprocess.Popen(["/bin/bash", "-c", app+" "+sys.argv[7]])
except IndexError:
subprocess.Popen(["/bin/bash", "-c", app])
# fix exception for Chrome (command = google-chrome-stable, but processname = chrome)
app = "chrome" if "chrome" in app else app
while t < 30:
ws2 = [w.split()[0:3] for w in get("wmctrl -lp").splitlines() if not w in ws1]
procs = [[(p, w[0]) for p in get("ps -e ww").splitlines() \
if app in p and w[2] in p] for w in ws2]
if len(procs) > 0:
w_id = procs[0][0][1]
cmd1 = "wmctrl -ir "+w_id+" -b remove,maximized_horz"
cmd2 = "wmctrl -ir "+w_id+" -b remove,maximized_vert"
# calculate the correction, related to the current workspace, marge for launcher and panel
pos_x = int(sys.argv[2]); pos_y = int(sys.argv[3]); x_marge = 65; y_marge = 35
pos_x = pos_x if pos_x > x_marge else x_marge; pos_y = pos_y if pos_y > y_marge else y_marge
x_relative = pos_x+current(target_vp)[0]
y_relative = pos_y+current(target_vp)[1]
# correct possible inaccurately set width / height
x_size = res[0]; y_size = res[1]
set_width = int(sys.argv[4]); set_height = int(sys.argv[5])
width = set_width if set_width+x_marge+pos_x < x_size else x_size - pos_x - x_marge
height = set_height if set_height+y_marge+pos_y < y_size else y_size - pos_y - y_marge
cmd3 = "wmctrl -ir "+w_id+" -e 0,"+str(x_relative)+","+str(y_relative)+","+str(width)+","+str(height)
for cmd in [cmd1, cmd2, cmd3]:
subprocess.call(["/bin/bash", "-c", cmd])
break
time.sleep(0.5)
t = t+1
Opening application windows with arguments
To finish the job, answering your question completely:
If you run the script as e.g.:
setwindow google-chrome 20 20 300 300 5
it will open a default window on the targeted desktop(s).
With the latest version of the script however, you can add an additional argument to open the application window, for example a url
:
setwindow <application> <x_position> <y_position> <horizontal_size> <vertical_size> <targeted_viewport> <(optional)_argument>
e.g.:
setwindow google-chrome 0 0 600 600 3 "--new-window http://askubuntu.com"
If the (extra) argument contains spaces, use quotes. The above example will open agoogle-chrome
window on viewport 3, opening the url
http://askubuntu.com
.
You can chain commands to open multiple windows/urls on different workspaces in one command, e.g.:
setwindow google-chrome 0 0 600 600 8 "--new-window http://askubuntu.com"&&setwindow google-chrome 0 0 600 600 7 "--new-window www.google.com"
Solution 2:
This expands on @Jacob Vlijim's great answer above with a slightly modified setwindow
script:
#!/usr/bin/env python
import time
import argparse
import subprocess
DEFAULT_WIDTH = '1920'
DEFAULT_HEIGHT = '1080'
def get_window_list():
window_list = subprocess.check_output(['/bin/bash', '-c', 'wmctrl -l'])
parsed_list = []
for line in window_list.splitlines():
window_info = line.split()
if window_info[1] != '-1':
parsed_list.append(window_info[0])
return parsed_list
def main(params):
old_list = get_window_list()
subprocess.Popen(['/bin/bash', '-c', params.command])
def get_diff(old):
new_list = get_window_list()
return list(set(new_list) - set(old))
diff = get_diff(old_list)
x = 0
while not diff:
if x == 10:
print 'window not found'
return
x += 1
diff = get_diff(old_list)
time.sleep(1)
if len(diff) > 1:
raise Exception(diff)
window_id = diff[0]
command_list = []
command_list.append('wmctrl -ir %s -t %s' % (window_id, params.desktop))
command_list.append('wmctrl -ir %s -b remove,maximized_horz,maximized_vert'
% window_id)
command_list.append('wmctrl -ir %s -e 0,%s,%s,%s,%s' %
(window_id, params.x_pos, params.y_pos, params.width, params.height))
for command in command_list:
subprocess.call(['/bin/bash', '-c', command])
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('command', type=str)
parser.add_argument('-d', '--desktop', default='0', type=str)
parser.add_argument('-x', '--x-pos', default='0', type=str)
parser.add_argument('-y', '--y-pos', default='0', type=str)
parser.add_argument('-w', '--width', default=DEFAULT_WIDTH, type=str)
parser.add_argument('-t', '--height', default=DEFAULT_HEIGHT, type=str)
args = parser.parse_args()
main(args)
A description of the changes:
-
python3
topython
(just a personal preference) -
sys.argv
toargparse
for a better command line interface - strict window id (and not process id) window parsing
- some programs use a single process id for multiple windows
-
while
loop 0.5 second to 1 full second sleep time - more verbose/readable variable names and pep8 adherence
- global constant variables for screen size instead of
xrandr
reliance
NOTE: This is a just a slightly improved version I wrote for personal use on Debian Jessie LXDE. Your results may vary.