30

I'm wondering is there any way to achieve the affect of the Ctrl-Alt-Keypad shortcuts in Unity using terminal commands instead? I want a command that sets a gui window to half the size of the screen, either left or right aligned.

By way of background, I'm writing a script that runs after log in. It uses Zenity to ask whether or not I want to open my development environment (GVim and IPython side-by-side). I have been trying to achieve two equal-sized windows for these programmes by using set lines= columns= in my .gvimrc and c.IPythonWidget.width = and c.IPythonWidget.height = in my ipython_qtconsole_config.py. However, there are problems associated with this approach.

1
  • I updated my answer because it didn't explain the topic in details enough, you might want to check the update
    – kos
    Apr 25, 2015 at 19:07

11 Answers 11

23

What you will run into

If you want to first call an application and, subsequently, place its window on a specific position and size, the time between calling the application and the moment the window actually appears, is unpredictable. If your system is occupied, it can be significantly longer than if it is idle.

You need a "smart" way to make sure the positioning/resizing is done (immediately) after the window appears.

Script to call an application, wait for it to appear and position it on the screen

With the script below, you can call an application and set the position and size it should appear on with the command:

<script> <application> <x-position> <y-position> <h-size> <v-size>

An few examples:

  • To call gnome-terminal and resize its window to 50% and place it on the right half:

    <script> gnome-terminal 840 0 50 100
    

    enter image description here

  • To call gedit, place its window on the left and call gnome-terminal, place it on the right (setting its v-size 46% to give it a little space between the windows):

    <script> gedit 0 0 46 100&&<script> gnome-terminal 860 0 46 100
    

    enter image description here

  • To call Inkscape, place its window in the left/upper quarter of the screen:

    <script> inkscape 0 0 50 50
    

    enter image description here

The script and how to use it

  1. install both xdotool and wmctrl. I used both since resizing with wmctrl can cause some peculiarities on (specifically) Unity.

    sudo apt-get install wmctrl
    sudo apt-get install xdotool
    
  2. Copy the script below into an empty file, save it as setwindow (no extension) in ~/bin; create the directory if necessary.
  3. Make the script executable (!)
  4. If you just created ~bin, run: source ~/.profile
  5. Test-run the script with the command (e.g.)

    setwindow gnome-terminal 0 0 50 100
    

    In other words:

    setwindow <application> <horizontal-position> <vertical-position> <horizontal-size (%)> <vertical-size (%)>
    

If all works fine, use the command wherever you need it.

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])

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 = "xdotool windowsize --sync "+procs[0][0][1]+" "+sys.argv[4]+"% "+sys.argv[5]+"%"
        cmd4 = "xdotool windowmove "+procs[0][0][1]+" "+sys.argv[2]+" "+sys.argv[3]
        for cmd in [cmd1, cmd2, cmd3, cmd4]:   
            subprocess.call(["/bin/bash", "-c", cmd])
        break
    time.sleep(0.5)
    t = t+1

What it does

When the script is called, it:

  1. starts up the application
  2. keeps an eye on the window list (usingwmctrl -lp)
  3. if a new window appears, it checks if the new window belongs to the called application (using ps -ef ww, comparing the pid of the window to the pid of the application)
  4. if so, it sets the size and position, according to your arguments. In case an application does not "show up" within appr. 15 seconds, the script assumes the application will not run due to an error. The script then terminates to prevent waiting for the new window infinitely.

Minor issue

In Unity, when you (re-)position and (re-)size a window with either wmctrl or xdotool, the window will always keep a small marge to the borders of your screen, unless you set it to 100%. You can see that in the image (3) above; while the inkscape window was placed on x position 0, you can still see a minor marge between the Unity Launcher and the inkscape window.

10
  • Dear Jacob, this is a fantastic script! One question: How can I find the dimension of an open window to be subsequently used together with the script?
    – orschiro
    Apr 26, 2016 at 5:46
  • Hi @orschiro I have to run for a meeting... will be back in a few hours :) Apr 26, 2016 at 5:48
  • 1
    Hi @orschiro, assuming you have wmctrl installed, the command: wmctrl -lG | grep <windowname> will show you an output like: 0x03600e4f 0 723 197 1114 563 jacob-System-Product-Name dimkeyboard.sh (~/Bureaublad) - gedit. In this output, the 3rd to the 6th column show the dimensions, x, y, width, width, height. Apr 26, 2016 at 8:40
  • Very appreciated Jacob! I think I did the setup right but the respective window does open in full-screen now. Any ideas?
    – orschiro
    Apr 26, 2016 at 12:21
  • 1
    I loved this script, but I had to make the following changes to the first block to make it work for me, because I added parameters to the application: appAndParams = sys.argv[1] app = appAndParams[0].split(' ', 1)[0] get = lambda x: subprocess.check_output(["/bin/bash", "-c",x]).decode("utf-8") ws1 = get("wmctrl -lp"); t = 0 subprocess.Popen(["/bin/bash", "-c", appAndParams]) </pre> <edit>it refuses to format that code.</edit> May 11, 2016 at 19:51
4

The actual command you want is something like

wmctrl -r :ACTIVE: -b add,maximized_vert && 
wmctrl -r :ACTIVE: -e 0,0,0,$HALF,-1

That will make the current window take up half the screen (change $HALF to the dimensions of your screen) and snap to the left hand side. To snap to the right, use

wmctrl -r :ACTIVE: -b add,maximized_vert && 
wmctrl -r :ACTIVE: -e 0,$HALF,0,$HALF,-1 

You can also play with wmctrl to get the ID of the windows you're interested in instead of using :ACTIVE:. I can't help there though since that depends on the windows in question. Have a look at man wmctrl for more.


I've written a script for that. I don't use Unity so I can't guarantee that it will work with it, but I see no reason why not. It needs wmctrl, xdpyinfo and disper to be installed:

sudo apt-get install wmctrl x11-utils disper

Then, save the script below as ~/bin/snap_windows.sh, make it executable with chmod a+x ~/bin/snap_windows.sh and you can run

snap_windows.sh r

To snap to the right hand side. Use l for the left side and no arguments to maximize the window. Note that it runs on the current window so you'll need to assign a shortcut to it if you want it to run on anything but the terminal.

The script is a bit more complicated than what you ask for because I've written it to work on both single and dual-monitor setups.

#!/usr/bin/env bash

## If no side has been given, maximize the current window and exit
if [ ! $1 ]
then
    wmctrl -r :ACTIVE: -b toggle,maximized_vert,maximized_horz
    exit
fi

## If a side has been given, continue
side=$1;
## How many screens are there?
screens=`disper -l | grep -c display`
## Get screen dimensions
WIDTH=`xdpyinfo | grep 'dimensions:' | cut -f 2 -d ':' | cut -f 1 -d 'x'`;
HALF=$(($WIDTH/2));

## If we are running on one screen, snap to edge of screen
if [ $screens == '1' ]
then
    ## Snap to the left hand side
    if [ $side == 'l' ]
    then
        ## wmctrl format: gravity,posx,posy,width,height
    wmctrl -r :ACTIVE: -b add,maximized_vert && wmctrl -r :ACTIVE: -e 0,0,0,$HALF,-1
    ## Snap to the right hand side
    else
    wmctrl -r :ACTIVE: -b add,maximized_vert && wmctrl -r :ACTIVE: -e 0,$HALF,0,$HALF,-1 
    fi
## If we are running on two screens, snap to edge of right hand screen
## I use 1600 because I know it is the size of my laptop display
## and that it is not the same as that of my 2nd monitor.
else
    LAPTOP=1600; ## Change this as approrpiate for your setup.
    let "WIDTH-=LAPTOP";
    SCREEN=$LAPTOP;
    HALF=$(($WIDTH/2));
    if [ $side == 'l' ]
    then
        wmctrl -r :ACTIVE: -b add,maximized_vert && wmctrl -r :ACTIVE: -e 0,$LAPTOP,0,$HALF,-1
    else
    let "SCREEN += HALF+2";
    wmctrl -r :ACTIVE: -b add,maximized_vert && wmctrl -r :ACTIVE: -e 0,$SCREEN,0,$HALF,-1;
    fi
fi
4

You can do this using xdotool.

To install xdotool you can run:

sudo apt-get update && sudo apt-get install xdotool

Then to send a Ctrl+Alt+<keypad_key> keystroke to the terminal X window you can run:

xdotool key Ctrl+Alt+<keypad_key_value>

*<keypad_key_value> = keypad key's value in the list below

To run a GUI program and send the keystroke to its X window (which in this case is the active window at the time of the xdotool command execution) you can run:

<command> && window="$(xdotool getactivewindow)" xdotool key --delay <delay> --window "$window" <keypad_key_value>

*<command> = command that opens the window you want to send the keystroke to; <delay> = time to wait in milliseconds before sending the keystroke; <keypad_key_value> = keypad key's value in the list below

Notice that in most cases you'll need to run the command you're issuing as a stand-alone process (e.g. by running nohup <command> & instead of <command> in the example above), otherwise xdotool won't be run until <command>'s execution is complete.

Also you'll need to set some delay, otherwise the keystroke will be sent before the target window is fully loaded in X (a delay around 500ms should do).

The possible values for <keypad_key_value> are:

  • 0: 90
  • 1: 87
  • 2: 88
  • 3: 89
  • 4: 83
  • 5: 84
  • 6: 85
  • 7: 79
  • 8: 80
  • 9: 81

As a thumb rule, to find out the the value of any key on the keyboard within the X environment, one can run xev and hit the key to output its value inside the terminal.

3
  • Is there something wrong with our edits?
    – Tim
    Apr 25, 2015 at 18:18
  • @Tim No, at least not the removal of the last line, which I agree it's pretty unuseful, but what can vary within a command is in my opinion better formatted if enclosed in angle brackets, which is the standard notation in language theory to refer to a syntactic category (referring to your edit) and I think that a command name it's better formatted if enclosed in backticks, in order to be immediately recognized as such (referring to A.B.'s edit); notice that not all of this is off the top of my head, I had doubts before and I asked this on meta
    – kos
    Apr 25, 2015 at 18:38
  • Yeah, it is fine as is now :) <> is the standard, and I go for `` around any command :)
    – Tim
    Apr 25, 2015 at 18:40
4

I have build an application named Worksets (on github) for Unity that lets you do this easily through a graphical user interface - It's free and open source.

tTray menu

It's basically a wrapper for the wmctrl and xdotool solutions listed as answers here, and provides an easy way to quickly make and save such setups.

1

I don't have the rep to comment directly on Jacob Vlijm's excellent post, but I modified the script to permit starting an application with arguments (necessary to use setwindow with gedit --new-window). Change:

if app in p and w[2] in p] for w in ws2]

to:

if app.split()[0] in p and w[2] in p] for w in ws2]
1
  • See my modified Jacob Vlijm's script below
    – Russo
    Apr 26, 2020 at 5:23
0

I played around with Terdon's script from above and added some new things (ability to select the monitor and set the height for each monitor). It would be extendable to adding more monitors I think. Hopefully others will find it useful.

Basic syntax:

prog_name monitor l/r/m window_name (optional)

Where prog_name is whatever you saved this code as; monitor is the monitor number e.g. 1 or 2; l/r/m is left or right or max; and window_name is the name (or a fraction of it's name) of the target window.

E.g.:

setwindow 1 m chrome

Bash script

#!/usr/bin/env bash
set -e
#######################-    Early Definitions    -#######################

snap () {
    wmctrl -r ${WIN} -b toggle,add,maximized_vert && wmctrl -r ${WIN} -e 0,${WINX},0,${WINWIDTH},${WINHEIGHT}
    }

DISPLAYWIDTH=`xdpyinfo | grep 'dimensions:' | cut -f 2 -d ':' | cut -f 1 -d 'x'`;       ## Get screen dimensions
LEFTSCREENWIDTH=1024    ## user set
LEFTSCREENHEIGHT=720    ## user set
RIGHTSCREENHEIGHT=960   ## user set
let "RIGHTSCREENWIDTH=(DISPLAYWIDTH-LEFTSCREENWIDTH)"

#############################-    Logic    -#############################

if [ ! ${3} ]; then
    WIN=":ACTIVE:"
else
    WIN=${3}
fi
case ${1} in
    1)  # monitor one
        LEFT=0
        WINHEIGHT=${LEFTSCREENHEIGHT}
        let "WINWIDTH=LEFTSCREENWIDTH/2"
    ;;
    2)  # monitor two
        let "LEFT=LEFTSCREENWIDTH"
        WINHEIGHT=${RIGHTSCREENHEIGHT}
        let "WINWIDTH=RIGHTSCREENWIDTH/2"
    ;;
    "") # error please select a monitor
        echo "please select a monitor (1 or 2)"
        exit 0
    ;;
esac
case ${2} in
    l|L)
        WINX=${LEFT}
        snap
    ;;
    r|R)
        let "WINX=LEFT+WINWIDTH"
        snap
    ;;
    ""|m|M)
        WINX=${LEFT}
        let "WINWIDTH=WINWIDTH*2"
        snap
    ;;
esac

exit 0
0

I have modified Jacob Vlijm's script to allow advanced arguments/parameters to be attached to the application.

For example if you want to run Firefox with two tabs in a bash script:

command = "firefox -new-tab -url https://duckduckgo.com -new-tab -url https://www.google.com"
path_to_your_setwindow_script/setwindow 0 0 32 100 $command

Then the modified Jacob Vlijm's script below will take the first 4 arguments for position and size settings, and then the rest of the arguments for how to run the application

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

print('inside setwindow')
print(sys.argv)
print(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])

commandCore = sys.argv[5]

command= sys.argv[5:];
commandStr = " ".join(command)

print('commandCore:', commandCore, '\ncommandStr:', commandStr)

#print('exiting now...')
#sys.exit()

get = lambda x: subprocess.check_output(["/bin/bash", "-c", x]).decode("utf-8")
ws1 = get("wmctrl -lp"); t = 0
subprocess.Popen(["/bin/bash", "-c", commandStr])

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 commandCore 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 = "xdotool windowsize --sync "+procs[0][0][1]+" "+sys.argv[3]+"% "+sys.argv[4]+"%"
        cmd4 = "xdotool windowmove "+procs[0][0][1]+" "+sys.argv[1]+" "+sys.argv[2]
        for cmd in [cmd1, cmd2, cmd3, cmd4]:   
            subprocess.call(["/bin/bash", "-c", cmd])
        break
    time.sleep(0.5)
    t = t+1
0

Shell Script which can be turned into a "~/Desktop/Terminals.desktop" file with a TERMINAL icon on the Desktop to kick it off with:

/usr/bin/gnome-terminal --window-with-profile=Red --geometry=100x52+200+0

/usr/bin/gnome-terminal --window-with-profile=Teal --geometry=100x52+400+0

/usr/bin/gnome-terminal --window-with-profile=Green --geometry=100x52+600+0

/usr/bin/gnome-terminal --window-with-profile=Blue --geometry=100x52+800+0

/usr/bin/gnome-terminal --window-with-profile=Cyan --geometry=100x52+1100+0

$ cat /home/$USER/Desktop/Terminals.desktop

[Desktop Entry]
Encoding=UTF-8
Version=1.0
Type=Application
Exec=/home/$USER/.terminals
TryExec=
Icon=/usr/share/pixmaps/blueprint-config-xfree.png
X-GNOME-DocPath=
Terminal=false
Name=Terminal
GenericName=
Comment=
0

For most programs, there is the very simple "-geometry" option :

xclock -geometry 150x150+20+20

kate -geometry 400x400+100+100

<appname> -geometry <height>x<width>+<posH>+<posV>
0

For simple tasks, you can use the following bash script.

#!/bin/bash

nemo &
sleep 2
LastWID=$(wmctrl -li | awk '{ print $1}' | sort | tail -n1)
wmctrl -ia $LastWID
wmctrl -i -r $LastWID -b remove,maximized_vert,maximized_horz
wmctrl -i -r $LastWID -e 0,900,300,700,700

For more flexibility, use the following script which is a fork of Russo's answer. Removed the xdotool dependency. Also (in my mind) simplified the code.

Put this script in PATH and use it using <script-name> <x-position> <y-position> <h-size> <v-size> <application> <application-arguments>

#!/usr/bin/env python3

import subprocess
import time
import sys

commandCore = sys.argv[5]

command= sys.argv[5:];
commandStr = " ".join(command)

get = lambda x: subprocess.check_output(["/bin/bash", "-c", x]).decode("utf-8")

w_id_pre_value = get("wmctrl -li | awk '{ print $1}' | wc -l").strip()
subprocess.Popen(["/bin/bash", "-c", commandStr],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)

while True:
    time.sleep(0.1)
    if w_id_pre_value < get("wmctrl -li | awk '{ print $1}' |wc -l").strip():
        break

w_id = get("wmctrl -li | awk '{ print $1}' | tail -n1").strip()

cmd1 = "wmctrl -is "+w_id
cmd2 = "wmctrl -ir "+w_id+" -b remove,maximized_vert,maximized_horz"
cmd3 = "wmctrl -ir "+w_id+" -e 0,"+sys.argv[1]+","+sys.argv[2]+","+sys.argv[3]+","+sys.argv[4]
for cmd in [cmd1, cmd2, cmd3]:
    subprocess.call(["/bin/bash", "-c", cmd])

The beauty of this python script is that, you can create a shell file like

#!/bin/bash

setwindow 0 0 960 1078 firefox
setwindow 960 0 960 1078 nemo
setwindow 1920 0 960 1078 codium

and open multiple apps in their designated positions.

Update 1:

If you have Python 3.5+, you can use the following code as well.

#!/usr/bin/env python3

import subprocess
import time
import sys

commandCore = sys.argv[5]

command= sys.argv[5:];
commandStr = " ".join(command)

w_id_pre_value = subprocess.run("wmctrl -li | awk '{ print $1}' | wc -l", shell=True, capture_output=True, text=True).stdout.strip()

subprocess.run(commandStr, shell=True)

while True:
    time.sleep(0.1)
    if w_id_pre_value < subprocess.run("wmctrl -li | awk '{ print $1}' |wc -l", shell=True, capture_output=True, text=True).stdout.strip():
        break

w_id = subprocess.run("wmctrl -li | awk '{ print $1}' | tail -n1", shell=True, capture_output=True, text=True).stdout.strip()

cmd1 = "wmctrl -is "+w_id
cmd2 = "wmctrl -ir "+w_id+" -b remove,maximized_vert,maximized_horz"
cmd3 = "wmctrl -ir "+w_id+" -e 0,"+sys.argv[1]+","+sys.argv[2]+","+sys.argv[3]+","+sys.argv[4]
for cmd in [cmd1, cmd2, cmd3]:
    subprocess.run(cmd, shell=True)

However, it has some issues.

  1. using shell=True has many downsides, and the worst are the possible security leaks
  2. "open multiple apps in their designated positions" does not work. Not sure why.
0

Some applications support the option --geometry making it possible to launch them at any position with any window size.

Run from commandline ~ $ app-cmd-line-name --help ( for example ~ $ gnome-terminal --help-all or ~ $ xed --help or ~ $ nemo --help ) to see in the list of supported options:

  -g, --geometry=GEOMETRY  Set size and pos. of the window (WIDTHxHEIGHT+X+Y)

Notice that launching the gnome-terminal you need to specify COLSxROWS+X+Y and not pixel WIDTHxHEIGHT+X+Y.

If --help does not work you can also use ~ $manapp-cmd-line-name or ~ $infoapp-cmd-line-name. Find the app-cmd-line-name by checking the content of the command line field of the launcher or looking at the content of the appropriate .desktop file used to launch the application.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .