Script Your Mouse Buttons to Control the i3 Window Manager

This post is a brief technical guide on how to implement a useful tweak that will allow you to control the i3 window manager with the Evoluent mouse. The principles presented here can be adapted to execute any script or command with a mouse button click (or release), so feel free to experiment. They can also be adapted to any window manager or mouse, so if all you care about is calling scripts with mouse actions you came to the right spot. Note to those in a rush: tl;dr at the bottom.

Even though the i3 window manager is very keyboard driven, some users may want to utilize their extra mouse buttons and map them to useful actions. With the i3 IPC this is very simple to do. This article will show how to map the two thumb buttons on the Evoluent Vertical Mouse 4 to cycle through existing workspaces on the currently focused display. The approach presented here can be easily adapted to cycle through all existing workspaces, some set of default ones, or even have the mouse actions trigger a different result entirely.

If you're not familiar with the i3 window manager yet, I strongly urge you to check it out. It's an amazing tiling window manager that weighs in at mere kilobytes and is extraordinarily fast. The default config supports a myriad of workspaces, windows can be infinitely nested using horizontal or vertical splits, and the program has endless customization capabilities.

The Evoluent Mouse is very popular among those who wish to optimize their ergonomic experience. It takes some getting used to because the buttons are on the side, but once the initial adjustment period is done it's much easier on the wrists. It comes with 6 buttons (which can be combined into 10 actions), and for the longest time I wondered why they bothered adding the two thumb buttons, until I realized what I could do with them.

Let's dive in.

Map the Buttons

First, some background. If you are using a gui on linux you are likely using X11, which provides the basic framework for interacting with input devices and drawing the UI. It is built using a nice client server architecture, thus allowing multiple users on a machine, persistent sessions, and even forwarding over SSH. One of the components in X is xinput, which responds to input from peripherals such as keyboard and mice. You can use xinput to configure your mouse (Evoluent or other) in many different ways, and it's even possible to swap buttons or disable them entirely.

You can see the devices attached to your system with xinput list. As you can see, each device has an ID and some details such as vendor and product hex codes. Once you figure out the ID of your mouse, you can remap it to change the button codes, or disable a button by setting the action to 0. On my system, I use the following commands:

ID=$(xinput list | grep Evoluent | awk '{print $8}' | cut -d'=' -f 2)  
xinput --query-state $ID  
xinput --set-button-map $ID 1 3 0 4 5 0 0 8 2 10 0  

What this means is that my first button is action 1 (left click), second button action 3 (right click), third is disabled, 4th and 5th (the scroll wheel) are untouched, and the 8th and 10th are the thumb buttons.

If you are using a different mouse, you can figure out your mouse button codes by launching xev and clicking. You will see a ton of output, and at the bottom there will be a string with the button number. You can verify that you have the right code by running xdotool click N and checking the result.

Now that we've established the mouse button codes, we need to add a listener that will call a script when the buttons are pressed.

Install xbindkeys and i3-py

  1. In order to simplify the process of communicating with i3's IPC, we will install i3-py. You can grab it from pip via pip install i3-py or download from github. Be sure to install it for python2, not python3.
  2. You will also need xbindkeys, which you can install from the default repos on most systems (apt-get install xbindkeys). Once you install the program, run xbindkeys --defaults > ~/.xbindkeysrc to populate the default config file.

Connecting the Script

Create a new file called hop.py under ~/bin (or some other location in your $PATH), and paste in the following script:

#!/usr/bin/env python
import i3  
import sys

# quit if not enough args
if len(sys.argv) < 2:  
    print "Usage: python hop.py up|down"
    sys.exit(-1)

 # figure out if we are going up one level or down
up = sys.argv[1] == 'up'

# get all workspaces
works = i3.get_workspaces()

# get display output
output = str([w['output'] for w in works if w['focused']][0])  
cur_name = str([w['name'] for w in works if w['focused']][0])

# get list of workspaces on this display
candidates = [str(w['name']) for w in works if w['output'] == output]

# get index of current workspace
index = candidates.index(cur_name)

# get target workspace name
if up:  
    target = (index + 1) % len(candidates)
else:  
    target = (index - 1) % len(candidates)

i3.command('workspace', candidates[target])  

As you can see, the script is extremely trivial. We pull in the i3 binds, figure out if we are going up one workspace or down, and call the i3 command. This script can easily be adapted to cycle through all open workspaces rather than only the ones on the active monitor, as well as having it cycle through a predefined set of workspaces even if they do not currently exist. It's also possible to add more commands to i3, for example you can have the status bar appear for half a second before the jump happens in case you want to see the names of the workspaces.

Configure xbindkeys

Finally, we need to set up xbindkeys to call our script when our mouse buttons are pressed. You can do that by simply adding the following lines at the bottom of ~/.xbindkeysrc. Be sure to modify the path and script name accordingly, as well as the button IDs if your mouse is different from mine.

"/home/<user>/bin/hop.py up"
  b:8
"/home/<user>/bin/hop.py down"
   b:10
"/home/<user>/bin/hop.py up"
  Mod4 + b:8
"/home/<user>/bin/hop.py down"
  Mod4 + b:10

You'll notice that I also mapped Mod4 + b:10, which allows me to trigger the script if I have my Meta key held down on my keyboard. The reason for this is I set my statusbar to hide by default, so at times it's useful to see the names of the workspaces as a cycle through them. If your Mod key is different in your i3 config adjust this line accordingly.

tl;dr

  1. Use xinput to get mouse button IDs and map accordingly.
  2. Install xbindkeys, generate default config with xbindkeys --defaults > ~/.xbindkeysrc. Append the lines from section above.
  3. Install i3-py and paste in contents of my script that hook into IPC binds, adjust script as needed.

Enjoy! I look forward to seeing your articles about other interesting tricks possible with this mechanism.