mirror of
https://gitlab.freedesktop.org/libinput/libinput.git
synced 2025-12-20 15:00:05 +01:00
tools: add a libinput analyze command with the per-slot-delta subcommand
I've been using this script ever since libinput record was available, might as well ship it with libinput so I don't have to remember where it lives. Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
This commit is contained in:
parent
927a7c0745
commit
e11bad41f5
5 changed files with 450 additions and 0 deletions
15
meson.build
15
meson.build
|
|
@ -577,7 +577,22 @@ configure_file(input : 'tools/libinput-measure.man',
|
||||||
install_dir : dir_man1,
|
install_dir : dir_man1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
libinput_analyze_sources = [ 'tools/libinput-analyze.c' ]
|
||||||
|
executable('libinput-analyze',
|
||||||
|
libinput_analyze_sources,
|
||||||
|
dependencies : deps_tools,
|
||||||
|
include_directories : [includes_src, includes_include],
|
||||||
|
install_dir : libinput_tool_path,
|
||||||
|
install : true,
|
||||||
|
)
|
||||||
|
configure_file(input : 'tools/libinput-analyze.man',
|
||||||
|
output : 'libinput-analyze.1',
|
||||||
|
configuration : man_config,
|
||||||
|
install_dir : dir_man1,
|
||||||
|
)
|
||||||
|
|
||||||
src_python_tools = files(
|
src_python_tools = files(
|
||||||
|
'tools/libinput-analyze-per-slot-delta.py',
|
||||||
'tools/libinput-measure-fuzz.py',
|
'tools/libinput-measure-fuzz.py',
|
||||||
'tools/libinput-measure-touchpad-tap.py',
|
'tools/libinput-measure-touchpad-tap.py',
|
||||||
'tools/libinput-measure-touchpad-pressure.py',
|
'tools/libinput-measure-touchpad-pressure.py',
|
||||||
|
|
|
||||||
69
tools/libinput-analyze-per-slot-delta.man
Normal file
69
tools/libinput-analyze-per-slot-delta.man
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
.TH libinput-analyze-per-slot-delta "1"
|
||||||
|
.SH NAME
|
||||||
|
libinput\-analyze\-per\-slot\-delta \- analyze the per-event delta movement for touch slots
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B libinput analyze per-slot-delta [\-\-help] [options] \fIrecording.yml\fI
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.PP
|
||||||
|
The
|
||||||
|
.B "libinput analyze per\-slot\-delta"
|
||||||
|
tool analyzes a recording made with
|
||||||
|
.B "libinput record"
|
||||||
|
and prints the delta movement per touch slot.
|
||||||
|
.PP
|
||||||
|
This is a debugging tool only, its output may change at any time. Do not
|
||||||
|
rely on the output.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP 8
|
||||||
|
.B \-\-help
|
||||||
|
Print help
|
||||||
|
.TP 8
|
||||||
|
.B \-\-use-mm
|
||||||
|
Print data in mm instead of device units
|
||||||
|
.TP 8
|
||||||
|
.B \-\-use-st
|
||||||
|
Use the single-touch ABS_X/ABS_Y instead of the multitouch axes
|
||||||
|
.TP 8
|
||||||
|
.B \-\-use-absolute
|
||||||
|
Print absolute coordinates, not deltas
|
||||||
|
.SH OUTPUT
|
||||||
|
An example output for a single finger touch on a touchpad supporting two
|
||||||
|
slots is below. This output is with the use of the
|
||||||
|
.B --use-mm
|
||||||
|
flag.
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
.sf
|
||||||
|
0.000000 +0ms: ++++++ | ************* |
|
||||||
|
0.021900 +21ms: →↘ +1.10/+0.14 | ************* |
|
||||||
|
0.033468 +11ms: →↘ +1.15/+0.19 | ************* |
|
||||||
|
0.043856 +10ms: →↘ +1.76/+0.22 | ************* |
|
||||||
|
0.053237 +9ms: →↘ +2.20/+0.19 | ************* |
|
||||||
|
.fi
|
||||||
|
.in
|
||||||
|
.PP
|
||||||
|
The entry
|
||||||
|
.B ++++++
|
||||||
|
indicates a finger has been put down,
|
||||||
|
.B ------
|
||||||
|
indicates the finger has lifted.
|
||||||
|
The left-most column is the absolute timestamp in seconds.microseconds
|
||||||
|
followed by the relative time of the event to the previous event. The arrows
|
||||||
|
indicate the approximate direction on a 16-point compass.
|
||||||
|
.PP
|
||||||
|
Each multitouch slot supported by the hardware has one column, where the
|
||||||
|
column shows asterisk
|
||||||
|
.B ********
|
||||||
|
no finger is down for that slot. Where the column shows spaces only, a
|
||||||
|
finger is down but no data is currently available.
|
||||||
|
.PP
|
||||||
|
In the above example, the third events occurs ~33ms into the recording, is
|
||||||
|
11ms after the previous event and has an east south-east direction. The
|
||||||
|
movement for this event was x: 1.15 and y: 0.19 mm. A second finger is not
|
||||||
|
currently down.
|
||||||
|
.SH LIBINPUT
|
||||||
|
Part of the
|
||||||
|
.B libinput(1)
|
||||||
|
suite
|
||||||
|
|
||||||
|
|
||||||
264
tools/libinput-analyze-per-slot-delta.py
Executable file
264
tools/libinput-analyze-per-slot-delta.py
Executable file
|
|
@ -0,0 +1,264 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8
|
||||||
|
# vim: set expandtab shiftwidth=4:
|
||||||
|
# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
|
||||||
|
#
|
||||||
|
# Copyright © 2018 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.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Measures the relative motion between touch events (based on slots)
|
||||||
|
#
|
||||||
|
# Input is a libinput record yaml file
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
import libevdev
|
||||||
|
|
||||||
|
|
||||||
|
COLOR_RESET = '\x1b[0m'
|
||||||
|
COLOR_RED = '\x1b[6;31m'
|
||||||
|
|
||||||
|
|
||||||
|
class SlotState:
|
||||||
|
NONE = 0
|
||||||
|
BEGIN = 1
|
||||||
|
UPDATE = 2
|
||||||
|
END = 3
|
||||||
|
|
||||||
|
|
||||||
|
class Slot:
|
||||||
|
index = 0
|
||||||
|
state = SlotState.NONE
|
||||||
|
x = 0
|
||||||
|
y = 0
|
||||||
|
dx = 0
|
||||||
|
dy = 0
|
||||||
|
dirty = False
|
||||||
|
|
||||||
|
|
||||||
|
class InputEvent:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.sec = data[0]
|
||||||
|
self.usec = data[1]
|
||||||
|
self.evtype = data[2]
|
||||||
|
self.evcode = data[3]
|
||||||
|
self.value = data[4]
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
global COLOR_RESET
|
||||||
|
global COLOR_RED
|
||||||
|
|
||||||
|
slots = []
|
||||||
|
xres, yres = 1, 1
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Measure delta between event frames for each slot")
|
||||||
|
parser.add_argument("--use-mm", action='store_true', help="Use mm instead of device deltas")
|
||||||
|
parser.add_argument("--use-st", action='store_true', help="Use ABS_X/ABS_Y instead of ABS_MT_POSITION_X/Y")
|
||||||
|
parser.add_argument("--use-absolute", action='store_true', help="Use absolute coordinates, not deltas")
|
||||||
|
parser.add_argument("path", metavar="recording",
|
||||||
|
nargs=1, help="Path to libinput-record YAML file")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not sys.stdout.isatty():
|
||||||
|
COLOR_RESET = ''
|
||||||
|
COLOR_RED = ''
|
||||||
|
|
||||||
|
yml = yaml.safe_load(open(args.path[0]))
|
||||||
|
device = yml['devices'][0]
|
||||||
|
absinfo = device['evdev']['absinfo']
|
||||||
|
try:
|
||||||
|
nslots = absinfo[libevdev.EV_ABS.ABS_MT_SLOT.value][1] + 1
|
||||||
|
except KeyError:
|
||||||
|
args.use_st = True
|
||||||
|
|
||||||
|
if args.use_st:
|
||||||
|
nslots = 1
|
||||||
|
|
||||||
|
slots = [Slot() for _ in range(0, nslots)]
|
||||||
|
|
||||||
|
marker_begin_slot = " ++++++ | " # noqa
|
||||||
|
marker_end_slot = " ------ | " # noqa
|
||||||
|
marker_empty_slot = " *********** | " # noqa
|
||||||
|
marker_no_data = " | " # noqa
|
||||||
|
marker_button = "..............." # noqa
|
||||||
|
|
||||||
|
if args.use_mm:
|
||||||
|
xres = 1.0 * absinfo[libevdev.EV_ABS.ABS_X.value][4]
|
||||||
|
yres = 1.0 * absinfo[libevdev.EV_ABS.ABS_Y.value][4]
|
||||||
|
if not xres or not yres:
|
||||||
|
print("Error: device doesn't have a resolution, cannot use mm")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
marker_empty_slot = " ************* | " # noqa
|
||||||
|
marker_no_data = " | " # noqa
|
||||||
|
marker_begin_slot = " ++++++ | " # noqa
|
||||||
|
marker_end_slot = " ------ | " # noqa
|
||||||
|
|
||||||
|
if args.use_st:
|
||||||
|
print("Warning: slot coordinates on FINGER/DOUBLETAP change may be incorrect")
|
||||||
|
|
||||||
|
slot = 0
|
||||||
|
last_time = None
|
||||||
|
|
||||||
|
for event in device['events']:
|
||||||
|
for evdev in event['evdev']:
|
||||||
|
s = slots[slot]
|
||||||
|
e = InputEvent(evdev)
|
||||||
|
evbit = libevdev.evbit(e.evtype, e.evcode)
|
||||||
|
|
||||||
|
if args.use_st:
|
||||||
|
# Note: this relies on the EV_KEY events to come in before the
|
||||||
|
# x/y events, otherwise the last/first event in each slot will
|
||||||
|
# be wrong.
|
||||||
|
if (evbit == libevdev.EV_KEY.BTN_TOOL_FINGER or
|
||||||
|
evbit == libevdev.EV_KEY.BTN_TOOL_PEN):
|
||||||
|
slot = 0
|
||||||
|
s = slots[slot]
|
||||||
|
s.dirty = True
|
||||||
|
if e.value:
|
||||||
|
s.state = SlotState.BEGIN
|
||||||
|
else:
|
||||||
|
s.state = SlotState.END
|
||||||
|
elif evbit == libevdev.EV_KEY.BTN_TOOL_DOUBLETAP:
|
||||||
|
if len(slots) > 1:
|
||||||
|
slot = 1
|
||||||
|
s = slots[slot]
|
||||||
|
s.dirty = True
|
||||||
|
if e.value:
|
||||||
|
s.state = SlotState.BEGIN
|
||||||
|
else:
|
||||||
|
s.state = SlotState.END
|
||||||
|
elif evbit == libevdev.EV_ABS.ABS_X:
|
||||||
|
if s.state == SlotState.UPDATE:
|
||||||
|
s.dx = e.value - s.x
|
||||||
|
s.x = e.value
|
||||||
|
s.dirty = True
|
||||||
|
elif evbit == libevdev.EV_ABS.ABS_Y:
|
||||||
|
if s.state == SlotState.UPDATE:
|
||||||
|
s.dy = e.value - s.y
|
||||||
|
s.y = e.value
|
||||||
|
s.dirty = True
|
||||||
|
else:
|
||||||
|
if evbit == libevdev.EV_ABS.ABS_MT_SLOT:
|
||||||
|
slot = e.value
|
||||||
|
s = slots[slot]
|
||||||
|
s.dirty = True
|
||||||
|
elif evbit == libevdev.EV_ABS.ABS_MT_TRACKING_ID:
|
||||||
|
if e.value == -1:
|
||||||
|
s.state = SlotState.END
|
||||||
|
else:
|
||||||
|
s.state = SlotState.BEGIN
|
||||||
|
s.dx = 0
|
||||||
|
s.dy = 0
|
||||||
|
s.dirty = True
|
||||||
|
elif evbit == libevdev.EV_ABS.ABS_MT_POSITION_X:
|
||||||
|
if s.state == SlotState.UPDATE:
|
||||||
|
s.dx = e.value - s.x
|
||||||
|
s.x = e.value
|
||||||
|
s.dirty = True
|
||||||
|
elif evbit == libevdev.EV_ABS.ABS_MT_POSITION_Y:
|
||||||
|
if s.state == SlotState.UPDATE:
|
||||||
|
s.dy = e.value - s.y
|
||||||
|
s.y = e.value
|
||||||
|
s.dirty = True
|
||||||
|
|
||||||
|
if (evbit == libevdev.EV_KEY.BTN_TOUCH or
|
||||||
|
(evbit == libevdev.EV_KEY.BTN_TOOL_DOUBLETAP and nslots < 2) or
|
||||||
|
(evbit == libevdev.EV_KEY.BTN_TOOL_TRIPLETAP and nslots < 3) or
|
||||||
|
(evbit == libevdev.EV_KEY.BTN_TOOL_QUADTAP and nslots < 4) or
|
||||||
|
(evbit == libevdev.EV_KEY.BTN_TOOL_QUINTTAP and nslots < 5)):
|
||||||
|
print(' {} {} {} {}'.format(marker_button,
|
||||||
|
evbit.name,
|
||||||
|
e.value,
|
||||||
|
marker_button))
|
||||||
|
|
||||||
|
if evbit == libevdev.EV_SYN.SYN_REPORT:
|
||||||
|
if last_time is None:
|
||||||
|
last_time = e.sec * 1000000 + e.usec
|
||||||
|
tdelta = 0
|
||||||
|
else:
|
||||||
|
t = e.sec * 1000000 + e.usec
|
||||||
|
tdelta = int((t - last_time) / 1000) # ms
|
||||||
|
last_time = t
|
||||||
|
|
||||||
|
print("{:2d}.{:06d} {:+4d}ms: ".format(e.sec, e.usec, tdelta), end='')
|
||||||
|
for sl in slots:
|
||||||
|
if sl.state == SlotState.NONE:
|
||||||
|
print(marker_empty_slot, end='')
|
||||||
|
elif sl.state == SlotState.BEGIN:
|
||||||
|
print(marker_begin_slot, end='')
|
||||||
|
elif sl.state == SlotState.END:
|
||||||
|
print(marker_end_slot, end='')
|
||||||
|
elif not sl.dirty:
|
||||||
|
print(marker_no_data, end='')
|
||||||
|
else:
|
||||||
|
if sl.dx != 0 and sl.dy != 0:
|
||||||
|
t = math.atan2(sl.dx, sl.dy)
|
||||||
|
t += math.pi # in [0, 2pi] range now
|
||||||
|
|
||||||
|
if t == 0:
|
||||||
|
t = 0.01
|
||||||
|
else:
|
||||||
|
t = t * 180.0 / math.pi
|
||||||
|
|
||||||
|
directions = ['↖↑', '↖←', '↙←', '↙↓', '↓↘', '→↘', '→↗', '↑↗']
|
||||||
|
direction = "{:3.0f}".format(t)
|
||||||
|
direction = directions[int(t / 45)]
|
||||||
|
elif sl.dy == 0:
|
||||||
|
if sl.dx < 0:
|
||||||
|
direction = '←←'
|
||||||
|
else:
|
||||||
|
direction = '→→'
|
||||||
|
else:
|
||||||
|
if sl.dy < 0:
|
||||||
|
direction = '↑↑'
|
||||||
|
else:
|
||||||
|
direction = '↓↓'
|
||||||
|
|
||||||
|
color = COLOR_RESET
|
||||||
|
|
||||||
|
if args.use_mm:
|
||||||
|
sl.dx /= xres
|
||||||
|
sl.dy /= yres
|
||||||
|
if math.hypot(sl.dx, sl.dy) > 7:
|
||||||
|
color = COLOR_RED
|
||||||
|
print("{} {}{:+3.2f}/{:+03.2f}{} | ".format(direction, color, sl.dx, sl.dy, COLOR_RESET), end='')
|
||||||
|
elif args.use_absolute:
|
||||||
|
print("{} {}{:4d}/{:4d}{} | ".format(direction, color, sl.x, sl.y, COLOR_RESET), end='')
|
||||||
|
else:
|
||||||
|
print("{} {}{:4d}/{:4d}{} | ".format(direction, color, sl.dx, sl.dy, COLOR_RESET), end='')
|
||||||
|
s.dx = 0
|
||||||
|
s.dy = 0
|
||||||
|
if sl.state == SlotState.BEGIN:
|
||||||
|
sl.state = SlotState.UPDATE
|
||||||
|
elif sl.state == SlotState.END:
|
||||||
|
sl.state = SlotState.NONE
|
||||||
|
|
||||||
|
sl.dirty = False
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(sys.argv)
|
||||||
72
tools/libinput-analyze.c
Normal file
72
tools/libinput-analyze.c
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "shared.h"
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
usage(void)
|
||||||
|
{
|
||||||
|
printf("Usage: libinput analyze [--help] <feature>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int option_index = 0;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
int c;
|
||||||
|
static struct option opts[] = {
|
||||||
|
{ "help", no_argument, 0, 'h' },
|
||||||
|
{ 0, 0, 0, 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
c = getopt_long(argc, argv, "+h", opts, &option_index);
|
||||||
|
if (c == -1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
switch(c) {
|
||||||
|
case 'h':
|
||||||
|
usage();
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
default:
|
||||||
|
usage();
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optind >= argc) {
|
||||||
|
usage();
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
argc--;
|
||||||
|
argv++;
|
||||||
|
|
||||||
|
return tools_exec_command("libinput-analyze", argc, argv);
|
||||||
|
}
|
||||||
30
tools/libinput-analyze.man
Normal file
30
tools/libinput-analyze.man
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
.TH libinput-analyze "1" "" "libinput @LIBINPUT_VERSION@" "libinput Manual"
|
||||||
|
.SH NAME
|
||||||
|
libinput\-analyze \- analyze device data
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B libinput analyze [\-\-help] \fI<feature> [<args>]\fR
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.PP
|
||||||
|
The
|
||||||
|
.B "libinput analyze"
|
||||||
|
tool analyzes device data. Depending on what is to
|
||||||
|
be analyzed, this tool may not create a libinput context.
|
||||||
|
.PP
|
||||||
|
This is a debugging tool only, its output may change at any time. Do not
|
||||||
|
rely on the output.
|
||||||
|
.PP
|
||||||
|
This tool may need to be run as root to have access to the
|
||||||
|
/dev/input/eventX nodes.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP 8
|
||||||
|
.B \-\-help
|
||||||
|
Print help
|
||||||
|
.SH FEATURES
|
||||||
|
Features that can be analyzed include
|
||||||
|
.TP 8
|
||||||
|
.B libinput\-analyze\-per-slot-delta(1)
|
||||||
|
analyze the delta per event per slot
|
||||||
|
.SH LIBINPUT
|
||||||
|
Part of the
|
||||||
|
.B libinput(1)
|
||||||
|
suite
|
||||||
Loading…
Add table
Reference in a new issue