2020-11-28

Window Tiling with Keyboard Shortcuts in Linux

On macOS I used BetterTouchTool to configure a bunch of keyboard shortcuts that allowed me to move and resize windows. This article describes how to setup your own system for window tiling with shortcuts on Linux. I don’t want to use a tiling window manager but I still want to be able to quickly make sure that the whole screen is covered with windows and no room is unused.

1 Arranging Windows with xdotool

xdotool is a command line tool to interface the X server (X11) on a unix based operating system. Here I’m using it to change the size and position of windows but it can do more than that. Have a look at its manpage if interested. The following script is a simple example on xdotool usage.

At first the script is querying the system for the connected monitor’s size and stores this information in the variables SW and SH for screen width and height, respectively.

Then the function set_win is defined which takes a position on the x and y axis as well as the size of a window as an input. The position is defined with the origin in the screen’s upper left corner with the y axis increasing downwards and the x axis to the right hand side. It will change the properties of the window that is currently in the foreground.

In the last line, the function set_win is called and is told to set the window’s position to (0, 0) which means the upper left corner of the window is in the upper left corner of the screen. The window’s size is set to (SW/2, SH), meaning the window will cover half the width of the screen and all of its height.

# grabbing screen width and height
SIZE=`xdpyinfo | awk '/dimensions/{print $2}'`
SW=`echo $SIZE | cut -f1 -dx`
SH=`echo $SIZE | cut -f2 -dx`

# function that changes a window's position and size
function set_win {
    # args: pos_x, pos_y size_x, size_y
    xdotool getactivewindow windowsize $3 $4
    xdotool getactivewindow windowmove $1 $2
}

set_win 0 0 $((SW/2)) $SH

The next snippet can be extended to take a parameter as an input and based on this parameter decide the arrangement of the window. The following section deals with a definition of these parameters.

case "$1" in
    lefthalf)  set_win 0         0  $((SW/2))  $SH ;;
    righthalf) set_win $((SW/2)) 0  $((SW/2))  $SH ;;
    # ... other layouts here ...
esac

The "$1" in the case statement refers to the first parameter that script was given, i.e. ./script.sh param1. If the script is invoked with lefthalf as its parameter, it will move the currently selected window accordingly.

And this is already all that’s necessary to move windows around using a script. With more cases in the switch-case statement, more possible window positions can be set. The full script that I use on my computer is given at the bottom of this page. The sections in between explain how keybindings in Gnome can be used to control a window’s position and size.

2 Keybindings for Window Layouts

There are numerous ways of arranging windows on a screen and remembering the shortcut for all of them can be difficult if the shortcut is chosen arbitrary. This is why I chose groups of keys whose position on the keyboard resemble the layout of windows I want to generate.

What that means in detail is shown in the figure below. The keys U, I, J and K form a square on the keyboard and once either of them is pressed, the active window will be sized to cover the top-left, top-right, bottom-left or bottom-right quarter of the screen.

Of course, changing a window’s position and size doesn’t happen when only the key is pressed, but will require a key combination. I like to go with Ctrl Alt <Key> to trigger the shortcut.

Shortcuts to arrange windows. The middle of the image shows parts of a keyboard. If either key is pressed together with the previously defined modifiers (e.g. Ctrl Alt) the window will move to the desired location. The window locations are shown in blue as the part of the screen that is occupied by a certain window.

Ctrl Alt D, Ctrl Alt T are already bound to a certain functionality in Ubuntu by default: Hiding all windows and opening a terminal, respectively. Ctrl Alt Left/Right are used to switch between workspaces by default. As I’m using the window tiling more frequently than the other functions I disabled them.

3 Setting the Shortcuts up in Gnome

I didn’t just want to setup the shortcuts manually using the GUI, but programmatically with a script. This allows me to reapply the same settings over multiple computers, propagate updates easily and have my preferred settings immediately on new machines.

For this purpose, I wrote a very small Python wrapper called gshortcuts (github) that interfaces Gnome’s command line interface, gsettings, to change the defined shortcuts.

3.1 Creating Custom Shortcuts

The following Python snippet shows how custom shortcuts can be created with gshortcuts. It uses the dictionary shortcut_keys to hold a key that a user can press and the according parameter that is given to the previously described script that updates a window’s size and position.

For example, if the key D is pressed together with Ctrl Alt, the script window_snaps.sh will be launched with the parameter leftthird, similar to launching it from the command line with ./window_snaps.sh leftthird.

from gshortcuts.custom_actions import set_new_shortcut

shortcut_keys = {
    # key: keyboard key, value: parameter for script
    "D": "leftthird",
    "Right": "lefthalf",  # right arrow key
    # ...
}

for key, cmd in shortcut_keys.items():
    set_new_shortcut(
        name=f"window_tiling_cmd_{cmd}",  # name of the shortcut
        command=f"/path/to/window_snaps.sh {cmd}",  # shell command to execute
        binding=f"<Ctrl><Alt>{key}"  # keyboard binding
    )

The for-loop then generates a new key binding for every key-value pair in the dictionary. The shortcut consists of a name which is displayed in the settings GUI, a command to execute (in this case the bash script that changes a window’s location and size with the corresponding parameter), and a set of keyboard keys that trigger this command. I chose to go with Ctrl Alt <key>.

3.2 Disabling default Shortcuts

In addition to setting up custom shortcuts, gshortcuts also comes with the ability (and some examples) to disable functionality that is activated in Gnome by default.

from gshortcuts.predefined_actions import *

disable_show_desktop()
disable_launch_terminal()
disable_switch_workspace_left_right()

These functions will disable Ubuntu’s standard keyboard bindings for Ctrl Alt D, Ctrl Alt T, Ctrl Alt Left and Ctrl Alt Right. Those are just a few examples and the project’s readme and it’s source code give advice on how to adjust more of Gnome’s settings.

4 My full window_snaps.sh Script:

The version of the script that I’m using doesn’t describe window positions in words like lower_left_quarter but rather with the keyboard keys that should trigger those positions.

#!/bin/bash

# Marius Montebaur, Nov 2020
# montebaur.tech, github.com/montioo

if ! command -v xdotool &> /dev/null
then
    notify-send "Setting window position failed." \
        "Install xdotool to fix this. On Ubuntu run 'sudo apt install xdotool'"
    exit
fi

# screen width and height
SIZE=`xdpyinfo | awk '/dimensions/{print $2}'`
SW=`echo $SIZE | cut -f1 -dx`
SH=`echo $SIZE | cut -f2 -dx`

function set_win {
    # args: pos_x, pos_y size_x, size_y
    # don't move Desktop
    if [ "$(xdotool getactivewindow getwindowname)" != "Desktop" ]; then
        xdotool getactivewindow windowsize $3 $4
        xdotool getactivewindow windowmove $1 $2
    fi
}

# To understand how the keys are ordered, look at their placement on the keyboard.
case "$1" in
    # quaters
    U)  set_win 0           0           $((SW/2))   $((SH/2)) ;;
    I)  set_win $((SW/2))   0           $((SW/2))   $((SH/2)) ;;
    J)  set_win 0           $((SH/2))   $((SW/2))   $((SH/2)) ;;
    K)  set_win $((SW/2))   $((SH/2))   $((SW/2))   $((SH/2)) ;;

    # thirds
    D)  set_win 0           0           $((SW/3))   $SH       ;;
    F)  set_win $((SW/3))   0           $((SW/3))   $SH       ;;
    G)  set_win $((2*SW/3)) 0           $((SW/3))   $SH       ;;

    # two thirds
    E)  set_win 0           0           $((2*SW/3)) $SH       ;;
    T)  set_win $((SW/3))   0           $((2*SW/3)) $SH       ;;

    # sixths
    # Br, As and Sk correspond to [, ' and ;
    O)  set_win 0           0           $((SW/3))   $((SH/2)) ;;
    P)  set_win $((SW/3))   0           $((SW/3))   $((SH/2)) ;;
    Br) set_win $((2*SW/3)) 0           $((SW/3))   $((SH/2)) ;;
    L)  set_win 0           $((SH/2))   $((SW/3))   $((SH/2)) ;;
    As) set_win $((SW/3))   $((SH/2))   $((SW/3))   $((SH/2)) ;;
    Sk) set_win $((2*SW/3)) $((SH/2))   $((SW/3))   $((SH/2)) ;;

    # halfs
    # RA, LA and EN correspond to ->, <- and Enter
    RA) set_win $((SW/2))   0           $((SW/2))   $SH       ;;
    LA) set_win 0           0           $((SW/2))   $SH       ;;
    # full screen
    EN) set_win 0           0           $SW         $SH       ;;
esac

5 Further Improvements

Currently, the script for setting a window’s position and size only supports one monitor. Since a lot of people are using multi monitor setups, adding support for that would be a valuable improvement. I don’t need this feature at the moment but will add it here should I decide to extend the script.