#!/usr/bin/env python3
# -*- coding: utf-8
# vim: set expandtab shiftwidth=4:
# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
#
# Copyright © 2021 Red Hat, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the 'Software'),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
# Prints the data from a libinput recording in a table format to ease
# debugging.
#
# Input is a libinput record yaml file
import argparse
import os
import sys
import yaml
import libevdev
# minimum width of a field in the table
MIN_FIELD_WIDTH = 6
# Default is to just return the value of an axis, but some axes want special
# formatting.
def format_value(code, value):
if code in (libevdev.EV_ABS.ABS_MISC, libevdev.EV_MSC.MSC_SERIAL):
return f"{value & 0xFFFFFFFF:#x}"
# Rel axes we always print the sign
if code.type == libevdev.EV_REL:
return f"{value:+d}"
return f"{value}"
# The list of axes we want to track
def is_tracked_axis(code, allowlist, denylist):
if code.type in (libevdev.EV_KEY, libevdev.EV_SW, libevdev.EV_SYN):
return False
# We don't do slots in this tool
if code.type == libevdev.EV_ABS:
if libevdev.EV_ABS.ABS_MT_SLOT <= code <= libevdev.EV_ABS.ABS_MAX:
return False
if allowlist:
return code in allowlist
else:
return code not in denylist
def main(argv):
parser = argparse.ArgumentParser(
description="Display a recording in a tabular format"
)
parser.add_argument(
"path", metavar="recording", nargs=1, help="Path to libinput-record YAML file"
)
parser.add_argument(
"--ignore",
metavar="ABS_X,ABS_Y,...",
default="",
help="A comma-separated list of axis names to ignore",
)
parser.add_argument(
"--only",
metavar="ABS_X,ABS_Y,...",
default="",
help="A comma-separated list of axis names to print, ignoring all others",
)
parser.add_argument(
"--print-state",
action="store_true",
default=False,
help="Always print all axis values, even unchanged ones",
)
args = parser.parse_args()
if args.ignore and args.only:
print("Only one of --ignore and --only may be given", file=sys.stderr)
sys.exit(2)
ignored_axes = [libevdev.evbit(axis) for axis in args.ignore.split(",") if axis]
only_axes = [libevdev.evbit(axis) for axis in args.only.split(",") if axis]
isatty = os.isatty(sys.stdout.fileno())
yml = yaml.safe_load(open(args.path[0]))
if yml["ndevices"] > 1:
print(f"WARNING: Using only first {yml['ndevices']} devices in recording")
device = yml["devices"][0]
if not device["events"]:
print(f"No events found in recording")
sys.exit(1)
def events():
"""
Yields the next event in the recording
"""
for event in device["events"]:
for evdev in event.get("evdev", []):
yield libevdev.InputEvent(
code=libevdev.evbit(evdev[2], evdev[3]),
value=evdev[4],
sec=evdev[0],
usec=evdev[1],
)
def interesting_axes(events):
"""
Yields the libevdev codes with the axes in this recording
"""
used_axes = []
for e in events:
if e.code not in used_axes and is_tracked_axis(
e.code, only_axes, ignored_axes
):
yield e.code
used_axes.append(e.code)
# Compile all axes that we want to print first
axes = sorted(
interesting_axes(events()), key=lambda x: x.type.value * 1000 + x.value
)
# Strip the REL_/ABS_ prefix for the headers
headers = [a.name[4:].rjust(MIN_FIELD_WIDTH) for a in axes]
# for easier formatting later, we keep the header field width in a dict
axes = {a: len(h) for a, h in zip(axes, headers)}
# Time is a special case, always the first entry
# Format uses ms only, we rarely ever care about µs
headers = [f"{'Time':<7s}"] + headers + ["Keys"]
header_line = f"{' | '.join(headers)}"
print(header_line)
print("-" * len(header_line))
current_codes = []
current_frame = {} # {evdev-code: value}
axes_in_use = {} # to print axes never sending events
last_fields = [] # to skip duplicate lines
continuation_count = 0
keystate = {}
keystate_changed = False
for e in events():
axes_in_use[e.code] = True
if e.code.type == libevdev.EV_KEY:
keystate[e.code] = e.value
keystate_changed = True
elif is_tracked_axis(e.code, only_axes, ignored_axes):
current_frame[e.code] = e.value
current_codes.append(e.code)
elif e.code == libevdev.EV_SYN.SYN_REPORT:
fields = []
for a in axes:
if args.print_state or a in current_codes:
s = format_value(a, current_frame.get(a, 0))
else:
s = ""
fields.append(s.rjust(max(MIN_FIELD_WIDTH, axes[a])))
current_codes = []
if last_fields != fields or keystate_changed:
last_fields = fields.copy()
keystate_changed = False
if continuation_count:
if not isatty:
print(f" ... +{continuation_count}", end="")
print("")
continuation_count = 0
fields.insert(0, f"{e.sec: 3d}.{e.usec//1000:03d}")
keys_down = [k.name for k, v in keystate.items() if v]
fields.append(", ".join(keys_down))
print(" | ".join(fields))
else:
continuation_count += 1
if isatty:
print(f"\r ... +{continuation_count}", end="", flush=True)
# Print out any rel/abs axes that not generate events in
# this recording
unused_axes = []
for evtype, evcodes in device["evdev"]["codes"].items():
for c in evcodes:
code = libevdev.evbit(int(evtype), int(c))
if (
is_tracked_axis(code, only_axes, ignored_axes)
and code not in axes_in_use
):
unused_axes.append(code)
if unused_axes:
print(
f"Axes present but without events: {', '.join([a.name for a in unused_axes])}"
)
for e in events():
if libevdev.EV_ABS.ABS_MT_SLOT <= code <= libevdev.EV_ABS.ABS_MAX:
print(
"WARNING: This recording contains multitouch data that is not supported by this tool."
)
break
if __name__ == "__main__":
try:
main(sys.argv)
except BrokenPipeError:
pass