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 os 34import sys 35import yaml 36import libevdev 37 38# minimum width of a field in the table 39MIN_FIELD_WIDTH = 6 40 41 42# Default is to just return the value of an axis, but some axes want special 43# formatting. 44def format_value(code, value): 45 if code in (libevdev.EV_ABS.ABS_MISC, libevdev.EV_MSC.MSC_SERIAL): 46 return f"{value & 0xFFFFFFFF:#x}" 47 48 # Rel axes we always print the sign 49 if code.type == libevdev.EV_REL: 50 return f"{value:+d}" 51 52 return f"{value}" 53 54 55# The list of axes we want to track 56def is_tracked_axis(code, allowlist, denylist): 57 if code.type in (libevdev.EV_KEY, libevdev.EV_SW, libevdev.EV_SYN): 58 return False 59 60 # We don't do slots in this tool 61 if code.type == libevdev.EV_ABS: 62 if libevdev.EV_ABS.ABS_MT_SLOT <= code <= libevdev.EV_ABS.ABS_MAX: 63 return False 64 65 if allowlist: 66 return code in allowlist 67 else: 68 return code not in denylist 69 70 71def main(argv): 72 parser = argparse.ArgumentParser( 73 description="Display a recording in a tabular format" 74 ) 75 parser.add_argument( 76 "path", metavar="recording", nargs=1, help="Path to libinput-record YAML file" 77 ) 78 parser.add_argument( 79 "--ignore", 80 metavar="ABS_X,ABS_Y,...", 81 default="", 82 help="A comma-separated list of axis names to ignore", 83 ) 84 parser.add_argument( 85 "--only", 86 metavar="ABS_X,ABS_Y,...", 87 default="", 88 help="A comma-separated list of axis names to print, ignoring all others", 89 ) 90 parser.add_argument( 91 "--print-state", 92 action="store_true", 93 default=False, 94 help="Always print all axis values, even unchanged ones", 95 ) 96 97 args = parser.parse_args() 98 if args.ignore and args.only: 99 print("Only one of --ignore and --only may be given", file=sys.stderr) 100 sys.exit(2) 101 102 ignored_axes = [libevdev.evbit(axis) for axis in args.ignore.split(",") if axis] 103 only_axes = [libevdev.evbit(axis) for axis in args.only.split(",") if axis] 104 105 isatty = os.isatty(sys.stdout.fileno()) 106 107 yml = yaml.safe_load(open(args.path[0])) 108 if yml["ndevices"] > 1: 109 print(f"WARNING: Using only first {yml['ndevices']} devices in recording") 110 device = yml["devices"][0] 111 112 if not device["events"]: 113 print(f"No events found in recording") 114 sys.exit(1) 115 116 def events(): 117 """ 118 Yields the next event in the recording 119 """ 120 for event in device["events"]: 121 for evdev in event.get("evdev", []): 122 yield libevdev.InputEvent( 123 code=libevdev.evbit(evdev[2], evdev[3]), 124 value=evdev[4], 125 sec=evdev[0], 126 usec=evdev[1], 127 ) 128 129 def interesting_axes(events): 130 """ 131 Yields the libevdev codes with the axes in this recording 132 """ 133 used_axes = [] 134 for e in events: 135 if e.code not in used_axes and is_tracked_axis( 136 e.code, only_axes, ignored_axes 137 ): 138 yield e.code 139 used_axes.append(e.code) 140 141 # Compile all axes that we want to print first 142 axes = sorted( 143 interesting_axes(events()), key=lambda x: x.type.value * 1000 + x.value 144 ) 145 # Strip the REL_/ABS_ prefix for the headers 146 headers = [a.name[4:].rjust(MIN_FIELD_WIDTH) for a in axes] 147 # for easier formatting later, we keep the header field width in a dict 148 axes = {a: len(h) for a, h in zip(axes, headers)} 149 150 # Time is a special case, always the first entry 151 # Format uses ms only, we rarely ever care about µs 152 headers = [f"{'Time':<7s}"] + headers + ["Keys"] 153 header_line = f"{' | '.join(headers)}" 154 print(header_line) 155 print("-" * len(header_line)) 156 157 current_codes = [] 158 current_frame = {} # {evdev-code: value} 159 axes_in_use = {} # to print axes never sending events 160 last_fields = [] # to skip duplicate lines 161 continuation_count = 0 162 163 keystate = {} 164 keystate_changed = False 165 166 for e in events(): 167 axes_in_use[e.code] = True 168 169 if e.code.type == libevdev.EV_KEY: 170 keystate[e.code] = e.value 171 keystate_changed = True 172 elif is_tracked_axis(e.code, only_axes, ignored_axes): 173 current_frame[e.code] = e.value 174 current_codes.append(e.code) 175 elif e.code == libevdev.EV_SYN.SYN_REPORT: 176 fields = [] 177 for a in axes: 178 if args.print_state or a in current_codes: 179 s = format_value(a, current_frame.get(a, 0)) 180 else: 181 s = "" 182 fields.append(s.rjust(max(MIN_FIELD_WIDTH, axes[a]))) 183 current_codes = [] 184 185 if last_fields != fields or keystate_changed: 186 last_fields = fields.copy() 187 keystate_changed = False 188 189 if continuation_count: 190 if not isatty: 191 print(f" ... +{continuation_count}", end="") 192 print("") 193 continuation_count = 0 194 195 fields.insert(0, f"{e.sec: 3d}.{e.usec//1000:03d}") 196 keys_down = [k.name for k, v in keystate.items() if v] 197 fields.append(", ".join(keys_down)) 198 print(" | ".join(fields)) 199 else: 200 continuation_count += 1 201 if isatty: 202 print(f"\r ... +{continuation_count}", end="", flush=True) 203 204 # Print out any rel/abs axes that not generate events in 205 # this recording 206 unused_axes = [] 207 for evtype, evcodes in device["evdev"]["codes"].items(): 208 for c in evcodes: 209 code = libevdev.evbit(int(evtype), int(c)) 210 if ( 211 is_tracked_axis(code, only_axes, ignored_axes) 212 and code not in axes_in_use 213 ): 214 unused_axes.append(code) 215 216 if unused_axes: 217 print( 218 f"Axes present but without events: {', '.join([a.name for a in unused_axes])}" 219 ) 220 221 for e in events(): 222 if libevdev.EV_ABS.ABS_MT_SLOT <= code <= libevdev.EV_ABS.ABS_MAX: 223 print( 224 "WARNING: This recording contains multitouch data that is not supported by this tool." 225 ) 226 break 227 228 229if __name__ == "__main__": 230 try: 231 main(sys.argv) 232 except BrokenPipeError: 233 pass 234