prevent xbindkeys from overriding mouse click when used for mouse button chording

I am trying to set up some mouse button chording, inspired by the Acme text editor: http://acme.cat-v.org/mouse

On Windows and Mac, I was able to set up the following using AutoHotKey and Hammerspoon respectively:

Left+Middle   = Cut   (Ctrl+x)
Left+Right    = Paste (Ctrl+v)
Middle+Left   = Return
Middle+Right  = Space
Right+Left    = Undo  (Ctrl+z)
Right+Middle  = Redo  (Ctrl+Y / Ctrl+Shift+z)

Middle+Scroll = Switch window

Having moved from Windows to Linux for my main computer, I’m trying to set up the same on Linux. I am running OpenSUSE Tumbleweed with X11 and XFCE.

For simplicity, I’ll focus only on cut (Left+Middle = Ctrl+x) for the following code examples.


Using xev I’ve found that while the left mouse button is down, the state variable is set to 0x100.

So a simple configuration for cut would be:

"xdotool key ctrl+x"
    m:0x100 + b:2

This does not work, and I get the infamous warning of conflicting program:

*** Warning ***
Please verify that there is not another program running
which captures one of the keys captured by xbindkeys.
It seems that there is a conflict, and xbindkeys can't
grab all the keys defined in its configuration file.

m:0x4 + b:2 does work (0x4 being Ctrl), so I’m not sure what the problem is.

Is there a way to get this to work?


Searching around I’ve found some setups for mouse chording, which I was able to adapt to my needs. These requires guile-support in xbindkeys though.

.xbindkeysrc.scm

(define actionperformed 0)

(define (first-binding)
"First binding"
  (xbindkey-function '("b:1") b1-second-binding) ;; Left Mouse Button
)

(define (reset-first-binding)
"Reset first binding"
  (ungrab-all-keys)
  (remove-all-keys)
  (set! actionperformed 0)
  (first-binding)
  (grab-all-keys)
)


(define (b1-second-binding)
"Left Mouse Button"
  (remove-all-keys)
  (ungrab-all-keys)

  (run-command "xdotool mousedown 1 &") ;; <--- THIS DOES NOT WORK! 

  ;; Left+Middle
  (xbindkey-function '("b:2")
    (lambda ()
      ;; Copy
      (run-command "xdotool key ctrl+x&")
      (run-command "xdotool mouseup 1&")
      (set! actionperformed 1)
    )
  )

  ;; Release left
  (xbindkey-function '(release "b:1") 
    (lambda ()
      (unless actionperformed (begin
        (run-command "xdotool mouseup 1&"))) 
      (reset-first-binding)
    )
  )
  (grab-all-keys)
)

(first-binding)

This does make Left+Middle perform a cut operation.

The problem is that the Left click is overridden, and I can therefore not make a simple left click or a text selection.

I added xdotool mousedown 1 to fix this, but it doesn’t work.

xdotool seems to not be able to trigger a mouse-event while the same mouse event is active.

Is there any way to prevent xbindkeys from overriding the normal action of buttons?


I have another mouse with a button 9, using this I can at least set up something.

Since all the button chords must start with the same button I can only really get 3 chords (+ scrolling), which is half of what I want.

Pressing the buttons is also very awkward, and I can’t consider this a permanent solution:

(define (first-binding)
  (xbindkey-function '("b:9") second-binding)
)

(define (second-binding)
  (remove-all-keys)
  (ungrab-all-keys)

  (xbindkey-function '("b:2") (lambda () (run-command "xdotool key ctrl+x")))

  (xbindkey-function '(Release "b:9") reset-first-binding)
)

(define (reset-first-binding)
  (ungrab-all-keys)
  (remove-all-keys)
  (first-binding)
  (grab-all-keys)
)

(first-binding)

Any ideas on ways to solve this?

Asked By: Levi

||

I finally solved it, but the workaround is a bit complicated (compared to the windows and mac versions anyway).

The idea is to use xinput to remap the mouse buttons for the usb mouse, causing the buttons to not function, but retaining the mouse movements.

Then listen to the mouse button events with evtest and trigger the mouse chording commands for detected button combos, or trigger normal button events when no combo is detected.

  1. install xinput and evtest
  2. run evtest and make note of your usb mouse (something like /dev/input/event3)
  3. run xinput -list and make note of the device id
  4. run xinput set-button-map {id} 0 0 0 0 0 0 0 0 0 0 (10 zeroes)
  5. run evtest /dev/input/event3 | ./mouse-chording.pl

content of mouse-chording.pl:

#!/usr/bin/perl -wlnF/s|,/

BEGIN
{
    our %btns         = (272 => 0, 273 => 0, 274 => 0);
    our $active_chord = 0;

    # mouse button codes
    our $left_code   = 272;
    our $right_code  = 273;
    our $middle_code = 274;
    our $scroll_code = 8;
}

# only handle events
next unless  0 == index $_, 'Event';
next unless -1 == index $_, 'SYN';

# extract event values
my ($time, $type, $code, $value) = @F[2, 5, 9, 13];

# skip mouse movements and hi-res scroll
next if $code == 11;
next if $type == 2 && $code != 8;

# keep track of pressed buttons
$btns{$code} = $value;

# emulate button presses
if ( ($code == $left_code   && !($btns{$right_code} || $btns{$middle_code}))
  || ($code == $right_code  && !($btns{$left_code}  || $btns{$middle_code}))
  || ($code == $middle_code && !($btns{$left_code}  || $btns{$right_code})) )
{
    if ($code == $left_code)
    {
        # left button can be held for selecting text
        system $value ? "xdotool mousedown 1&" : "xdotool mouseup 1&";
    }
    elsif($value == 0 && not $active_chord)
    {
        # middle and right will only trigger click - cannot be held
        system "xdotool click " . ($code == $middle_code ? 2 : 3) . "&";
    }

    $active_chord = 0;
    next;
}
# emulate scroll wheel
elsif ($code == 8)
{
    system( sprintf("xdotool click %d&", $value > 0 ? 4 : 5) );
}

# only do chords when button is pressed (not released)
next unless $value;

# Left + Middle = Cut
if ($btns{$left_code} && $code == $middle_code)
{
    system "xdotool mouseup 1&"; # prevent accidental selection after snarf
    system "xdotool key ctrl+c key ctrl+x&";
    $active_chord = 1;
    next;
}

# Left + Right = Paste
if ($btns{$left_code} && $code == $right_code)
{
    system "xdotool key ctrl+v&";
    $active_chord = 1;
    next;
}

END
{
    # release buttons to prevent getting stuck when closing
    system "xdotool mouseup $_&" for (1..3);
}

This solution is maintained along side the windows and mac versions on github: https://github.com/levigutt/mouse-chording/

PRs are welcome! 🙂

Answered By: Levi
Categories: Answers Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.