0

Long story short: I have a keyboard with a touchpad which is recognized as a "pointer" on xinput, and has "keyboard pointer" capabilities in libinput (in opposition to being recognized as a touchpad). The libinput property "Disable-w-typing" is not avaliabe (it has "n/a" as the value on "libinput list-devices"). Also Ubuntu doesn't recognize it as a touchpad, so I can't use the Ubuntu embedded solution for disabling the touchpad while typing.

Reading through lots of related questions here and elsewhere, I've managed to adapt this python script to my problem. Here's my version of it:

import os
import time 
import subprocess
import threading

def main():
    touch = os.popen("xinput list --id-only 'pointer:SINO WEALTH USB KEYBOARD'").read()[:-1]
    keyboard = os.popen("xinput list --id-only 'keyboard:SINO WEALTH USB KEYBOARD'").read()[:-1]
    subprocess.call('xinput set-prop '+touch+' 142 1', shell=True)
    p = subprocess.Popen('xinput test '+keyboard, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    clickTime = [0, 0]
    def checkTime():
        keys = [37, 50, 62, 64, 105, 108, 133]
        while True:
            out = p.stdout.readline()
            if len(out) < 1:
                break
            key = int(out.split()[-1])
            if key not in keys:
                clickTime[0] = time.time()

    t = threading.Thread(target=checkTime)
    t.start()

    lastTime = 0
    touchpad = True
    while True:
        inactive = time.time() - clickTime[0]
        # print ('inactive for', inactive)
        if inactive > 1:            
            if not touchpad:
                print ('Enable touchpad')
                subprocess.call('xinput set-prop '+touch+' 142 1', shell=True)
            touchpad = True
        else:
            if touchpad:
                print ('Disable touchpad')
                subprocess.call('xinput set-prop '+touch+' 142 0', shell=True)
            touchpad = False
        time.sleep(0.5)

    retval = p.wait()

if __name__ == '__main__':
    main()

The script works just fine. As soon as I start typing the touchpad is disabled. The only problem is that it takes about 1s for the touchpad to get enabled back, which is kinda long, and I haven't found no way to make this delay smaller. Setting "time.sleep(0.5)" to a smaller number seems like an obvious choice, but setting it to 0.05 for example, only seems to make the script more cpu-hungry, but it makes no visible change on the delay between me stop typing and the touchpad getting reactivated.

My goal precisely is to able to deactivate the touchpad while typing and get the touchpad activated back around 300ms after I stop typing.

I don't need to solve this problem using python necessarily, but that's the only way I was able to address it on the first place. As answers, I can accept suggestions for changing this very python script, or maybe guidance on how to solve this with a bash script, or really any idea that guide me to solve this (thinking outside the box is welcome also).

Running Ubuntu 19.04.

2 Answers2

0

Polling the inactive time at regular intervals as your current script does means that the touchpad will only be en-/disabled at a specific frequency. This leads to the following trade-off:

  • If the interval is too long, the time until the touchpad is enabled or disabled can be as long as the interval, in your case half a second.
  • Polling at a high frequency (e.g. every millisecond) would make the script react very fast, but leads to higher CPU load.

Conceptually, you can do the following:

  • Wait until a keyboard event occurs.
  • Disable the touchpad.
  • Based on the latest key event timestamp, the current system time and the delay after which you want to enable the touchpad again, you can calculate when you need to check for key events again and sleep until then. If after that time no additional key events have occured, enable the touchpad. Otherwise, calculate when you have to check again and repeat this step.

For example: A key was pressed at time 0ms. You want to enable the touchpad again after 350ms, so you know you can sleep 350ms. When you wake up, you see that another key was pressed at time 250ms. Based on that timestamp, the current system time (350ms) and the specified delay (350ms), you now know that you need to check 350ms after the last key event, i.e. at time 600ms, so you can sleep for 250ms again.

This way, you can make sure that the touchpad is immediately disabled when a key was pressed, and enabled again very close to 350ms after the last key was released without having to poll at a high frequency.


This script uses python-evdev to read key events. This has the advantage that we don't have to do any threading ourselves and we can easily wait for key events using a selector. The drawback is that the script needs read permissions on the keyboard's evdev device node so it has to be run as root (unless you want to add your user to the input group or change permissions by a udev rule).

Run sudo apt install python3-evdev to install python-evdev for python3. Change KEYBOARD_NAME, TOUCHPAD_NAME and DELAY to the desired values:

#!/bin/env python3

import subprocess
import time
import evdev
from evdev import ecodes
import selectors
from selectors import DefaultSelector, EVENT_READ


DELAY = 0.35 # time in seconds after which the touchpad will be enabled again

KEYBOARD_NAME = "SINO WEALTH USB KEYBOARD" # the name as shown by evtest
TOUCHPAD_NAME = "pointer:SINO WEALTH USB KEYBOARD" # the name as shown by xinput list

lastKeyPress = 0
touchpadDisabled = False
touchpadId = -1

ignoreKeycodes = [ecodes.KEY_LEFTCTRL, ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT, ecodes.KEY_LEFTALT, ecodes.KEY_RIGHTCTRL, ecodes.KEY_RIGHTALT, ecodes.KEY_LEFTMETA]

def getKeyboard():
    for device in evdev.list_devices():
        evdevDevice = evdev.InputDevice(device)
        if evdevDevice.name == KEYBOARD_NAME:
            # If touchpad and keyboard have the same name, check if the device has an ESC key (which a touchpad probably doesn't have)
            caps = evdevDevice.capabilities()
            if ecodes.EV_KEY in caps:
                if ecodes.KEY_ESC in caps[ecodes.EV_KEY]:
                    return evdevDevice
        evdevDevice.close()
    raise OSError("Unable to find keyboard: " + KEYBOARD_NAME)

def updateLastKeypress(event):
    global lastKeyPress
    if event.type == ecodes.EV_KEY:
        if not event.code in ignoreKeycodes:
            lastKeyPress = event.timestamp()

def enableTouchpad(force=False):
    global touchpadDisabled, touchpadId
    if touchpadDisabled or force:
        process = subprocess.run(["xinput", "set-prop", str(touchpadId), "143", "1"])
        touchpadDisabled = False

def disableTouchpad(force=False):
    global touchpadDisabled, touchpadId
    if not touchpadDisabled or force:
        process = subprocess.run(["xinput", "set-prop", str(touchpadId), "143", "0"])
        touchpadDisabled = True

def main():
    global touchpadId
    keyboard = getKeyboard()
    touchpadId = subprocess.check_output(["xinput", "list" , "--id-only", TOUCHPAD_NAME]).decode("UTF-8").strip() # this will raise an exception if it fails since xinput will exit with non-zero status.
    selector = selectors.DefaultSelector()
    selector.register(keyboard, selectors.EVENT_READ)

    while True:
        enableTouchpad()
        for key, mask in selector.select(): # this is where we wait for key events. Execution blocks until an event is available.
            device = key.fileobj

            while True: # we will stay in this loop until we can enable the touchpad again
                try:
                    for event in device.read():
                        updateLastKeypress(event)
                except BlockingIOError: # this will be raised by device.read() if there is no more event to read
                    pass
                timeToSleep = (lastKeyPress + DELAY) - time.time()
                if timeToSleep <= 0.005: # you can set this to 0, but that may result in unnecessarily short (and imperceptible) sleep times.
                    # touchpad can be enabled again, so break loop.
                    break
                else:
                    # disable touchpad and wait until we need to check next. disableTouchpad() takes care of only invoking xinput if necessary.
                    disableTouchpad()
                    time.sleep(timeToSleep)

if __name__ == "__main__":
    try:
        main()
    except:
        # make sure the touchpad is enabled again when any error occurs
        enableTouchpad(force=True)
        raise
danzel
  • 6,044
  • Both of your answers work perfectly. I'm so amazed! You've changed the "set-prop" to 143 instead of 142 (which is the correct one for my device), but aside from that, it worked instantaneously on my tablet-PC. Not sure which one to choose as the correct one. Would you please elaborate on the advantages of using a selector instead of doing the threading ourselves? Do you think it has a better overall performance (being less CPU hungry), or is it just better code practice? – Miguel Prytoluk Jun 12 '19 at 21:52
0

EDIT:

This is probably the proper answer to your question. I managed to make it work independent of key repeat settings, and it is more thread-safe than the previous version. I'll leave the other answer and make it a pure evdev solution for people using Wayland (when I get the time).

import time 
import subprocess
import threading
from queue import Queue, Empty

ENABLE = True # only for readability
DISABLE = False

TOUCHPAD_NAME = 'pointer:SINO WEALTH USB KEYBOARD'
KEYBOARD_NAME = 'keyboard:SINO WEALTH USB KEYBOARD'
DELAY = 0.3

def setDeviceEnabled(id, enabled):
    subprocess.call(['xinput', 'set-prop', str(id), 'Device Enabled', '1' if enabled else '0'])
    print('enabled' if enabled else 'disabled')

def main():
    touchpadId = subprocess.check_output(['xinput', 'list' , '--id-only', TOUCHPAD_NAME]).decode('UTF-8').strip()
    keyboardId = subprocess.check_output(['xinput', 'list' , '--id-only', KEYBOARD_NAME]).decode('UTF-8').strip()
    p = subprocess.Popen('xinput test ' + str(keyboardId), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    # we queue events to make sure the main thread doesn't miss consecutive events that happen too fast.
    eventQueue = Queue()

    def eventProducer():
        keysPressed = 0 # the number of keys currently pressed. We only need to enable/disable the touchpad if this transitions to/from 0, respectively.
        ignoreKeyCodes = [37, 50, 62, 64, 105, 108, 133]
        while True:
            out = p.stdout.readline()
            if len(out) < 1:
                break
            event = out.split()
            if not event[0] == b'key': # only react to key events. Enabling a real touchpad results in a "what's this" event on all input devices
                continue
            keyCode = int(event[2])
            if keyCode not in ignoreKeyCodes:
                if event[1] == b'press':
                    keysPressed += 1
                    if keysPressed == 1: # transition from 0 to 1 keys, disable touchpad
                        eventQueue.put([DISABLE])
                else:
                    keysPressed -= 1
                if keysPressed < 1: # transition from 1 to 0 keys, enable touchpad
                    keysPressed = 0 # in case we missed a press (e.g. a key was already pressed on startup), make sure this doesn't become negative
                    eventQueue.put([ENABLE, time.time()])

    t = threading.Thread(target=eventProducer)
    t.start()

    touchpadEnabled = True
    latestEvent = eventQueue.get()
    try:
        while True:
            if latestEvent[0] == DISABLE:
                if touchpadEnabled:
                    setDeviceEnabled(touchpadId, False)
                    touchpadEnabled = False
                latestEvent = eventQueue.get()
            else:
                timeToEnable = latestEvent[1] + DELAY - time.time()
                try:
                    latestEvent = eventQueue.get(timeout = timeToEnable) # Since the last event was ENABLE, the next event must be DISABLE. If it doesn't arrive until the timeout, we can enable the touchpad again.
                except Empty: # executed if no DISABLE event arrived until timeout
                    if not touchpadEnabled:
                        setDeviceEnabled(touchpadId, True)
                        touchpadEnabled = True
                    latestEvent = eventQueue.get()
    finally:
        # reenable the touchpad in any case
        setDeviceEnabled(touchpadId, True)

    retval = p.wait()

if __name__ == '__main__':
    main()
danzel
  • 6,044