Use a mouse button as a modifier in Linux by using xbindkeys

Logitech mouse with side buttons

The default behavior of the extra buttons on the side of a mouse is forward/backward navigation. I rarely use that type of navigation, as I usually open every link in a new browser tab and switch between them instead. Personally, this functionality is at best useless and at worst annoying when I accidentally press "backwards" while reading a page.

There are many ways to repurpose these buttons, using manufacturer provided software or within specific applications and games. In my experience, these tools are always limited to simple mapping to keyboard combinations. I would like to move beyond that, by transforming the buttons into modifiers.

The solution

The functionality I had in mind was to control the system volume using the scroll wheel, but only while the modifier button (formerly the "backward" button) was pressed. Thankfully, I found a forum thread on LinuxQuestions.org where the user Zero Angel implemented something similar using xbindkeys.

xbindkeys, as the name suggests, binds key and mouse button events to shell commands. In addition to supporting a simple mapping from button to command, like described above, it also accepts a script as a configuration file at ~/.xbindkeysrc.scm, making complex logic possible. The script must be written in Guile Scheme.

Included below is an adaptation of Zero Angel's script, with modifications for my music player and volume control purposes, and some helpful comments. To use it yourself, you'll want to:

  1. Install xbindkeys (and playerctl for the music player controls) using your package manager.
  2. Find out what number each mouse button corresponds to (e.g. use xev -event button). Usually, button 8 is "backwards", and 9 is "forwards".
  3. Decide which button you want to use as the modifier. Here, I used "b:8", i.e. the "backwards" button. Simply replace all occurrences of it in the script if you want to use another.
  4. Define the button mappings in effect when the modifier is pressed, by modifying each run-command inside the enter-mouse-modifier function. As it stands, the mappings are:
    • Left button "b:1" → previous track
    • Middle button "b:2" → toggle mute
    • Right button "b:3" → next track
    • Scroll wheel up "b:4" → volume up
    • Scroll wheel down "b:5" → volume down
    • If none of the above are pressed, and the modifier "b:8" is released → toggle pause
  5. Run xbindkeys --nodaemon to test your script. Press Ctrl+C to stop it, make your changes to the script, and repeat.
  6. When you're satisfied with the result, find a way to run it automatically at startup. I added xbindkeys to my existing ~/.xinitrc file. If you use a desktop environment such as Gnome or KDE, look for their way of running things at startup instead.

The script

;; ~/.xbindkeysrc.scm
;; Makes a mouse button into a modifier.
;; Credit: Zero Angel at linuxquestions.org

;; Tracks whether anything was pressed
;; while the modifier was active.
(define actionperformed 0)

(define (install-modifier-binding)
  ;; Use button 8 as modifier
  (xbindkey-function '("b:8") enter-mouse-modifier))

(define (leave-mouse-modifier)
  ;; Reset everything!
  ;; Stop processing keys
  (ungrab-all-keys)
  ;; Remove all bindings
  (remove-all-keys)
  ;; Reset mouse chord state
  (set! actionperformed 0)
  ;; Reset modifier keys
  (run-command
    "xdotool keyup ctrl keyup alt keyup shift keyup super&")

  ;; Add bindings again
  (install-modifier-binding)
  ;; Start processing keys again
  (grab-all-keys))

(define (enter-mouse-modifier)
  ;; Stop processing keys
  (ungrab-all-keys)
  ;; Remove all bindings
  (remove-all-keys)

  ;; Add the relevant bindings back

  ;; If pressing the left button...
  (xbindkey-function
    '("b:1")
    (lambda ()
      ;; Previous track.
      (run-command "playerctl previous")
      (set! actionperformed 1)))

  ;; If pressing the middle button...
  (xbindkey-function
    '("b:2")
    (lambda ()
      ;; Mute.
      (run-command "pactl set-sink-mute @DEFAULT_SINK@ toggle")
      (set! actionperformed 1)))

  ;; If pressing the right button...
  (xbindkey-function
    '("b:3")
    (lambda ()
      ;; Next track.
      (run-command "playerctl next")
      (set! actionperformed 1)))

  ;; If scrolling up...
  (xbindkey-function
    '("b:4")
    (lambda ()
      ;; Volume up.
      (run-command "pactl set-sink-volume @DEFAULT_SINK@ +5%")
      (set! actionperformed 1)))

  ;; If scrolling down...
  (xbindkey-function
    '("b:5")
    (lambda ()
      ;; Volume down.
      (run-command "pactl set-sink-volume @DEFAULT_SINK@ -5%")
      (set! actionperformed 1)))

  ;; When the modifier is released...
  (xbindkey-function
    '(release "b:8")
    (lambda ()
      ;; If no other key was pressed...
      (if (= actionperformed 0)
        ;; Pause.
        (run-command "playerctl play-pause"))

      ;; Finally, leave the modifier.
      (leave-mouse-modifier)))

  ;; Bindings are set,
  ;; start processing keys again.
  (grab-all-keys))

;; Main
(install-modifier-binding)