HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux spn-python 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 x86_64
User: arjun (1000)
PHP: 8.1.2-1ubuntu2.20
Disabled: NONE
Upload Files
File: //snap/gnome-46-2404/current/usr/libexec/libinput/libinput-analyze-buttons
#!/usr/bin/env python3
# -*- coding: utf-8
# vim: set expandtab shiftwidth=4:
# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
#
# Copyright © 2024 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

from dataclasses import dataclass
import argparse
import os
import sys
import yaml
import libevdev

COLOR_RESET = "\x1b[0m"
COLOR_RED = "\x1b[6;31m"


def micros(e: libevdev.InputEvent):
    return e.usec + e.sec * 1_000_000


@dataclass
class Timestamp:
    sec: int
    usec: int

    @property
    def micros(self) -> int:
        return self.usec + self.sec * 1_000_000


@dataclass
class ButtonFrame:
    delta_ms: int  # delta time to last button (not evdev!) frame
    evdev_delta_ms: int  # delta time to last evdev frame
    events: list[libevdev.InputEvent]  # BTN_ events only

    @property
    def timestamp(self) -> Timestamp:
        e = self.events[0]
        return Timestamp(e.sec, e.usec)

    def value(self, code: libevdev.EventCode) -> bool | None:
        for e in self.events:
            if e.matches(code):
                return e.value
        return None

    def values(self, codes: list[libevdev.EventCode]) -> list[bool | None]:
        return [self.value(code) for code in codes]


def frames(events):
    last_timestamp = None
    current_frame = None
    last_frame = None
    for e in events:
        if last_timestamp is None:
            last_timestamp = micros(e)
        if e.type == libevdev.EV_SYN:
            last_timestamp = micros(e)
            if current_frame is not None:
                yield current_frame
            last_frame = current_frame
            current_frame = None
        elif e.type == libevdev.EV_KEY:
            if e.code.name.startswith("BTN_") and not e.code.name.startswith(
                "BTN_TOOL_"
            ):
                timestamp = micros(e)
                evdev_delta = (timestamp - last_timestamp) // 1000

                if last_frame is not None:
                    delta = (timestamp - last_frame.timestamp.micros) // 1000
                else:
                    delta = 0

                if current_frame is None:
                    current_frame = ButtonFrame(
                        delta_ms=delta, evdev_delta_ms=evdev_delta, events=[e]
                    )
                else:
                    current_frame.events.append(e)


def main(argv):
    parser = argparse.ArgumentParser(description="Display button events in a recording")
    parser.add_argument(
        "--threshold",
        type=int,
        default=25,
        help="Mark any time delta above this threshold (in ms)",
    )
    parser.add_argument(
        "path", metavar="recording", nargs=1, help="Path to libinput-record YAML file"
    )
    args = parser.parse_args()
    isatty = os.isatty(sys.stdout.fileno())
    if not isatty:
        global COLOR_RESET
        global COLOR_RED
        COLOR_RESET = ""
        COLOR_RED = ""

    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("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],
                )

    # These are the buttons we possibly care about, but we filter to the ones
    # found on this device anyway
    buttons = [
        libevdev.EV_KEY.BTN_LEFT,
        libevdev.EV_KEY.BTN_MIDDLE,
        libevdev.EV_KEY.BTN_RIGHT,
        libevdev.EV_KEY.BTN_SIDE,
        libevdev.EV_KEY.BTN_EXTRA,
        libevdev.EV_KEY.BTN_FORWARD,
        libevdev.EV_KEY.BTN_BACK,
        libevdev.EV_KEY.BTN_TASK,
        libevdev.EV_KEY.BTN_TOUCH,
        libevdev.EV_KEY.BTN_STYLUS,
        libevdev.EV_KEY.BTN_STYLUS2,
        libevdev.EV_KEY.BTN_STYLUS3,
        libevdev.EV_KEY.BTN_0,
        libevdev.EV_KEY.BTN_1,
        libevdev.EV_KEY.BTN_2,
        libevdev.EV_KEY.BTN_3,
        libevdev.EV_KEY.BTN_4,
        libevdev.EV_KEY.BTN_5,
        libevdev.EV_KEY.BTN_6,
        libevdev.EV_KEY.BTN_7,
        libevdev.EV_KEY.BTN_8,
        libevdev.EV_KEY.BTN_9,
    ]

    def filter_buttons(buttons):
        return filter(
            lambda c: c in buttons,
            map(lambda c: libevdev.evbit("EV_KEY", c), device["evdev"]["codes"][1]),
        )

    buttons = list(filter_buttons(buttons))

    # all BTN_STYLUS will have a header of S - meh
    btn_headers = " │ ".join(b.name[4] for b in buttons)
    print(f"{'Timestamp':^13s} │ {'Delta':^8s} │ {btn_headers}")
    last_btn_vals = [None] * len(buttons)

    def btnchar(b, last):
        if b == 1:
            return "┬"
        if b == 0:
            return "┴"
        return "│" if last else " "

    for frame in frames(events()):
        ts = frame.timestamp
        if frame.timestamp.micros > 0 and frame.delta_ms < args.threshold:
            color = COLOR_RED
        else:
            color = ""
        btn_vals = frame.values(buttons)
        btn_strs = " │ ".join(
            [btnchar(b, last) for b, last in zip(btn_vals, last_btn_vals)]
        )

        last_btn_vals = [
            b if b is not None else last for b, last in zip(btn_vals, last_btn_vals)
        ]

        print(
            f"{color}{ts.sec:6d}.{ts.usec:06d} │ {frame.delta_ms:6d}ms │ {btn_strs}{COLOR_RESET}"
        )


if __name__ == "__main__":
    try:
        main(sys.argv)
    except BrokenPipeError:
        pass