Today’s post is a fairly short one. I’ve used Thinkpads for quite a while, first a T450s, then a T495s. I’m a huge fan of them, even the current generations. One thing I especially like is the button layout: because of the trackpoint (a.k.a. the “nub” mouse pointer), I get an extra set of physical buttons above my trackpad, including a middle mouse button. I find these buttons absolutely invaluable to my minute-to-minute usage of my laptop.

The problem began when I had to replace my T495s keyboard due to a fault. I needed a replacement quick, so official Lenovo parts were out. I ended up settling on a relatively cheap Amazon replacement from an off-brand. While the keyboard itself was relatively fine, I was almost immediately struck by a major problem, and one that seems to plague many Thinkpad users: the mouse would move by itself due to the faulty sensor in the trackpoint. This is often called “trackpoint drift”.

This is an extremely annoying condition, since it not only moves the mouse in an unwanted way, but can often completely override the trackpad input. So I wanted to find a solution to stop this. Luckily for me, I don’t actually use the trackpoint for it’s mouse movement functions at all, so my first thought turned to disabling it entirely.

The problem is that, of course, this thing is just a mouse. If you turn off the trackpoint, you also turn off its buttons. So that option was completely out.

Next, I did some searching on ways to disable just the mouse functionality while retaining the buttons. This is much harder than you might think (or, is exactly as hard as you may think, depending on perspective).

Luckily though I was able to stumble upon a random Arch Linux forums thread where someone posted a hacky (elegant) solution to this. Specifically, post #6 from the user “k395” mentions a solution he came up with that leverages the evtest command (Debian package evtest) to capture the mouse events from the device, and then use a Perl wrapper to the xdotool command (Debian package xdotool) to manually generate the appropriate mouse button events. What a solution!

sudo evtest --grab /dev/input/event21 | perl -ne 'system("xdotool mouse".($2?"down ":"up ").($1-271)) if /Event:.*code (.*) \(BTN.* value (.)/'

“k395”’s one-liner solution

I had to do a bit of modification here though. First of all, I needed to determine exactly what /dev/input/event node was the one for my trackpoint. Luckily, running evtest with no arguments enters an interactive mode that lets you see what each event node maps to. Unfortunately I haven’t found a way to get this programatically, but these seem to be stable across reboots so simply grabbing the correct value is sufficient for me. In my case, the node is /dev/input/event6 for the Elantech TrackPoint.

$ sudo evtest
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0:      AT Translated Set 2 keyboard
/dev/input/event1:      Power Button
/dev/input/event2:      Lid Switch
/dev/input/event3:      Video Bus
/dev/input/event4:      Sleep Button
/dev/input/event5:      Power Button
/dev/input/event6:      ETPS/2 Elantech TrackPoint
/dev/input/event7:      ETPS/2 Elantech Touchpad
/dev/input/event8:      PC Speaker
/dev/input/event9:      ThinkPad Extra Buttons
/dev/input/event10:     HD-Audio Generic HDMI/DP,pcm=3
/dev/input/event11:     HD-Audio Generic HDMI/DP,pcm=7
/dev/input/event12:     HD-Audio Generic HDMI/DP,pcm=8
/dev/input/event13:     Integrated Camera: Integrated C
/dev/input/event14:     Integrated Camera: Integrated I
/dev/input/event15:     HDA Digital PCBeep
/dev/input/event16:     HD-Audio Generic Mic
/dev/input/event17:     HD-Audio Generic Headphone
Select the device event number [0-17]: ^C

Getting the list of event inputs for my system

But that wasn’t the only issue. Unfortunately this basic implementation lacks support for the middle mouse button, and given that I use it quite extensively (for Linux middle-button quickpaste, closing tabs in Firefox, etc.), I needed that functionality.

This prompted me to rewrite the Perl-based one-liner into a slightly easier to read Python version, and implemented middle button support as well. I also added a bit of debouncing to avoid very rapid presses resulting in 2 xdotool events in very rapid succession. I then put everything together into a script which I called disable-trackpoint.

#!/bin/bash
set -o xtrace

# We assume display 0 on the laptop
export DISPLAY=:0
# Event ID found via evtest
event_id=6

# Grab events and pipe to our xdotool parser
/usr/bin/evtest --grab /dev/input/event${event_id} | /usr/bin/python3 -c '
from os import system
from sys import stdin
from time import sleep
from re import search, sub
last_time = 0.0
for line in map(str.rstrip, stdin):
    if search(r"^Event", line) and search(r"EV_KEY", line):
        event = line.split()
        time = float(event[2].strip(","))
        button = sub(r"\W+", "", event[8])
        action = event[10]

        # Debounce button presses to 0.05 seconds
        if action == "1":
            if (time - 0.05) < last_time:
                continue
            last_time = time

        # Action 1 is a "down" (press), 0 is an "up" (release)
        if action == "1":
            cmd = "mousedown"
        else:
            cmd = "mouseup"

        # Buttons generally map this way: Left mouse is 1, middle is 2, right is 3, wheel up is 4, wheel down is 5.
        if button == "BTN_LEFT":
            btn = "1"
        elif button == "BTN_MIDDLE":
            btn = "2"
        elif button == "BTN_RIGHT":
            btn = "3"

        # Run xdotool with cmd and btn
        system(f"/usr/bin/xdotool {cmd} {btn}")
'

The disable-trackpoint script

There is one major downside here: this script does not function properly under Wayland. It seems to work but button presses are mapped to incorrect windows. You must use Xorg for this to work. For me that’s not a huge deal as I’ve never really found much benefit to one over the other, but it’s worth noting if you try this yourself.

Finally, I set this script to run automatically in a systemd unit file which will start it on boot and ensure it keeps trying to start until the display is initialized.

[Unit]
Description = Fix trackpoint problems by disabling it
Wants = multi-user.target

[Service]
Type = simple
ExecStart = /usr/local/sbin/disable-trackpoint
Restart = on-failure
RestartSec = 5
StartLimitInterval = 5
StartLimitBurst = 99

[Install]
WantedBy = multi-user.target

The disable-trackpoint service unit (at /etc/systemd/system/disable-trackpoint.service)

One systemctl enable later, and there we go: I have a disabled trackpoint but with enabled buttons, and can finally stop chasing my mouse cursor across the screen! While this is certainly a dirty hack, spawning a lot of processes, it does seem to work for me and hopefully someone else too.