Adding touch controls to the i3 Window Manager
30th September 2016 - Linux
A while back I got myself a second hand Surface Pro 1. It's a great device, and with a little tweaking I got it running Linux just fine. At first I started out with what I was currently using as the operating system on my main machine. Keeping all my machines running the same Linux flavour makes setting them up and maintaining them easier, not to mention that I don't have to remember which programs are used on the different machines for the same task (file managers and such are typically integrated with the desktop environment and will vary between Linux distributions). This distribution worked fine, it was running XFCE which meant that it had a graphical approach that translated well to a touch-screen scenario. Maybe not as well as something like Gnome 3 which is created partially for touch devices but well enough. But when I changed my main machine to run Manjaro i3 Community edition things got a bit more complicated. The window manager i3 is created with a design philosophy of getting out of the way and letting the user control their windows by using only the keyboard. This means that there is very little in the way of window decoration, in fact I have turned of everything but a slight border. It's also a tiling window manager meaning that instead of creating floating windows that the user can drag around it aligns all the windows in a neat grid. This is great for my normal usage with a keyboard, but for a touch enabled setup this was less than ideal.
Taking inspiration
I'm not sure when the idea first occurred to me but I knew that there had to be a better way. Looking at most touch-first devices however they seem to be pretty limited in their window support. It's only fairly recently that Android supports splitting the screen between two applications and iOS doesn't have any such feature as far as I'm aware. Of course the Surface line of lap-top/tablet hybrids from Microsoft is the notable exception in that they include the full Windows operating system. This rendition however didn't please me much, their solution to the layout problem is simply to make window decoration comically big to make them easier to touch thus taking up large amounts of space on already small screens. While working on my laptop I realised that really the only thing I needed in order to at least be able to use the device was some simple controls for window management.
Problems with implementing the solution
As many of you might know i3 isn't really intended for this kind of usage. In fact its very spartan design philosophy means that it doesn't even support many of the more common panel or dock configurations. Notable lack of support is for dock that stay on an edge but hovers over windows and panels and docks along the sides of the screen (top and bottom still works). This meant that the options were fairly limited when it came to selecting a dock or panel implementation to apply my controls to. I landed on using the panel from XFCE4, it's highly customisable and focuses more on extensibility than having all the bells and whistles out of the box. Setting up the panel was fairly straight forward. I played around with various transparency settings and styles but landed on simply creating it the same solid colour as my i3 bar as it made the entire bar area look like a single system and gave it a unified look.
To show and hide the panel I wanted to use the soft-button on the device as it wasn't used for anything else. It however gives the same keycode as a left super button (the one typically marked with the Windows logo) and is indistinguishable from the actual left super key on an attached keyboard. This meant that pressing super, which is my i3 modifier key, would mean toggling the bar which would get annoying fast. So I wrote a script which check for connected devices and made sure that neither the type-cover nor any USB device labelled keyboard was connected in which case it toggles the panel. Unlike i3 the XFCE panel doesn't really support any way of control through bash scripts or an IPC. So in order to hide and show the bar I had to use a hack of polling the X server for windows and then map/unmap the one belonging to the XFCE4 panel. There is however a weird issue with this were an extra window named "xfce4-panel" appears in the hierarchy when the first one is hidden. So simply getting the only XFCE4 panel instance won't work as it will show this random blank window from time to time. To get around this I check the height of the window and make sure it is the height of the panel before I show and hide it, it's ugly but it works.
A slight issue was also with how window focus was handled as i3 works by issuing commands to the currently focused window. When selecting options in the XFCE4 panel it would hide the option menu before it ran the command. This meant that the command would always be applied to the window below the option selection menu as the focus would follow the cursor which just landed there, this was however easily solved by turning off the feature in i3 were focus follows the mouse and rather requires a click or keyboard navigation to explicitly give focus to a window. It took a bit of getting used to but it works perfectly with the touch solution.
The end result
After tinkering with this for a while I finally landed on something I liked. I created a short video shown below to highlight how everything works. As far as the setup goes it's pretty self explanatory how to actually add the controls to the XFCE4 panel as they are all simply commands to i3 or launching other programs (most notably the program launcher Panther, which works well with touch controls). For the script that shows and hides the bar it is very tailored to how I specifically wanted my setup to work and the quirks of my computer. Because of that there isn't much value in sharing it but I will add it here none the less if anyone wants to modify it for their use.
#!/bin/bash
WID=$(xwininfo -root -tree -children | grep "xfce4-panel" |
grep "x66" | { read first _ ; echo $first; }) # Get the window ID of the panel
STATE=$(xwininfo -id $WID | grep "Map State:" | head -n1 |
awk -F: '{print $2}' | xargs) # Get current map state
# This is the detachable Surface keyboard
SAM=$(ls /dev/input/by-id | grep "usb-MICROSOFT_SAM_0.1.0000-if02")
# This is a general search for keyboard devices, matches my bluetooth keyboard
KEYBOARD=$(ls /dev/input/by-id | grep "Keyboard")
# If no keyboard is attached
if [ "$SAM" = "" ] && [ "$KEYBOARD" = "" ] ; then
# Disable fullscreen if enabled
i3-msg -q "fullscreen disable"
if test "$STATE" = "IsUnMapped" ; then
xdotool windowmap "$WID"
else
xdotool windowunmap "$WID"
fi
else
xdotool windowunmap "$WID"
fi