Use python black for all pyhon file formatting

Let's enforce a consistent (and verifiable) style everywhere.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
This commit is contained in:
Peter Hutterer 2021-01-25 13:12:25 +10:00
parent c0364f2317
commit 1dbdef8fdb
12 changed files with 1291 additions and 922 deletions

View file

@ -747,13 +747,14 @@ usr-bin-env-python@fedora:33:
/bin/false /bin/false
fi fi
flake8@fedora:33: python-format@fedora:33:
extends: extends:
- .fedora-build@template - .fedora-build@template
before_script: before_script:
- dnf install -y python3-flake8 - dnf install -y black
script: script:
- flake8-3 --ignore=W501,E501,W504,E741 $(git grep -l '^#!/usr/bin/env python3') - black $(git grep -l '^#!/usr/bin/env python3')
- git diff --exit-code || (echo "Please run Black against all Python files" && false)
# #

View file

@ -542,13 +542,14 @@ usr-bin-env-python@{{distro.name}}:{{version}}:
/bin/false /bin/false
fi fi
flake8@{{distro.name}}:{{version}}: python-format@{{distro.name}}:{{version}}:
extends: extends:
- .{{distro.name}}-build@template - .{{distro.name}}-build@template
before_script: before_script:
- dnf install -y python3-flake8 - dnf install -y black
script: script:
- flake8-3 --ignore=W501,E501,W504,E741 $(git grep -l '^#!/usr/bin/env python3') - black $(git grep -l '^#!/usr/bin/env python3')
- git diff --exit-code || (echo "Please run Black against all Python files" && false)
{% endfor %} {% endfor %}

View file

@ -12,95 +12,106 @@ import json
import sys import sys
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
aparser = argparse.ArgumentParser(description='Turns a Meson test log into a JUnit report') aparser = argparse.ArgumentParser(
aparser.add_argument('--project-name', metavar='NAME', description="Turns a Meson test log into a JUnit report"
help='The project name', )
default='unknown') aparser.add_argument(
aparser.add_argument('--job-id', metavar='ID', "--project-name", metavar="NAME", help="The project name", default="unknown"
help='The job ID for the report', )
default='Unknown') aparser.add_argument(
aparser.add_argument('--branch', metavar='NAME', "--job-id", metavar="ID", help="The job ID for the report", default="Unknown"
help='Branch of the project being tested', )
default='master') aparser.add_argument(
aparser.add_argument('--output', metavar='FILE', "--branch",
help='The output file, stdout by default', metavar="NAME",
type=argparse.FileType('w', encoding='UTF-8'), help="Branch of the project being tested",
default=sys.stdout) default="master",
aparser.add_argument('infile', metavar='FILE', )
help='The input testlog.json, stdin by default', aparser.add_argument(
type=argparse.FileType('r', encoding='UTF-8'), "--output",
default=sys.stdin) metavar="FILE",
help="The output file, stdout by default",
type=argparse.FileType("w", encoding="UTF-8"),
default=sys.stdout,
)
aparser.add_argument(
"infile",
metavar="FILE",
help="The input testlog.json, stdin by default",
type=argparse.FileType("r", encoding="UTF-8"),
default=sys.stdin,
)
args = aparser.parse_args() args = aparser.parse_args()
outfile = args.output outfile = args.output
testsuites = ET.Element('testsuites') testsuites = ET.Element("testsuites")
testsuites.set('id', '{}/{}'.format(args.job_id, args.branch)) testsuites.set("id", "{}/{}".format(args.job_id, args.branch))
testsuites.set('package', args.project_name) testsuites.set("package", args.project_name)
testsuites.set('timestamp', datetime.datetime.utcnow().isoformat(timespec='minutes')) testsuites.set("timestamp", datetime.datetime.utcnow().isoformat(timespec="minutes"))
suites = {} suites = {}
for line in args.infile: for line in args.infile:
data = json.loads(line) data = json.loads(line)
(full_suite, unit_name) = data['name'].split(' / ') (full_suite, unit_name) = data["name"].split(" / ")
(project_name, suite_name) = full_suite.split(':') (project_name, suite_name) = full_suite.split(":")
duration = data['duration'] duration = data["duration"]
return_code = data['returncode'] return_code = data["returncode"]
log = data['stdout'] log = data["stdout"]
unit = { unit = {
'suite': suite_name, "suite": suite_name,
'name': unit_name, "name": unit_name,
'duration': duration, "duration": duration,
'returncode': return_code, "returncode": return_code,
'stdout': log, "stdout": log,
} }
units = suites.setdefault(suite_name, []) units = suites.setdefault(suite_name, [])
units.append(unit) units.append(unit)
for name, units in suites.items(): for name, units in suites.items():
print('Processing suite {} (units: {})'.format(name, len(units))) print("Processing suite {} (units: {})".format(name, len(units)))
def if_failed(unit): def if_failed(unit):
if unit['returncode'] != 0: if unit["returncode"] != 0:
return True return True
return False return False
def if_succeded(unit): def if_succeded(unit):
if unit['returncode'] == 0: if unit["returncode"] == 0:
return True return True
return False return False
successes = list(filter(if_succeded, units)) successes = list(filter(if_succeded, units))
failures = list(filter(if_failed, units)) failures = list(filter(if_failed, units))
print(' - {}: {} pass, {} fail'.format(name, len(successes), len(failures))) print(" - {}: {} pass, {} fail".format(name, len(successes), len(failures)))
testsuite = ET.SubElement(testsuites, 'testsuite') testsuite = ET.SubElement(testsuites, "testsuite")
testsuite.set('name', '{}/{}'.format(args.project_name, name)) testsuite.set("name", "{}/{}".format(args.project_name, name))
testsuite.set('tests', str(len(units))) testsuite.set("tests", str(len(units)))
testsuite.set('errors', str(len(failures))) testsuite.set("errors", str(len(failures)))
testsuite.set('failures', str(len(failures))) testsuite.set("failures", str(len(failures)))
for unit in successes: for unit in successes:
testcase = ET.SubElement(testsuite, 'testcase') testcase = ET.SubElement(testsuite, "testcase")
testcase.set('classname', '{}/{}'.format(args.project_name, unit['suite'])) testcase.set("classname", "{}/{}".format(args.project_name, unit["suite"]))
testcase.set('name', unit['name']) testcase.set("name", unit["name"])
testcase.set('time', str(unit['duration'])) testcase.set("time", str(unit["duration"]))
for unit in failures: for unit in failures:
testcase = ET.SubElement(testsuite, 'testcase') testcase = ET.SubElement(testsuite, "testcase")
testcase.set('classname', '{}/{}'.format(args.project_name, unit['suite'])) testcase.set("classname", "{}/{}".format(args.project_name, unit["suite"]))
testcase.set('name', unit['name']) testcase.set("name", unit["name"])
testcase.set('time', str(unit['duration'])) testcase.set("time", str(unit["duration"]))
failure = ET.SubElement(testcase, 'failure') failure = ET.SubElement(testcase, "failure")
failure.set('classname', '{}/{}'.format(args.project_name, unit['suite'])) failure.set("classname", "{}/{}".format(args.project_name, unit["suite"]))
failure.set('name', unit['name']) failure.set("name", unit["name"])
failure.set('type', 'error') failure.set("type", "error")
failure.text = unit['stdout'] failure.text = unit["stdout"]
output = ET.tostring(testsuites, encoding='unicode') output = ET.tostring(testsuites, encoding="unicode")
outfile.write(output) outfile.write(output)

View file

@ -36,15 +36,16 @@ import yaml
import libevdev import libevdev
COLOR_RESET = '\x1b[0m' COLOR_RESET = "\x1b[0m"
COLOR_RED = '\x1b[6;31m' COLOR_RED = "\x1b[6;31m"
class SlotFormatter(): class SlotFormatter:
width = 16 width = 16
def __init__(self, is_absolute=False, resolution=None, def __init__(
threshold=None, ignore_below=None): self, is_absolute=False, resolution=None, threshold=None, ignore_below=None
):
self.threshold = threshold self.threshold = threshold
self.ignore_below = ignore_below self.ignore_below = ignore_below
self.resolution = resolution self.resolution = resolution
@ -54,19 +55,19 @@ class SlotFormatter():
self.filtered = False self.filtered = False
def __str__(self): def __str__(self):
return ' | '.join(self.slots) return " | ".join(self.slots)
def format_slot(self, slot): def format_slot(self, slot):
if slot.state == SlotState.BEGIN: if slot.state == SlotState.BEGIN:
self.slots.append('+++++++'.center(self.width)) self.slots.append("+++++++".center(self.width))
self.have_data = True self.have_data = True
elif slot.state == SlotState.END: elif slot.state == SlotState.END:
self.slots.append('-------'.center(self.width)) self.slots.append("-------".center(self.width))
self.have_data = True self.have_data = True
elif slot.state == SlotState.NONE: elif slot.state == SlotState.NONE:
self.slots.append(('*' * (self.width - 2)).center(self.width)) self.slots.append(("*" * (self.width - 2)).center(self.width))
elif not slot.dirty: elif not slot.dirty:
self.slots.append(' '.center(self.width)) self.slots.append(" ".center(self.width))
else: else:
if self.resolution is not None: if self.resolution is not None:
dx, dy = slot.dx / self.resolution[0], slot.dy / self.resolution[1] dx, dy = slot.dx / self.resolution[0], slot.dy / self.resolution[1]
@ -81,35 +82,39 @@ class SlotFormatter():
else: else:
t = t * 180.0 / math.pi t = t * 180.0 / math.pi
directions = ['↖↑', '↖←', '↙←', '↙↓', '↓↘', '→↘', '→↗', '↑↗'] directions = ["↖↑", "↖←", "↙←", "↙↓", "↓↘", "→↘", "→↗", "↑↗"]
direction = directions[int(t / 45)] direction = directions[int(t / 45)]
elif dy == 0: elif dy == 0:
if dx < 0: if dx < 0:
direction = '←←' direction = "←←"
else: else:
direction = '→→' direction = "→→"
else: else:
if dy < 0: if dy < 0:
direction = '↑↑' direction = "↑↑"
else: else:
direction = '↓↓' direction = "↓↓"
color = '' color = ""
reset = '' reset = ""
if not self.is_absolute: if not self.is_absolute:
if self.ignore_below is not None or self.threshold is not None: if self.ignore_below is not None or self.threshold is not None:
dist = math.hypot(dx, dy) dist = math.hypot(dx, dy)
if self.ignore_below is not None and dist < self.ignore_below: if self.ignore_below is not None and dist < self.ignore_below:
self.slots.append(' '.center(self.width)) self.slots.append(" ".center(self.width))
self.filtered = True self.filtered = True
return return
if self.threshold is not None and dist >= self.threshold: if self.threshold is not None and dist >= self.threshold:
color = COLOR_RED color = COLOR_RED
reset = COLOR_RESET reset = COLOR_RESET
if isinstance(dx, int) and isinstance(dy, int): if isinstance(dx, int) and isinstance(dy, int):
string = "{} {}{:+4d}/{:+4d}{}".format(direction, color, dx, dy, reset) string = "{} {}{:+4d}/{:+4d}{}".format(
direction, color, dx, dy, reset
)
else: else:
string = "{} {}{:+3.2f}/{:+03.2f}{}".format(direction, color, dx, dy, reset) string = "{} {}{:+3.2f}/{:+03.2f}{}".format(
direction, color, dx, dy, reset
)
else: else:
x, y = slot.x, slot.y x, y = slot.x, slot.y
string = "{} {}{:4d}/{:4d}{}".format(direction, color, x, y, reset) string = "{} {}{:4d}/{:4d}{}".format(direction, color, x, y, reset)
@ -144,23 +149,46 @@ def main(argv):
slots = [] slots = []
xres, yres = 1, 1 xres, yres = 1, 1
parser = argparse.ArgumentParser(description="Measure delta between event frames for each slot") parser = argparse.ArgumentParser(
parser.add_argument("--use-mm", action='store_true', help="Use mm instead of device deltas") description="Measure delta between event frames for each slot"
parser.add_argument("--use-st", action='store_true', help="Use ABS_X/ABS_Y instead of ABS_MT_POSITION_X/Y") )
parser.add_argument("--use-absolute", action='store_true', help="Use absolute coordinates, not deltas") parser.add_argument(
parser.add_argument("path", metavar="recording", "--use-mm", action="store_true", help="Use mm instead of device deltas"
nargs=1, help="Path to libinput-record YAML file") )
parser.add_argument("--threshold", type=float, default=None, help="Mark any delta above this threshold") parser.add_argument(
parser.add_argument("--ignore-below", type=float, default=None, help="Ignore any delta below this threshold") "--use-st",
action="store_true",
help="Use ABS_X/ABS_Y instead of ABS_MT_POSITION_X/Y",
)
parser.add_argument(
"--use-absolute",
action="store_true",
help="Use absolute coordinates, not deltas",
)
parser.add_argument(
"path", metavar="recording", nargs=1, help="Path to libinput-record YAML file"
)
parser.add_argument(
"--threshold",
type=float,
default=None,
help="Mark any delta above this threshold",
)
parser.add_argument(
"--ignore-below",
type=float,
default=None,
help="Ignore any delta below this threshold",
)
args = parser.parse_args() args = parser.parse_args()
if not sys.stdout.isatty(): if not sys.stdout.isatty():
COLOR_RESET = '' COLOR_RESET = ""
COLOR_RED = '' COLOR_RED = ""
yml = yaml.safe_load(open(args.path[0])) yml = yaml.safe_load(open(args.path[0]))
device = yml['devices'][0] device = yml["devices"][0]
absinfo = device['evdev']['absinfo'] absinfo = device["evdev"]["absinfo"]
try: try:
nslots = absinfo[libevdev.EV_ABS.ABS_MT_SLOT.value][1] + 1 nslots = absinfo[libevdev.EV_ABS.ABS_MT_SLOT.value][1] + 1
except KeyError: except KeyError:
@ -195,11 +223,15 @@ def main(argv):
nskipped_lines = 0 nskipped_lines = 0
for event in device['events']: for event in device["events"]:
for evdev in event['evdev']: for evdev in event["evdev"]:
s = slots[slot] s = slots[slot]
e = libevdev.InputEvent(code=libevdev.evbit(evdev[2], evdev[3]), e = libevdev.InputEvent(
value=evdev[4], sec=evdev[0], usec=evdev[1]) code=libevdev.evbit(evdev[2], evdev[3]),
value=evdev[4],
sec=evdev[0],
usec=evdev[1],
)
if e.code in tool_bits: if e.code in tool_bits:
tool_bits[e.code] = e.value tool_bits[e.code] = e.value
@ -208,8 +240,10 @@ def main(argv):
# Note: this relies on the EV_KEY events to come in before the # Note: this relies on the EV_KEY events to come in before the
# x/y events, otherwise the last/first event in each slot will # x/y events, otherwise the last/first event in each slot will
# be wrong. # be wrong.
if (e.code == libevdev.EV_KEY.BTN_TOOL_FINGER or if (
e.code == libevdev.EV_KEY.BTN_TOOL_PEN): e.code == libevdev.EV_KEY.BTN_TOOL_FINGER
or e.code == libevdev.EV_KEY.BTN_TOOL_PEN
):
slot = 0 slot = 0
s = slots[slot] s = slots[slot]
s.dirty = True s.dirty = True
@ -251,7 +285,7 @@ def main(argv):
s.dirty = True s.dirty = True
# bcm5974 cycles through slot numbers, so let's say all below # bcm5974 cycles through slot numbers, so let's say all below
# our current slot number was used # our current slot number was used
for sl in slots[:slot + 1]: for sl in slots[: slot + 1]:
sl.used = True sl.used = True
elif e.code == libevdev.EV_ABS.ABS_MT_TRACKING_ID: elif e.code == libevdev.EV_ABS.ABS_MT_TRACKING_ID:
if e.value == -1: if e.value == -1:
@ -290,11 +324,11 @@ def main(argv):
last_time = t last_time = t
tools = [ tools = [
(libevdev.EV_KEY.BTN_TOOL_QUINTTAP, 'QIN'), (libevdev.EV_KEY.BTN_TOOL_QUINTTAP, "QIN"),
(libevdev.EV_KEY.BTN_TOOL_QUADTAP, 'QAD'), (libevdev.EV_KEY.BTN_TOOL_QUADTAP, "QAD"),
(libevdev.EV_KEY.BTN_TOOL_TRIPLETAP, 'TRI'), (libevdev.EV_KEY.BTN_TOOL_TRIPLETAP, "TRI"),
(libevdev.EV_KEY.BTN_TOOL_DOUBLETAP, 'DBL'), (libevdev.EV_KEY.BTN_TOOL_DOUBLETAP, "DBL"),
(libevdev.EV_KEY.BTN_TOUCH, 'TOU'), (libevdev.EV_KEY.BTN_TOUCH, "TOU"),
] ]
for bit, string in tools: for bit, string in tools:
@ -302,12 +336,14 @@ def main(argv):
tool_state = string tool_state = string
break break
else: else:
tool_state = ' ' tool_state = " "
fmt = SlotFormatter(is_absolute=args.use_absolute, fmt = SlotFormatter(
resolution=(xres, yres) if args.use_mm else None, is_absolute=args.use_absolute,
threshold=args.threshold, resolution=(xres, yres) if args.use_mm else None,
ignore_below=args.ignore_below) threshold=args.threshold,
ignore_below=args.ignore_below,
)
for sl in [s for s in slots if s.used]: for sl in [s for s in slots if s.used]:
fmt.format_slot(sl) fmt.format_slot(sl)
@ -323,11 +359,21 @@ def main(argv):
if nskipped_lines > 0: if nskipped_lines > 0:
print("") print("")
nskipped_lines = 0 nskipped_lines = 0
print("{:2d}.{:06d} {:+5d}ms {}: {}".format(e.sec, e.usec, tdelta, tool_state, fmt)) print(
"{:2d}.{:06d} {:+5d}ms {}: {}".format(
e.sec, e.usec, tdelta, tool_state, fmt
)
)
elif fmt.filtered: elif fmt.filtered:
nskipped_lines += 1 nskipped_lines += 1
print("\r", " " * 21, "... {} below threshold".format(nskipped_lines), flush=True, end='') print(
"\r",
" " * 21,
"... {} below threshold".format(nskipped_lines),
flush=True,
end="",
)
if __name__ == '__main__': if __name__ == "__main__":
main(sys.argv) main(sys.argv)

View file

@ -28,26 +28,29 @@ import os
import sys import sys
import argparse import argparse
import subprocess import subprocess
try: try:
import libevdev import libevdev
import pyudev import pyudev
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
print('Error: {}'.format(str(e)), file=sys.stderr) print("Error: {}".format(str(e)), file=sys.stderr)
print('One or more python modules are missing. Please install those ' print(
'modules and re-run this tool.') "One or more python modules are missing. Please install those "
"modules and re-run this tool."
)
sys.exit(1) sys.exit(1)
DEFAULT_HWDB_FILE = '/usr/lib/udev/hwdb.d/60-evdev.hwdb' DEFAULT_HWDB_FILE = "/usr/lib/udev/hwdb.d/60-evdev.hwdb"
OVERRIDE_HWDB_FILE = '/etc/udev/hwdb.d/99-touchpad-fuzz-override.hwdb' OVERRIDE_HWDB_FILE = "/etc/udev/hwdb.d/99-touchpad-fuzz-override.hwdb"
class tcolors: class tcolors:
GREEN = '\033[92m' GREEN = "\033[92m"
RED = '\033[91m' RED = "\033[91m"
YELLOW = '\033[93m' YELLOW = "\033[93m"
BOLD = '\033[1m' BOLD = "\033[1m"
NORMAL = '\033[0m' NORMAL = "\033[0m"
def print_bold(msg, **kwargs): def print_bold(msg, **kwargs):
@ -81,44 +84,53 @@ class Device(libevdev.Device):
else: else:
self.path = path self.path = path
fd = open(self.path, 'rb') fd = open(self.path, "rb")
super().__init__(fd) super().__init__(fd)
context = pyudev.Context() context = pyudev.Context()
self.udev_device = pyudev.Devices.from_device_file(context, self.path) self.udev_device = pyudev.Devices.from_device_file(context, self.path)
def find_touch_device(self): def find_touch_device(self):
context = pyudev.Context() context = pyudev.Context()
for device in context.list_devices(subsystem='input'): for device in context.list_devices(subsystem="input"):
if not device.get('ID_INPUT_TOUCHPAD', 0): if not device.get("ID_INPUT_TOUCHPAD", 0):
continue continue
if not device.device_node or \ if not device.device_node or not device.device_node.startswith(
not device.device_node.startswith('/dev/input/event'): "/dev/input/event"
):
continue continue
return device.device_node return device.device_node
print('Unable to find a touch device.', file=sys.stderr) print("Unable to find a touch device.", file=sys.stderr)
sys.exit(1) sys.exit(1)
def check_property(self): def check_property(self):
'''Return a tuple of (xfuzz, yfuzz) with the fuzz as set in the libinput """Return a tuple of (xfuzz, yfuzz) with the fuzz as set in the libinput
property. Returns None if the property doesn't exist''' property. Returns None if the property doesn't exist"""
axes = { axes = {
0x00: self.udev_device.get('LIBINPUT_FUZZ_00'), 0x00: self.udev_device.get("LIBINPUT_FUZZ_00"),
0x01: self.udev_device.get('LIBINPUT_FUZZ_01'), 0x01: self.udev_device.get("LIBINPUT_FUZZ_01"),
0x35: self.udev_device.get('LIBINPUT_FUZZ_35'), 0x35: self.udev_device.get("LIBINPUT_FUZZ_35"),
0x36: self.udev_device.get('LIBINPUT_FUZZ_36'), 0x36: self.udev_device.get("LIBINPUT_FUZZ_36"),
} }
if axes[0x35] is not None: if axes[0x35] is not None:
if axes[0x35] != axes[0x00]: if axes[0x35] != axes[0x00]:
print_bold('WARNING: fuzz mismatch ABS_X: {}, ABS_MT_POSITION_X: {}'.format(axes[0x00], axes[0x35])) print_bold(
"WARNING: fuzz mismatch ABS_X: {}, ABS_MT_POSITION_X: {}".format(
axes[0x00], axes[0x35]
)
)
if axes[0x36] is not None: if axes[0x36] is not None:
if axes[0x36] != axes[0x01]: if axes[0x36] != axes[0x01]:
print_bold('WARNING: fuzz mismatch ABS_Y: {}, ABS_MT_POSITION_Y: {}'.format(axes[0x01], axes[0x36])) print_bold(
"WARNING: fuzz mismatch ABS_Y: {}, ABS_MT_POSITION_Y: {}".format(
axes[0x01], axes[0x36]
)
)
xfuzz = axes[0x35] or axes[0x00] xfuzz = axes[0x35] or axes[0x00]
yfuzz = axes[0x36] or axes[0x01] yfuzz = axes[0x36] or axes[0x01]
@ -126,27 +138,34 @@ class Device(libevdev.Device):
if xfuzz is None and yfuzz is None: if xfuzz is None and yfuzz is None:
return None return None
if ((xfuzz is not None and yfuzz is None) or if (xfuzz is not None and yfuzz is None) or (
(xfuzz is None and yfuzz is not None)): xfuzz is None and yfuzz is not None
raise InvalidConfigurationError('fuzz should be set for both axes') ):
raise InvalidConfigurationError("fuzz should be set for both axes")
return (int(xfuzz), int(yfuzz)) return (int(xfuzz), int(yfuzz))
def check_axes(self): def check_axes(self):
''' """
Returns a tuple of (xfuzz, yfuzz) with the fuzz as set on the device Returns a tuple of (xfuzz, yfuzz) with the fuzz as set on the device
axis. Returns None if no fuzz is set. axis. Returns None if no fuzz is set.
''' """
if not self.has(libevdev.EV_ABS.ABS_X) or not self.has(libevdev.EV_ABS.ABS_Y): if not self.has(libevdev.EV_ABS.ABS_X) or not self.has(libevdev.EV_ABS.ABS_Y):
raise InvalidDeviceError('device does not have x/y axes') raise InvalidDeviceError("device does not have x/y axes")
if self.has(libevdev.EV_ABS.ABS_MT_POSITION_X) != self.has(libevdev.EV_ABS.ABS_MT_POSITION_Y): if self.has(libevdev.EV_ABS.ABS_MT_POSITION_X) != self.has(
raise InvalidDeviceError('device does not have both multitouch axes') libevdev.EV_ABS.ABS_MT_POSITION_Y
):
raise InvalidDeviceError("device does not have both multitouch axes")
xfuzz = (self.absinfo[libevdev.EV_ABS.ABS_X].fuzz or xfuzz = (
self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X].fuzz) self.absinfo[libevdev.EV_ABS.ABS_X].fuzz
yfuzz = (self.absinfo[libevdev.EV_ABS.ABS_Y].fuzz or or self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X].fuzz
self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_Y].fuzz) )
yfuzz = (
self.absinfo[libevdev.EV_ABS.ABS_Y].fuzz
or self.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_Y].fuzz
)
if xfuzz == 0 and yfuzz == 0: if xfuzz == 0 and yfuzz == 0:
return None return None
@ -155,13 +174,13 @@ class Device(libevdev.Device):
def print_fuzz(what, fuzz): def print_fuzz(what, fuzz):
print(' Checking {}... '.format(what), end='') print(" Checking {}... ".format(what), end="")
if fuzz is None: if fuzz is None:
print('not set') print("not set")
elif fuzz == (0, 0): elif fuzz == (0, 0):
print('is zero') print("is zero")
else: else:
print('x={} y={}'.format(*fuzz)) print("x={} y={}".format(*fuzz))
def handle_existing_entry(device, fuzz): def handle_existing_entry(device, fuzz):
@ -174,10 +193,10 @@ def handle_existing_entry(device, fuzz):
# If the lines aren't in the same order in the file, it'll be a false # If the lines aren't in the same order in the file, it'll be a false
# negative. # negative.
overrides = { overrides = {
0x00: device.udev_device.get('EVDEV_ABS_00'), 0x00: device.udev_device.get("EVDEV_ABS_00"),
0x01: device.udev_device.get('EVDEV_ABS_01'), 0x01: device.udev_device.get("EVDEV_ABS_01"),
0x35: device.udev_device.get('EVDEV_ABS_35'), 0x35: device.udev_device.get("EVDEV_ABS_35"),
0x36: device.udev_device.get('EVDEV_ABS_36'), 0x36: device.udev_device.get("EVDEV_ABS_36"),
} }
has_existing_rules = False has_existing_rules = False
@ -188,85 +207,98 @@ def handle_existing_entry(device, fuzz):
if not has_existing_rules: if not has_existing_rules:
return False return False
print_red('Error! ', end='') print_red("Error! ", end="")
print('This device already has axis overrides defined') print("This device already has axis overrides defined")
print('') print("")
print_bold('Searching for existing override...') print_bold("Searching for existing override...")
# Construct a template that looks like a hwdb entry (values only) from # Construct a template that looks like a hwdb entry (values only) from
# the udev property values # the udev property values
template = [' EVDEV_ABS_00={}'.format(overrides[0x00]), template = [
' EVDEV_ABS_01={}'.format(overrides[0x01])] " EVDEV_ABS_00={}".format(overrides[0x00]),
" EVDEV_ABS_01={}".format(overrides[0x01]),
]
if overrides[0x35] is not None: if overrides[0x35] is not None:
template += [' EVDEV_ABS_35={}'.format(overrides[0x35]), template += [
' EVDEV_ABS_36={}'.format(overrides[0x36])] " EVDEV_ABS_35={}".format(overrides[0x35]),
" EVDEV_ABS_36={}".format(overrides[0x36]),
]
print('Checking in {}... '.format(OVERRIDE_HWDB_FILE), end='') print("Checking in {}... ".format(OVERRIDE_HWDB_FILE), end="")
entry, prefix, lineno = check_file_for_lines(OVERRIDE_HWDB_FILE, template) entry, prefix, lineno = check_file_for_lines(OVERRIDE_HWDB_FILE, template)
if entry is not None: if entry is not None:
print_green('found') print_green("found")
print('The existing hwdb entry can be overwritten') print("The existing hwdb entry can be overwritten")
return False return False
else: else:
print_red('not found') print_red("not found")
print('Checking in {}... '.format(DEFAULT_HWDB_FILE), end='') print("Checking in {}... ".format(DEFAULT_HWDB_FILE), end="")
entry, prefix, lineno = check_file_for_lines(DEFAULT_HWDB_FILE, template) entry, prefix, lineno = check_file_for_lines(DEFAULT_HWDB_FILE, template)
if entry is not None: if entry is not None:
print_green('found') print_green("found")
else: else:
print_red('not found') print_red("not found")
print('The device has a hwdb override defined but it\'s not where I expected it to be.') print(
print('Please look at the libinput documentation for more details.') "The device has a hwdb override defined but it's not where I expected it to be."
print('Exiting now.') )
print("Please look at the libinput documentation for more details.")
print("Exiting now.")
return True return True
print_bold('Probable entry for this device found in line {}:'.format(lineno)) print_bold("Probable entry for this device found in line {}:".format(lineno))
print('\n'.join(prefix + entry)) print("\n".join(prefix + entry))
print('') print("")
print_bold('Suggested new entry for this device:') print_bold("Suggested new entry for this device:")
new_entry = [] new_entry = []
for i in range(0, len(template)): for i in range(0, len(template)):
parts = entry[i].split(':') parts = entry[i].split(":")
while len(parts) < 4: while len(parts) < 4:
parts.append('') parts.append("")
parts[3] = str(fuzz) parts[3] = str(fuzz)
new_entry.append(':'.join(parts)) new_entry.append(":".join(parts))
print('\n'.join(prefix + new_entry)) print("\n".join(prefix + new_entry))
print('') print("")
# Not going to overwrite the 60-evdev.hwdb entry with this program, too # Not going to overwrite the 60-evdev.hwdb entry with this program, too
# risky. And it may not be our device match anyway. # risky. And it may not be our device match anyway.
print_bold('You must now:') print_bold("You must now:")
print('\n'.join(( print(
'1. Check the above suggestion for sanity. Does it match your device?', "\n".join(
'2. Open {} and amend the existing entry'.format(DEFAULT_HWDB_FILE), (
' as recommended above', "1. Check the above suggestion for sanity. Does it match your device?",
'', "2. Open {} and amend the existing entry".format(DEFAULT_HWDB_FILE),
' The property format is:', " as recommended above",
' EVDEV_ABS_00=min:max:resolution:fuzz', "",
'', " The property format is:",
' Leave the entry as-is and only add or amend the fuzz value.', " EVDEV_ABS_00=min:max:resolution:fuzz",
' A non-existent value can be skipped, e.g. this entry sets the ', "",
' resolution to 32 and the fuzz to 8', " Leave the entry as-is and only add or amend the fuzz value.",
' EVDEV_ABS_00=::32:8', " A non-existent value can be skipped, e.g. this entry sets the ",
'', " resolution to 32 and the fuzz to 8",
'3. Save the edited file', " EVDEV_ABS_00=::32:8",
'4. Say Y to the next prompt'))) "",
"3. Save the edited file",
"4. Say Y to the next prompt",
)
)
)
cont = input('Continue? [Y/n] ') cont = input("Continue? [Y/n] ")
if cont == 'n': if cont == "n":
raise KeyboardInterrupt raise KeyboardInterrupt
if test_hwdb_entry(device, fuzz): if test_hwdb_entry(device, fuzz):
print_bold('Please test the new fuzz setting by restarting libinput') print_bold("Please test the new fuzz setting by restarting libinput")
print_bold('Then submit a pull request for this hwdb entry change to ' print_bold(
'to systemd at http://github.com/systemd/systemd') "Then submit a pull request for this hwdb entry change to "
"to systemd at http://github.com/systemd/systemd"
)
else: else:
print_bold('The new fuzz setting did not take effect.') print_bold("The new fuzz setting did not take effect.")
print_bold('Did you edit the correct file?') print_bold("Did you edit the correct file?")
print('Please look at the libinput documentation for more details.') print("Please look at the libinput documentation for more details.")
print('Exiting now.') print("Exiting now.")
return True return True
@ -274,47 +306,49 @@ def handle_existing_entry(device, fuzz):
def reload_and_trigger_udev(device): def reload_and_trigger_udev(device):
import time import time
print('Running systemd-hwdb update') print("Running systemd-hwdb update")
subprocess.run(['systemd-hwdb', 'update'], check=True) subprocess.run(["systemd-hwdb", "update"], check=True)
syspath = device.path.replace('/dev/input/', '/sys/class/input/') syspath = device.path.replace("/dev/input/", "/sys/class/input/")
time.sleep(2) time.sleep(2)
print('Running udevadm trigger {}'.format(syspath)) print("Running udevadm trigger {}".format(syspath))
subprocess.run(['udevadm', 'trigger', syspath], check=True) subprocess.run(["udevadm", "trigger", syspath], check=True)
time.sleep(2) time.sleep(2)
def test_hwdb_entry(device, fuzz): def test_hwdb_entry(device, fuzz):
reload_and_trigger_udev(device) reload_and_trigger_udev(device)
print_bold('Testing... ', end='') print_bold("Testing... ", end="")
d = Device(device.path) d = Device(device.path)
f = d.check_axes() f = d.check_axes()
if f is not None: if f is not None:
if f == (fuzz, fuzz): if f == (fuzz, fuzz):
print_yellow('Warning') print_yellow("Warning")
print_bold('The hwdb applied to the device but libinput\'s udev ' print_bold(
'rules have not picked it up. This should only happen' "The hwdb applied to the device but libinput's udev "
'if libinput is not installed') "rules have not picked it up. This should only happen"
"if libinput is not installed"
)
return True return True
else: else:
print_red('Error') print_red("Error")
return False return False
else: else:
f = d.check_property() f = d.check_property()
if f is not None and f == (fuzz, fuzz): if f is not None and f == (fuzz, fuzz):
print_green('Success') print_green("Success")
return True return True
else: else:
print_red('Error') print_red("Error")
return False return False
def check_file_for_lines(path, template): def check_file_for_lines(path, template):
''' """
Checks file at path for the lines given in template. If found, the Checks file at path for the lines given in template. If found, the
return value is a tuple of the matching lines and the prefix (i.e. the return value is a tuple of the matching lines and the prefix (i.e. the
two lines before the matching lines) two lines before the matching lines)
''' """
try: try:
lines = [l[:-1] for l in open(path).readlines()] lines = [l[:-1] for l in open(path).readlines()]
idx = -1 idx = -1
@ -322,12 +356,12 @@ def check_file_for_lines(path, template):
while idx < len(lines) - 1: while idx < len(lines) - 1:
idx += 1 idx += 1
line = lines[idx] line = lines[idx]
if not line.startswith(' EVDEV_ABS_00'): if not line.startswith(" EVDEV_ABS_00"):
continue continue
if lines[idx:idx + len(template)] != template: if lines[idx : idx + len(template)] != template:
continue continue
return (lines[idx:idx + len(template)], lines[idx - 2:idx], idx) return (lines[idx : idx + len(template)], lines[idx - 2 : idx], idx)
except IndexError: except IndexError:
pass pass
@ -338,43 +372,51 @@ def check_file_for_lines(path, template):
def write_udev_rule(device, fuzz): def write_udev_rule(device, fuzz):
'''Write out a udev rule that may match the device, run udevadm trigger and """Write out a udev rule that may match the device, run udevadm trigger and
check if the udev rule worked. Of course, there's plenty to go wrong... check if the udev rule worked. Of course, there's plenty to go wrong...
''' """
print('') print("")
print_bold('Guessing a udev rule to overwrite the fuzz') print_bold("Guessing a udev rule to overwrite the fuzz")
# Some devices match better on pvr, others on pn, so we get to try both. yay # Some devices match better on pvr, others on pn, so we get to try both. yay
modalias = open('/sys/class/dmi/id/modalias').readlines()[0] modalias = open("/sys/class/dmi/id/modalias").readlines()[0]
ms = modalias.split(':') ms = modalias.split(":")
svn, pn, pvr = None, None, None svn, pn, pvr = None, None, None
for m in ms: for m in ms:
if m.startswith('svn'): if m.startswith("svn"):
svn = m svn = m
elif m.startswith('pn'): elif m.startswith("pn"):
pn = m pn = m
elif m.startswith('pvr'): elif m.startswith("pvr"):
pvr = m pvr = m
# Let's print out both to inform and/or confuse the user # Let's print out both to inform and/or confuse the user
template = '\n'.join(('# {} {}', template = "\n".join(
'evdev:name:{}:dmi:*:{}*:{}*:', (
' EVDEV_ABS_00=:::{}', "# {} {}",
' EVDEV_ABS_01=:::{}', "evdev:name:{}:dmi:*:{}*:{}*:",
' EVDEV_ABS_35=:::{}', " EVDEV_ABS_00=:::{}",
' EVDEV_ABS_36=:::{}', " EVDEV_ABS_01=:::{}",
'')) " EVDEV_ABS_35=:::{}",
rule1 = template.format(svn[3:], device.name, device.name, svn, pvr, fuzz, fuzz, fuzz, fuzz) " EVDEV_ABS_36=:::{}",
rule2 = template.format(svn[3:], device.name, device.name, svn, pn, fuzz, fuzz, fuzz, fuzz) "",
)
)
rule1 = template.format(
svn[3:], device.name, device.name, svn, pvr, fuzz, fuzz, fuzz, fuzz
)
rule2 = template.format(
svn[3:], device.name, device.name, svn, pn, fuzz, fuzz, fuzz, fuzz
)
print('Full modalias is: {}'.format(modalias)) print("Full modalias is: {}".format(modalias))
print() print()
print_bold('Suggested udev rule, option 1:') print_bold("Suggested udev rule, option 1:")
print(rule1) print(rule1)
print() print()
print_bold('Suggested udev rule, option 2:') print_bold("Suggested udev rule, option 2:")
print(rule2) print(rule2)
print('') print("")
# The weird hwdb matching behavior means we match on the least specific # The weird hwdb matching behavior means we match on the least specific
# rule (i.e. most wildcards) first although that was supposed to be fixed in # rule (i.e. most wildcards) first although that was supposed to be fixed in
@ -386,77 +428,88 @@ def write_udev_rule(device, fuzz):
return return
while True: while True:
print_bold('Wich rule do you want to to test? 1 or 2? ', end='') print_bold("Wich rule do you want to to test? 1 or 2? ", end="")
yesno = input('Ctrl+C to exit ') yesno = input("Ctrl+C to exit ")
if yesno == '1': if yesno == "1":
rule = rule1 rule = rule1
break break
elif yesno == '2': elif yesno == "2":
rule = rule2 rule = rule2
break break
fname = OVERRIDE_HWDB_FILE fname = OVERRIDE_HWDB_FILE
try: try:
fd = open(fname, 'x') fd = open(fname, "x")
except FileExistsError: except FileExistsError:
yesno = input('File {} exists, overwrite? [Y/n] '.format(fname)) yesno = input("File {} exists, overwrite? [Y/n] ".format(fname))
if yesno.lower == 'n': if yesno.lower == "n":
return return
fd = open(fname, 'w') fd = open(fname, "w")
fd.write('# File generated by libinput measure fuzz\n\n') fd.write("# File generated by libinput measure fuzz\n\n")
fd.write(rule) fd.write(rule)
fd.close() fd.close()
if test_hwdb_entry(device, fuzz): if test_hwdb_entry(device, fuzz):
print('Your hwdb override file is in {}'.format(fname)) print("Your hwdb override file is in {}".format(fname))
print_bold('Please test the new fuzz setting by restarting libinput') print_bold("Please test the new fuzz setting by restarting libinput")
print_bold('Then submit a pull request for this hwdb entry to ' print_bold(
'systemd at http://github.com/systemd/systemd') "Then submit a pull request for this hwdb entry to "
"systemd at http://github.com/systemd/systemd"
)
else: else:
print('The hwdb entry failed to apply to the device.') print("The hwdb entry failed to apply to the device.")
print('Removing hwdb file again.') print("Removing hwdb file again.")
os.remove(fname) os.remove(fname)
reload_and_trigger_udev(device) reload_and_trigger_udev(device)
print_bold('What now?') print_bold("What now?")
print('1. Re-run this program and try the other suggested udev rule. If that fails,') print(
print('2. File a bug with the suggested udev rule at http://github.com/systemd/systemd') "1. Re-run this program and try the other suggested udev rule. If that fails,"
)
print(
"2. File a bug with the suggested udev rule at http://github.com/systemd/systemd"
)
def main(args): def main(args):
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Print fuzz settings and/or suggest udev rules for the fuzz to be adjusted.' description="Print fuzz settings and/or suggest udev rules for the fuzz to be adjusted."
) )
parser.add_argument('path', metavar='/dev/input/event0', parser.add_argument(
nargs='?', type=str, help='Path to device (optional)') "path",
parser.add_argument('--fuzz', type=int, help='Suggested fuzz') metavar="/dev/input/event0",
nargs="?",
type=str,
help="Path to device (optional)",
)
parser.add_argument("--fuzz", type=int, help="Suggested fuzz")
args = parser.parse_args() args = parser.parse_args()
try: try:
device = Device(args.path) device = Device(args.path)
print_bold('Using {}: {}'.format(device.name, device.path)) print_bold("Using {}: {}".format(device.name, device.path))
fuzz = device.check_property() fuzz = device.check_property()
print_fuzz('udev property', fuzz) print_fuzz("udev property", fuzz)
fuzz = device.check_axes() fuzz = device.check_axes()
print_fuzz('axes', fuzz) print_fuzz("axes", fuzz)
userfuzz = args.fuzz userfuzz = args.fuzz
if userfuzz is not None: if userfuzz is not None:
write_udev_rule(device, userfuzz) write_udev_rule(device, userfuzz)
except PermissionError: except PermissionError:
print('Permission denied, please re-run as root') print("Permission denied, please re-run as root")
except InvalidConfigurationError as e: except InvalidConfigurationError as e:
print('Error: {}'.format(e)) print("Error: {}".format(e))
except InvalidDeviceError as e: except InvalidDeviceError as e:
print('Error: {}'.format(e)) print("Error: {}".format(e))
except KeyboardInterrupt: except KeyboardInterrupt:
print('Exited on user request') print("Exited on user request")
if __name__ == '__main__': if __name__ == "__main__":
main(sys.argv) main(sys.argv)

View file

@ -27,21 +27,25 @@
import sys import sys
import subprocess import subprocess
import argparse import argparse
try: try:
import libevdev import libevdev
import pyudev import pyudev
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
print('Error: {}'.format(str(e)), file=sys.stderr) print("Error: {}".format(str(e)), file=sys.stderr)
print('One or more python modules are missing. Please install those ' print(
'modules and re-run this tool.') "One or more python modules are missing. Please install those "
"modules and re-run this tool."
)
sys.exit(1) sys.exit(1)
class Range(object): class Range(object):
"""Class to keep a min/max of a value around""" """Class to keep a min/max of a value around"""
def __init__(self): def __init__(self):
self.min = float('inf') self.min = float("inf")
self.max = float('-inf') self.max = float("-inf")
def update(self, value): def update(self, value):
self.min = min(self.min, value) self.min = min(self.min, value)
@ -148,7 +152,9 @@ class TouchSequence(object):
self.major_range.min, self.major_range.max self.major_range.min, self.major_range.max
) )
if self.device.has_minor: if self.device.has_minor:
s += "minor: [{:3d}..{:3d}] ".format(self.minor_range.min, self.minor_range.max) s += "minor: [{:3d}..{:3d}] ".format(
self.minor_range.min, self.minor_range.max
)
if self.was_down: if self.was_down:
s += " down" s += " down"
if self.was_palm: if self.was_palm:
@ -160,10 +166,12 @@ class TouchSequence(object):
def _str_state(self): def _str_state(self):
touch = self.points[-1] touch = self.points[-1]
s = "{}, tags: {} {} {}".format(touch, s = "{}, tags: {} {} {}".format(
"down" if self.is_down else " ", touch,
"palm" if self.is_palm else " ", "down" if self.is_down else " ",
"thumb" if self.is_thumb else " ") "palm" if self.is_palm else " ",
"thumb" if self.is_thumb else " ",
)
return s return s
@ -178,7 +186,7 @@ class Device(libevdev.Device):
else: else:
self.path = path self.path = path
fd = open(self.path, 'rb') fd = open(self.path, "rb")
super().__init__(fd) super().__init__(fd)
print("Using {}: {}\n".format(self.name, self.path)) print("Using {}: {}\n".format(self.name, self.path))
@ -200,13 +208,15 @@ class Device(libevdev.Device):
def find_touch_device(self): def find_touch_device(self):
context = pyudev.Context() context = pyudev.Context()
for device in context.list_devices(subsystem='input'): for device in context.list_devices(subsystem="input"):
if not device.get('ID_INPUT_TOUCHPAD', 0) and \ if not device.get("ID_INPUT_TOUCHPAD", 0) and not device.get(
not device.get('ID_INPUT_TOUCHSCREEN', 0): "ID_INPUT_TOUCHSCREEN", 0
):
continue continue
if not device.device_node or \ if not device.device_node or not device.device_node.startswith(
not device.device_node.startswith('/dev/input/event'): "/dev/input/event"
):
continue continue
return device.device_node return device.device_node
@ -215,21 +225,24 @@ class Device(libevdev.Device):
sys.exit(1) sys.exit(1)
def _init_thresholds_from_quirks(self): def _init_thresholds_from_quirks(self):
command = ['libinput', 'quirks', 'list', self.path] command = ["libinput", "quirks", "list", self.path]
cmd = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) cmd = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if cmd.returncode != 0: if cmd.returncode != 0:
print("Error querying quirks: {}".format(cmd.stderr.decode('utf-8')), file=sys.stderr) print(
"Error querying quirks: {}".format(cmd.stderr.decode("utf-8")),
file=sys.stderr,
)
return return
stdout = cmd.stdout.decode('utf-8') stdout = cmd.stdout.decode("utf-8")
quirks = [q.split('=') for q in stdout.split('\n')] quirks = [q.split("=") for q in stdout.split("\n")]
for q in quirks: for q in quirks:
if q[0] == 'AttrPalmSizeThreshold': if q[0] == "AttrPalmSizeThreshold":
self.palm = int(q[1]) self.palm = int(q[1])
elif q[0] == 'AttrTouchSizeRange': elif q[0] == "AttrTouchSizeRange":
self.down, self.up = colon_tuple(q[1]) self.down, self.up = colon_tuple(q[1])
elif q[0] == 'AttrThumbSizeThreshold': elif q[0] == "AttrThumbSizeThreshold":
self.thumb = int(q[1]) self.thumb = int(q[1])
def start_new_sequence(self, tracking_id): def start_new_sequence(self, tracking_id):
@ -239,13 +252,17 @@ class Device(libevdev.Device):
return self.sequences[-1] return self.sequences[-1]
def handle_key(self, event): def handle_key(self, event):
tapcodes = [libevdev.EV_KEY.BTN_TOOL_DOUBLETAP, tapcodes = [
libevdev.EV_KEY.BTN_TOOL_TRIPLETAP, libevdev.EV_KEY.BTN_TOOL_DOUBLETAP,
libevdev.EV_KEY.BTN_TOOL_QUADTAP, libevdev.EV_KEY.BTN_TOOL_TRIPLETAP,
libevdev.EV_KEY.BTN_TOOL_QUINTTAP] libevdev.EV_KEY.BTN_TOOL_QUADTAP,
libevdev.EV_KEY.BTN_TOOL_QUINTTAP,
]
if event.code in tapcodes and event.value > 0: if event.code in tapcodes and event.value > 0:
print("\rThis tool cannot handle multiple fingers, " print(
"output will be invalid", file=sys.stderr) "\rThis tool cannot handle multiple fingers, " "output will be invalid",
file=sys.stderr,
)
def handle_abs(self, event): def handle_abs(self, event):
if event.matches(libevdev.EV_ABS.ABS_MT_TRACKING_ID): if event.matches(libevdev.EV_ABS.ABS_MT_TRACKING_ID):
@ -271,9 +288,11 @@ class Device(libevdev.Device):
try: try:
self.current_sequence().append(self.touch) self.current_sequence().append(self.touch)
print("\r{}".format(self.current_sequence()), end="") print("\r{}".format(self.current_sequence()), end="")
self.touch = Touch(major=self.touch.major, self.touch = Touch(
minor=self.touch.minor, major=self.touch.major,
orientation=self.touch.orientation) minor=self.touch.minor,
orientation=self.touch.orientation,
)
except IndexError: except IndexError:
pass pass
@ -290,8 +309,10 @@ class Device(libevdev.Device):
print("Touch sizes used: {}:{}".format(self.down, self.up)) print("Touch sizes used: {}:{}".format(self.down, self.up))
print("Palm size used: {}".format(self.palm)) print("Palm size used: {}".format(self.palm))
print("Thumb size used: {}".format(self.thumb)) print("Thumb size used: {}".format(self.thumb))
print("Place a single finger on the device to measure touch size.\n" print(
"Ctrl+C to exit\n") "Place a single finger on the device to measure touch size.\n"
"Ctrl+C to exit\n"
)
while True: while True:
for event in self.events(): for event in self.events():
@ -300,11 +321,11 @@ class Device(libevdev.Device):
def colon_tuple(string): def colon_tuple(string):
try: try:
ts = string.split(':') ts = string.split(":")
t = tuple([int(x) for x in ts]) t = tuple([int(x) for x in ts])
if len(t) == 2 and t[0] >= t[1]: if len(t) == 2 and t[0] >= t[1]:
return t return t
except: # noqa except: # noqa
pass pass
msg = "{} is not in format N:M (N >= M)".format(string) msg = "{} is not in format N:M (N >= M)".format(string)
@ -313,13 +334,25 @@ def colon_tuple(string):
def main(args): def main(args):
parser = argparse.ArgumentParser(description="Measure touch size and orientation") parser = argparse.ArgumentParser(description="Measure touch size and orientation")
parser.add_argument('path', metavar='/dev/input/event0', parser.add_argument(
nargs='?', type=str, help='Path to device (optional)') "path",
parser.add_argument('--touch-thresholds', metavar='down:up', metavar="/dev/input/event0",
type=colon_tuple, nargs="?",
help='Thresholds when a touch is logically down or up') type=str,
parser.add_argument('--palm-threshold', metavar='t', help="Path to device (optional)",
type=int, help='Threshold when a touch is a palm') )
parser.add_argument(
"--touch-thresholds",
metavar="down:up",
type=colon_tuple,
help="Thresholds when a touch is logically down or up",
)
parser.add_argument(
"--palm-threshold",
metavar="t",
type=int,
help="Threshold when a touch is a palm",
)
args = parser.parse_args() args = parser.parse_args()
try: try:
@ -337,7 +370,9 @@ def main(args):
except (PermissionError, OSError): except (PermissionError, OSError):
print("Error: failed to open device") print("Error: failed to open device")
except InvalidDeviceError as e: except InvalidDeviceError as e:
print("This device does not have the capabilities for size-based touch detection.") print(
"This device does not have the capabilities for size-based touch detection."
)
print("Details: {}".format(e)) print("Details: {}".format(e))

View file

@ -27,13 +27,16 @@
import sys import sys
import subprocess import subprocess
import argparse import argparse
try: try:
import libevdev import libevdev
import pyudev import pyudev
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
print('Error: {}'.format(str(e)), file=sys.stderr) print("Error: {}".format(str(e)), file=sys.stderr)
print('One or more python modules are missing. Please install those ' print(
'modules and re-run this tool.') "One or more python modules are missing. Please install those "
"modules and re-run this tool."
)
sys.exit(1) sys.exit(1)
@ -48,35 +51,35 @@ class TableFormatter(object):
return sum(self.colwidths) + 1 return sum(self.colwidths) + 1
def headers(self, args): def headers(self, args):
s = '|' s = "|"
align = self.ALIGNMENT - 1 # account for | align = self.ALIGNMENT - 1 # account for |
for arg in args: for arg in args:
# +2 because we want space left/right of text # +2 because we want space left/right of text
w = ((len(arg) + 2 + align) // align) * align w = ((len(arg) + 2 + align) // align) * align
self.colwidths.append(w + 1) self.colwidths.append(w + 1)
s += ' {:^{width}s} |'.format(arg, width=w - 2) s += " {:^{width}s} |".format(arg, width=w - 2)
return s return s
def values(self, args): def values(self, args):
s = '|' s = "|"
for w, arg in zip(self.colwidths, args): for w, arg in zip(self.colwidths, args):
w -= 1 # width includes | separator w -= 1 # width includes | separator
if type(arg) == str: if type(arg) == str:
# We want space margins for strings # We want space margins for strings
s += ' {:{width}s} |'.format(arg, width=w - 2) s += " {:{width}s} |".format(arg, width=w - 2)
elif type(arg) == bool: elif type(arg) == bool:
s += '{:^{width}s}|'.format('x' if arg else ' ', width=w) s += "{:^{width}s}|".format("x" if arg else " ", width=w)
else: else:
s += '{:^{width}d}|'.format(arg, width=w) s += "{:^{width}d}|".format(arg, width=w)
if len(args) < len(self.colwidths): if len(args) < len(self.colwidths):
s += '|'.rjust(self.width - len(s), ' ') s += "|".rjust(self.width - len(s), " ")
return s return s
def separator(self): def separator(self):
return '+' + '-' * (self.width - 2) + '+' return "+" + "-" * (self.width - 2) + "+"
fmt = TableFormatter() fmt = TableFormatter()
@ -84,9 +87,10 @@ fmt = TableFormatter()
class Range(object): class Range(object):
"""Class to keep a min/max of a value around""" """Class to keep a min/max of a value around"""
def __init__(self): def __init__(self):
self.min = float('inf') self.min = float("inf")
self.max = float('-inf') self.max = float("-inf")
def update(self, value): def update(self, value):
self.min = min(self.min, value) self.min = min(self.min, value)
@ -157,19 +161,47 @@ class TouchSequence(object):
def _str_summary(self): def _str_summary(self):
if not self.points: if not self.points:
return fmt.values([self.tracking_id, False, False, False, False, return fmt.values(
'No pressure values recorded']) [
self.tracking_id,
False,
False,
False,
False,
"No pressure values recorded",
]
)
s = fmt.values([self.tracking_id, self.was_down, True, self.was_palm, s = fmt.values(
self.was_thumb, self.prange.min, self.prange.max, 0, [
self.avg(), self.median()]) self.tracking_id,
self.was_down,
True,
self.was_palm,
self.was_thumb,
self.prange.min,
self.prange.max,
0,
self.avg(),
self.median(),
]
)
return s return s
def _str_state(self): def _str_state(self):
s = fmt.values([self.tracking_id, self.is_down, not self.is_down, s = fmt.values(
self.is_palm, self.is_thumb, self.prange.min, [
self.prange.max, self.points[-1].pressure]) self.tracking_id,
self.is_down,
not self.is_down,
self.is_palm,
self.is_thumb,
self.prange.min,
self.prange.max,
self.points[-1].pressure,
]
)
return s return s
@ -184,7 +216,7 @@ class Device(libevdev.Device):
else: else:
self.path = path self.path = path
fd = open(self.path, 'rb') fd = open(self.path, "rb")
super().__init__(fd) super().__init__(fd)
print("Using {}: {}\n".format(self.name, self.path)) print("Using {}: {}\n".format(self.name, self.path))
@ -195,7 +227,9 @@ class Device(libevdev.Device):
absinfo = self.absinfo[libevdev.EV_ABS.ABS_PRESSURE] absinfo = self.absinfo[libevdev.EV_ABS.ABS_PRESSURE]
self.has_mt_pressure = False self.has_mt_pressure = False
if absinfo is None: if absinfo is None:
raise InvalidDeviceError("Device does not have ABS_PRESSURE or ABS_MT_PRESSURE") raise InvalidDeviceError(
"Device does not have ABS_PRESSURE or ABS_MT_PRESSURE"
)
prange = absinfo.maximum - absinfo.minimum prange = absinfo.maximum - absinfo.minimum
@ -210,12 +244,13 @@ class Device(libevdev.Device):
def find_touchpad_device(self): def find_touchpad_device(self):
context = pyudev.Context() context = pyudev.Context()
for device in context.list_devices(subsystem='input'): for device in context.list_devices(subsystem="input"):
if not device.get('ID_INPUT_TOUCHPAD', 0): if not device.get("ID_INPUT_TOUCHPAD", 0):
continue continue
if not device.device_node or \ if not device.device_node or not device.device_node.startswith(
not device.device_node.startswith('/dev/input/event'): "/dev/input/event"
):
continue continue
return device.device_node return device.device_node
@ -223,21 +258,24 @@ class Device(libevdev.Device):
sys.exit(1) sys.exit(1)
def _init_thresholds_from_quirks(self): def _init_thresholds_from_quirks(self):
command = ['libinput', 'quirks', 'list', self.path] command = ["libinput", "quirks", "list", self.path]
cmd = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) cmd = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if cmd.returncode != 0: if cmd.returncode != 0:
print("Error querying quirks: {}".format(cmd.stderr.decode('utf-8')), file=sys.stderr) print(
"Error querying quirks: {}".format(cmd.stderr.decode("utf-8")),
file=sys.stderr,
)
return return
stdout = cmd.stdout.decode('utf-8') stdout = cmd.stdout.decode("utf-8")
quirks = [q.split('=') for q in stdout.split('\n')] quirks = [q.split("=") for q in stdout.split("\n")]
for q in quirks: for q in quirks:
if q[0] == 'AttrPalmPressureThreshold': if q[0] == "AttrPalmPressureThreshold":
self.palm = int(q[1]) self.palm = int(q[1])
elif q[0] == 'AttrPressureRange': elif q[0] == "AttrPressureRange":
self.down, self.up = colon_tuple(q[1]) self.down, self.up = colon_tuple(q[1])
elif q[0] == 'AttrThumbPressureThreshold': elif q[0] == "AttrThumbPressureThreshold":
self.thumb = int(q[1]) self.thumb = int(q[1])
def start_new_sequence(self, tracking_id): def start_new_sequence(self, tracking_id):
@ -252,11 +290,13 @@ def handle_key(device, event):
libevdev.EV_KEY.BTN_TOOL_DOUBLETAP, libevdev.EV_KEY.BTN_TOOL_DOUBLETAP,
libevdev.EV_KEY.BTN_TOOL_TRIPLETAP, libevdev.EV_KEY.BTN_TOOL_TRIPLETAP,
libevdev.EV_KEY.BTN_TOOL_QUADTAP, libevdev.EV_KEY.BTN_TOOL_QUADTAP,
libevdev.EV_KEY.BTN_TOOL_QUINTTAP libevdev.EV_KEY.BTN_TOOL_QUINTTAP,
] ]
if event.code in tapcodes and event.value > 0: if event.code in tapcodes and event.value > 0:
print('\r\033[2KThis tool cannot handle multiple fingers, ' print(
'output will be invalid') "\r\033[2KThis tool cannot handle multiple fingers, "
"output will be invalid"
)
def handle_abs(device, event): def handle_abs(device, event):
@ -271,8 +311,9 @@ def handle_abs(device, event):
except IndexError: except IndexError:
# If the finger was down at startup # If the finger was down at startup
pass pass
elif (event.matches(libevdev.EV_ABS.ABS_MT_PRESSURE) or elif event.matches(libevdev.EV_ABS.ABS_MT_PRESSURE) or (
(event.matches(libevdev.EV_ABS.ABS_PRESSURE) and not device.has_mt_pressure)): event.matches(libevdev.EV_ABS.ABS_PRESSURE) and not device.has_mt_pressure
):
try: try:
s = device.current_sequence() s = device.current_sequence()
s.append(Touch(pressure=event.value)) s.append(Touch(pressure=event.value))
@ -290,24 +331,26 @@ def handle_event(device, event):
def loop(device): def loop(device):
print('This is an interactive tool') print("This is an interactive tool")
print() print()
print("Place a single finger on the touchpad to measure pressure values.") print("Place a single finger on the touchpad to measure pressure values.")
print('Check that:') print("Check that:")
print('- touches subjectively perceived as down are tagged as down') print("- touches subjectively perceived as down are tagged as down")
print('- touches with a thumb are tagged as thumb') print("- touches with a thumb are tagged as thumb")
print('- touches with a palm are tagged as palm') print("- touches with a palm are tagged as palm")
print() print()
print('If the touch states do not match the interaction, re-run') print("If the touch states do not match the interaction, re-run")
print('with --touch-thresholds=down:up using observed pressure values.') print("with --touch-thresholds=down:up using observed pressure values.")
print('See --help for more options.') print("See --help for more options.")
print() print()
print("Press Ctrl+C to exit") print("Press Ctrl+C to exit")
print() print()
headers = fmt.headers(['Touch', 'down', 'up', 'palm', 'thumb', 'min', 'max', 'p', 'avg', 'median']) headers = fmt.headers(
["Touch", "down", "up", "palm", "thumb", "min", "max", "p", "avg", "median"]
)
print(fmt.separator()) print(fmt.separator())
print(fmt.values(['Thresh', device.down, device.up, device.palm, device.thumb])) print(fmt.values(["Thresh", device.down, device.up, device.palm, device.thumb]))
print(fmt.separator()) print(fmt.separator())
print(headers) print(headers)
print(fmt.separator()) print(fmt.separator())
@ -319,11 +362,11 @@ def loop(device):
def colon_tuple(string): def colon_tuple(string):
try: try:
ts = string.split(':') ts = string.split(":")
t = tuple([int(x) for x in ts]) t = tuple([int(x) for x in ts])
if len(t) == 2 and t[0] >= t[1]: if len(t) == 2 and t[0] >= t[1]:
return t return t
except: # noqa except: # noqa
pass pass
msg = "{} is not in format N:M (N >= M)".format(string) msg = "{} is not in format N:M (N >= M)".format(string)
@ -331,24 +374,31 @@ def colon_tuple(string):
def main(args): def main(args):
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description="Measure touchpad pressure values")
description="Measure touchpad pressure values" parser.add_argument(
"path",
metavar="/dev/input/event0",
nargs="?",
type=str,
help="Path to device (optional)",
) )
parser.add_argument( parser.add_argument(
'path', metavar='/dev/input/event0', nargs='?', type=str, "--touch-thresholds",
help='Path to device (optional)' metavar="down:up",
type=colon_tuple,
help="Thresholds when a touch is logically down or up",
) )
parser.add_argument( parser.add_argument(
'--touch-thresholds', metavar='down:up', type=colon_tuple, "--palm-threshold",
help='Thresholds when a touch is logically down or up' metavar="t",
type=int,
help="Threshold when a touch is a palm",
) )
parser.add_argument( parser.add_argument(
'--palm-threshold', metavar='t', type=int, "--thumb-threshold",
help='Threshold when a touch is a palm' metavar="t",
) type=int,
parser.add_argument( help="Threshold when a touch is a thumb",
'--thumb-threshold', metavar='t', type=int,
help='Threshold when a touch is a thumb'
) )
args = parser.parse_args() args = parser.parse_args()
@ -366,13 +416,15 @@ def main(args):
loop(device) loop(device)
except KeyboardInterrupt: except KeyboardInterrupt:
print('\r\033[2K{}'.format(fmt.separator())) print("\r\033[2K{}".format(fmt.separator()))
print() print()
except (PermissionError, OSError): except (PermissionError, OSError):
print("Error: failed to open device") print("Error: failed to open device")
except InvalidDeviceError as e: except InvalidDeviceError as e:
print("This device does not have the capabilities for pressure-based touch detection.") print(
"This device does not have the capabilities for pressure-based touch detection."
)
print("Details: {}".format(e)) print("Details: {}".format(e))

View file

@ -26,13 +26,16 @@
import sys import sys
import argparse import argparse
try: try:
import libevdev import libevdev
import pyudev import pyudev
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
print('Error: {}'.format(str(e)), file=sys.stderr) print("Error: {}".format(str(e)), file=sys.stderr)
print('One or more python modules are missing. Please install those ' print(
'modules and re-run this tool.') "One or more python modules are missing. Please install those "
"modules and re-run this tool."
)
sys.exit(1) sys.exit(1)
@ -51,15 +54,15 @@ class Touchpad(object):
x = evdev.absinfo[libevdev.EV_ABS.ABS_X] x = evdev.absinfo[libevdev.EV_ABS.ABS_X]
y = evdev.absinfo[libevdev.EV_ABS.ABS_Y] y = evdev.absinfo[libevdev.EV_ABS.ABS_Y]
if not x or not y: if not x or not y:
raise DeviceError('Device does not have an x or axis') raise DeviceError("Device does not have an x or axis")
if not x.resolution or not y.resolution: if not x.resolution or not y.resolution:
print('Device does not have resolutions.', file=sys.stderr) print("Device does not have resolutions.", file=sys.stderr)
x.resolution = 1 x.resolution = 1
y.resolution = 1 y.resolution = 1
self.xrange = (x.maximum - x.minimum) self.xrange = x.maximum - x.minimum
self.yrange = (y.maximum - y.minimum) self.yrange = y.maximum - y.minimum
self.width = self.xrange / x.resolution self.width = self.xrange / x.resolution
self.height = self.yrange / y.resolution self.height = self.yrange / y.resolution
@ -70,7 +73,12 @@ class Touchpad(object):
# terminal character space is (guesswork) ca 2.3 times as high as # terminal character space is (guesswork) ca 2.3 times as high as
# wide. # wide.
self.columns = 30 self.columns = 30
self.rows = int(self.columns * (self.yrange // y.resolution) // (self.xrange // x.resolution) / 2.3) self.rows = int(
self.columns
* (self.yrange // y.resolution)
// (self.xrange // x.resolution)
/ 2.3
)
self.pos = Point(0, 0) self.pos = Point(0, 0)
self.min = Point() self.min = Point()
self.max = Point() self.max = Point()
@ -87,8 +95,8 @@ class Touchpad(object):
def x(self, x): def x(self, x):
self._x.minimum = min(self.x.minimum, x) self._x.minimum = min(self.x.minimum, x)
self._x.maximum = max(self.x.maximum, x) self._x.maximum = max(self.x.maximum, x)
self.min.x = min(x, self.min.x or 0xffffffff) self.min.x = min(x, self.min.x or 0xFFFFFFFF)
self.max.x = max(x, self.max.x or -0xffffffff) self.max.x = max(x, self.max.x or -0xFFFFFFFF)
# we calculate the position based on the original range. # we calculate the position based on the original range.
# this means on devices with a narrower range than advertised, not # this means on devices with a narrower range than advertised, not
# all corners may be reachable in the touchpad drawing. # all corners may be reachable in the touchpad drawing.
@ -98,8 +106,8 @@ class Touchpad(object):
def y(self, y): def y(self, y):
self._y.minimum = min(self.y.minimum, y) self._y.minimum = min(self.y.minimum, y)
self._y.maximum = max(self.y.maximum, y) self._y.maximum = max(self.y.maximum, y)
self.min.y = min(y, self.min.y or 0xffffffff) self.min.y = min(y, self.min.y or 0xFFFFFFFF)
self.max.y = max(y, self.max.y or -0xffffffff) self.max.y = max(y, self.max.y or -0xFFFFFFFF)
# we calculate the position based on the original range. # we calculate the position based on the original range.
# this means on devices with a narrower range than advertised, not # this means on devices with a narrower range than advertised, not
# all corners may be reachable in the touchpad drawing. # all corners may be reachable in the touchpad drawing.
@ -107,61 +115,61 @@ class Touchpad(object):
def update_from_data(self): def update_from_data(self):
if None in [self.min.x, self.min.y, self.max.x, self.max.y]: if None in [self.min.x, self.min.y, self.max.x, self.max.y]:
raise DeviceError('Insufficient data to continue') raise DeviceError("Insufficient data to continue")
self._x.minimum = self.min.x self._x.minimum = self.min.x
self._x.maximum = self.max.x self._x.maximum = self.max.x
self._y.minimum = self.min.y self._y.minimum = self.min.y
self._y.maximum = self.max.y self._y.maximum = self.max.y
def draw(self): def draw(self):
print('Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]'.format( print(
self.min.x if self.min.x is not None else 0, "Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]".format(
self.max.x if self.max.x is not None else 0, self.min.x if self.min.x is not None else 0,
self.min.y if self.min.y is not None else 0, self.max.x if self.max.x is not None else 0,
self.max.y if self.max.y is not None else 0)) self.min.y if self.min.y is not None else 0,
self.max.y if self.max.y is not None else 0,
)
)
print() print()
print('Move one finger along all edges of the touchpad'.center(self.columns)) print("Move one finger along all edges of the touchpad".center(self.columns))
print('until the detected axis range stops changing.'.center(self.columns)) print("until the detected axis range stops changing.".center(self.columns))
top = int(self.pos.y * self.rows) top = int(self.pos.y * self.rows)
print('+{}+'.format(''.ljust(self.columns, '-'))) print("+{}+".format("".ljust(self.columns, "-")))
for row in range(0, top): for row in range(0, top):
print('|{}|'.format(''.ljust(self.columns))) print("|{}|".format("".ljust(self.columns)))
left = int(self.pos.x * self.columns) left = int(self.pos.x * self.columns)
right = max(0, self.columns - 1 - left) right = max(0, self.columns - 1 - left)
print('|{}{}{}|'.format( print("|{}{}{}|".format("".ljust(left), "O", "".ljust(right)))
''.ljust(left),
'O',
''.ljust(right)))
for row in range(top + 1, self.rows): for row in range(top + 1, self.rows):
print('|{}|'.format(''.ljust(self.columns))) print("|{}|".format("".ljust(self.columns)))
print('+{}+'.format(''.ljust(self.columns, '-'))) print("+{}+".format("".ljust(self.columns, "-")))
print('Press Ctrl+C to stop'.center(self.columns)) print("Press Ctrl+C to stop".center(self.columns))
print('\033[{}A'.format(self.rows + 8), flush=True) print("\033[{}A".format(self.rows + 8), flush=True)
self.rows_printed = self.rows + 8 self.rows_printed = self.rows + 8
def erase(self): def erase(self):
# Erase all previous lines so we're not left with rubbish # Erase all previous lines so we're not left with rubbish
for row in range(self.rows_printed): for row in range(self.rows_printed):
print('\033[K') print("\033[K")
print('\033[{}A'.format(self.rows_printed)) print("\033[{}A".format(self.rows_printed))
def dimension(string): def dimension(string):
try: try:
ts = string.split('x') ts = string.split("x")
t = tuple([int(x) for x in ts]) t = tuple([int(x) for x in ts])
if len(t) == 2: if len(t) == 2:
return t return t
except: # noqa except: # noqa
pass pass
msg = "{} is not in format WxH".format(string) msg = "{} is not in format WxH".format(string)
@ -173,82 +181,95 @@ def between(v1, v2, deviation):
def dmi_modalias_match(modalias): def dmi_modalias_match(modalias):
modalias = modalias.split(':') modalias = modalias.split(":")
dmi = {'svn': None, 'pvr': None, 'pn': None} dmi = {"svn": None, "pvr": None, "pn": None}
for m in modalias: for m in modalias:
for key in dmi: for key in dmi:
if m.startswith(key): if m.startswith(key):
dmi[key] = m[len(key):] dmi[key] = m[len(key) :]
# Based on the current 60-evdev.hwdb, Lenovo uses pvr and everyone else # Based on the current 60-evdev.hwdb, Lenovo uses pvr and everyone else
# uses pn to provide a human-identifiable match # uses pn to provide a human-identifiable match
if dmi['svn'] == 'LENOVO': if dmi["svn"] == "LENOVO":
return 'dmi:*svn{}:*pvr{}*'.format(dmi['svn'], dmi['pvr']) return "dmi:*svn{}:*pvr{}*".format(dmi["svn"], dmi["pvr"])
else: else:
return 'dmi:*svn{}:*pn{}*'.format(dmi['svn'], dmi['pn']) return "dmi:*svn{}:*pn{}*".format(dmi["svn"], dmi["pn"])
def main(args): def main(args):
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description="Measure the touchpad size")
description="Measure the touchpad size" parser.add_argument(
"size",
metavar="WxH",
type=dimension,
help="Touchpad size (width by height) in mm",
) )
parser.add_argument( parser.add_argument(
'size', metavar='WxH', type=dimension, "path",
help='Touchpad size (width by height) in mm', metavar="/dev/input/event0",
) nargs="?",
parser.add_argument( type=str,
'path', metavar='/dev/input/event0', nargs='?', type=str, help="Path to device (optional)",
help='Path to device (optional)'
) )
context = pyudev.Context() context = pyudev.Context()
args = parser.parse_args() args = parser.parse_args()
if not args.path: if not args.path:
for device in context.list_devices(subsystem='input'): for device in context.list_devices(subsystem="input"):
if (device.get('ID_INPUT_TOUCHPAD', 0) and if device.get("ID_INPUT_TOUCHPAD", 0) and (
(device.device_node or '').startswith('/dev/input/event')): device.device_node or ""
).startswith("/dev/input/event"):
args.path = device.device_node args.path = device.device_node
name = 'unknown' name = "unknown"
parent = device parent = device
while parent is not None: while parent is not None:
n = parent.get('NAME', None) n = parent.get("NAME", None)
if n: if n:
name = n name = n
break break
parent = parent.parent parent = parent.parent
print('Using {}: {}'.format(name, device.device_node)) print("Using {}: {}".format(name, device.device_node))
break break
else: else:
print('Unable to find a touchpad device.', file=sys.stderr) print("Unable to find a touchpad device.", file=sys.stderr)
return 1 return 1
dev = pyudev.Devices.from_device_file(context, args.path) dev = pyudev.Devices.from_device_file(context, args.path)
overrides = [p for p in dev.properties if p.startswith('EVDEV_ABS')] overrides = [p for p in dev.properties if p.startswith("EVDEV_ABS")]
if overrides: if overrides:
print() print()
print('********************************************************************') print("********************************************************************")
print('WARNING: axis overrides already in place for this device:') print("WARNING: axis overrides already in place for this device:")
for prop in overrides: for prop in overrides:
print(' {}={}'.format(prop, dev.properties[prop])) print(" {}={}".format(prop, dev.properties[prop]))
print('The systemd hwdb already overrides the axis ranges and/or resolution.') print("The systemd hwdb already overrides the axis ranges and/or resolution.")
print('This tool is not needed unless you want to verify the axis overrides.') print("This tool is not needed unless you want to verify the axis overrides.")
print('********************************************************************') print("********************************************************************")
print() print()
try: try:
fd = open(args.path, 'rb') fd = open(args.path, "rb")
evdev = libevdev.Device(fd) evdev = libevdev.Device(fd)
touchpad = Touchpad(evdev) touchpad = Touchpad(evdev)
print('Kernel specified touchpad size: {:.1f}x{:.1f}mm'.format(touchpad.width, touchpad.height)) print(
print('User specified touchpad size: {:.1f}x{:.1f}mm'.format(*args.size)) "Kernel specified touchpad size: {:.1f}x{:.1f}mm".format(
touchpad.width, touchpad.height
)
)
print("User specified touchpad size: {:.1f}x{:.1f}mm".format(*args.size))
print() print()
print('Kernel axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]'.format( print(
touchpad.x.minimum, touchpad.x.maximum, "Kernel axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]".format(
touchpad.y.minimum, touchpad.y.maximum)) touchpad.x.minimum,
touchpad.x.maximum,
touchpad.y.minimum,
touchpad.y.maximum,
)
)
print('Put your finger on the touchpad to start\033[1A') print("Put your finger on the touchpad to start\033[1A")
try: try:
touchpad.draw() touchpad.draw()
@ -264,15 +285,27 @@ def main(args):
touchpad.erase() touchpad.erase()
touchpad.update_from_data() touchpad.update_from_data()
print('Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]'.format( print(
touchpad.x.minimum, touchpad.x.maximum, "Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]".format(
touchpad.y.minimum, touchpad.y.maximum)) touchpad.x.minimum,
touchpad.x.maximum,
touchpad.y.minimum,
touchpad.y.maximum,
)
)
touchpad.x.resolution = round((touchpad.x.maximum - touchpad.x.minimum) / args.size[0]) touchpad.x.resolution = round(
touchpad.y.resolution = round((touchpad.y.maximum - touchpad.y.minimum) / args.size[1]) (touchpad.x.maximum - touchpad.x.minimum) / args.size[0]
)
touchpad.y.resolution = round(
(touchpad.y.maximum - touchpad.y.minimum) / args.size[1]
)
print('Resolutions calculated based on user-specified size: x {}, y {} units/mm'.format( print(
touchpad.x.resolution, touchpad.y.resolution)) "Resolutions calculated based on user-specified size: x {}, y {} units/mm".format(
touchpad.x.resolution, touchpad.y.resolution
)
)
# If both x/y are within some acceptable deviation, we skip the axis # If both x/y are within some acceptable deviation, we skip the axis
# overrides and only override the resolution # overrides and only override the resolution
@ -287,50 +320,73 @@ def main(args):
if skip: if skip:
print() print()
print('Note: Axis ranges within acceptable deviation, skipping min/max override') print(
"Note: Axis ranges within acceptable deviation, skipping min/max override"
)
print() print()
print() print()
print('Suggested hwdb entry:') print("Suggested hwdb entry:")
use_dmi = evdev.id['bustype'] not in [0x03, 0x05] # USB, Bluetooth use_dmi = evdev.id["bustype"] not in [0x03, 0x05] # USB, Bluetooth
if use_dmi: if use_dmi:
modalias = open('/sys/class/dmi/id/modalias').read().strip() modalias = open("/sys/class/dmi/id/modalias").read().strip()
print('Note: the dmi modalias match is a guess based on your machine\'s modalias:') print(
print(' ', modalias) "Note: the dmi modalias match is a guess based on your machine's modalias:"
print('Please verify that this is the most sensible match and adjust if necessary.') )
print(" ", modalias)
print(
"Please verify that this is the most sensible match and adjust if necessary."
)
print('-8<--------------------------') print("-8<--------------------------")
print('# Laptop model description (e.g. Lenovo X1 Carbon 5th)') print("# Laptop model description (e.g. Lenovo X1 Carbon 5th)")
if use_dmi: if use_dmi:
print('evdev:name:{}:{}*'.format(evdev.name, dmi_modalias_match(modalias))) print("evdev:name:{}:{}*".format(evdev.name, dmi_modalias_match(modalias)))
else: else:
print('evdev:input:b{:04X}v{:04X}p{:04X}*'.format( print(
evdev.id['bustype'], evdev.id['vendor'], evdev.id['product'])) "evdev:input:b{:04X}v{:04X}p{:04X}*".format(
print(' EVDEV_ABS_00={}:{}:{}'.format( evdev.id["bustype"], evdev.id["vendor"], evdev.id["product"]
touchpad.x.minimum if not skip else '', )
touchpad.x.maximum if not skip else '', )
touchpad.x.resolution)) print(
print(' EVDEV_ABS_01={}:{}:{}'.format( " EVDEV_ABS_00={}:{}:{}".format(
touchpad.y.minimum if not skip else '', touchpad.x.minimum if not skip else "",
touchpad.y.maximum if not skip else '', touchpad.x.maximum if not skip else "",
touchpad.y.resolution)) touchpad.x.resolution,
)
)
print(
" EVDEV_ABS_01={}:{}:{}".format(
touchpad.y.minimum if not skip else "",
touchpad.y.maximum if not skip else "",
touchpad.y.resolution,
)
)
if evdev.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X]: if evdev.absinfo[libevdev.EV_ABS.ABS_MT_POSITION_X]:
print(' EVDEV_ABS_35={}:{}:{}'.format( print(
touchpad.x.minimum if not skip else '', " EVDEV_ABS_35={}:{}:{}".format(
touchpad.x.maximum if not skip else '', touchpad.x.minimum if not skip else "",
touchpad.x.resolution)) touchpad.x.maximum if not skip else "",
print(' EVDEV_ABS_36={}:{}:{}'.format( touchpad.x.resolution,
touchpad.y.minimum if not skip else '', )
touchpad.y.maximum if not skip else '', )
touchpad.y.resolution)) print(
print('-8<--------------------------') " EVDEV_ABS_36={}:{}:{}".format(
print('Instructions on what to do with this snippet are in /usr/lib/udev/hwdb.d/60-evdev.hwdb') touchpad.y.minimum if not skip else "",
touchpad.y.maximum if not skip else "",
touchpad.y.resolution,
)
)
print("-8<--------------------------")
print(
"Instructions on what to do with this snippet are in /usr/lib/udev/hwdb.d/60-evdev.hwdb"
)
except DeviceError as e: except DeviceError as e:
print('Error: {}'.format(e), file=sys.stderr) print("Error: {}".format(e), file=sys.stderr)
return 1 return 1
except PermissionError: except PermissionError:
print('Unable to open device. Please run me as root', file=sys.stderr) print("Unable to open device. Please run me as root", file=sys.stderr)
return 1 return 1
return 0 return 0

View file

@ -26,14 +26,17 @@
import sys import sys
import argparse import argparse
try: try:
import libevdev import libevdev
import textwrap import textwrap
import pyudev import pyudev
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
print('Error: {}'.format(e), file=sys.stderr) print("Error: {}".format(e), file=sys.stderr)
print('One or more python modules are missing. Please install those ' print(
'modules and re-run this tool.') "One or more python modules are missing. Please install those "
"modules and re-run this tool."
)
sys.exit(1) sys.exit(1)
print_dest = sys.stdout print_dest = sys.stdout
@ -66,7 +69,7 @@ class Touch(object):
@up.setter @up.setter
def up(self, up): def up(self, up):
assert(up > self.down) assert up > self.down
self._up = up self._up = up
@property @property
@ -88,7 +91,7 @@ class Device(libevdev.Device):
self.path = self._find_touch_device() self.path = self._find_touch_device()
else: else:
self.path = path self.path = path
fd = open(self.path, 'rb') fd = open(self.path, "rb")
super().__init__(fd) super().__init__(fd)
print("Using {}: {}\n".format(self.name, self.path)) print("Using {}: {}\n".format(self.name, self.path))
@ -101,18 +104,19 @@ class Device(libevdev.Device):
def _find_touch_device(self): def _find_touch_device(self):
context = pyudev.Context() context = pyudev.Context()
device_node = None device_node = None
for device in context.list_devices(subsystem='input'): for device in context.list_devices(subsystem="input"):
if (not device.device_node or if not device.device_node or not device.device_node.startswith(
not device.device_node.startswith('/dev/input/event')): "/dev/input/event"
):
continue continue
# pick the touchpad by default, fallback to the first # pick the touchpad by default, fallback to the first
# touchscreen only when there is no touchpad # touchscreen only when there is no touchpad
if device.get('ID_INPUT_TOUCHPAD', 0): if device.get("ID_INPUT_TOUCHPAD", 0):
device_node = device.device_node device_node = device.device_node
break break
if device.get('ID_INPUT_TOUCHSCREEN', 0) and device_node is None: if device.get("ID_INPUT_TOUCHSCREEN", 0) and device_node is None:
device_node = device.device_node device_node = device.device_node
if device_node is not None: if device_node is not None:
@ -127,17 +131,19 @@ class Device(libevdev.Device):
self.touches.append(t) self.touches.append(t)
else: else:
self.touches[-1].up = tv2us(event.sec, event.usec) self.touches[-1].up = tv2us(event.sec, event.usec)
msg("\rTouch sequences detected: {}".format(len(self.touches)), msg("\rTouch sequences detected: {}".format(len(self.touches)), end="")
end='')
def handle_key(self, event): def handle_key(self, event):
tapcodes = [libevdev.EV_KEY.BTN_TOOL_DOUBLETAP, tapcodes = [
libevdev.EV_KEY.BTN_TOOL_TRIPLETAP, libevdev.EV_KEY.BTN_TOOL_DOUBLETAP,
libevdev.EV_KEY.BTN_TOOL_QUADTAP, libevdev.EV_KEY.BTN_TOOL_TRIPLETAP,
libevdev.EV_KEY.BTN_TOOL_QUINTTAP] libevdev.EV_KEY.BTN_TOOL_QUADTAP,
libevdev.EV_KEY.BTN_TOOL_QUINTTAP,
]
if event.code in tapcodes and event.value > 0: if event.code in tapcodes and event.value > 0:
error("\rThis tool cannot handle multiple fingers, " error(
"output will be invalid") "\rThis tool cannot handle multiple fingers, " "output will be invalid"
)
return return
if event.matches(libevdev.EV_KEY.BTN_TOUCH): if event.matches(libevdev.EV_KEY.BTN_TOUCH):
@ -146,9 +152,11 @@ class Device(libevdev.Device):
def handle_syn(self, event): def handle_syn(self, event):
if self.touch.dirty: if self.touch.dirty:
self.current_sequence().append(self.touch) self.current_sequence().append(self.touch)
self.touch = Touch(major=self.touch.major, self.touch = Touch(
minor=self.touch.minor, major=self.touch.major,
orientation=self.touch.orientation) minor=self.touch.minor,
orientation=self.touch.orientation,
)
def handle_event(self, event): def handle_event(self, event):
if event.matches(libevdev.EV_KEY): if event.matches(libevdev.EV_KEY):
@ -182,7 +190,9 @@ class Device(libevdev.Device):
def print_dat(self): def print_dat(self):
print("# libinput-measure-touchpad-tap") print("# libinput-measure-touchpad-tap")
print(textwrap.dedent('''\ print(
textwrap.dedent(
"""\
# File contents: # File contents:
# This file contains multiple prints of the data in # This file contains multiple prints of the data in
# different sort order. Row number is index of touch # different sort order. Row number is index of touch
@ -197,7 +207,9 @@ class Device(libevdev.Device):
# 4: touch down time in ms, offset by first event # 4: touch down time in ms, offset by first event
# 5: touch up time in ms, offset by first event # 5: touch up time in ms, offset by first event
# 6: time delta in ms # 6: time delta in ms
''')) """
)
)
deltas = [t for t in self.touches] deltas = [t for t in self.touches]
deltas_sorted = sorted(deltas, key=lambda t: t.tdelta) deltas_sorted = sorted(deltas, key=lambda t: t.tdelta)
@ -205,28 +217,44 @@ class Device(libevdev.Device):
offset = deltas[0].down offset = deltas[0].down
for t1, t2 in zip(deltas, deltas_sorted): for t1, t2 in zip(deltas, deltas_sorted):
print(t1.down - offset, t1.up - offset, t1.tdelta, print(
t2.down - offset, t2.up - offset, t2.tdelta) t1.down - offset,
t1.up - offset,
t1.tdelta,
t2.down - offset,
t2.up - offset,
t2.tdelta,
)
def print(self, format): def print(self, format):
if not self.touches: if not self.touches:
error("No tap data available") error("No tap data available")
return return
if format == 'summary': if format == "summary":
self.print_summary() self.print_summary()
elif format == 'dat': elif format == "dat":
self.print_dat() self.print_dat()
def main(args): def main(args):
parser = argparse.ArgumentParser(description="Measure tap-to-click properties of devices") parser = argparse.ArgumentParser(
parser.add_argument('path', metavar='/dev/input/event0', description="Measure tap-to-click properties of devices"
nargs='?', type=str, help='Path to device (optional)') )
parser.add_argument('--format', metavar='format', parser.add_argument(
choices=['summary', 'dat'], "path",
default='summary', metavar="/dev/input/event0",
help='data format to print ("summary" or "dat")') nargs="?",
type=str,
help="Path to device (optional)",
)
parser.add_argument(
"--format",
metavar="format",
choices=["summary", "dat"],
default="summary",
help='data format to print ("summary" or "dat")',
)
args = parser.parse_args() args = parser.parse_args()
if not sys.stdout.isatty(): if not sys.stdout.isatty():
@ -235,13 +263,15 @@ def main(args):
try: try:
device = Device(args.path) device = Device(args.path)
error("Ready for recording data.\n" error(
"Tap the touchpad multiple times with a single finger only.\n" "Ready for recording data.\n"
"For useful data we recommend at least 20 taps.\n" "Tap the touchpad multiple times with a single finger only.\n"
"Ctrl+C to exit") "For useful data we recommend at least 20 taps.\n"
"Ctrl+C to exit"
)
device.read_events() device.read_events()
except KeyboardInterrupt: except KeyboardInterrupt:
msg('') msg("")
device.print(args.format) device.print(args.format)
except (PermissionError, OSError) as e: except (PermissionError, OSError) as e:
error("Error: failed to open device. {}".format(e)) error("Error: failed to open device. {}".format(e))

View file

@ -34,7 +34,7 @@ from pkg_resources import parse_version
class TestYaml(unittest.TestCase): class TestYaml(unittest.TestCase):
filename = '' filename = ""
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -42,163 +42,166 @@ class TestYaml(unittest.TestCase):
cls.yaml = yaml.safe_load(f) cls.yaml = yaml.safe_load(f)
def dict_key_crosscheck(self, d, keys): def dict_key_crosscheck(self, d, keys):
'''Check that each key in d is in keys, and that each key is in d''' """Check that each key in d is in keys, and that each key is in d"""
self.assertEqual(sorted(d.keys()), sorted(keys)) self.assertEqual(sorted(d.keys()), sorted(keys))
def libinput_events(self, filter=None): def libinput_events(self, filter=None):
'''Returns all libinput events in the recording, regardless of the """Returns all libinput events in the recording, regardless of the
device''' device"""
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
events = d['events'] events = d["events"]
if not events: if not events:
raise unittest.SkipTest() raise unittest.SkipTest()
for e in events: for e in events:
try: try:
libinput = e['libinput'] libinput = e["libinput"]
except KeyError: except KeyError:
continue continue
for ev in libinput: for ev in libinput:
if (filter is None or ev['type'] == filter or if (
isinstance(filter, list) and ev['type'] in filter): filter is None
or ev["type"] == filter
or isinstance(filter, list)
and ev["type"] in filter
):
yield ev yield ev
def test_sections_exist(self): def test_sections_exist(self):
sections = ['version', 'ndevices', 'libinput', 'system', 'devices'] sections = ["version", "ndevices", "libinput", "system", "devices"]
for section in sections: for section in sections:
self.assertIn(section, self.yaml) self.assertIn(section, self.yaml)
def test_version(self): def test_version(self):
version = self.yaml['version'] version = self.yaml["version"]
self.assertTrue(isinstance(version, int)) self.assertTrue(isinstance(version, int))
self.assertEqual(version, 1) self.assertEqual(version, 1)
def test_ndevices(self): def test_ndevices(self):
ndevices = self.yaml['ndevices'] ndevices = self.yaml["ndevices"]
self.assertTrue(isinstance(ndevices, int)) self.assertTrue(isinstance(ndevices, int))
self.assertGreaterEqual(ndevices, 1) self.assertGreaterEqual(ndevices, 1)
self.assertEqual(ndevices, len(self.yaml['devices'])) self.assertEqual(ndevices, len(self.yaml["devices"]))
def test_libinput(self): def test_libinput(self):
libinput = self.yaml['libinput'] libinput = self.yaml["libinput"]
version = libinput['version'] version = libinput["version"]
self.assertTrue(isinstance(version, str)) self.assertTrue(isinstance(version, str))
self.assertGreaterEqual(parse_version(version), self.assertGreaterEqual(parse_version(version), parse_version("1.10.0"))
parse_version('1.10.0')) git = libinput["git"]
git = libinput['git']
self.assertTrue(isinstance(git, str)) self.assertTrue(isinstance(git, str))
self.assertNotEqual(git, 'unknown') self.assertNotEqual(git, "unknown")
def test_system(self): def test_system(self):
system = self.yaml['system'] system = self.yaml["system"]
kernel = system['kernel'] kernel = system["kernel"]
self.assertTrue(isinstance(kernel, str)) self.assertTrue(isinstance(kernel, str))
self.assertEqual(kernel, os.uname().release) self.assertEqual(kernel, os.uname().release)
dmi = system['dmi'] dmi = system["dmi"]
self.assertTrue(isinstance(dmi, str)) self.assertTrue(isinstance(dmi, str))
with open('/sys/class/dmi/id/modalias') as f: with open("/sys/class/dmi/id/modalias") as f:
sys_dmi = f.read()[:-1] # trailing newline sys_dmi = f.read()[:-1] # trailing newline
self.assertEqual(dmi, sys_dmi) self.assertEqual(dmi, sys_dmi)
def test_devices_sections_exist(self): def test_devices_sections_exist(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
self.assertIn('node', d) self.assertIn("node", d)
self.assertIn('evdev', d) self.assertIn("evdev", d)
self.assertIn('udev', d) self.assertIn("udev", d)
def test_evdev_sections_exist(self): def test_evdev_sections_exist(self):
sections = ['name', 'id', 'codes', 'properties'] sections = ["name", "id", "codes", "properties"]
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
evdev = d['evdev'] evdev = d["evdev"]
for s in sections: for s in sections:
self.assertIn(s, evdev) self.assertIn(s, evdev)
def test_evdev_name(self): def test_evdev_name(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
evdev = d['evdev'] evdev = d["evdev"]
name = evdev['name'] name = evdev["name"]
self.assertTrue(isinstance(name, str)) self.assertTrue(isinstance(name, str))
self.assertGreaterEqual(len(name), 5) self.assertGreaterEqual(len(name), 5)
def test_evdev_id(self): def test_evdev_id(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
evdev = d['evdev'] evdev = d["evdev"]
id = evdev['id'] id = evdev["id"]
self.assertTrue(isinstance(id, list)) self.assertTrue(isinstance(id, list))
self.assertEqual(len(id), 4) self.assertEqual(len(id), 4)
self.assertGreater(id[0], 0) self.assertGreater(id[0], 0)
self.assertGreater(id[1], 0) self.assertGreater(id[1], 0)
def test_evdev_properties(self): def test_evdev_properties(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
evdev = d['evdev'] evdev = d["evdev"]
properties = evdev['properties'] properties = evdev["properties"]
self.assertTrue(isinstance(properties, list)) self.assertTrue(isinstance(properties, list))
def test_hid(self): def test_hid(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
hid = d['hid'] hid = d["hid"]
self.assertTrue(isinstance(hid, list)) self.assertTrue(isinstance(hid, list))
for byte in hid: for byte in hid:
self.assertGreaterEqual(byte, 0) self.assertGreaterEqual(byte, 0)
self.assertLessEqual(byte, 255) self.assertLessEqual(byte, 255)
def test_udev_sections_exist(self): def test_udev_sections_exist(self):
sections = ['properties'] sections = ["properties"]
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
udev = d['udev'] udev = d["udev"]
for s in sections: for s in sections:
self.assertIn(s, udev) self.assertIn(s, udev)
def test_udev_properties(self): def test_udev_properties(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
udev = d['udev'] udev = d["udev"]
properties = udev['properties'] properties = udev["properties"]
self.assertTrue(isinstance(properties, list)) self.assertTrue(isinstance(properties, list))
self.assertGreater(len(properties), 0) self.assertGreater(len(properties), 0)
self.assertIn('ID_INPUT=1', properties) self.assertIn("ID_INPUT=1", properties)
for p in properties: for p in properties:
self.assertTrue(re.match('[A-Z0-9_]+=.+', p)) self.assertTrue(re.match("[A-Z0-9_]+=.+", p))
def test_udev_id_inputs(self): def test_udev_id_inputs(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
udev = d['udev'] udev = d["udev"]
properties = udev['properties'] properties = udev["properties"]
id_inputs = [p for p in properties if p.startswith('ID_INPUT')] id_inputs = [p for p in properties if p.startswith("ID_INPUT")]
# We expect ID_INPUT and ID_INPUT_something, but might get more # We expect ID_INPUT and ID_INPUT_something, but might get more
# than one of the latter # than one of the latter
self.assertGreaterEqual(len(id_inputs), 2) self.assertGreaterEqual(len(id_inputs), 2)
def test_events_have_section(self): def test_events_have_section(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
events = d['events'] events = d["events"]
if not events: if not events:
raise unittest.SkipTest() raise unittest.SkipTest()
for e in events: for e in events:
self.assertTrue('evdev' in e or 'libinput' in e) self.assertTrue("evdev" in e or "libinput" in e)
def test_events_evdev(self): def test_events_evdev(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
events = d['events'] events = d["events"]
if not events: if not events:
raise unittest.SkipTest() raise unittest.SkipTest()
for e in events: for e in events:
try: try:
evdev = e['evdev'] evdev = e["evdev"]
except KeyError: except KeyError:
continue continue
@ -213,28 +216,28 @@ class TestYaml(unittest.TestCase):
self.assertLessEqual(ev_syn[4], 1) self.assertLessEqual(ev_syn[4], 1)
def test_events_evdev_syn_report(self): def test_events_evdev_syn_report(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
events = d['events'] events = d["events"]
if not events: if not events:
raise unittest.SkipTest() raise unittest.SkipTest()
for e in events: for e in events:
try: try:
evdev = e['evdev'] evdev = e["evdev"]
except KeyError: except KeyError:
continue continue
for ev in evdev[:-1]: for ev in evdev[:-1]:
self.assertFalse(ev[2] == 0 and ev[3] == 0) self.assertFalse(ev[2] == 0 and ev[3] == 0)
def test_events_libinput(self): def test_events_libinput(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
events = d['events'] events = d["events"]
if not events: if not events:
raise unittest.SkipTest() raise unittest.SkipTest()
for e in events: for e in events:
try: try:
libinput = e['libinput'] libinput = e["libinput"]
except KeyError: except KeyError:
continue continue
@ -243,20 +246,35 @@ class TestYaml(unittest.TestCase):
self.assertTrue(isinstance(ev, dict)) self.assertTrue(isinstance(ev, dict))
def test_events_libinput_type(self): def test_events_libinput_type(self):
types = ['POINTER_MOTION', 'POINTER_MOTION_ABSOLUTE', 'POINTER_AXIS', types = [
'POINTER_BUTTON', 'DEVICE_ADDED', 'KEYBOARD_KEY', "POINTER_MOTION",
'TOUCH_DOWN', 'TOUCH_MOTION', 'TOUCH_UP', 'TOUCH_FRAME', "POINTER_MOTION_ABSOLUTE",
'GESTURE_SWIPE_BEGIN', 'GESTURE_SWIPE_UPDATE', "POINTER_AXIS",
'GESTURE_SWIPE_END', 'GESTURE_PINCH_BEGIN', "POINTER_BUTTON",
'GESTURE_PINCH_UPDATE', 'GESTURE_PINCH_END', "DEVICE_ADDED",
'TABLET_TOOL_AXIS', 'TABLET_TOOL_PROXIMITY', "KEYBOARD_KEY",
'TABLET_TOOL_BUTTON', 'TABLET_TOOL_TIP', "TOUCH_DOWN",
'TABLET_PAD_STRIP', 'TABLET_PAD_RING', "TOUCH_MOTION",
'TABLET_PAD_BUTTON', 'SWITCH_TOGGLE', "TOUCH_UP",
] "TOUCH_FRAME",
"GESTURE_SWIPE_BEGIN",
"GESTURE_SWIPE_UPDATE",
"GESTURE_SWIPE_END",
"GESTURE_PINCH_BEGIN",
"GESTURE_PINCH_UPDATE",
"GESTURE_PINCH_END",
"TABLET_TOOL_AXIS",
"TABLET_TOOL_PROXIMITY",
"TABLET_TOOL_BUTTON",
"TABLET_TOOL_TIP",
"TABLET_PAD_STRIP",
"TABLET_PAD_RING",
"TABLET_PAD_BUTTON",
"SWITCH_TOGGLE",
]
for e in self.libinput_events(): for e in self.libinput_events():
self.assertIn('type', e) self.assertIn("type", e)
self.assertIn(e['type'], types) self.assertIn(e["type"], types)
def test_events_libinput_time(self): def test_events_libinput_time(self):
# DEVICE_ADDED has no time # DEVICE_ADDED has no time
@ -264,51 +282,51 @@ class TestYaml(unittest.TestCase):
# libinput event. # libinput event.
try: try:
for e in list(self.libinput_events())[2:]: for e in list(self.libinput_events())[2:]:
self.assertIn('time', e) self.assertIn("time", e)
self.assertGreater(e['time'], 0.0) self.assertGreater(e["time"], 0.0)
self.assertLess(e['time'], 60.0) self.assertLess(e["time"], 60.0)
except IndexError: except IndexError:
pass pass
def test_events_libinput_device_added(self): def test_events_libinput_device_added(self):
keys = ['type', 'seat', 'logical_seat'] keys = ["type", "seat", "logical_seat"]
for e in self.libinput_events('DEVICE_ADDED'): for e in self.libinput_events("DEVICE_ADDED"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
self.assertEqual(e['seat'], 'seat0') self.assertEqual(e["seat"], "seat0")
self.assertEqual(e['logical_seat'], 'default') self.assertEqual(e["logical_seat"], "default")
def test_events_libinput_pointer_motion(self): def test_events_libinput_pointer_motion(self):
keys = ['type', 'time', 'delta', 'unaccel'] keys = ["type", "time", "delta", "unaccel"]
for e in self.libinput_events('POINTER_MOTION'): for e in self.libinput_events("POINTER_MOTION"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
delta = e['delta'] delta = e["delta"]
self.assertTrue(isinstance(delta, list)) self.assertTrue(isinstance(delta, list))
self.assertEqual(len(delta), 2) self.assertEqual(len(delta), 2)
for d in delta: for d in delta:
self.assertTrue(isinstance(d, float)) self.assertTrue(isinstance(d, float))
unaccel = e['unaccel'] unaccel = e["unaccel"]
self.assertTrue(isinstance(unaccel, list)) self.assertTrue(isinstance(unaccel, list))
self.assertEqual(len(unaccel), 2) self.assertEqual(len(unaccel), 2)
for d in unaccel: for d in unaccel:
self.assertTrue(isinstance(d, float)) self.assertTrue(isinstance(d, float))
def test_events_libinput_pointer_button(self): def test_events_libinput_pointer_button(self):
keys = ['type', 'time', 'button', 'state', 'seat_count'] keys = ["type", "time", "button", "state", "seat_count"]
for e in self.libinput_events('POINTER_BUTTON'): for e in self.libinput_events("POINTER_BUTTON"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
button = e['button'] button = e["button"]
self.assertGreater(button, 0x100) # BTN_0 self.assertGreater(button, 0x100) # BTN_0
self.assertLess(button, 0x160) # KEY_OK self.assertLess(button, 0x160) # KEY_OK
state = e['state'] state = e["state"]
self.assertIn(state, ['pressed', 'released']) self.assertIn(state, ["pressed", "released"])
scount = e['seat_count'] scount = e["seat_count"]
self.assertGreaterEqual(scount, 0) self.assertGreaterEqual(scount, 0)
def test_events_libinput_pointer_absolute(self): def test_events_libinput_pointer_absolute(self):
keys = ['type', 'time', 'point', 'transformed'] keys = ["type", "time", "point", "transformed"]
for e in self.libinput_events('POINTER_MOTION_ABSOLUTE'): for e in self.libinput_events("POINTER_MOTION_ABSOLUTE"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
point = e['point'] point = e["point"]
self.assertTrue(isinstance(point, list)) self.assertTrue(isinstance(point, list))
self.assertEqual(len(point), 2) self.assertEqual(len(point), 2)
for p in point: for p in point:
@ -316,7 +334,7 @@ class TestYaml(unittest.TestCase):
self.assertGreater(p, 0.0) self.assertGreater(p, 0.0)
self.assertLess(p, 300.0) self.assertLess(p, 300.0)
transformed = e['transformed'] transformed = e["transformed"]
self.assertTrue(isinstance(transformed, list)) self.assertTrue(isinstance(transformed, list))
self.assertEqual(len(transformed), 2) self.assertEqual(len(transformed), 2)
for t in transformed: for t in transformed:
@ -325,25 +343,24 @@ class TestYaml(unittest.TestCase):
self.assertLess(t, 100.0) self.assertLess(t, 100.0)
def test_events_libinput_touch(self): def test_events_libinput_touch(self):
keys = ['type', 'time', 'slot', 'seat_slot'] keys = ["type", "time", "slot", "seat_slot"]
for e in self.libinput_events(): for e in self.libinput_events():
if (not e['type'].startswith('TOUCH_') or if not e["type"].startswith("TOUCH_") or e["type"] == "TOUCH_FRAME":
e['type'] == 'TOUCH_FRAME'):
continue continue
for k in keys: for k in keys:
self.assertIn(k, e.keys()) self.assertIn(k, e.keys())
slot = e['slot'] slot = e["slot"]
seat_slot = e['seat_slot'] seat_slot = e["seat_slot"]
self.assertGreaterEqual(slot, 0) self.assertGreaterEqual(slot, 0)
self.assertGreaterEqual(seat_slot, 0) self.assertGreaterEqual(seat_slot, 0)
def test_events_libinput_touch_down(self): def test_events_libinput_touch_down(self):
keys = ['type', 'time', 'slot', 'seat_slot', 'point', 'transformed'] keys = ["type", "time", "slot", "seat_slot", "point", "transformed"]
for e in self.libinput_events('TOUCH_DOWN'): for e in self.libinput_events("TOUCH_DOWN"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
point = e['point'] point = e["point"]
self.assertTrue(isinstance(point, list)) self.assertTrue(isinstance(point, list))
self.assertEqual(len(point), 2) self.assertEqual(len(point), 2)
for p in point: for p in point:
@ -351,7 +368,7 @@ class TestYaml(unittest.TestCase):
self.assertGreater(p, 0.0) self.assertGreater(p, 0.0)
self.assertLess(p, 300.0) self.assertLess(p, 300.0)
transformed = e['transformed'] transformed = e["transformed"]
self.assertTrue(isinstance(transformed, list)) self.assertTrue(isinstance(transformed, list))
self.assertEqual(len(transformed), 2) self.assertEqual(len(transformed), 2)
for t in transformed: for t in transformed:
@ -360,10 +377,10 @@ class TestYaml(unittest.TestCase):
self.assertLess(t, 100.0) self.assertLess(t, 100.0)
def test_events_libinput_touch_motion(self): def test_events_libinput_touch_motion(self):
keys = ['type', 'time', 'slot', 'seat_slot', 'point', 'transformed'] keys = ["type", "time", "slot", "seat_slot", "point", "transformed"]
for e in self.libinput_events('TOUCH_MOTION'): for e in self.libinput_events("TOUCH_MOTION"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
point = e['point'] point = e["point"]
self.assertTrue(isinstance(point, list)) self.assertTrue(isinstance(point, list))
self.assertEqual(len(point), 2) self.assertEqual(len(point), 2)
for p in point: for p in point:
@ -371,7 +388,7 @@ class TestYaml(unittest.TestCase):
self.assertGreater(p, 0.0) self.assertGreater(p, 0.0)
self.assertLess(p, 300.0) self.assertLess(p, 300.0)
transformed = e['transformed'] transformed = e["transformed"]
self.assertTrue(isinstance(transformed, list)) self.assertTrue(isinstance(transformed, list))
self.assertEqual(len(transformed), 2) self.assertEqual(len(transformed), 2)
for t in transformed: for t in transformed:
@ -380,25 +397,25 @@ class TestYaml(unittest.TestCase):
self.assertLess(t, 100.0) self.assertLess(t, 100.0)
def test_events_libinput_touch_frame(self): def test_events_libinput_touch_frame(self):
devices = self.yaml['devices'] devices = self.yaml["devices"]
for d in devices: for d in devices:
events = d['events'] events = d["events"]
if not events: if not events:
raise unittest.SkipTest() raise unittest.SkipTest()
for e in events: for e in events:
try: try:
evdev = e['libinput'] evdev = e["libinput"]
except KeyError: except KeyError:
continue continue
need_frame = False need_frame = False
for ev in evdev: for ev in evdev:
t = ev['type'] t = ev["type"]
if not t.startswith('TOUCH_'): if not t.startswith("TOUCH_"):
self.assertFalse(need_frame) self.assertFalse(need_frame)
continue continue
if t == 'TOUCH_FRAME': if t == "TOUCH_FRAME":
self.assertTrue(need_frame) self.assertTrue(need_frame)
need_frame = False need_frame = False
else: else:
@ -407,177 +424,175 @@ class TestYaml(unittest.TestCase):
self.assertFalse(need_frame) self.assertFalse(need_frame)
def test_events_libinput_gesture_pinch(self): def test_events_libinput_gesture_pinch(self):
keys = ['type', 'time', 'nfingers', 'delta', keys = ["type", "time", "nfingers", "delta", "unaccel", "angle_delta", "scale"]
'unaccel', 'angle_delta', 'scale'] for e in self.libinput_events(
for e in self.libinput_events(['GESTURE_PINCH_BEGIN', ["GESTURE_PINCH_BEGIN", "GESTURE_PINCH_UPDATE", "GESTURE_PINCH_END"]
'GESTURE_PINCH_UPDATE', ):
'GESTURE_PINCH_END']):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
delta = e['delta'] delta = e["delta"]
self.assertTrue(isinstance(delta, list)) self.assertTrue(isinstance(delta, list))
self.assertEqual(len(delta), 2) self.assertEqual(len(delta), 2)
for d in delta: for d in delta:
self.assertTrue(isinstance(d, float)) self.assertTrue(isinstance(d, float))
unaccel = e['unaccel'] unaccel = e["unaccel"]
self.assertTrue(isinstance(unaccel, list)) self.assertTrue(isinstance(unaccel, list))
self.assertEqual(len(unaccel), 2) self.assertEqual(len(unaccel), 2)
for d in unaccel: for d in unaccel:
self.assertTrue(isinstance(d, float)) self.assertTrue(isinstance(d, float))
adelta = e['angle_delta'] adelta = e["angle_delta"]
self.assertTrue(isinstance(adelta, list)) self.assertTrue(isinstance(adelta, list))
self.assertEqual(len(adelta), 2) self.assertEqual(len(adelta), 2)
for d in adelta: for d in adelta:
self.assertTrue(isinstance(d, float)) self.assertTrue(isinstance(d, float))
scale = e['scale'] scale = e["scale"]
self.assertTrue(isinstance(scale, list)) self.assertTrue(isinstance(scale, list))
self.assertEqual(len(scale), 2) self.assertEqual(len(scale), 2)
for d in scale: for d in scale:
self.assertTrue(isinstance(d, float)) self.assertTrue(isinstance(d, float))
def test_events_libinput_gesture_swipe(self): def test_events_libinput_gesture_swipe(self):
keys = ['type', 'time', 'nfingers', 'delta', keys = ["type", "time", "nfingers", "delta", "unaccel"]
'unaccel'] for e in self.libinput_events(
for e in self.libinput_events(['GESTURE_SWIPE_BEGIN', ["GESTURE_SWIPE_BEGIN", "GESTURE_SWIPE_UPDATE", "GESTURE_SWIPE_END"]
'GESTURE_SWIPE_UPDATE', ):
'GESTURE_SWIPE_END']):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
delta = e['delta'] delta = e["delta"]
self.assertTrue(isinstance(delta, list)) self.assertTrue(isinstance(delta, list))
self.assertEqual(len(delta), 2) self.assertEqual(len(delta), 2)
for d in delta: for d in delta:
self.assertTrue(isinstance(d, float)) self.assertTrue(isinstance(d, float))
unaccel = e['unaccel'] unaccel = e["unaccel"]
self.assertTrue(isinstance(unaccel, list)) self.assertTrue(isinstance(unaccel, list))
self.assertEqual(len(unaccel), 2) self.assertEqual(len(unaccel), 2)
for d in unaccel: for d in unaccel:
self.assertTrue(isinstance(d, float)) self.assertTrue(isinstance(d, float))
def test_events_libinput_tablet_pad_button(self): def test_events_libinput_tablet_pad_button(self):
keys = ['type', 'time', 'button', 'state', 'mode', 'is-toggle'] keys = ["type", "time", "button", "state", "mode", "is-toggle"]
for e in self.libinput_events('TABLET_PAD_BUTTON'): for e in self.libinput_events("TABLET_PAD_BUTTON"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
b = e['button'] b = e["button"]
self.assertTrue(isinstance(b, int)) self.assertTrue(isinstance(b, int))
self.assertGreaterEqual(b, 0) self.assertGreaterEqual(b, 0)
self.assertLessEqual(b, 16) self.assertLessEqual(b, 16)
state = e['state'] state = e["state"]
self.assertIn(state, ['pressed', 'released']) self.assertIn(state, ["pressed", "released"])
m = e['mode'] m = e["mode"]
self.assertTrue(isinstance(m, int)) self.assertTrue(isinstance(m, int))
self.assertGreaterEqual(m, 0) self.assertGreaterEqual(m, 0)
self.assertLessEqual(m, 3) self.assertLessEqual(m, 3)
t = e['is-toggle'] t = e["is-toggle"]
self.assertTrue(isinstance(t, bool)) self.assertTrue(isinstance(t, bool))
def test_events_libinput_tablet_pad_ring(self): def test_events_libinput_tablet_pad_ring(self):
keys = ['type', 'time', 'number', 'position', 'source', 'mode'] keys = ["type", "time", "number", "position", "source", "mode"]
for e in self.libinput_events('TABLET_PAD_RING'): for e in self.libinput_events("TABLET_PAD_RING"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
n = e['number'] n = e["number"]
self.assertTrue(isinstance(n, int)) self.assertTrue(isinstance(n, int))
self.assertGreaterEqual(n, 0) self.assertGreaterEqual(n, 0)
self.assertLessEqual(n, 4) self.assertLessEqual(n, 4)
p = e['position'] p = e["position"]
self.assertTrue(isinstance(p, float)) self.assertTrue(isinstance(p, float))
if p != -1.0: # special 'end' case if p != -1.0: # special 'end' case
self.assertGreaterEqual(p, 0.0) self.assertGreaterEqual(p, 0.0)
self.assertLess(p, 360.0) self.assertLess(p, 360.0)
m = e['mode'] m = e["mode"]
self.assertTrue(isinstance(m, int)) self.assertTrue(isinstance(m, int))
self.assertGreaterEqual(m, 0) self.assertGreaterEqual(m, 0)
self.assertLessEqual(m, 3) self.assertLessEqual(m, 3)
s = e['source'] s = e["source"]
self.assertIn(s, ['finger', 'unknown']) self.assertIn(s, ["finger", "unknown"])
def test_events_libinput_tablet_pad_strip(self): def test_events_libinput_tablet_pad_strip(self):
keys = ['type', 'time', 'number', 'position', 'source', 'mode'] keys = ["type", "time", "number", "position", "source", "mode"]
for e in self.libinput_events('TABLET_PAD_STRIP'): for e in self.libinput_events("TABLET_PAD_STRIP"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
n = e['number'] n = e["number"]
self.assertTrue(isinstance(n, int)) self.assertTrue(isinstance(n, int))
self.assertGreaterEqual(n, 0) self.assertGreaterEqual(n, 0)
self.assertLessEqual(n, 4) self.assertLessEqual(n, 4)
p = e['position'] p = e["position"]
self.assertTrue(isinstance(p, float)) self.assertTrue(isinstance(p, float))
if p != -1.0: # special 'end' case if p != -1.0: # special 'end' case
self.assertGreaterEqual(p, 0.0) self.assertGreaterEqual(p, 0.0)
self.assertLessEqual(p, 1.0) self.assertLessEqual(p, 1.0)
m = e['mode'] m = e["mode"]
self.assertTrue(isinstance(m, int)) self.assertTrue(isinstance(m, int))
self.assertGreaterEqual(m, 0) self.assertGreaterEqual(m, 0)
self.assertLessEqual(m, 3) self.assertLessEqual(m, 3)
s = e['source'] s = e["source"]
self.assertIn(s, ['finger', 'unknown']) self.assertIn(s, ["finger", "unknown"])
def test_events_libinput_tablet_tool_proximity(self): def test_events_libinput_tablet_tool_proximity(self):
keys = ['type', 'time', 'proximity', 'tool-type', 'serial', 'axes'] keys = ["type", "time", "proximity", "tool-type", "serial", "axes"]
for e in self.libinput_events('TABLET_TOOL_PROXIMITY'): for e in self.libinput_events("TABLET_TOOL_PROXIMITY"):
for k in keys: for k in keys:
self.assertIn(k, e) self.assertIn(k, e)
p = e['proximity'] p = e["proximity"]
self.assertIn(p, ['in', 'out']) self.assertIn(p, ["in", "out"])
p = e['tool-type'] p = e["tool-type"]
self.assertIn(p, ['pen', 'eraser', 'brush', 'airbrush', 'mouse', self.assertIn(
'lens', 'unknown']) p, ["pen", "eraser", "brush", "airbrush", "mouse", "lens", "unknown"]
)
s = e['serial'] s = e["serial"]
self.assertTrue(isinstance(s, int)) self.assertTrue(isinstance(s, int))
self.assertGreaterEqual(s, 0) self.assertGreaterEqual(s, 0)
a = e['axes'] a = e["axes"]
for ax in e['axes']: for ax in e["axes"]:
self.assertIn(a, 'pdtrsw') self.assertIn(a, "pdtrsw")
def test_events_libinput_tablet_tool(self): def test_events_libinput_tablet_tool(self):
keys = ['type', 'time', 'tip'] keys = ["type", "time", "tip"]
for e in self.libinput_events(['TABLET_TOOL_AXIS', for e in self.libinput_events(["TABLET_TOOL_AXIS", "TABLET_TOOL_TIP"]):
'TABLET_TOOL_TIP']):
for k in keys: for k in keys:
self.assertIn(k, e) self.assertIn(k, e)
t = e['tip'] t = e["tip"]
self.assertIn(t, ['down', 'up']) self.assertIn(t, ["down", "up"])
def test_events_libinput_tablet_tool_button(self): def test_events_libinput_tablet_tool_button(self):
keys = ['type', 'time', 'button', 'state'] keys = ["type", "time", "button", "state"]
for e in self.libinput_events('TABLET_TOOL_BUTTON'): for e in self.libinput_events("TABLET_TOOL_BUTTON"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
b = e['button'] b = e["button"]
# STYLUS, STYLUS2, STYLUS3 # STYLUS, STYLUS2, STYLUS3
self.assertIn(b, [0x14b, 0x14c, 0x139]) self.assertIn(b, [0x14B, 0x14C, 0x139])
s = e['state'] s = e["state"]
self.assertIn(s, ['pressed', 'released']) self.assertIn(s, ["pressed", "released"])
def test_events_libinput_tablet_tool_axes(self): def test_events_libinput_tablet_tool_axes(self):
for e in self.libinput_events(['TABLET_TOOL_PROXIMITY', for e in self.libinput_events(
'TABLET_TOOL_AXIS', ["TABLET_TOOL_PROXIMITY", "TABLET_TOOL_AXIS", "TABLET_TOOL_TIP"]
'TABLET_TOOL_TIP']): ):
point = e['point'] point = e["point"]
self.assertTrue(isinstance(point, list)) self.assertTrue(isinstance(point, list))
self.assertEqual(len(point), 2) self.assertEqual(len(point), 2)
for p in point: for p in point:
@ -585,7 +600,7 @@ class TestYaml(unittest.TestCase):
self.assertGreater(p, 0.0) self.assertGreater(p, 0.0)
try: try:
tilt = e['tilt'] tilt = e["tilt"]
self.assertTrue(isinstance(tilt, list)) self.assertTrue(isinstance(tilt, list))
self.assertEqual(len(tilt), 2) self.assertEqual(len(tilt), 2)
for t in tilt: for t in tilt:
@ -594,70 +609,75 @@ class TestYaml(unittest.TestCase):
pass pass
try: try:
d = e['distance'] d = e["distance"]
self.assertTrue(isinstance(d, float)) self.assertTrue(isinstance(d, float))
self.assertGreaterEqual(d, 0.0) self.assertGreaterEqual(d, 0.0)
self.assertNotIn('pressure', e) self.assertNotIn("pressure", e)
except KeyError: except KeyError:
pass pass
try: try:
p = e['pressure'] p = e["pressure"]
self.assertTrue(isinstance(p, float)) self.assertTrue(isinstance(p, float))
self.assertGreaterEqual(p, 0.0) self.assertGreaterEqual(p, 0.0)
self.assertNotIn('distance', e) self.assertNotIn("distance", e)
except KeyError: except KeyError:
pass pass
try: try:
r = e['rotation'] r = e["rotation"]
self.assertTrue(isinstance(r, float)) self.assertTrue(isinstance(r, float))
self.assertGreaterEqual(r, 0.0) self.assertGreaterEqual(r, 0.0)
except KeyError: except KeyError:
pass pass
try: try:
s = e['slider'] s = e["slider"]
self.assertTrue(isinstance(s, float)) self.assertTrue(isinstance(s, float))
self.assertGreaterEqual(s, 0.0) self.assertGreaterEqual(s, 0.0)
except KeyError: except KeyError:
pass pass
try: try:
w = e['wheel'] w = e["wheel"]
self.assertTrue(isinstance(w, float)) self.assertTrue(isinstance(w, float))
self.assertGreaterEqual(w, 0.0) self.assertGreaterEqual(w, 0.0)
self.assertIn('wheel-discrete', e) self.assertIn("wheel-discrete", e)
wd = e['wheel-discrete'] wd = e["wheel-discrete"]
self.assertTrue(isinstance(wd, 1)) self.assertTrue(isinstance(wd, 1))
self.assertGreaterEqual(wd, 0.0) self.assertGreaterEqual(wd, 0.0)
def sign(x): def sign(x):
(1, -1)[x < 0] (1, -1)[x < 0]
self.assertTrue(sign(w), sign(wd)) self.assertTrue(sign(w), sign(wd))
except KeyError: except KeyError:
pass pass
def test_events_libinput_switch(self): def test_events_libinput_switch(self):
keys = ['type', 'time', 'switch', 'state'] keys = ["type", "time", "switch", "state"]
for e in self.libinput_events('SWITCH_TOGGLE'): for e in self.libinput_events("SWITCH_TOGGLE"):
self.dict_key_crosscheck(e, keys) self.dict_key_crosscheck(e, keys)
s = e['switch'] s = e["switch"]
self.assertTrue(isinstance(s, int)) self.assertTrue(isinstance(s, int))
self.assertIn(s, [0x00, 0x01]) self.assertIn(s, [0x00, 0x01])
# yaml converts on/off to true/false # yaml converts on/off to true/false
state = e['state'] state = e["state"]
self.assertTrue(isinstance(state, bool)) self.assertTrue(isinstance(state, bool))
if __name__ == '__main__': if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Verify a YAML recording') parser = argparse.ArgumentParser(description="Verify a YAML recording")
parser.add_argument('recording', metavar='recorded-file.yaml', parser.add_argument(
type=str, help='Path to device recording') "recording",
parser.add_argument('--verbose', action='store_true') metavar="recorded-file.yaml",
type=str,
help="Path to device recording",
)
parser.add_argument("--verbose", action="store_true")
args, remainder = parser.parse_known_args() args, remainder = parser.parse_known_args()
TestYaml.filename = args.recording TestYaml.filename = args.recording
verbosity = 1 verbosity = 1

View file

@ -35,9 +35,11 @@ try:
import yaml import yaml
import pyudev import pyudev
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
print('Error: {}'.format(e), file=sys.stderr) print("Error: {}".format(e), file=sys.stderr)
print('One or more python modules are missing. Please install those ' print(
'modules and re-run this tool.') "One or more python modules are missing. Please install those "
"modules and re-run this tool."
)
sys.exit(1) sys.exit(1)
@ -53,25 +55,27 @@ class YamlException(Exception):
def fetch(yaml, key): def fetch(yaml, key):
'''Helper function to avoid confusing a YAML error with a """Helper function to avoid confusing a YAML error with a
normal KeyError bug''' normal KeyError bug"""
try: try:
return yaml[key] return yaml[key]
except KeyError: except KeyError:
raise YamlException('Failed to get \'{}\' from recording.'.format(key)) raise YamlException("Failed to get '{}' from recording.".format(key))
def check_udev_properties(yaml_data, uinput): def check_udev_properties(yaml_data, uinput):
''' """
Compare the properties our new uinput device has with the ones from the Compare the properties our new uinput device has with the ones from the
recording and ring the alarm bell if one of them is off. recording and ring the alarm bell if one of them is off.
''' """
yaml_udev_section = fetch(yaml_data, 'udev') yaml_udev_section = fetch(yaml_data, "udev")
yaml_udev_props = fetch(yaml_udev_section, 'properties') yaml_udev_props = fetch(yaml_udev_section, "properties")
yaml_props = {k: v for (k, v) in [prop.split('=', maxsplit=1) for prop in yaml_udev_props]} yaml_props = {
k: v for (k, v) in [prop.split("=", maxsplit=1) for prop in yaml_udev_props]
}
try: try:
# We don't assign this one to virtual devices # We don't assign this one to virtual devices
del yaml_props['LIBINPUT_DEVICE_GROUP'] del yaml_props["LIBINPUT_DEVICE_GROUP"]
except KeyError: except KeyError:
pass pass
@ -82,42 +86,52 @@ def check_udev_properties(yaml_data, uinput):
for name, value in udev_device.properties.items(): for name, value in udev_device.properties.items():
if name in yaml_props: if name in yaml_props:
if yaml_props[name] != value: if yaml_props[name] != value:
error(f'Warning: udev property mismatch: recording has {name}={yaml_props[name]}, device has {name}={value}') error(
f"Warning: udev property mismatch: recording has {name}={yaml_props[name]}, device has {name}={value}"
)
del yaml_props[name] del yaml_props[name]
else: else:
# The list of properties we add to the recording, see libinput-record.c # The list of properties we add to the recording, see libinput-record.c
prefixes = ("ID_INPUT", "LIBINPUT", "EVDEV_ABS", "MOUSE_DPI", "POINTINGSTICK_") prefixes = (
"ID_INPUT",
"LIBINPUT",
"EVDEV_ABS",
"MOUSE_DPI",
"POINTINGSTICK_",
)
for prefix in prefixes: for prefix in prefixes:
if name.startswith(prefix): if name.startswith(prefix):
error(f'Warning: unexpected property: {name}={value}') error(f"Warning: unexpected property: {name}={value}")
# the ones we found above were removed from the dict # the ones we found above were removed from the dict
for name, value in yaml_props.items(): for name, value in yaml_props.items():
error(f'Warning: device is missing recorded udev property: {name}={value}') error(f"Warning: device is missing recorded udev property: {name}={value}")
def create(device): def create(device):
evdev = fetch(device, 'evdev') evdev = fetch(device, "evdev")
d = libevdev.Device() d = libevdev.Device()
d.name = fetch(evdev, 'name') d.name = fetch(evdev, "name")
ids = fetch(evdev, 'id') ids = fetch(evdev, "id")
if len(ids) != 4: if len(ids) != 4:
raise YamlException('Invalid ID format: {}'.format(ids)) raise YamlException("Invalid ID format: {}".format(ids))
d.id = dict(zip(['bustype', 'vendor', 'product', 'version'], ids)) d.id = dict(zip(["bustype", "vendor", "product", "version"], ids))
codes = fetch(evdev, 'codes') codes = fetch(evdev, "codes")
for evtype, evcodes in codes.items(): for evtype, evcodes in codes.items():
for code in evcodes: for code in evcodes:
data = None data = None
if evtype == libevdev.EV_ABS.value: if evtype == libevdev.EV_ABS.value:
values = fetch(evdev, 'absinfo')[code] values = fetch(evdev, "absinfo")[code]
absinfo = libevdev.InputAbsInfo(minimum=values[0], absinfo = libevdev.InputAbsInfo(
maximum=values[1], minimum=values[0],
fuzz=values[2], maximum=values[1],
flat=values[3], fuzz=values[2],
resolution=values[4]) flat=values[3],
resolution=values[4],
)
data = absinfo data = absinfo
elif evtype == libevdev.EV_REP.value: elif evtype == libevdev.EV_REP.value:
if code == libevdev.EV_REP.REP_DELAY.value: if code == libevdev.EV_REP.REP_DELAY.value:
@ -126,7 +140,7 @@ def create(device):
data = 20 data = 20
d.enable(libevdev.evbit(evtype, code), data=data) d.enable(libevdev.evbit(evtype, code), data=data)
properties = fetch(evdev, 'properties') properties = fetch(evdev, "properties")
for prop in properties: for prop in properties:
d.enable(libevdev.propbit(prop)) d.enable(libevdev.propbit(prop))
@ -140,30 +154,39 @@ def create(device):
def print_events(devnode, indent, evs): def print_events(devnode, indent, evs):
devnode = os.path.basename(devnode) devnode = os.path.basename(devnode)
for e in evs: for e in evs:
print("{}: {}{:06d}.{:06d} {} / {:<20s} {:4d}".format( print(
devnode, ' ' * (indent * 8), e.sec, e.usec, e.type.name, e.code.name, e.value)) "{}: {}{:06d}.{:06d} {} / {:<20s} {:4d}".format(
devnode,
" " * (indent * 8),
e.sec,
e.usec,
e.type.name,
e.code.name,
e.value,
)
)
def replay(device, verbose): def replay(device, verbose):
events = fetch(device, 'events') events = fetch(device, "events")
if events is None: if events is None:
return return
uinput = device['__uinput'] uinput = device["__uinput"]
# The first event may have a nonzero offset but we want to replay # The first event may have a nonzero offset but we want to replay
# immediately regardless. When replaying multiple devices, the first # immediately regardless. When replaying multiple devices, the first
# offset is the offset from the first event on any device. # offset is the offset from the first event on any device.
offset = time.time() - device['__first_event_offset'] offset = time.time() - device["__first_event_offset"]
if offset < 0: if offset < 0:
error('WARNING: event time offset is in the future, refusing to replay') error("WARNING: event time offset is in the future, refusing to replay")
return return
# each 'evdev' set contains one SYN_REPORT so we only need to check for # each 'evdev' set contains one SYN_REPORT so we only need to check for
# the time offset once per event # the time offset once per event
for event in events: for event in events:
try: try:
evdev = fetch(event, 'evdev') evdev = fetch(event, "evdev")
except YamlException: except YamlException:
continue continue
@ -174,25 +197,31 @@ def replay(device, verbose):
if evtime - now > 150 / 1e6: # 150 µs error margin if evtime - now > 150 / 1e6: # 150 µs error margin
time.sleep(evtime - now - 150 / 1e6) time.sleep(evtime - now - 150 / 1e6)
evs = [libevdev.InputEvent(libevdev.evbit(e[2], e[3]), value=e[4], sec=e[0], usec=e[1]) for e in evdev] evs = [
libevdev.InputEvent(
libevdev.evbit(e[2], e[3]), value=e[4], sec=e[0], usec=e[1]
)
for e in evdev
]
uinput.send_events(evs) uinput.send_events(evs)
if verbose: if verbose:
print_events(uinput.devnode, device['__index'], evs) print_events(uinput.devnode, device["__index"], evs)
def first_timestamp(device): def first_timestamp(device):
try: try:
events = fetch(device, 'events') events = fetch(device, "events")
if events is None: if events is None:
raise YamlException('No events from this device') raise YamlException("No events from this device")
evdev = fetch(events[0], 'evdev') evdev = fetch(events[0], "evdev")
(sec, usec, *_) = evdev[0] (sec, usec, *_) = evdev[0]
return sec + usec / 1.e6 return sec + usec / 1.0e6
except YamlException: except YamlException:
import math import math
return math.inf return math.inf
@ -204,7 +233,7 @@ def wrap(func, *args):
def loop(args, recording): def loop(args, recording):
devices = fetch(recording, 'devices') devices = fetch(recording, "devices")
# All devices need to start replaying at the same time, so let's find # All devices need to start replaying at the same time, so let's find
# the very first event and offset everything by that timestamp. # the very first event and offset everything by that timestamp.
@ -212,13 +241,13 @@ def loop(args, recording):
for idx, d in enumerate(devices): for idx, d in enumerate(devices):
uinput = create(d) uinput = create(d)
print('{}: {}'.format(uinput.devnode, uinput.name)) print("{}: {}".format(uinput.devnode, uinput.name))
d['__uinput'] = uinput # cheaper to hide it in the dict then work around it d["__uinput"] = uinput # cheaper to hide it in the dict then work around it
d['__index'] = idx d["__index"] = idx
d['__first_event_offset'] = toffset d["__first_event_offset"] = toffset
while True: while True:
input('Hit enter to start replaying') input("Hit enter to start replaying")
processes = [] processes = []
for d in devices: for d in devices:
@ -236,66 +265,83 @@ def loop(args, recording):
def create_device_quirk(device): def create_device_quirk(device):
try: try:
quirks = fetch(device, 'quirks') quirks = fetch(device, "quirks")
if not quirks: if not quirks:
return None return None
except YamlException: except YamlException:
return None return None
# Where the device has a quirk, we match on name, vendor and product. # Where the device has a quirk, we match on name, vendor and product.
# That's the best match we can assemble here from the info we have. # That's the best match we can assemble here from the info we have.
evdev = fetch(device, 'evdev') evdev = fetch(device, "evdev")
name = fetch(evdev, 'name') name = fetch(evdev, "name")
id = fetch(evdev, 'id') id = fetch(evdev, "id")
quirk = ('[libinput-replay {name}]\n' quirk = (
'MatchName={name}\n' "[libinput-replay {name}]\n"
'MatchVendor=0x{id[1]:04X}\n' "MatchName={name}\n"
'MatchProduct=0x{id[2]:04X}\n').format(name=name, id=id) "MatchVendor=0x{id[1]:04X}\n"
quirk += '\n'.join(quirks) "MatchProduct=0x{id[2]:04X}\n"
).format(name=name, id=id)
quirk += "\n".join(quirks)
return quirk return quirk
def setup_quirks(recording): def setup_quirks(recording):
devices = fetch(recording, 'devices') devices = fetch(recording, "devices")
overrides = None overrides = None
quirks = [] quirks = []
for d in devices: for d in devices:
if 'quirks' in d: if "quirks" in d:
quirk = create_device_quirk(d) quirk = create_device_quirk(d)
if quirk: if quirk:
quirks.append(quirk) quirks.append(quirk)
if not quirks: if not quirks:
return None return None
overrides = Path('/etc/libinput/local-overrides.quirks') overrides = Path("/etc/libinput/local-overrides.quirks")
if overrides.exists(): if overrides.exists():
print('{} exists, please move it out of the way first'.format(overrides), file=sys.stderr) print(
"{} exists, please move it out of the way first".format(overrides),
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
overrides.parent.mkdir(exist_ok=True) overrides.parent.mkdir(exist_ok=True)
with overrides.open('w+') as fd: with overrides.open("w+") as fd:
fd.write('# This file was generated by libinput replay\n') fd.write("# This file was generated by libinput replay\n")
fd.write('# Unless libinput replay is running right now, remove this file.\n') fd.write("# Unless libinput replay is running right now, remove this file.\n")
fd.write('\n\n'.join(quirks)) fd.write("\n\n".join(quirks))
return overrides return overrides
def check_file(recording): def check_file(recording):
version = fetch(recording, 'version') version = fetch(recording, "version")
if version != SUPPORTED_FILE_VERSION: if version != SUPPORTED_FILE_VERSION:
raise YamlException('Invalid file format: {}, expected {}'.format(version, SUPPORTED_FILE_VERSION)) raise YamlException(
"Invalid file format: {}, expected {}".format(
version, SUPPORTED_FILE_VERSION
)
)
ndevices = fetch(recording, 'ndevices') ndevices = fetch(recording, "ndevices")
devices = fetch(recording, 'devices') devices = fetch(recording, "devices")
if ndevices != len(devices): if ndevices != len(devices):
error('WARNING: truncated file, expected {} devices, got {}'.format(ndevices, len(devices))) error(
"WARNING: truncated file, expected {} devices, got {}".format(
ndevices, len(devices)
)
)
def main(): def main():
parser = argparse.ArgumentParser(description='Replay a device recording') parser = argparse.ArgumentParser(description="Replay a device recording")
parser.add_argument('recording', metavar='recorded-file.yaml', parser.add_argument(
type=str, help='Path to device recording') "recording",
parser.add_argument('--verbose', action='store_true') metavar="recorded-file.yaml",
type=str,
help="Path to device recording",
)
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args() args = parser.parse_args()
quirks_file = None quirks_file = None
@ -309,13 +355,13 @@ def main():
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
except (PermissionError, OSError) as e: except (PermissionError, OSError) as e:
error('Error: failed to open device: {}'.format(e)) error("Error: failed to open device: {}".format(e))
except YamlException as e: except YamlException as e:
error('Error: failed to parse recording: {}'.format(e)) error("Error: failed to parse recording: {}".format(e))
finally: finally:
if quirks_file: if quirks_file:
quirks_file.unlink() quirks_file.unlink()
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View file

@ -32,15 +32,15 @@ import logging
try: try:
import pytest import pytest
except ImportError: except ImportError:
print('Failed to import pytest. Skipping.', file=sys.stderr) print("Failed to import pytest. Skipping.", file=sys.stderr)
sys.exit(77) sys.exit(77)
logger = logging.getLogger('test') logger = logging.getLogger("test")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
if '@DISABLE_WARNING@' != 'yes': if "@DISABLE_WARNING@" != "yes":
print('This is the source file, run the one in the meson builddir instead') print("This is the source file, run the one in the meson builddir instead")
sys.exit(1) sys.exit(1)
@ -49,9 +49,13 @@ def _disable_coredump():
def run_command(args): def run_command(args):
logger.debug('run command: {}'.format(' '.join(args))) logger.debug("run command: {}".format(" ".join(args)))
with subprocess.Popen(args, preexec_fn=_disable_coredump, with subprocess.Popen(
stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p: args,
preexec_fn=_disable_coredump,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as p:
try: try:
p.wait(0.7) p.wait(0.7)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
@ -59,11 +63,11 @@ def run_command(args):
stdout, stderr = p.communicate(timeout=5) stdout, stderr = p.communicate(timeout=5)
if p.returncode == -3: if p.returncode == -3:
p.returncode = 0 p.returncode = 0
return p.returncode, stdout.decode('UTF-8'), stderr.decode('UTF-8') return p.returncode, stdout.decode("UTF-8"), stderr.decode("UTF-8")
class LibinputTool(object): class LibinputTool(object):
libinput_tool = 'libinput' libinput_tool = "libinput"
subtool = None subtool = None
def __init__(self, subtool=None): def __init__(self, subtool=None):
@ -92,43 +96,43 @@ class LibinputTool(object):
def run_command_unrecognized_option(self, args): def run_command_unrecognized_option(self, args):
rc, stdout, stderr = self.run_command(args) rc, stdout, stderr = self.run_command(args)
assert rc == 2, (rc, stdout, stderr) assert rc == 2, (rc, stdout, stderr)
assert stdout.startswith('Usage') or stdout == '' assert stdout.startswith("Usage") or stdout == ""
assert 'unrecognized option' in stderr assert "unrecognized option" in stderr
def run_command_missing_arg(self, args): def run_command_missing_arg(self, args):
rc, stdout, stderr = self.run_command(args) rc, stdout, stderr = self.run_command(args)
assert rc == 2, (rc, stdout, stderr) assert rc == 2, (rc, stdout, stderr)
assert stdout.startswith('Usage') or stdout == '' assert stdout.startswith("Usage") or stdout == ""
assert 'requires an argument' in stderr assert "requires an argument" in stderr
def run_command_unrecognized_tool(self, args): def run_command_unrecognized_tool(self, args):
rc, stdout, stderr = self.run_command(args) rc, stdout, stderr = self.run_command(args)
assert rc == 2, (rc, stdout, stderr) assert rc == 2, (rc, stdout, stderr)
assert stdout.startswith('Usage') or stdout == '' assert stdout.startswith("Usage") or stdout == ""
assert 'is not installed' in stderr assert "is not installed" in stderr
class LibinputDebugGui(LibinputTool): class LibinputDebugGui(LibinputTool):
def __init__(self, subtool='debug-gui'): def __init__(self, subtool="debug-gui"):
assert subtool == 'debug-gui' assert subtool == "debug-gui"
super().__init__(subtool) super().__init__(subtool)
debug_gui_enabled = '@MESON_ENABLED_DEBUG_GUI@' == 'True' debug_gui_enabled = "@MESON_ENABLED_DEBUG_GUI@" == "True"
if not debug_gui_enabled: if not debug_gui_enabled:
pytest.skip() pytest.skip()
if not os.getenv('DISPLAY') and not os.getenv('WAYLAND_DISPLAY'): if not os.getenv("DISPLAY") and not os.getenv("WAYLAND_DISPLAY"):
pytest.skip() pytest.skip()
# 77 means gtk_init() failed, which is probably because you can't # 77 means gtk_init() failed, which is probably because you can't
# connect to the display server. # connect to the display server.
rc, _, _ = self.run_command(['--help']) rc, _, _ = self.run_command(["--help"])
if rc == 77: if rc == 77:
pytest.skip() pytest.skip()
def get_tool(subtool=None): def get_tool(subtool=None):
if subtool == 'debug-gui': if subtool == "debug-gui":
return LibinputDebugGui() return LibinputDebugGui()
else: else:
return LibinputTool(subtool) return LibinputTool(subtool)
@ -139,110 +143,110 @@ def libinput():
return get_tool() return get_tool()
@pytest.fixture(params=['debug-events', 'debug-gui']) @pytest.fixture(params=["debug-events", "debug-gui"])
def libinput_debug_tool(request): def libinput_debug_tool(request):
yield get_tool(request.param) yield get_tool(request.param)
@pytest.fixture @pytest.fixture
def libinput_debug_events(): def libinput_debug_events():
return get_tool('debug-events') return get_tool("debug-events")
@pytest.fixture @pytest.fixture
def libinput_debug_gui(): def libinput_debug_gui():
return get_tool('debug-gui') return get_tool("debug-gui")
@pytest.fixture @pytest.fixture
def libinput_record(): def libinput_record():
return get_tool('record') return get_tool("record")
def test_help(libinput): def test_help(libinput):
stdout, stderr = libinput.run_command_success(['--help']) stdout, stderr = libinput.run_command_success(["--help"])
assert stdout.startswith('Usage:') assert stdout.startswith("Usage:")
assert stderr == '' assert stderr == ""
def test_version(libinput): def test_version(libinput):
stdout, stderr = libinput.run_command_success(['--version']) stdout, stderr = libinput.run_command_success(["--version"])
assert stdout.startswith('1') assert stdout.startswith("1")
assert stderr == '' assert stderr == ""
@pytest.mark.parametrize('argument', ['--banana', '--foo', '--quiet', '--verbose']) @pytest.mark.parametrize("argument", ["--banana", "--foo", "--quiet", "--verbose"])
def test_invalid_arguments(libinput, argument): def test_invalid_arguments(libinput, argument):
libinput.run_command_unrecognized_option([argument]) libinput.run_command_unrecognized_option([argument])
@pytest.mark.parametrize('tool', [['foo'], ['debug'], ['foo', '--quiet']]) @pytest.mark.parametrize("tool", [["foo"], ["debug"], ["foo", "--quiet"]])
def test_invalid_tool(libinput, tool): def test_invalid_tool(libinput, tool):
libinput.run_command_unrecognized_tool(tool) libinput.run_command_unrecognized_tool(tool)
def test_udev_seat(libinput_debug_tool): def test_udev_seat(libinput_debug_tool):
libinput_debug_tool.run_command_missing_arg(['--udev']) libinput_debug_tool.run_command_missing_arg(["--udev"])
libinput_debug_tool.run_command_success(['--udev', 'seat0']) libinput_debug_tool.run_command_success(["--udev", "seat0"])
libinput_debug_tool.run_command_success(['--udev', 'seat1']) libinput_debug_tool.run_command_success(["--udev", "seat1"])
@pytest.mark.skipif(os.environ.get('UDEV_NOT_AVAILABLE'), reason='udev required') @pytest.mark.skipif(os.environ.get("UDEV_NOT_AVAILABLE"), reason="udev required")
def test_device_arg(libinput_debug_tool): def test_device_arg(libinput_debug_tool):
libinput_debug_tool.run_command_missing_arg(['--device']) libinput_debug_tool.run_command_missing_arg(["--device"])
libinput_debug_tool.run_command_success(['--device', '/dev/input/event0']) libinput_debug_tool.run_command_success(["--device", "/dev/input/event0"])
libinput_debug_tool.run_command_success(['--device', '/dev/input/event1']) libinput_debug_tool.run_command_success(["--device", "/dev/input/event1"])
libinput_debug_tool.run_command_success(['/dev/input/event0']) libinput_debug_tool.run_command_success(["/dev/input/event0"])
options = { options = {
'pattern': ['sendevents'], "pattern": ["sendevents"],
# enable/disable options # enable/disable options
'enable-disable': [ "enable-disable": [
'tap', "tap",
'drag', "drag",
'drag-lock', "drag-lock",
'middlebutton', "middlebutton",
'natural-scrolling', "natural-scrolling",
'left-handed', "left-handed",
'dwt' "dwt",
], ],
# options with distinct values # options with distinct values
'enums': { "enums": {
'set-click-method': ['none', 'clickfinger', 'buttonareas'], "set-click-method": ["none", "clickfinger", "buttonareas"],
'set-scroll-method': ['none', 'twofinger', 'edge', 'button'], "set-scroll-method": ["none", "twofinger", "edge", "button"],
'set-profile': ['adaptive', 'flat'], "set-profile": ["adaptive", "flat"],
'set-tap-map': ['lrm', 'lmr'], "set-tap-map": ["lrm", "lmr"],
}, },
# options with a range # options with a range
'ranges': { "ranges": {
'set-speed': (float, -1.0, +1.0), "set-speed": (float, -1.0, +1.0),
} },
} }
# Options that allow for glob patterns # Options that allow for glob patterns
@pytest.mark.parametrize('option', options['pattern']) @pytest.mark.parametrize("option", options["pattern"])
def test_options_pattern(libinput_debug_tool, option): def test_options_pattern(libinput_debug_tool, option):
libinput_debug_tool.run_command_success(['--disable-{}'.format(option), '*']) libinput_debug_tool.run_command_success(["--disable-{}".format(option), "*"])
libinput_debug_tool.run_command_success(['--disable-{}'.format(option), 'abc*']) libinput_debug_tool.run_command_success(["--disable-{}".format(option), "abc*"])
@pytest.mark.parametrize('option', options['enable-disable']) @pytest.mark.parametrize("option", options["enable-disable"])
def test_options_enable_disable(libinput_debug_tool, option): def test_options_enable_disable(libinput_debug_tool, option):
libinput_debug_tool.run_command_success(['--enable-{}'.format(option)]) libinput_debug_tool.run_command_success(["--enable-{}".format(option)])
libinput_debug_tool.run_command_success(['--disable-{}'.format(option)]) libinput_debug_tool.run_command_success(["--disable-{}".format(option)])
@pytest.mark.parametrize('option', options['enums'].items()) @pytest.mark.parametrize("option", options["enums"].items())
def test_options_enums(libinput_debug_tool, option): def test_options_enums(libinput_debug_tool, option):
name, values = option name, values = option
for v in values: for v in values:
libinput_debug_tool.run_command_success(['--{}'.format(name), v]) libinput_debug_tool.run_command_success(["--{}".format(name), v])
libinput_debug_tool.run_command_success(['--{}={}'.format(name, v)]) libinput_debug_tool.run_command_success(["--{}={}".format(name, v)])
@pytest.mark.parametrize('option', options['ranges'].items()) @pytest.mark.parametrize("option", options["ranges"].items())
def test_options_ranges(libinput_debug_tool, option): def test_options_ranges(libinput_debug_tool, option):
name, values = option name, values = option
range_type, minimum, maximum = values range_type, minimum, maximum = values
@ -250,114 +254,128 @@ def test_options_ranges(libinput_debug_tool, option):
step = (maximum - minimum) / 10.0 step = (maximum - minimum) / 10.0
value = minimum value = minimum
while value < maximum: while value < maximum:
libinput_debug_tool.run_command_success(['--{}'.format(name), str(value)]) libinput_debug_tool.run_command_success(["--{}".format(name), str(value)])
libinput_debug_tool.run_command_success(['--{}={}'.format(name, value)]) libinput_debug_tool.run_command_success(["--{}={}".format(name, value)])
value += step value += step
libinput_debug_tool.run_command_success(['--{}'.format(name), str(maximum)]) libinput_debug_tool.run_command_success(["--{}".format(name), str(maximum)])
libinput_debug_tool.run_command_success(['--{}={}'.format(name, maximum)]) libinput_debug_tool.run_command_success(["--{}={}".format(name, maximum)])
def test_apply_to(libinput_debug_tool): def test_apply_to(libinput_debug_tool):
libinput_debug_tool.run_command_missing_arg(['--apply-to']) libinput_debug_tool.run_command_missing_arg(["--apply-to"])
libinput_debug_tool.run_command_success(['--apply-to', '*foo*']) libinput_debug_tool.run_command_success(["--apply-to", "*foo*"])
libinput_debug_tool.run_command_success(['--apply-to', 'foobar']) libinput_debug_tool.run_command_success(["--apply-to", "foobar"])
libinput_debug_tool.run_command_success(['--apply-to', 'any']) libinput_debug_tool.run_command_success(["--apply-to", "any"])
@pytest.mark.parametrize('args', [['--verbose'], ['--quiet'], @pytest.mark.parametrize(
['--verbose', '--quiet'], "args",
['--quiet', '--verbose']]) [["--verbose"], ["--quiet"], ["--verbose", "--quiet"], ["--quiet", "--verbose"]],
)
def test_debug_events_verbose_quiet(libinput_debug_events, args): def test_debug_events_verbose_quiet(libinput_debug_events, args):
libinput_debug_events.run_command_success(args) libinput_debug_events.run_command_success(args)
@pytest.mark.parametrize('arg', ['--banana', '--foo', '--version']) @pytest.mark.parametrize("arg", ["--banana", "--foo", "--version"])
def test_invalid_args(libinput_debug_tool, arg): def test_invalid_args(libinput_debug_tool, arg):
libinput_debug_tool.run_command_unrecognized_option([arg]) libinput_debug_tool.run_command_unrecognized_option([arg])
def test_libinput_debug_events_multiple_devices(libinput_debug_events): def test_libinput_debug_events_multiple_devices(libinput_debug_events):
libinput_debug_events.run_command_success(['--device', '/dev/input/event0', '/dev/input/event1']) libinput_debug_events.run_command_success(
["--device", "/dev/input/event0", "/dev/input/event1"]
)
# same event path multiple times? meh, your problem # same event path multiple times? meh, your problem
libinput_debug_events.run_command_success(['--device', '/dev/input/event0', '/dev/input/event0']) libinput_debug_events.run_command_success(
libinput_debug_events.run_command_success(['/dev/input/event0', '/dev/input/event1']) ["--device", "/dev/input/event0", "/dev/input/event0"]
)
libinput_debug_events.run_command_success(
["/dev/input/event0", "/dev/input/event1"]
)
def test_libinput_debug_events_too_many_devices(libinput_debug_events): def test_libinput_debug_events_too_many_devices(libinput_debug_events):
# Too many arguments just bails with the usage message # Too many arguments just bails with the usage message
rc, stdout, stderr = libinput_debug_events.run_command(['/dev/input/event0'] * 61) rc, stdout, stderr = libinput_debug_events.run_command(["/dev/input/event0"] * 61)
assert rc == 2, (stdout, stderr) assert rc == 2, (stdout, stderr)
@pytest.mark.parametrize('arg', ['--quiet']) @pytest.mark.parametrize("arg", ["--quiet"])
def test_libinput_debug_gui_invalid_arg(libinput_debug_gui, arg): def test_libinput_debug_gui_invalid_arg(libinput_debug_gui, arg):
libinput_debug_gui.run_command_unrecognized_option([arg]) libinput_debug_gui.run_command_unrecognized_option([arg])
def test_libinput_debug_gui_verbose(libinput_debug_gui): def test_libinput_debug_gui_verbose(libinput_debug_gui):
libinput_debug_gui.run_command_success(['--verbose']) libinput_debug_gui.run_command_success(["--verbose"])
@pytest.mark.parametrize('arg', ['--help', '--show-keycodes', '--with-libinput']) @pytest.mark.parametrize("arg", ["--help", "--show-keycodes", "--with-libinput"])
def test_libinput_record_args(libinput_record, arg): def test_libinput_record_args(libinput_record, arg):
libinput_record.run_command_success([arg]) libinput_record.run_command_success([arg])
def test_libinput_record_multiple_arg(libinput_record): def test_libinput_record_multiple_arg(libinput_record):
# this arg is deprecated and a noop # this arg is deprecated and a noop
libinput_record.run_command_success(['--multiple']) libinput_record.run_command_success(["--multiple"])
@pytest.fixture @pytest.fixture
def recording(tmp_path): def recording(tmp_path):
return str((tmp_path / 'record.out').resolve()) return str((tmp_path / "record.out").resolve())
def test_libinput_record_all(libinput_record, recording): def test_libinput_record_all(libinput_record, recording):
libinput_record.run_command_success(['--all', '-o', recording]) libinput_record.run_command_success(["--all", "-o", recording])
libinput_record.run_command_success(['--all', recording]) libinput_record.run_command_success(["--all", recording])
def test_libinput_record_outfile(libinput_record, recording): def test_libinput_record_outfile(libinput_record, recording):
libinput_record.run_command_success(['-o', recording]) libinput_record.run_command_success(["-o", recording])
libinput_record.run_command_success(['--output-file', recording]) libinput_record.run_command_success(["--output-file", recording])
libinput_record.run_command_success(['--output-file={}'.format(recording)]) libinput_record.run_command_success(["--output-file={}".format(recording)])
def test_libinput_record_single(libinput_record, recording): def test_libinput_record_single(libinput_record, recording):
libinput_record.run_command_success(['/dev/input/event0']) libinput_record.run_command_success(["/dev/input/event0"])
libinput_record.run_command_success(['-o', recording, '/dev/input/event0']) libinput_record.run_command_success(["-o", recording, "/dev/input/event0"])
libinput_record.run_command_success(['/dev/input/event0', recording]) libinput_record.run_command_success(["/dev/input/event0", recording])
libinput_record.run_command_success([recording, '/dev/input/event0']) libinput_record.run_command_success([recording, "/dev/input/event0"])
def test_libinput_record_multiple(libinput_record, recording): def test_libinput_record_multiple(libinput_record, recording):
libinput_record.run_command_success(['-o', recording, '/dev/input/event0', '/dev/input/event1']) libinput_record.run_command_success(
libinput_record.run_command_success([recording, '/dev/input/event0', '/dev/input/event1']) ["-o", recording, "/dev/input/event0", "/dev/input/event1"]
libinput_record.run_command_success(['/dev/input/event0', '/dev/input/event1', recording]) )
libinput_record.run_command_success(
[recording, "/dev/input/event0", "/dev/input/event1"]
)
libinput_record.run_command_success(
["/dev/input/event0", "/dev/input/event1", recording]
)
def test_libinput_record_autorestart(libinput_record, recording): def test_libinput_record_autorestart(libinput_record, recording):
libinput_record.run_command_invalid(['--autorestart']) libinput_record.run_command_invalid(["--autorestart"])
libinput_record.run_command_invalid(['--autorestart=2']) libinput_record.run_command_invalid(["--autorestart=2"])
libinput_record.run_command_success(['-o', recording, '--autorestart=2']) libinput_record.run_command_success(["-o", recording, "--autorestart=2"])
def main(): def main():
args = ['-m', 'pytest'] args = ["-m", "pytest"]
try: try:
import xdist # noqa import xdist # noqa
args += ['-n', 'auto']
args += ["-n", "auto"]
except ImportError: except ImportError:
logger.info('python-xdist missing, this test will be slow') logger.info("python-xdist missing, this test will be slow")
pass pass
args += ['@MESON_BUILD_ROOT@'] args += ["@MESON_BUILD_ROOT@"]
os.environ['LIBINPUT_RUNNING_TEST_SUITE'] = '1' os.environ["LIBINPUT_RUNNING_TEST_SUITE"] = "1"
return subprocess.run([sys.executable] + args).returncode return subprocess.run([sys.executable] + args).returncode
if __name__ == '__main__': if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())