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)

enter image description here


However, if you are on viewport 6:
the same window would be positioned on 20 (x), -550 (y) enter image description here


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.

  1. Make sure wmctrl is installed:

    sudo apt-get install wmctrl
    
  2. 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.

  3. If you just created ~/bin, either run the command source ~/.profile or log out/in to make the directory available in $PATH.
  4. 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:

  1. python3 to python (just a personal preference)
  2. sys.argv to argparse for a better command line interface
  3. strict window id (and not process id) window parsing
    • some programs use a single process id for multiple windows
  4. while loop 0.5 second to 1 full second sleep time
  5. more verbose/readable variable names and pep8 adherence
  6. 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.