1#!/usr/bin/env python3 2# -*- coding: utf-8 3# vim: set expandtab shiftwidth=4: 4# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */ 5# 6# Copyright © 2021 Red Hat, Inc. 7# 8# Permission is hereby granted, free of charge, to any person obtaining a 9# copy of this software and associated documentation files (the 'Software'), 10# to deal in the Software without restriction, including without limitation 11# the rights to use, copy, modify, merge, publish, distribute, sublicense, 12# and/or sell copies of the Software, and to permit persons to whom the 13# Software is furnished to do so, subject to the following conditions: 14# 15# The above copyright notice and this permission notice (including the next 16# paragraph) shall be included in all copies or substantial portions of the 17# Software. 18# 19# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 22# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25# DEALINGS IN THE SOFTWARE. 26# 27# Prints the data from a libinput recording in a table format to ease 28# debugging. 29# 30# Input is a libinput record yaml file 31 32import argparse 33import sys 34import yaml 35import libevdev 36 37# minimum width of a field in the table 38MIN_FIELD_WIDTH = 6 39 40 41# Default is to just return the value of an axis, but some axes want special 42# formatting. 43def format_value(code, value): 44 if code in (libevdev.EV_ABS.ABS_MISC, libevdev.EV_MSC.MSC_SERIAL): 45 return f"{value & 0xFFFFFFFF:#x}" 46 47 # Rel axes we always print the sign 48 if code.type == libevdev.EV_REL: 49 return f"{value:+d}" 50 51 return f"{value}" 52 53 54# The list of axes we want to track 55def is_tracked_axis(code): 56 if code.type in (libevdev.EV_KEY, libevdev.EV_SW, libevdev.EV_SYN): 57 return False 58 59 # We don't do slots in this tool 60 if code.type == libevdev.EV_ABS: 61 if libevdev.EV_ABS.ABS_MT_SLOT <= code <= libevdev.EV_ABS.ABS_MAX: 62 return False 63 64 return True 65 66 67def main(argv): 68 parser = argparse.ArgumentParser( 69 description="Display a recording in a tabular format" 70 ) 71 parser.add_argument( 72 "path", metavar="recording", nargs=1, help="Path to libinput-record YAML file" 73 ) 74 args = parser.parse_args() 75 76 yml = yaml.safe_load(open(args.path[0])) 77 if yml["ndevices"] > 1: 78 print(f"WARNING: Using only first {yml['ndevices']} devices in recording") 79 device = yml["devices"][0] 80 81 if not device["events"]: 82 print(f"No events found in recording") 83 sys.exit(1) 84 85 def events(): 86 """ 87 Yields the next event in the recording 88 """ 89 for event in device["events"]: 90 for evdev in event.get("evdev", []): 91 yield libevdev.InputEvent( 92 code=libevdev.evbit(evdev[2], evdev[3]), 93 value=evdev[4], 94 sec=evdev[0], 95 usec=evdev[1], 96 ) 97 98 def interesting_axes(events): 99 """ 100 Yields the libevdev codes with the axes in this recording 101 """ 102 used_axes = [] 103 for e in events: 104 if e.code not in used_axes and is_tracked_axis(e.code): 105 yield e.code 106 used_axes.append(e.code) 107 108 # Compile all axes that we want to print first 109 axes = sorted( 110 interesting_axes(events()), key=lambda x: x.type.value * 1000 + x.value 111 ) 112 # Strip the REL_/ABS_ prefix for the headers 113 headers = [a.name[4:].rjust(MIN_FIELD_WIDTH) for a in axes] 114 # for easier formatting later, we keep the header field width in a dict 115 axes = {a: len(h) for a, h in zip(axes, headers)} 116 117 # Time is a special case, always the first entry 118 # Format uses ms only, we rarely ever care about µs 119 headers = [f"{'Time':<7s}"] + headers + ["Keys"] 120 header_line = f"{' | '.join(headers)}" 121 print(header_line) 122 print("-" * len(header_line)) 123 124 current_frame = {} # {evdev-code: value} 125 axes_in_use = {} # to print axes never sending events 126 last_fields = [] # to skip duplicate lines 127 continuation_count = 0 128 129 keystate = {} 130 keystate_changed = False 131 132 for e in events(): 133 axes_in_use[e.code] = True 134 135 if e.code.type == libevdev.EV_KEY: 136 keystate[e.code] = e.value 137 keystate_changed = True 138 elif is_tracked_axis(e.code): 139 current_frame[e.code] = e.value 140 elif e.code == libevdev.EV_SYN.SYN_REPORT: 141 fields = [] 142 for a in axes: 143 s = format_value(a, current_frame[a]) if a in current_frame else " " 144 fields.append(s.rjust(max(MIN_FIELD_WIDTH, axes[a]))) 145 current_frame = {} 146 147 if last_fields != fields or keystate_changed: 148 last_fields = fields.copy() 149 keystate_changed = False 150 151 if continuation_count: 152 continuation_count = 0 153 print("") 154 155 fields.insert(0, f"{e.sec: 3d}.{e.usec//1000:03d}") 156 keys_down = [k.name for k, v in keystate.items() if v] 157 fields.append(", ".join(keys_down)) 158 print(" | ".join(fields)) 159 else: 160 continuation_count += 1 161 print(f"\r ... +{continuation_count}", end="", flush=True) 162 163 # Print out any rel/abs axes that not generate events in 164 # this recording 165 unused_axes = [] 166 for evtype, evcodes in device["evdev"]["codes"].items(): 167 for c in evcodes: 168 code = libevdev.evbit(int(evtype), int(c)) 169 if is_tracked_axis(code) and code not in axes_in_use: 170 unused_axes.append(code) 171 172 if unused_axes: 173 print( 174 f"Axes present but without events: {', '.join([a.name for a in unused_axes])}" 175 ) 176 177 for e in events(): 178 if libevdev.EV_ABS.ABS_MT_SLOT <= code <= libevdev.EV_ABS.ABS_MAX: 179 print( 180 "WARNING: This recording contains multitouch data that is not supported by this tool." 181 ) 182 break 183 184 185if __name__ == "__main__": 186 try: 187 main(sys.argv) 188 except BrokenPipeError: 189 pass 190