3

I'm trying to write a script to identify all open chrome windows and move them into a grid layout on a large screen

I'm not sure how to find out what the best resolutions would be so I was going to manually add them in to a array so if 1 chrome window was available then maximise that, if 2 chrome windows available then go to a array for sizes for that?

At the moment I can move every window on the screen (does break my display when doing this) but I can see to work out how to move just the chrome screens?

The script below are some ideas I had but please point in the correct direction as at the moment the script doesn't work

#!/bin/bash
#Chrome window crontroller 


# Monitor 1920 X 1800

# Choose array for number of screens available 

# Different screen positions 
G=0
win1_X=5;      win1_Y=24;    win1_W=639;   win1_H=499;
win2_X=642;    win2_Y=24;    win2_W=639;   win2_H=499;
win3_X=1280;   win3_Y=24;    win3_W=639;   win3_H=499;
win4_X=5;      win4_Y=552;   win4_W=639;   win4_H=499;

ChromesAvailable()
{
    CA=$(wmctrl -lx | grep Chromium | wc -l)
}


GetNumOfChrome()
{
  WID=$(wmctrl -l | grep n | awk '{print$1}')
  #echo "INFO: window id = $WID"
}


PlaceWindow()
{
  X=${n}_X; Y=${n}_Y; W=${n}_W; H=${n}_H; 
  wmctrl -i -r "$WID" -e $G,${!X},${!Y},${!W},${!H}
}

if [ "$#" -gt 0 ]; then
 case "$1" in

        *)
            echo "ERROR: invalid option $1"
            echo "see --help for usage"
            exit 1
            ;;
  esac
  exit 0
else
for n in win{1..4}; do
    GetNumOfChrome
    PlaceWindow
done

fi

Edited - To explain things better :-)

Using grep n will load every open window on the system so I tried to use grep Chromimum but the script doesn't like this

 GetNumOfChrome()
    {
      WID=$(wmctrl -l | grep n | awk '{print$1}')
      #echo "INFO: window id = $WID"
    }
Jacob Vlijm
  • 83,767
Grimlockz
  • 133
  • Please [edit] your question and explain exactly how the script fails. "Does not work" is not very informative :). Also, why are you running wmctrl -l | grep n? Was that supposed to be grep -n? – terdon May 14 '15 at 10:43
  • So, what is the grep -n supposed to do? That will print any lines containing the letter n, how is that relevant? – terdon May 14 '15 at 10:55
  • The n should feed into the PlaceWindow function that gets it's the details for the window sizing from the top. I'm happy to go down a different route if you think that is better – Grimlockz May 14 '15 at 11:01
  • So what is your main goal ? Open bunch of windows and set it to different size and see which one works better ? – Sergiy Kolodyazhnyy May 14 '15 at 17:37

2 Answers2

3

The script below will tile an arbitrary number of chrome or chromium windows in a Nx2 grid (N rows, 2 columns) where N depends on the number of open windows. If there is only one window, that window will be maximized (or unmaximized if it is already maximized).

#!/usr/bin/env bash

#################################################
# Exit if there are no running chrome processes #
#################################################
pgrep "chrom[e|ium]" &>/dev/null || 
    echo "No Chrom[e|ium] processes are running" 1>&2 && exit
#########################
# Get screen dimensions #
#########################
read width x height < <(xrandr | grep -Po 'current\s*\K.*?(?=,)' )

###################################################################
# Get the names of all Chrome windows. I am using PIDs because    #
# searching by name will match anything with chrome/chromium in   #
# the title, not only chromium windows. It also matches a firefox #
# window open on this AU question, for example.                   #
###################################################################
mapfile -t windows < 
    <(wmctrl -pl | grep -f <(pgrep "chrom[e|ium]") | 
                   cut -d' ' -f1)

####################################
# Get the number of Chrome windows #
####################################
numofwins=${#windows[@]}

#########################################
# Initialize the x and y positions to 0 #
#########################################
x=0
y=0

#############################################
# Get 1/2 the number of windows, rounded up #
#############################################
halfwins=$(printf "%.f" "$(echo $numofwins/2 | bc -l | 
                           awk '{print int($1+0.5)}')")

######################################################
# If there's only one window, maximize/unmaximize it #
######################################################
[[ $numofwins -eq 1 ]] && 
    wmctrl -i -r "${windows[@]}" -b toggle,maximized_vert,maximized_horz &&  
    exit;

##########################################################################
# The height of each window will be the height of the display divided by #
# half the number of windows                                             #
##########################################################################
winheight=$(printf "%.f" "$(echo $height/$halfwins | bc -l)")

##################################################################
# The width of each window will be half the width of the display #
##################################################################
winwidth=$(($width/2))

##################################
# Iterate over each window found #
##################################
for winID in "${windows[@]}"
do
    ########################################
    # Increment a counter. This is used to #
    # know when we should change rows.     #
    ########################################
    let c++
    ###############################
    # Position the current window #
    ###############################
    wmctrl -i -r "$winID" -e 0,$x,$y,$winwidth,$winheight
    ##################################################
    # If the counter is a multiple of 2, change rows #
    ##################################################
    if [[ $((c % 2)) -eq 0 ]]
    then
        y=$((y+$winheight+2))
        x=0
    #######################################
    # If it isn't, move to the right only #
    #######################################
    else
        x=$((x+$winwidth+2))
    fi
done                                
terdon
  • 100,812
  • Again, a good answer more. +1 :) – A.B. May 14 '15 at 17:51
  • @terdon thanks for all the work on this, I'm excited to use the script but when I run it I just get a list of 3042 3051 3052 3058 3081 3104 3150 3217 4509 4808 and no window move, I have 2 chrome windows open and another 3 other programs open as well – Grimlockz May 15 '15 at 05:36
  • Also I've done a process grep and with only 2 chrome window opens I get this ps -ef | grep "chrom[e|ium] | awk '{print $8}' chromium-browser /usr/lib/chromium-browser/chrome-sandbox chromium-browser chromium-browser chromium-browser /usr/lib/chromium-browser/chro chromium-browser /usr/lib/chromium-browser/chro /usr/lib/chromium-browser/chro /usr/lib/chromium-browser/chro – Grimlockz May 15 '15 at 05:53
  • @Grimlockz that output should no longer be printed. The ps will return all sorts of things, yes, but I'm using the ps output to grep through the output of wmctrl so only processes listed by both should be shown. I don't know why it fails on your system, it works on mine but I don't use unity. That might be an issue. Does uity work with wmctrl? Try pinging me in the Ubuntu [chat] room and we can debug it. – terdon May 15 '15 at 11:18
  • I think using it on Unity is an issue, since it does not work on my system as well. wmctrl does work on Unity, but at least one issue is that the wmctrl resize/move command does not work as expected without at least a few px from both launcher and panel. – Jacob Vlijm May 15 '15 at 11:32
  • @JacobVlijm yeah, I used a tiny offset of 2 pixels. I can't test properly on my system because i) I have tint2 running which takes up some screen real estate and ii) I use cinnamon whose panel is a little wonky. It does place windows in a grid though. Does it just do nothing on your system or are the windows placed wrongly? – terdon May 15 '15 at 11:32
2

A different approach is to arrange the windows form a pre- defined(customizable) grid (columns/rows)

An example:

enter image description here

rearranged into (cols set to 3, rows set to 2):

enter image description here

rearranged into (cols set to 4, rows set to 2):

enter image description here

The script below can be used to do that. As said, the number of columns&rows can be set, as well as the padding between the windows. The script calculates then the positions the windows should be arranged into, as well as their sizes.

Using the wmctrl command on Unity

The wmctrl command shows some peculiarities when used to move windows to- or very nearby the launcher or the panel. Therefore the margins:

left_margin = 70; top_margin = 30

cannot be set to zero. You have to keep at least a few px distance to both the panel and the launcher. I'd suggest leaving both margins- values as they are. All other values, padding, columns and rows you can play around with and set it as you like.

The script

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

#--- set your preferences below: columns, rows, padding between windows, margin(s)
cols = 2; rows = 2; padding = 10; left_margin = 70; top_margin = 30
#---

get = lambda cmd: subprocess.check_output(["/bin/bash", "-c", cmd]).decode("utf-8")
def get_res():
    xr = get("xrandr").split(); pos = xr.index("current")
    return [int(xr[pos+1]), int(xr[pos+3].replace(",", "") )]

# get resolution
res = get_res()
# define (calculate) the area to divide
area_h = res[0] - left_margin; area_v = res[1] - top_margin
# create a list of calculated coordinates
x_coords = [int(left_margin+area_h/cols*n) for n in range(cols)]
y_coords = [int(top_margin+area_v/rows*n) for n in range(rows)]
coords = sum([[(cx, cy) for cx in x_coords] for cy in y_coords], [])
# calculate the corresponding window size, given the padding, margins, columns and rows
w_size = [str(int(area_h/cols - padding)), str(int(area_v/rows - padding))]
# find windows of the application, identified by their pid
pids = [p.split()[0] for p in get("ps -e").splitlines() if sys.argv[1] in p]
w_list = sum([[w.split()[0] for w in get("wmctrl -lp").splitlines() if p in w] for p in pids], [])
print(pids, w_list, coords)
# remove possibly maximization, move the windows
for n, w in enumerate(w_list):
    data = (",").join([str(item) for item in coords[n]])+","+(",").join(w_size)
    cmd1 = "wmctrl -ir "+w+" -b remove,maximized_horz"
    cmd2 = "wmctrl -ir "+w+" -b remove,maximized_vert"
    cmd3 = "wmctrl -ir "+w+" -e 0,"+data
    for cmd in [cmd1, cmd2, cmd3]:
        subprocess.Popen(["/bin/bash", "-c", cmd])

How to use

  1. Make sure wmctrl is installed :)
  2. Copy thye script into an empty file, save it as rearrange_windows.py
  3. In the head section of the script, set your preferences
  4. Run it by the command:

    python3 /path/to/rearrange_windows.py <application>
    

    example: to rearrange chromium windows:

    python3 /path/to/rearrange_windows.py chromium
    

    to rearrange chrome windows

    python3 /path/to/rearrange_windows.py chrome
    

Note

The script can be used to put windows of any application into a grid, since the process name of the application is used as an argument.


EDIT

Dynamic version

below a dynamic version of the script, as requested in a comment. This version of the script calculates the number of columns and rows, depending on the number of windows. The proportions of the rearranged window(s) is similar to the proportions of the screen.

The setup and the use is pretty much the same as the version above, only the number of columns and rows is now set automatically.

#!/usr/bin/env python3
import subprocess
import getpass
import sys
import math

#--- set your preferences below: padding between windows, margin(s)
padding = 10; left_margin = 70; top_margin = 30
#---

get = lambda cmd: subprocess.check_output(["/bin/bash", "-c", cmd]).decode("utf-8")
def get_res():
    xr = get("xrandr").split(); pos = xr.index("current")
    return [int(xr[pos+1]), int(xr[pos+3].replace(",", "") )]

# find windows of the application, identified by their pid
pids = [p.split()[0] for p in get("ps -e").splitlines() if sys.argv[1] in p]
w_list = sum([[w.split()[0] for w in get("wmctrl -lp").splitlines() if p in w] for p in pids], [])
# calculate the columns/rows, depending on the number of windows
cols = math.ceil(math.sqrt(len(w_list))); rows = cols
# define (calculate) the area to divide
res = get_res()
area_h = res[0] - left_margin; area_v = res[1] - top_margin
# create a list of calculated coordinates
x_coords = [int(left_margin+area_h/cols*n) for n in range(cols)]
y_coords = [int(top_margin+area_v/rows*n) for n in range(rows)]
coords = sum([[(cx, cy) for cx in x_coords] for cy in y_coords], [])
# calculate the corresponding window size, given the padding, margins, columns and rows
if cols != 0:
    w_size = [str(int(area_h/cols - padding)), str(int(area_v/rows - padding))]
# remove possibly maximization, move the windows
for n, w in enumerate(w_list):
    data = (",").join([str(item) for item in coords[n]])+","+(",").join(w_size)
    cmd1 = "wmctrl -ir "+w+" -b remove,maximized_horz"
    cmd2 = "wmctrl -ir "+w+" -b remove,maximized_vert"
    cmd3 = "wmctrl -ir "+w+" -e 0,"+data
    for cmd in [cmd1, cmd2, cmd3]:
        subprocess.call(["/bin/bash", "-c", cmd])

See below the examples with a varying number of opened windows:

enter image description here enter image description here

enter image description here enter image description here

Explanation (second script)

Finding the specific windows

  1. The command:

    wmctrl -lp
    

    lists all windows, in the format:

    0x19c00085  0 14838  jacob-System-Product-Name *Niet-opgeslagen document 1 - gedit
    

    where the first column is the window's unique id, and the third column is the pid of the application that owns the window.

  2. The command:

    ps -e
    

    lists all processes, in the format:

    14838 ?        00:00:02 gedit
    

    where the first column is the application's pid, the last one is the process name.

  3. By comparing these two lists, we can find all windows (id of-) which belong to a specific application (called w_list in the script, as the result of line 17/18 in the script):

    pids = [p.split()[0] for p in get("ps -e").splitlines() if sys.argv[1] in p]
    w_list = sum([[w.split()[0] for w in get("wmctrl -lp").splitlines() if p in w] for p in pids], [])
    

Calculating the number of rows/columns

  1. If we make the windows of the same proportions as the screen, that means the number of columns is equal to the number of rows.
  2. That implies that both the number of colums and rows are equal to the rounded up square root of the number of windows to rearrange. That is done in line 20:

    cols = math.ceil(math.sqrt(len(w_list))); rows = cols
    

Calculating the window size and position

  1. Once we have the number of columns, all we need to do is divide the available area (screen resolution - left margin/top margin) in the columns/rows and we have the targeted window size, which is then diminished by the padding, as set in the head of the script:

    w_size = [str(int(area_h/cols - padding)), str(int(area_v/rows - padding))]
    
  2. The horizontal (x) positions are the result of the product(s) of the horizontal window size (including padding) times the column number, in a range of the number of columns. for example: if I have 3 colums of 300 px, the resulting x-positions are:

    [0, 300, 600]
    
  3. The vertical (y) positions are calculated likewise. Both lists are then combined into a list of coordinates, in which the windows will be rearranged.

    This is done in line 26-28 of the script:

    x_coords = [int(left_margin+area_h/cols*n) for n in range(cols)]
    y_coords = [int(top_margin+area_v/rows*n) for n in range(rows)]
    coords = sum([[(cx, cy) for cx in x_coords] for cy in y_coords], [])
    
  4. The actual rearranging finally (after unmaximizing possibly maximized windows) is done from line 33 and further.

Jacob Vlijm
  • 83,767
  • Thanks for the answer when I run the script nothing happens, i've checked that I've got the correct bits installed – Grimlockz May 15 '15 at 05:48
  • @Grimlockz That is odd, I just rechecked the script. Could you add a line: print(pids, w_list, coords) after: w_list = sum([[w.split()[0].... (after line 26) and see what it sais when run from the terminal? – Jacob Vlijm May 15 '15 at 06:47
  • sure ['3051'] [] [(70, 30), (452, 30), (835, 30), (1217, 30), (70, 320), (452, 320), (835, 320), (1217, 320), (70, 610), (452, 610), (835, 610), (1217, 610)] – Grimlockz May 15 '15 at 07:03
  • @Grimlockz in your question, you mention chrome, but from the comment on terdon's answer (and looking into your script) I understand you mean chromium, which has a different process nam. I changed the script to rearrange windows of any application, with the process name as an argument. Please try again. – Jacob Vlijm May 15 '15 at 07:21
  • Thanks , that seems to work fine for 2 windows but when tried on 6 windows I get this python3 chrome3.py chromium ['3042', '3052', '3058', '3081', '3104', '3150', '3217', '4509', '7660', '7784', '7813', '7832', '7856', '7940', '8050'] ['0x03e00001', '0x03e00054', '0x03e000d8', '0x03e000d9', '0x03e000da', '0x03e000db'] [(70, 30), (835, 30), (70, 465), (835, 465)] Traceback (most recent call last): File "chrome3.py", line 31, in <module> data = (",").join([str(item) for item in coords[n]])+","+(",").join(w_size) IndexError: list index out of range – Grimlockz May 15 '15 at 07:26
  • @Grimlockz That means that your windows (6 as it seems) do not fit in a grid of four (2x2). so you need to increase the number of columns/rows, as set in cols = 2; rows = 2 in the head section of your script. You need at least: cols = 3; rows = 2 for six windows. The script can easily be made to put outnumbering windows on top of existing windows however, starting in the same positions again. – Jacob Vlijm May 15 '15 at 07:29
  • Ahhh Thats silly of me...woops - How would you make it dynamic so to fit all chromium windows into a gred depending on how many are active – Grimlockz May 15 '15 at 07:31
  • @Grimlockz posted an additional "dynamic" version. – Jacob Vlijm May 15 '15 at 08:17
  • Your dynamic version throws an IndexError: list index out of range error on my system, complaining about line 17 pids = [p.split()[0] for p in get("ps -e").splitlines() if sys.argv[1] in p]. Same thing happens with 0,1,2,3 or 4 open windows. – terdon May 15 '15 at 11:42
  • @terdon I dare not ask, but did you use the application's process name as an argument :) ? That is about the only IndeError that can occur, since p.split()[0] only appears if p.split()[0] appears in the output of ps -e, and there simply must be a first column then in the line. sys.argv[1] however will of course occur if the argument (processname) is missing. – Jacob Vlijm May 15 '15 at 11:54
  • 1
    @JacobVlijm repeat after me: "users are idiots, users are idiots, users are idiots". No I hadn't :) Now it does tile the windows but not all of them. With three windows, it will tile 2 correctly and the third is maximized. With 4, one takes the right size, one takes half the screen and the other two are maximized. With two, both take more or less the right size but are placed at the top right of my screen, one over the other. – terdon May 15 '15 at 11:55
  • @terdon would you do me the favour to run the script, but with an additional line print(cmd3) after cmd3 = (line 36). I'd like to see if there is an error in the script (wrong command), or that wmctrl chokes somehow in a timing issue. – Jacob Vlijm May 15 '15 at 12:05