• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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