Controlling Subprocess Window Position from the Terminal

Controlling Subprocess Window Position from the Terminal

I wanted a way to open processes from a script and have the resulting window placed in a specific position on the screen. There seems to be only a few options to get this done on Linux. The top contenders are: wmctrl, xdotool and devilspie2 but as far as I can tell, none of these tools assist in getting the window ID if it’s created in a child process. I started down the road of using the ps command, but switched to Python and the psutil module for it’s convenient Process.children() method. Also, I decided to use the wmctrl command to actually modify the window geometry.

The first step was to get wmctrl’s window ID for a specific process ID. This is assuming that there is only one window for this process or that magically, the first window found is the correct one. The following wmctrl command prints information for all open windows, the results of which are then parsed for the specific process ID.

def window_id(proc_id):
    proc = subprocess.Popen(['wmctrl', '-lp'],
                            env=os.environ,
                            stdout=subprocess.PIPE,
                            universal_newlines=True)
    out = proc.communicate()[0]
    for l in out.split('\n'):
        s = l.strip().split()
        if len(s) > 1 and int(s[2]) == proc_id:
            return s[0]

There are two problems with this function: 1. the window may not be created immediately, so we have to poll for a hopefully short while, and 2. the window may be spawned by a child process if the parent is a script that turns around and calls another executable for example. Both of these can be resolved by polling periodically the parent process and all children. This function polls every 10th of a second for up to 3 seconds which may or may not be enough depending on the weight of the application I suppose.

def wait_for_window(proc_id, timeout=3):
    wid = None
    poll_interval = 0.1
    attempts = max(1, timeout // poll_interval)
    while wid is None and attempts > 0:
        attempts -= 1
        wid = window_id(proc_id)
        if wid is None:
            proc = psutil.Process(proc_id)
            for child_proc in proc.children(recursive=True):
                wid = window_id(child_proc.pid)
                if wid is not None:
                    break
        if wid is None:
            time.sleep(poll_interval)
    return wid

Great! Once we have the window ID, it’s a simple matter to set the geometry using wmctrl. This function uses the above code to get the window ID and passes the geometry parameter unchanged to wmctrl.

def resize_window(pid, geometry):
    wid = wait_for_window(pid)
    if wid is None:
        print(f'could not get window for process ID: {pid}')
    else:
        cmd = ['wmctrl', '-i', '-r', wid, '-e', geometry]
        subprocess.Popen(cmd, env=os.environ)

The documentation for wmctrl’s -e option looks like this:

-r <WIN> -e <MVARG>  Resize and move the window around the desktop.
                     The format of the <MVARG> argument is described
                     below.

<MVARG> Specifies a change to the position and size
        of the window. The format of the argument is:

        <G>,<X>,<Y>,<W>,<H>

        <G>: Gravity specified as a number. The numbers are
             defined in the EWMH specification. The value of
             zero is particularly useful, it means "use the
             default gravity of the window".
        <X>,<Y>: Coordinates of new position of the window.
        <W>,<H>: New width and height of the window.

        The value of -1 may appear in place of
        any of the <X>, <Y>, <W> and <H> properties
        to left the property unchanged.

Finally, to wrap up the script into something we can prepend to any command on the terminal, we add a __main__ section.

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-g', '--geometry')
    parser.add_argument('cmd', nargs=argparse.REMAINDER)

    args = parser.parse_args()

    main_proc = subprocess.Popen(args.cmd, env=os.environ)
    print(f'main process PID: {main_proc.pid}')

    resize_window(main_proc.pid, args.geometry)

    sys.exit(main_proc.wait())

The entire script can be found here and can be used to open some process that spawns a GUI using the X windows system:

> open-window.py --geometry 0,2000,0,800,800 gedit

This is not without some serious drawbacks though. It doesn’t play well with programs that communicate with some server process that’s responsible for presenting the windows. In this case, the windows cannot be found in the children of the program being run. Furthermore, if the program presents multiple windows, there is no guarantee which window will be resized. After this exercise, I’m beginning to see a lot of difficulties in making the script more general and for the time being this serves my purposes.

open-window.py