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
fi
flake8@fedora:33:
python-format@fedora:33:
extends:
- .fedora-build@template
before_script:
- dnf install -y python3-flake8
- dnf install -y black
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
fi
flake8@{{distro.name}}:{{version}}:
python-format@{{distro.name}}:{{version}}:
extends:
- .{{distro.name}}-build@template
before_script:
- dnf install -y python3-flake8
- dnf install -y black
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 %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,13 +26,16 @@
import sys
import argparse
try:
import libevdev
import pyudev
except ModuleNotFoundError as e:
print('Error: {}'.format(str(e)), file=sys.stderr)
print('One or more python modules are missing. Please install those '
'modules and re-run this tool.')
print("Error: {}".format(str(e)), file=sys.stderr)
print(
"One or more python modules are missing. Please install those "
"modules and re-run this tool."
)
sys.exit(1)
@ -51,15 +54,15 @@ class Touchpad(object):
x = evdev.absinfo[libevdev.EV_ABS.ABS_X]
y = evdev.absinfo[libevdev.EV_ABS.ABS_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:
print('Device does not have resolutions.', file=sys.stderr)
print("Device does not have resolutions.", file=sys.stderr)
x.resolution = 1
y.resolution = 1
self.xrange = (x.maximum - x.minimum)
self.yrange = (y.maximum - y.minimum)
self.xrange = x.maximum - x.minimum
self.yrange = y.maximum - y.minimum
self.width = self.xrange / x.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
# wide.
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.min = Point()
self.max = Point()
@ -87,8 +95,8 @@ class Touchpad(object):
def x(self, x):
self._x.minimum = min(self.x.minimum, x)
self._x.maximum = max(self.x.maximum, x)
self.min.x = min(x, self.min.x or 0xffffffff)
self.max.x = max(x, self.max.x or -0xffffffff)
self.min.x = min(x, self.min.x or 0xFFFFFFFF)
self.max.x = max(x, self.max.x or -0xFFFFFFFF)
# we calculate the position based on the original range.
# this means on devices with a narrower range than advertised, not
# all corners may be reachable in the touchpad drawing.
@ -98,8 +106,8 @@ class Touchpad(object):
def y(self, y):
self._y.minimum = min(self.y.minimum, y)
self._y.maximum = max(self.y.maximum, y)
self.min.y = min(y, self.min.y or 0xffffffff)
self.max.y = max(y, self.max.y or -0xffffffff)
self.min.y = min(y, self.min.y or 0xFFFFFFFF)
self.max.y = max(y, self.max.y or -0xFFFFFFFF)
# we calculate the position based on the original range.
# this means on devices with a narrower range than advertised, not
# all corners may be reachable in the touchpad drawing.
@ -107,61 +115,61 @@ class Touchpad(object):
def update_from_data(self):
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.maximum = self.max.x
self._y.minimum = self.min.y
self._y.maximum = self.max.y
def draw(self):
print('Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]'.format(
self.min.x if self.min.x is not None else 0,
self.max.x if self.max.x 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(
"Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]".format(
self.min.x if self.min.x is not None else 0,
self.max.x if self.max.x 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('Move one finger along all edges of the touchpad'.center(self.columns))
print('until the detected axis range stops changing.'.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))
top = int(self.pos.y * self.rows)
print('+{}+'.format(''.ljust(self.columns, '-')))
print("+{}+".format("".ljust(self.columns, "-")))
for row in range(0, top):
print('|{}|'.format(''.ljust(self.columns)))
print("|{}|".format("".ljust(self.columns)))
left = int(self.pos.x * self.columns)
right = max(0, self.columns - 1 - left)
print('|{}{}{}|'.format(
''.ljust(left),
'O',
''.ljust(right)))
print("|{}{}{}|".format("".ljust(left), "O", "".ljust(right)))
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
def erase(self):
# Erase all previous lines so we're not left with rubbish
for row in range(self.rows_printed):
print('\033[K')
print('\033[{}A'.format(self.rows_printed))
print("\033[K")
print("\033[{}A".format(self.rows_printed))
def dimension(string):
try:
ts = string.split('x')
ts = string.split("x")
t = tuple([int(x) for x in ts])
if len(t) == 2:
return t
except: # noqa
except: # noqa
pass
msg = "{} is not in format WxH".format(string)
@ -173,82 +181,95 @@ def between(v1, v2, deviation):
def dmi_modalias_match(modalias):
modalias = modalias.split(':')
dmi = {'svn': None, 'pvr': None, 'pn': None}
modalias = modalias.split(":")
dmi = {"svn": None, "pvr": None, "pn": None}
for m in modalias:
for key in dmi:
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
# uses pn to provide a human-identifiable match
if dmi['svn'] == 'LENOVO':
return 'dmi:*svn{}:*pvr{}*'.format(dmi['svn'], dmi['pvr'])
if dmi["svn"] == "LENOVO":
return "dmi:*svn{}:*pvr{}*".format(dmi["svn"], dmi["pvr"])
else:
return 'dmi:*svn{}:*pn{}*'.format(dmi['svn'], dmi['pn'])
return "dmi:*svn{}:*pn{}*".format(dmi["svn"], dmi["pn"])
def main(args):
parser = argparse.ArgumentParser(
description="Measure the touchpad size"
parser = argparse.ArgumentParser(description="Measure the touchpad size")
parser.add_argument(
"size",
metavar="WxH",
type=dimension,
help="Touchpad size (width by height) in mm",
)
parser.add_argument(
'size', metavar='WxH', type=dimension,
help='Touchpad size (width by height) in mm',
)
parser.add_argument(
'path', metavar='/dev/input/event0', nargs='?', type=str,
help='Path to device (optional)'
"path",
metavar="/dev/input/event0",
nargs="?",
type=str,
help="Path to device (optional)",
)
context = pyudev.Context()
args = parser.parse_args()
if not args.path:
for device in context.list_devices(subsystem='input'):
if (device.get('ID_INPUT_TOUCHPAD', 0) and
(device.device_node or '').startswith('/dev/input/event')):
for device in context.list_devices(subsystem="input"):
if device.get("ID_INPUT_TOUCHPAD", 0) and (
device.device_node or ""
).startswith("/dev/input/event"):
args.path = device.device_node
name = 'unknown'
name = "unknown"
parent = device
while parent is not None:
n = parent.get('NAME', None)
n = parent.get("NAME", None)
if n:
name = n
break
parent = parent.parent
print('Using {}: {}'.format(name, device.device_node))
print("Using {}: {}".format(name, device.device_node))
break
else:
print('Unable to find a touchpad device.', file=sys.stderr)
print("Unable to find a touchpad device.", file=sys.stderr)
return 1
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:
print()
print('********************************************************************')
print('WARNING: axis overrides already in place for this device:')
print("********************************************************************")
print("WARNING: axis overrides already in place for this device:")
for prop in overrides:
print(' {}={}'.format(prop, dev.properties[prop]))
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('********************************************************************')
print(" {}={}".format(prop, dev.properties[prop]))
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("********************************************************************")
print()
try:
fd = open(args.path, 'rb')
fd = open(args.path, "rb")
evdev = libevdev.Device(fd)
touchpad = Touchpad(evdev)
print('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(
"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('Kernel axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]'.format(
touchpad.x.minimum, touchpad.x.maximum,
touchpad.y.minimum, touchpad.y.maximum))
print(
"Kernel axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]".format(
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:
touchpad.draw()
@ -264,15 +285,27 @@ def main(args):
touchpad.erase()
touchpad.update_from_data()
print('Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]'.format(
touchpad.x.minimum, touchpad.x.maximum,
touchpad.y.minimum, touchpad.y.maximum))
print(
"Detected axis range: x [{:4d}..{:4d}], y [{:4d}..{:4d}]".format(
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.y.resolution = round((touchpad.y.maximum - touchpad.y.minimum) / args.size[1])
touchpad.x.resolution = round(
(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(
touchpad.x.resolution, touchpad.y.resolution))
print(
"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
# overrides and only override the resolution
@ -287,50 +320,73 @@ def main(args):
if skip:
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('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:
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(' ', modalias)
print('Please verify that this is the most sensible match and adjust if necessary.')
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(" ", modalias)
print(
"Please verify that this is the most sensible match and adjust if necessary."
)
print('-8<--------------------------')
print('# Laptop model description (e.g. Lenovo X1 Carbon 5th)')
print("-8<--------------------------")
print("# Laptop model description (e.g. Lenovo X1 Carbon 5th)")
if use_dmi:
print('evdev:name:{}:{}*'.format(evdev.name, dmi_modalias_match(modalias)))
print("evdev:name:{}:{}*".format(evdev.name, dmi_modalias_match(modalias)))
else:
print('evdev:input:b{:04X}v{:04X}p{:04X}*'.format(
evdev.id['bustype'], evdev.id['vendor'], evdev.id['product']))
print(' EVDEV_ABS_00={}:{}:{}'.format(
touchpad.x.minimum if not skip else '',
touchpad.x.maximum if not skip else '',
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))
print(
"evdev:input:b{:04X}v{:04X}p{:04X}*".format(
evdev.id["bustype"], evdev.id["vendor"], evdev.id["product"]
)
)
print(
" EVDEV_ABS_00={}:{}:{}".format(
touchpad.x.minimum if not skip else "",
touchpad.x.maximum if not skip else "",
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]:
print(' EVDEV_ABS_35={}:{}:{}'.format(
touchpad.x.minimum if not skip else '',
touchpad.x.maximum if not skip else '',
touchpad.x.resolution))
print(' EVDEV_ABS_36={}:{}:{}'.format(
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')
print(
" EVDEV_ABS_35={}:{}:{}".format(
touchpad.x.minimum if not skip else "",
touchpad.x.maximum if not skip else "",
touchpad.x.resolution,
)
)
print(
" EVDEV_ABS_36={}:{}:{}".format(
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:
print('Error: {}'.format(e), file=sys.stderr)
print("Error: {}".format(e), file=sys.stderr)
return 1
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 0

View file

@ -26,14 +26,17 @@
import sys
import argparse
try:
import libevdev
import textwrap
import pyudev
except ModuleNotFoundError as e:
print('Error: {}'.format(e), file=sys.stderr)
print('One or more python modules are missing. Please install those '
'modules and re-run this tool.')
print("Error: {}".format(e), file=sys.stderr)
print(
"One or more python modules are missing. Please install those "
"modules and re-run this tool."
)
sys.exit(1)
print_dest = sys.stdout
@ -66,7 +69,7 @@ class Touch(object):
@up.setter
def up(self, up):
assert(up > self.down)
assert up > self.down
self._up = up
@property
@ -88,7 +91,7 @@ class Device(libevdev.Device):
self.path = self._find_touch_device()
else:
self.path = path
fd = open(self.path, 'rb')
fd = open(self.path, "rb")
super().__init__(fd)
print("Using {}: {}\n".format(self.name, self.path))
@ -101,18 +104,19 @@ class Device(libevdev.Device):
def _find_touch_device(self):
context = pyudev.Context()
device_node = None
for device in context.list_devices(subsystem='input'):
if (not device.device_node or
not device.device_node.startswith('/dev/input/event')):
for device in context.list_devices(subsystem="input"):
if not device.device_node or not device.device_node.startswith(
"/dev/input/event"
):
continue
# pick the touchpad by default, fallback to the first
# 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
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
if device_node is not None:
@ -127,17 +131,19 @@ class Device(libevdev.Device):
self.touches.append(t)
else:
self.touches[-1].up = tv2us(event.sec, event.usec)
msg("\rTouch sequences detected: {}".format(len(self.touches)),
end='')
msg("\rTouch sequences detected: {}".format(len(self.touches)), end="")
def handle_key(self, event):
tapcodes = [libevdev.EV_KEY.BTN_TOOL_DOUBLETAP,
libevdev.EV_KEY.BTN_TOOL_TRIPLETAP,
libevdev.EV_KEY.BTN_TOOL_QUADTAP,
libevdev.EV_KEY.BTN_TOOL_QUINTTAP]
tapcodes = [
libevdev.EV_KEY.BTN_TOOL_DOUBLETAP,
libevdev.EV_KEY.BTN_TOOL_TRIPLETAP,
libevdev.EV_KEY.BTN_TOOL_QUADTAP,
libevdev.EV_KEY.BTN_TOOL_QUINTTAP,
]
if event.code in tapcodes and event.value > 0:
error("\rThis tool cannot handle multiple fingers, "
"output will be invalid")
error(
"\rThis tool cannot handle multiple fingers, " "output will be invalid"
)
return
if event.matches(libevdev.EV_KEY.BTN_TOUCH):
@ -146,9 +152,11 @@ class Device(libevdev.Device):
def handle_syn(self, event):
if self.touch.dirty:
self.current_sequence().append(self.touch)
self.touch = Touch(major=self.touch.major,
minor=self.touch.minor,
orientation=self.touch.orientation)
self.touch = Touch(
major=self.touch.major,
minor=self.touch.minor,
orientation=self.touch.orientation,
)
def handle_event(self, event):
if event.matches(libevdev.EV_KEY):
@ -182,7 +190,9 @@ class Device(libevdev.Device):
def print_dat(self):
print("# libinput-measure-touchpad-tap")
print(textwrap.dedent('''\
print(
textwrap.dedent(
"""\
# File contents:
# This file contains multiple prints of the data in
# 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
# 5: touch up time in ms, offset by first event
# 6: time delta in ms
'''))
"""
)
)
deltas = [t for t in self.touches]
deltas_sorted = sorted(deltas, key=lambda t: t.tdelta)
@ -205,28 +217,44 @@ class Device(libevdev.Device):
offset = deltas[0].down
for t1, t2 in zip(deltas, deltas_sorted):
print(t1.down - offset, t1.up - offset, t1.tdelta,
t2.down - offset, t2.up - offset, t2.tdelta)
print(
t1.down - offset,
t1.up - offset,
t1.tdelta,
t2.down - offset,
t2.up - offset,
t2.tdelta,
)
def print(self, format):
if not self.touches:
error("No tap data available")
return
if format == 'summary':
if format == "summary":
self.print_summary()
elif format == 'dat':
elif format == "dat":
self.print_dat()
def main(args):
parser = argparse.ArgumentParser(description="Measure tap-to-click properties of devices")
parser.add_argument('path', metavar='/dev/input/event0',
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")')
parser = argparse.ArgumentParser(
description="Measure tap-to-click properties of devices"
)
parser.add_argument(
"path",
metavar="/dev/input/event0",
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()
if not sys.stdout.isatty():
@ -235,13 +263,15 @@ def main(args):
try:
device = Device(args.path)
error("Ready for recording data.\n"
"Tap the touchpad multiple times with a single finger only.\n"
"For useful data we recommend at least 20 taps.\n"
"Ctrl+C to exit")
error(
"Ready for recording data.\n"
"Tap the touchpad multiple times with a single finger only.\n"
"For useful data we recommend at least 20 taps.\n"
"Ctrl+C to exit"
)
device.read_events()
except KeyboardInterrupt:
msg('')
msg("")
device.print(args.format)
except (PermissionError, OSError) as 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):
filename = ''
filename = ""
@classmethod
def setUpClass(cls):
@ -42,163 +42,166 @@ class TestYaml(unittest.TestCase):
cls.yaml = yaml.safe_load(f)
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))
def libinput_events(self, filter=None):
'''Returns all libinput events in the recording, regardless of the
device'''
devices = self.yaml['devices']
"""Returns all libinput events in the recording, regardless of the
device"""
devices = self.yaml["devices"]
for d in devices:
events = d['events']
events = d["events"]
if not events:
raise unittest.SkipTest()
for e in events:
try:
libinput = e['libinput']
libinput = e["libinput"]
except KeyError:
continue
for ev in libinput:
if (filter is None or ev['type'] == filter or
isinstance(filter, list) and ev['type'] in filter):
if (
filter is None
or ev["type"] == filter
or isinstance(filter, list)
and ev["type"] in filter
):
yield ev
def test_sections_exist(self):
sections = ['version', 'ndevices', 'libinput', 'system', 'devices']
sections = ["version", "ndevices", "libinput", "system", "devices"]
for section in sections:
self.assertIn(section, self.yaml)
def test_version(self):
version = self.yaml['version']
version = self.yaml["version"]
self.assertTrue(isinstance(version, int))
self.assertEqual(version, 1)
def test_ndevices(self):
ndevices = self.yaml['ndevices']
ndevices = self.yaml["ndevices"]
self.assertTrue(isinstance(ndevices, int))
self.assertGreaterEqual(ndevices, 1)
self.assertEqual(ndevices, len(self.yaml['devices']))
self.assertEqual(ndevices, len(self.yaml["devices"]))
def test_libinput(self):
libinput = self.yaml['libinput']
version = libinput['version']
libinput = self.yaml["libinput"]
version = libinput["version"]
self.assertTrue(isinstance(version, str))
self.assertGreaterEqual(parse_version(version),
parse_version('1.10.0'))
git = libinput['git']
self.assertGreaterEqual(parse_version(version), parse_version("1.10.0"))
git = libinput["git"]
self.assertTrue(isinstance(git, str))
self.assertNotEqual(git, 'unknown')
self.assertNotEqual(git, "unknown")
def test_system(self):
system = self.yaml['system']
kernel = system['kernel']
system = self.yaml["system"]
kernel = system["kernel"]
self.assertTrue(isinstance(kernel, str))
self.assertEqual(kernel, os.uname().release)
dmi = system['dmi']
dmi = system["dmi"]
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
self.assertEqual(dmi, sys_dmi)
def test_devices_sections_exist(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
self.assertIn('node', d)
self.assertIn('evdev', d)
self.assertIn('udev', d)
self.assertIn("node", d)
self.assertIn("evdev", d)
self.assertIn("udev", d)
def test_evdev_sections_exist(self):
sections = ['name', 'id', 'codes', 'properties']
devices = self.yaml['devices']
sections = ["name", "id", "codes", "properties"]
devices = self.yaml["devices"]
for d in devices:
evdev = d['evdev']
evdev = d["evdev"]
for s in sections:
self.assertIn(s, evdev)
def test_evdev_name(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
evdev = d['evdev']
name = evdev['name']
evdev = d["evdev"]
name = evdev["name"]
self.assertTrue(isinstance(name, str))
self.assertGreaterEqual(len(name), 5)
def test_evdev_id(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
evdev = d['evdev']
id = evdev['id']
evdev = d["evdev"]
id = evdev["id"]
self.assertTrue(isinstance(id, list))
self.assertEqual(len(id), 4)
self.assertGreater(id[0], 0)
self.assertGreater(id[1], 0)
def test_evdev_properties(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
evdev = d['evdev']
properties = evdev['properties']
evdev = d["evdev"]
properties = evdev["properties"]
self.assertTrue(isinstance(properties, list))
def test_hid(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
hid = d['hid']
hid = d["hid"]
self.assertTrue(isinstance(hid, list))
for byte in hid:
self.assertGreaterEqual(byte, 0)
self.assertLessEqual(byte, 255)
def test_udev_sections_exist(self):
sections = ['properties']
devices = self.yaml['devices']
sections = ["properties"]
devices = self.yaml["devices"]
for d in devices:
udev = d['udev']
udev = d["udev"]
for s in sections:
self.assertIn(s, udev)
def test_udev_properties(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
udev = d['udev']
properties = udev['properties']
udev = d["udev"]
properties = udev["properties"]
self.assertTrue(isinstance(properties, list))
self.assertGreater(len(properties), 0)
self.assertIn('ID_INPUT=1', properties)
self.assertIn("ID_INPUT=1", 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):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
udev = d['udev']
properties = udev['properties']
id_inputs = [p for p in properties if p.startswith('ID_INPUT')]
udev = d["udev"]
properties = udev["properties"]
id_inputs = [p for p in properties if p.startswith("ID_INPUT")]
# We expect ID_INPUT and ID_INPUT_something, but might get more
# than one of the latter
self.assertGreaterEqual(len(id_inputs), 2)
def test_events_have_section(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
events = d['events']
events = d["events"]
if not events:
raise unittest.SkipTest()
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):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
events = d['events']
events = d["events"]
if not events:
raise unittest.SkipTest()
for e in events:
try:
evdev = e['evdev']
evdev = e["evdev"]
except KeyError:
continue
@ -213,28 +216,28 @@ class TestYaml(unittest.TestCase):
self.assertLessEqual(ev_syn[4], 1)
def test_events_evdev_syn_report(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
events = d['events']
events = d["events"]
if not events:
raise unittest.SkipTest()
for e in events:
try:
evdev = e['evdev']
evdev = e["evdev"]
except KeyError:
continue
for ev in evdev[:-1]:
self.assertFalse(ev[2] == 0 and ev[3] == 0)
def test_events_libinput(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
events = d['events']
events = d["events"]
if not events:
raise unittest.SkipTest()
for e in events:
try:
libinput = e['libinput']
libinput = e["libinput"]
except KeyError:
continue
@ -243,20 +246,35 @@ class TestYaml(unittest.TestCase):
self.assertTrue(isinstance(ev, dict))
def test_events_libinput_type(self):
types = ['POINTER_MOTION', 'POINTER_MOTION_ABSOLUTE', 'POINTER_AXIS',
'POINTER_BUTTON', 'DEVICE_ADDED', 'KEYBOARD_KEY',
'TOUCH_DOWN', 'TOUCH_MOTION', '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',
]
types = [
"POINTER_MOTION",
"POINTER_MOTION_ABSOLUTE",
"POINTER_AXIS",
"POINTER_BUTTON",
"DEVICE_ADDED",
"KEYBOARD_KEY",
"TOUCH_DOWN",
"TOUCH_MOTION",
"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():
self.assertIn('type', e)
self.assertIn(e['type'], types)
self.assertIn("type", e)
self.assertIn(e["type"], types)
def test_events_libinput_time(self):
# DEVICE_ADDED has no time
@ -264,51 +282,51 @@ class TestYaml(unittest.TestCase):
# libinput event.
try:
for e in list(self.libinput_events())[2:]:
self.assertIn('time', e)
self.assertGreater(e['time'], 0.0)
self.assertLess(e['time'], 60.0)
self.assertIn("time", e)
self.assertGreater(e["time"], 0.0)
self.assertLess(e["time"], 60.0)
except IndexError:
pass
def test_events_libinput_device_added(self):
keys = ['type', 'seat', 'logical_seat']
for e in self.libinput_events('DEVICE_ADDED'):
keys = ["type", "seat", "logical_seat"]
for e in self.libinput_events("DEVICE_ADDED"):
self.dict_key_crosscheck(e, keys)
self.assertEqual(e['seat'], 'seat0')
self.assertEqual(e['logical_seat'], 'default')
self.assertEqual(e["seat"], "seat0")
self.assertEqual(e["logical_seat"], "default")
def test_events_libinput_pointer_motion(self):
keys = ['type', 'time', 'delta', 'unaccel']
for e in self.libinput_events('POINTER_MOTION'):
keys = ["type", "time", "delta", "unaccel"]
for e in self.libinput_events("POINTER_MOTION"):
self.dict_key_crosscheck(e, keys)
delta = e['delta']
delta = e["delta"]
self.assertTrue(isinstance(delta, list))
self.assertEqual(len(delta), 2)
for d in delta:
self.assertTrue(isinstance(d, float))
unaccel = e['unaccel']
unaccel = e["unaccel"]
self.assertTrue(isinstance(unaccel, list))
self.assertEqual(len(unaccel), 2)
for d in unaccel:
self.assertTrue(isinstance(d, float))
def test_events_libinput_pointer_button(self):
keys = ['type', 'time', 'button', 'state', 'seat_count']
for e in self.libinput_events('POINTER_BUTTON'):
keys = ["type", "time", "button", "state", "seat_count"]
for e in self.libinput_events("POINTER_BUTTON"):
self.dict_key_crosscheck(e, keys)
button = e['button']
button = e["button"]
self.assertGreater(button, 0x100) # BTN_0
self.assertLess(button, 0x160) # KEY_OK
state = e['state']
self.assertIn(state, ['pressed', 'released'])
scount = e['seat_count']
state = e["state"]
self.assertIn(state, ["pressed", "released"])
scount = e["seat_count"]
self.assertGreaterEqual(scount, 0)
def test_events_libinput_pointer_absolute(self):
keys = ['type', 'time', 'point', 'transformed']
for e in self.libinput_events('POINTER_MOTION_ABSOLUTE'):
keys = ["type", "time", "point", "transformed"]
for e in self.libinput_events("POINTER_MOTION_ABSOLUTE"):
self.dict_key_crosscheck(e, keys)
point = e['point']
point = e["point"]
self.assertTrue(isinstance(point, list))
self.assertEqual(len(point), 2)
for p in point:
@ -316,7 +334,7 @@ class TestYaml(unittest.TestCase):
self.assertGreater(p, 0.0)
self.assertLess(p, 300.0)
transformed = e['transformed']
transformed = e["transformed"]
self.assertTrue(isinstance(transformed, list))
self.assertEqual(len(transformed), 2)
for t in transformed:
@ -325,25 +343,24 @@ class TestYaml(unittest.TestCase):
self.assertLess(t, 100.0)
def test_events_libinput_touch(self):
keys = ['type', 'time', 'slot', 'seat_slot']
keys = ["type", "time", "slot", "seat_slot"]
for e in self.libinput_events():
if (not e['type'].startswith('TOUCH_') or
e['type'] == 'TOUCH_FRAME'):
if not e["type"].startswith("TOUCH_") or e["type"] == "TOUCH_FRAME":
continue
for k in keys:
self.assertIn(k, e.keys())
slot = e['slot']
seat_slot = e['seat_slot']
slot = e["slot"]
seat_slot = e["seat_slot"]
self.assertGreaterEqual(slot, 0)
self.assertGreaterEqual(seat_slot, 0)
def test_events_libinput_touch_down(self):
keys = ['type', 'time', 'slot', 'seat_slot', 'point', 'transformed']
for e in self.libinput_events('TOUCH_DOWN'):
keys = ["type", "time", "slot", "seat_slot", "point", "transformed"]
for e in self.libinput_events("TOUCH_DOWN"):
self.dict_key_crosscheck(e, keys)
point = e['point']
point = e["point"]
self.assertTrue(isinstance(point, list))
self.assertEqual(len(point), 2)
for p in point:
@ -351,7 +368,7 @@ class TestYaml(unittest.TestCase):
self.assertGreater(p, 0.0)
self.assertLess(p, 300.0)
transformed = e['transformed']
transformed = e["transformed"]
self.assertTrue(isinstance(transformed, list))
self.assertEqual(len(transformed), 2)
for t in transformed:
@ -360,10 +377,10 @@ class TestYaml(unittest.TestCase):
self.assertLess(t, 100.0)
def test_events_libinput_touch_motion(self):
keys = ['type', 'time', 'slot', 'seat_slot', 'point', 'transformed']
for e in self.libinput_events('TOUCH_MOTION'):
keys = ["type", "time", "slot", "seat_slot", "point", "transformed"]
for e in self.libinput_events("TOUCH_MOTION"):
self.dict_key_crosscheck(e, keys)
point = e['point']
point = e["point"]
self.assertTrue(isinstance(point, list))
self.assertEqual(len(point), 2)
for p in point:
@ -371,7 +388,7 @@ class TestYaml(unittest.TestCase):
self.assertGreater(p, 0.0)
self.assertLess(p, 300.0)
transformed = e['transformed']
transformed = e["transformed"]
self.assertTrue(isinstance(transformed, list))
self.assertEqual(len(transformed), 2)
for t in transformed:
@ -380,25 +397,25 @@ class TestYaml(unittest.TestCase):
self.assertLess(t, 100.0)
def test_events_libinput_touch_frame(self):
devices = self.yaml['devices']
devices = self.yaml["devices"]
for d in devices:
events = d['events']
events = d["events"]
if not events:
raise unittest.SkipTest()
for e in events:
try:
evdev = e['libinput']
evdev = e["libinput"]
except KeyError:
continue
need_frame = False
for ev in evdev:
t = ev['type']
if not t.startswith('TOUCH_'):
t = ev["type"]
if not t.startswith("TOUCH_"):
self.assertFalse(need_frame)
continue
if t == 'TOUCH_FRAME':
if t == "TOUCH_FRAME":
self.assertTrue(need_frame)
need_frame = False
else:
@ -407,177 +424,175 @@ class TestYaml(unittest.TestCase):
self.assertFalse(need_frame)
def test_events_libinput_gesture_pinch(self):
keys = ['type', 'time', 'nfingers', 'delta',
'unaccel', 'angle_delta', 'scale']
for e in self.libinput_events(['GESTURE_PINCH_BEGIN',
'GESTURE_PINCH_UPDATE',
'GESTURE_PINCH_END']):
keys = ["type", "time", "nfingers", "delta", "unaccel", "angle_delta", "scale"]
for e in self.libinput_events(
["GESTURE_PINCH_BEGIN", "GESTURE_PINCH_UPDATE", "GESTURE_PINCH_END"]
):
self.dict_key_crosscheck(e, keys)
delta = e['delta']
delta = e["delta"]
self.assertTrue(isinstance(delta, list))
self.assertEqual(len(delta), 2)
for d in delta:
self.assertTrue(isinstance(d, float))
unaccel = e['unaccel']
unaccel = e["unaccel"]
self.assertTrue(isinstance(unaccel, list))
self.assertEqual(len(unaccel), 2)
for d in unaccel:
self.assertTrue(isinstance(d, float))
adelta = e['angle_delta']
adelta = e["angle_delta"]
self.assertTrue(isinstance(adelta, list))
self.assertEqual(len(adelta), 2)
for d in adelta:
self.assertTrue(isinstance(d, float))
scale = e['scale']
scale = e["scale"]
self.assertTrue(isinstance(scale, list))
self.assertEqual(len(scale), 2)
for d in scale:
self.assertTrue(isinstance(d, float))
def test_events_libinput_gesture_swipe(self):
keys = ['type', 'time', 'nfingers', 'delta',
'unaccel']
for e in self.libinput_events(['GESTURE_SWIPE_BEGIN',
'GESTURE_SWIPE_UPDATE',
'GESTURE_SWIPE_END']):
keys = ["type", "time", "nfingers", "delta", "unaccel"]
for e in self.libinput_events(
["GESTURE_SWIPE_BEGIN", "GESTURE_SWIPE_UPDATE", "GESTURE_SWIPE_END"]
):
self.dict_key_crosscheck(e, keys)
delta = e['delta']
delta = e["delta"]
self.assertTrue(isinstance(delta, list))
self.assertEqual(len(delta), 2)
for d in delta:
self.assertTrue(isinstance(d, float))
unaccel = e['unaccel']
unaccel = e["unaccel"]
self.assertTrue(isinstance(unaccel, list))
self.assertEqual(len(unaccel), 2)
for d in unaccel:
self.assertTrue(isinstance(d, float))
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)
b = e['button']
b = e["button"]
self.assertTrue(isinstance(b, int))
self.assertGreaterEqual(b, 0)
self.assertLessEqual(b, 16)
state = e['state']
self.assertIn(state, ['pressed', 'released'])
state = e["state"]
self.assertIn(state, ["pressed", "released"])
m = e['mode']
m = e["mode"]
self.assertTrue(isinstance(m, int))
self.assertGreaterEqual(m, 0)
self.assertLessEqual(m, 3)
t = e['is-toggle']
t = e["is-toggle"]
self.assertTrue(isinstance(t, bool))
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)
n = e['number']
n = e["number"]
self.assertTrue(isinstance(n, int))
self.assertGreaterEqual(n, 0)
self.assertLessEqual(n, 4)
p = e['position']
p = e["position"]
self.assertTrue(isinstance(p, float))
if p != -1.0: # special 'end' case
self.assertGreaterEqual(p, 0.0)
self.assertLess(p, 360.0)
m = e['mode']
m = e["mode"]
self.assertTrue(isinstance(m, int))
self.assertGreaterEqual(m, 0)
self.assertLessEqual(m, 3)
s = e['source']
self.assertIn(s, ['finger', 'unknown'])
s = e["source"]
self.assertIn(s, ["finger", "unknown"])
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)
n = e['number']
n = e["number"]
self.assertTrue(isinstance(n, int))
self.assertGreaterEqual(n, 0)
self.assertLessEqual(n, 4)
p = e['position']
p = e["position"]
self.assertTrue(isinstance(p, float))
if p != -1.0: # special 'end' case
self.assertGreaterEqual(p, 0.0)
self.assertLessEqual(p, 1.0)
m = e['mode']
m = e["mode"]
self.assertTrue(isinstance(m, int))
self.assertGreaterEqual(m, 0)
self.assertLessEqual(m, 3)
s = e['source']
self.assertIn(s, ['finger', 'unknown'])
s = e["source"]
self.assertIn(s, ["finger", "unknown"])
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:
self.assertIn(k, e)
p = e['proximity']
self.assertIn(p, ['in', 'out'])
p = e["proximity"]
self.assertIn(p, ["in", "out"])
p = e['tool-type']
self.assertIn(p, ['pen', 'eraser', 'brush', 'airbrush', 'mouse',
'lens', 'unknown'])
p = e["tool-type"]
self.assertIn(
p, ["pen", "eraser", "brush", "airbrush", "mouse", "lens", "unknown"]
)
s = e['serial']
s = e["serial"]
self.assertTrue(isinstance(s, int))
self.assertGreaterEqual(s, 0)
a = e['axes']
for ax in e['axes']:
self.assertIn(a, 'pdtrsw')
a = e["axes"]
for ax in e["axes"]:
self.assertIn(a, "pdtrsw")
def test_events_libinput_tablet_tool(self):
keys = ['type', 'time', 'tip']
keys = ["type", "time", "tip"]
for e in self.libinput_events(['TABLET_TOOL_AXIS',
'TABLET_TOOL_TIP']):
for e in self.libinput_events(["TABLET_TOOL_AXIS", "TABLET_TOOL_TIP"]):
for k in keys:
self.assertIn(k, e)
t = e['tip']
self.assertIn(t, ['down', 'up'])
t = e["tip"]
self.assertIn(t, ["down", "up"])
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)
b = e['button']
b = e["button"]
# STYLUS, STYLUS2, STYLUS3
self.assertIn(b, [0x14b, 0x14c, 0x139])
self.assertIn(b, [0x14B, 0x14C, 0x139])
s = e['state']
self.assertIn(s, ['pressed', 'released'])
s = e["state"]
self.assertIn(s, ["pressed", "released"])
def test_events_libinput_tablet_tool_axes(self):
for e in self.libinput_events(['TABLET_TOOL_PROXIMITY',
'TABLET_TOOL_AXIS',
'TABLET_TOOL_TIP']):
for e in self.libinput_events(
["TABLET_TOOL_PROXIMITY", "TABLET_TOOL_AXIS", "TABLET_TOOL_TIP"]
):
point = e['point']
point = e["point"]
self.assertTrue(isinstance(point, list))
self.assertEqual(len(point), 2)
for p in point:
@ -585,7 +600,7 @@ class TestYaml(unittest.TestCase):
self.assertGreater(p, 0.0)
try:
tilt = e['tilt']
tilt = e["tilt"]
self.assertTrue(isinstance(tilt, list))
self.assertEqual(len(tilt), 2)
for t in tilt:
@ -594,70 +609,75 @@ class TestYaml(unittest.TestCase):
pass
try:
d = e['distance']
d = e["distance"]
self.assertTrue(isinstance(d, float))
self.assertGreaterEqual(d, 0.0)
self.assertNotIn('pressure', e)
self.assertNotIn("pressure", e)
except KeyError:
pass
try:
p = e['pressure']
p = e["pressure"]
self.assertTrue(isinstance(p, float))
self.assertGreaterEqual(p, 0.0)
self.assertNotIn('distance', e)
self.assertNotIn("distance", e)
except KeyError:
pass
try:
r = e['rotation']
r = e["rotation"]
self.assertTrue(isinstance(r, float))
self.assertGreaterEqual(r, 0.0)
except KeyError:
pass
try:
s = e['slider']
s = e["slider"]
self.assertTrue(isinstance(s, float))
self.assertGreaterEqual(s, 0.0)
except KeyError:
pass
try:
w = e['wheel']
w = e["wheel"]
self.assertTrue(isinstance(w, float))
self.assertGreaterEqual(w, 0.0)
self.assertIn('wheel-discrete', e)
wd = e['wheel-discrete']
self.assertIn("wheel-discrete", e)
wd = e["wheel-discrete"]
self.assertTrue(isinstance(wd, 1))
self.assertGreaterEqual(wd, 0.0)
def sign(x):
(1, -1)[x < 0]
self.assertTrue(sign(w), sign(wd))
except KeyError:
pass
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)
s = e['switch']
s = e["switch"]
self.assertTrue(isinstance(s, int))
self.assertIn(s, [0x00, 0x01])
# yaml converts on/off to true/false
state = e['state']
state = e["state"]
self.assertTrue(isinstance(state, bool))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Verify a YAML recording')
parser.add_argument('recording', metavar='recorded-file.yaml',
type=str, help='Path to device recording')
parser.add_argument('--verbose', action='store_true')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Verify a YAML recording")
parser.add_argument(
"recording",
metavar="recorded-file.yaml",
type=str,
help="Path to device recording",
)
parser.add_argument("--verbose", action="store_true")
args, remainder = parser.parse_known_args()
TestYaml.filename = args.recording
verbosity = 1

View file

@ -35,9 +35,11 @@ try:
import yaml
import pyudev
except ModuleNotFoundError as e:
print('Error: {}'.format(e), file=sys.stderr)
print('One or more python modules are missing. Please install those '
'modules and re-run this tool.')
print("Error: {}".format(e), file=sys.stderr)
print(
"One or more python modules are missing. Please install those "
"modules and re-run this tool."
)
sys.exit(1)
@ -53,25 +55,27 @@ class YamlException(Exception):
def fetch(yaml, key):
'''Helper function to avoid confusing a YAML error with a
normal KeyError bug'''
"""Helper function to avoid confusing a YAML error with a
normal KeyError bug"""
try:
return yaml[key]
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):
'''
"""
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.
'''
yaml_udev_section = fetch(yaml_data, 'udev')
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_udev_section = fetch(yaml_data, "udev")
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]
}
try:
# We don't assign this one to virtual devices
del yaml_props['LIBINPUT_DEVICE_GROUP']
del yaml_props["LIBINPUT_DEVICE_GROUP"]
except KeyError:
pass
@ -82,42 +86,52 @@ def check_udev_properties(yaml_data, uinput):
for name, value in udev_device.properties.items():
if name in yaml_props:
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]
else:
# 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:
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
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):
evdev = fetch(device, 'evdev')
evdev = fetch(device, "evdev")
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:
raise YamlException('Invalid ID format: {}'.format(ids))
d.id = dict(zip(['bustype', 'vendor', 'product', 'version'], ids))
raise YamlException("Invalid ID format: {}".format(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 code in evcodes:
data = None
if evtype == libevdev.EV_ABS.value:
values = fetch(evdev, 'absinfo')[code]
absinfo = libevdev.InputAbsInfo(minimum=values[0],
maximum=values[1],
fuzz=values[2],
flat=values[3],
resolution=values[4])
values = fetch(evdev, "absinfo")[code]
absinfo = libevdev.InputAbsInfo(
minimum=values[0],
maximum=values[1],
fuzz=values[2],
flat=values[3],
resolution=values[4],
)
data = absinfo
elif evtype == libevdev.EV_REP.value:
if code == libevdev.EV_REP.REP_DELAY.value:
@ -126,7 +140,7 @@ def create(device):
data = 20
d.enable(libevdev.evbit(evtype, code), data=data)
properties = fetch(evdev, 'properties')
properties = fetch(evdev, "properties")
for prop in properties:
d.enable(libevdev.propbit(prop))
@ -140,30 +154,39 @@ def create(device):
def print_events(devnode, indent, evs):
devnode = os.path.basename(devnode)
for e in evs:
print("{}: {}{:06d}.{:06d} {} / {:<20s} {:4d}".format(
devnode, ' ' * (indent * 8), e.sec, e.usec, e.type.name, e.code.name, e.value))
print(
"{}: {}{:06d}.{:06d} {} / {:<20s} {:4d}".format(
devnode,
" " * (indent * 8),
e.sec,
e.usec,
e.type.name,
e.code.name,
e.value,
)
)
def replay(device, verbose):
events = fetch(device, 'events')
events = fetch(device, "events")
if events is None:
return
uinput = device['__uinput']
uinput = device["__uinput"]
# The first event may have a nonzero offset but we want to replay
# immediately regardless. When replaying multiple devices, the first
# 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:
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
# each 'evdev' set contains one SYN_REPORT so we only need to check for
# the time offset once per event
for event in events:
try:
evdev = fetch(event, 'evdev')
evdev = fetch(event, "evdev")
except YamlException:
continue
@ -174,25 +197,31 @@ def replay(device, verbose):
if evtime - now > 150 / 1e6: # 150 µs error margin
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)
if verbose:
print_events(uinput.devnode, device['__index'], evs)
print_events(uinput.devnode, device["__index"], evs)
def first_timestamp(device):
try:
events = fetch(device, 'events')
events = fetch(device, "events")
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]
return sec + usec / 1.e6
return sec + usec / 1.0e6
except YamlException:
import math
return math.inf
@ -204,7 +233,7 @@ def wrap(func, *args):
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
# the very first event and offset everything by that timestamp.
@ -212,13 +241,13 @@ def loop(args, recording):
for idx, d in enumerate(devices):
uinput = create(d)
print('{}: {}'.format(uinput.devnode, uinput.name))
d['__uinput'] = uinput # cheaper to hide it in the dict then work around it
d['__index'] = idx
d['__first_event_offset'] = toffset
print("{}: {}".format(uinput.devnode, uinput.name))
d["__uinput"] = uinput # cheaper to hide it in the dict then work around it
d["__index"] = idx
d["__first_event_offset"] = toffset
while True:
input('Hit enter to start replaying')
input("Hit enter to start replaying")
processes = []
for d in devices:
@ -236,66 +265,83 @@ def loop(args, recording):
def create_device_quirk(device):
try:
quirks = fetch(device, 'quirks')
quirks = fetch(device, "quirks")
if not quirks:
return None
except YamlException:
return None
# 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.
evdev = fetch(device, 'evdev')
name = fetch(evdev, 'name')
id = fetch(evdev, 'id')
quirk = ('[libinput-replay {name}]\n'
'MatchName={name}\n'
'MatchVendor=0x{id[1]:04X}\n'
'MatchProduct=0x{id[2]:04X}\n').format(name=name, id=id)
quirk += '\n'.join(quirks)
evdev = fetch(device, "evdev")
name = fetch(evdev, "name")
id = fetch(evdev, "id")
quirk = (
"[libinput-replay {name}]\n"
"MatchName={name}\n"
"MatchVendor=0x{id[1]:04X}\n"
"MatchProduct=0x{id[2]:04X}\n"
).format(name=name, id=id)
quirk += "\n".join(quirks)
return quirk
def setup_quirks(recording):
devices = fetch(recording, 'devices')
devices = fetch(recording, "devices")
overrides = None
quirks = []
for d in devices:
if 'quirks' in d:
if "quirks" in d:
quirk = create_device_quirk(d)
if quirk:
quirks.append(quirk)
if not quirks:
return None
overrides = Path('/etc/libinput/local-overrides.quirks')
overrides = Path("/etc/libinput/local-overrides.quirks")
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)
overrides.parent.mkdir(exist_ok=True)
with overrides.open('w+') as fd:
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('\n\n'.join(quirks))
with overrides.open("w+") as fd:
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("\n\n".join(quirks))
return overrides
def check_file(recording):
version = fetch(recording, 'version')
version = fetch(recording, "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')
devices = fetch(recording, 'devices')
ndevices = fetch(recording, "ndevices")
devices = fetch(recording, "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():
parser = argparse.ArgumentParser(description='Replay a device recording')
parser.add_argument('recording', metavar='recorded-file.yaml',
type=str, help='Path to device recording')
parser.add_argument('--verbose', action='store_true')
parser = argparse.ArgumentParser(description="Replay a device recording")
parser.add_argument(
"recording",
metavar="recorded-file.yaml",
type=str,
help="Path to device recording",
)
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()
quirks_file = None
@ -309,13 +355,13 @@ def main():
except KeyboardInterrupt:
pass
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:
error('Error: failed to parse recording: {}'.format(e))
error("Error: failed to parse recording: {}".format(e))
finally:
if quirks_file:
quirks_file.unlink()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View file

@ -32,15 +32,15 @@ import logging
try:
import pytest
except ImportError:
print('Failed to import pytest. Skipping.', file=sys.stderr)
print("Failed to import pytest. Skipping.", file=sys.stderr)
sys.exit(77)
logger = logging.getLogger('test')
logger = logging.getLogger("test")
logger.setLevel(logging.DEBUG)
if '@DISABLE_WARNING@' != 'yes':
print('This is the source file, run the one in the meson builddir instead')
if "@DISABLE_WARNING@" != "yes":
print("This is the source file, run the one in the meson builddir instead")
sys.exit(1)
@ -49,9 +49,13 @@ def _disable_coredump():
def run_command(args):
logger.debug('run command: {}'.format(' '.join(args)))
with subprocess.Popen(args, preexec_fn=_disable_coredump,
stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
logger.debug("run command: {}".format(" ".join(args)))
with subprocess.Popen(
args,
preexec_fn=_disable_coredump,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as p:
try:
p.wait(0.7)
except subprocess.TimeoutExpired:
@ -59,11 +63,11 @@ def run_command(args):
stdout, stderr = p.communicate(timeout=5)
if p.returncode == -3:
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):
libinput_tool = 'libinput'
libinput_tool = "libinput"
subtool = None
def __init__(self, subtool=None):
@ -92,43 +96,43 @@ class LibinputTool(object):
def run_command_unrecognized_option(self, args):
rc, stdout, stderr = self.run_command(args)
assert rc == 2, (rc, stdout, stderr)
assert stdout.startswith('Usage') or stdout == ''
assert 'unrecognized option' in stderr
assert stdout.startswith("Usage") or stdout == ""
assert "unrecognized option" in stderr
def run_command_missing_arg(self, args):
rc, stdout, stderr = self.run_command(args)
assert rc == 2, (rc, stdout, stderr)
assert stdout.startswith('Usage') or stdout == ''
assert 'requires an argument' in stderr
assert stdout.startswith("Usage") or stdout == ""
assert "requires an argument" in stderr
def run_command_unrecognized_tool(self, args):
rc, stdout, stderr = self.run_command(args)
assert rc == 2, (rc, stdout, stderr)
assert stdout.startswith('Usage') or stdout == ''
assert 'is not installed' in stderr
assert stdout.startswith("Usage") or stdout == ""
assert "is not installed" in stderr
class LibinputDebugGui(LibinputTool):
def __init__(self, subtool='debug-gui'):
assert subtool == 'debug-gui'
def __init__(self, subtool="debug-gui"):
assert subtool == "debug-gui"
super().__init__(subtool)
debug_gui_enabled = '@MESON_ENABLED_DEBUG_GUI@' == 'True'
debug_gui_enabled = "@MESON_ENABLED_DEBUG_GUI@" == "True"
if not debug_gui_enabled:
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()
# 77 means gtk_init() failed, which is probably because you can't
# connect to the display server.
rc, _, _ = self.run_command(['--help'])
rc, _, _ = self.run_command(["--help"])
if rc == 77:
pytest.skip()
def get_tool(subtool=None):
if subtool == 'debug-gui':
if subtool == "debug-gui":
return LibinputDebugGui()
else:
return LibinputTool(subtool)
@ -139,110 +143,110 @@ def libinput():
return get_tool()
@pytest.fixture(params=['debug-events', 'debug-gui'])
@pytest.fixture(params=["debug-events", "debug-gui"])
def libinput_debug_tool(request):
yield get_tool(request.param)
@pytest.fixture
def libinput_debug_events():
return get_tool('debug-events')
return get_tool("debug-events")
@pytest.fixture
def libinput_debug_gui():
return get_tool('debug-gui')
return get_tool("debug-gui")
@pytest.fixture
def libinput_record():
return get_tool('record')
return get_tool("record")
def test_help(libinput):
stdout, stderr = libinput.run_command_success(['--help'])
assert stdout.startswith('Usage:')
assert stderr == ''
stdout, stderr = libinput.run_command_success(["--help"])
assert stdout.startswith("Usage:")
assert stderr == ""
def test_version(libinput):
stdout, stderr = libinput.run_command_success(['--version'])
assert stdout.startswith('1')
assert stderr == ''
stdout, stderr = libinput.run_command_success(["--version"])
assert stdout.startswith("1")
assert stderr == ""
@pytest.mark.parametrize('argument', ['--banana', '--foo', '--quiet', '--verbose'])
@pytest.mark.parametrize("argument", ["--banana", "--foo", "--quiet", "--verbose"])
def test_invalid_arguments(libinput, 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):
libinput.run_command_unrecognized_tool(tool)
def test_udev_seat(libinput_debug_tool):
libinput_debug_tool.run_command_missing_arg(['--udev'])
libinput_debug_tool.run_command_success(['--udev', 'seat0'])
libinput_debug_tool.run_command_success(['--udev', 'seat1'])
libinput_debug_tool.run_command_missing_arg(["--udev"])
libinput_debug_tool.run_command_success(["--udev", "seat0"])
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):
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/event1'])
libinput_debug_tool.run_command_success(['/dev/input/event0'])
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/event1"])
libinput_debug_tool.run_command_success(["/dev/input/event0"])
options = {
'pattern': ['sendevents'],
"pattern": ["sendevents"],
# enable/disable options
'enable-disable': [
'tap',
'drag',
'drag-lock',
'middlebutton',
'natural-scrolling',
'left-handed',
'dwt'
"enable-disable": [
"tap",
"drag",
"drag-lock",
"middlebutton",
"natural-scrolling",
"left-handed",
"dwt",
],
# options with distinct values
'enums': {
'set-click-method': ['none', 'clickfinger', 'buttonareas'],
'set-scroll-method': ['none', 'twofinger', 'edge', 'button'],
'set-profile': ['adaptive', 'flat'],
'set-tap-map': ['lrm', 'lmr'],
"enums": {
"set-click-method": ["none", "clickfinger", "buttonareas"],
"set-scroll-method": ["none", "twofinger", "edge", "button"],
"set-profile": ["adaptive", "flat"],
"set-tap-map": ["lrm", "lmr"],
},
# options with a range
'ranges': {
'set-speed': (float, -1.0, +1.0),
}
"ranges": {
"set-speed": (float, -1.0, +1.0),
},
}
# 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):
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), "*"])
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):
libinput_debug_tool.run_command_success(['--enable-{}'.format(option)])
libinput_debug_tool.run_command_success(['--disable-{}'.format(option)])
libinput_debug_tool.run_command_success(["--enable-{}".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):
name, values = option
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):
name, values = option
range_type, minimum, maximum = values
@ -250,114 +254,128 @@ def test_options_ranges(libinput_debug_tool, option):
step = (maximum - minimum) / 10.0
value = minimum
while value < maximum:
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), str(value)])
libinput_debug_tool.run_command_success(["--{}={}".format(name, value)])
value += step
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), str(maximum)])
libinput_debug_tool.run_command_success(["--{}={}".format(name, maximum)])
def test_apply_to(libinput_debug_tool):
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', 'foobar'])
libinput_debug_tool.run_command_success(['--apply-to', 'any'])
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", "foobar"])
libinput_debug_tool.run_command_success(["--apply-to", "any"])
@pytest.mark.parametrize('args', [['--verbose'], ['--quiet'],
['--verbose', '--quiet'],
['--quiet', '--verbose']])
@pytest.mark.parametrize(
"args",
[["--verbose"], ["--quiet"], ["--verbose", "--quiet"], ["--quiet", "--verbose"]],
)
def test_debug_events_verbose_quiet(libinput_debug_events, 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):
libinput_debug_tool.run_command_unrecognized_option([arg])
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
libinput_debug_events.run_command_success(['--device', '/dev/input/event0', '/dev/input/event0'])
libinput_debug_events.run_command_success(['/dev/input/event0', '/dev/input/event1'])
libinput_debug_events.run_command_success(
["--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):
# 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)
@pytest.mark.parametrize('arg', ['--quiet'])
@pytest.mark.parametrize("arg", ["--quiet"])
def test_libinput_debug_gui_invalid_arg(libinput_debug_gui, arg):
libinput_debug_gui.run_command_unrecognized_option([arg])
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):
libinput_record.run_command_success([arg])
def test_libinput_record_multiple_arg(libinput_record):
# this arg is deprecated and a noop
libinput_record.run_command_success(['--multiple'])
libinput_record.run_command_success(["--multiple"])
@pytest.fixture
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):
libinput_record.run_command_success(['--all', '-o', recording])
libinput_record.run_command_success(['--all', recording])
libinput_record.run_command_success(["--all", "-o", recording])
libinput_record.run_command_success(["--all", recording])
def test_libinput_record_outfile(libinput_record, recording):
libinput_record.run_command_success(['-o', recording])
libinput_record.run_command_success(['--output-file', recording])
libinput_record.run_command_success(['--output-file={}'.format(recording)])
libinput_record.run_command_success(["-o", recording])
libinput_record.run_command_success(["--output-file", recording])
libinput_record.run_command_success(["--output-file={}".format(recording)])
def test_libinput_record_single(libinput_record, recording):
libinput_record.run_command_success(['/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([recording, '/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(["/dev/input/event0", recording])
libinput_record.run_command_success([recording, "/dev/input/event0"])
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([recording, '/dev/input/event0', '/dev/input/event1'])
libinput_record.run_command_success(['/dev/input/event0', '/dev/input/event1', recording])
libinput_record.run_command_success(
["-o", recording, "/dev/input/event0", "/dev/input/event1"]
)
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):
libinput_record.run_command_invalid(['--autorestart'])
libinput_record.run_command_invalid(['--autorestart=2'])
libinput_record.run_command_success(['-o', recording, '--autorestart=2'])
libinput_record.run_command_invalid(["--autorestart"])
libinput_record.run_command_invalid(["--autorestart=2"])
libinput_record.run_command_success(["-o", recording, "--autorestart=2"])
def main():
args = ['-m', 'pytest']
args = ["-m", "pytest"]
try:
import xdist # noqa
args += ['-n', 'auto']
args += ["-n", "auto"]
except ImportError:
logger.info('python-xdist missing, this test will be slow')
logger.info("python-xdist missing, this test will be slow")
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
if __name__ == '__main__':
if __name__ == "__main__":
raise SystemExit(main())