summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/page-hierarchy.dox1
-rw-r--r--doc/touchpad-jitter.dox90
-rw-r--r--meson.build10
-rw-r--r--src/evdev-mt-touchpad.c10
-rwxr-xr-xtools/libinput-measure-fuzz464
-rw-r--r--tools/libinput-measure-fuzz.man30
-rw-r--r--tools/libinput-measure.man3
7 files changed, 606 insertions, 2 deletions
diff --git a/doc/page-hierarchy.dox b/doc/page-hierarchy.dox
index b54d3adf..d7f31bf1 100644
--- a/doc/page-hierarchy.dox
+++ b/doc/page-hierarchy.dox
@@ -10,6 +10,7 @@
- @subpage t440_support
- @subpage touchpad_jumping_cursor
- @subpage absolute_coordinate_ranges
+- @subpage touchpad_jitter
@page touchscreens Touchscreens
diff --git a/doc/touchpad-jitter.dox b/doc/touchpad-jitter.dox
new file mode 100644
index 00000000..6d65894c
--- /dev/null
+++ b/doc/touchpad-jitter.dox
@@ -0,0 +1,90 @@
+/**
+@page touchpad_jitter Touchpad jitter
+
+Touchpad jitter describes random movement by a few pixels even when the
+user's finger is unmoving.
+
+libinput has a mechanism called a **hysteresis** to avoid that jitter. When
+active, movement with in the **hysteresis margin** is discarded. If the
+movement delta is larger than the margin, the movement is passed on as
+pointer movement. This is a simplified summary, developers should
+read the implementation of the hysteresis in `src/evdev.c`.
+
+libinput uses the kernel `fuzz` value to determine the size of the
+hysteresis. Users should override this with a udev hwdb entry where the
+device itself does not provide the correct value.
+
+@section touchpad_jitter_fuzz_override Overriding the hysteresis margins
+
+libinput provides the debugging tool `libinput measure fuzz` to help edit or
+test a fuzz value. This tool is interactive and provides a udev hwdb entry
+that matches the device. To check if a fuzz is currently present, simply run
+without arguments or with the touchpad's device node:
+
+@verbatim
+$ sudo libinput measure fuzz
+Using Synaptics TM2668-002: /dev/input/event17
+ Checking udev property... not set
+ Checking axes... x=16 y=16
+@endverbatim
+
+In the above output, the axis fuzz is set to 16. To set a specific fuzz, run
+with the `--fuzz=<value>` argument.
+
+@verbatim
+$ sudo libinput measure fuzz --fuzz=8
+@endverbatim
+
+The tool will attempt to construct a hwdb file that matches your touchpad
+device. Follow the printed prompts.
+
+In the ideal case, the tool will provide you with a file that can be
+submitted to the systemd repo for inclusion.
+
+However, hwdb entry creation is difficult to automate and it's likely
+that the tools fails in doing so, especially if an existing entry is already
+present.
+
+Below is the outline of what a user needs to do to override a device's fuzz
+value in case the `libinput measure fuzz` tool fails.
+
+Check with `udevadm info /sys/class/input/eventX` (replace your device node
+number) whether an existing hwdb override exists. If the `EVDEV_ABS_`
+properties are present, the hwdb overried exists. Find the file that
+contains that entry, most likely in `/etc/udev/hwdb.d` or
+`/usr/lib/udev/hwdb.d`.
+
+The content of the property is a set of values in the format
+`EVDEV_ABS_00=min:max:resolution:fuzz`. You need to set the `fuzz` part,
+leaving the remainder of the property as-is. Values may be empty, e.g. a
+property that only sets resolution and fuzz reads as `EVDEV_ABS_00=::32:8`.
+
+If no properties exist, your hwdb.entry should look approximately like this:
+@verbatim
+evdev:name:Synaptics TM2668-002:dmi:*:svnLENOVO*:pvrThinkPadT440s*:
+ EVDEV_ABS_00=:::8
+ EVDEV_ABS_01=:::8
+ EVDEV_ABS_35=:::8
+ EVDEV_ABS_36=:::8
+@endverbatim
+
+Substitute the `name` field with the device name (see the output of
+`libinput measure fuzz` and the DMI match content with your hardware. See
+@ref hwdb_modifying for details.
+
+Once the hwdb entry has been modified, added, or created, @ref
+hwdb_reloading "reload the hwdb". Once reloaded, @ref libinput-record
+"libinput record" should show the new fuzz value for the axes.
+
+Restart the host and libinput should pick up the revised fuzz values.
+
+@section kernel_fuzz Kernel fuzz
+
+A fuzz set on an absolute axis in the kernel causes the kernel to apply
+hysteresis-like behavior to the axis. Unfortunately, this behavior leads to
+inconsistent deltas. To avoid this, libinput sets the kernel fuzz on the
+device to 0 to disable this kernel behavior but remembers what the fuzz was
+on startup. The fuzz is stored in the `LIBINPUT_FUZZ_XX` udev property, on
+startup libinput will check that property as well as the axis itself.
+
+*/
diff --git a/meson.build b/meson.build
index c2cd0a6f..8d36357e 100644
--- a/meson.build
+++ b/meson.build
@@ -322,6 +322,7 @@ if get_option('documentation')
meson.source_root() + '/doc/tools.dox',
meson.source_root() + '/doc/touchpad-jumping-cursors.dox',
meson.source_root() + '/doc/touchpad-pressure.dox',
+ meson.source_root() + '/doc/touchpad-jitter.dox',
meson.source_root() + '/doc/touchpads.dox',
meson.source_root() + '/doc/trackpoints.dox',
meson.source_root() + '/doc/what-is-libinput.dox',
@@ -450,6 +451,15 @@ configure_file(input : 'tools/libinput-measure.man',
install_dir : join_paths(get_option('mandir'), 'man1')
)
+install_data('tools/libinput-measure-fuzz',
+ install_dir : libinput_tool_path)
+configure_file(input : 'tools/libinput-measure-fuzz.man',
+ output : 'libinput-measure-fuzz.1',
+ configuration : man_config,
+ install : true,
+ install_dir : join_paths(get_option('mandir'), 'man1')
+ )
+
install_data('tools/libinput-measure-touchpad-tap',
install_dir : libinput_tool_path)
configure_file(input : 'tools/libinput-measure-touchpad-tap.man',
diff --git a/src/evdev-mt-touchpad.c b/src/evdev-mt-touchpad.c
index ce694c28..47bfe228 100644
--- a/src/evdev-mt-touchpad.c
+++ b/src/evdev-mt-touchpad.c
@@ -192,7 +192,10 @@ tp_detect_wobbling(struct tp_dispatch *tp,
t->hysteresis.x_motion_history |= (1 << 2);
if (t->hysteresis.x_motion_history == r_l_r) {
tp->hysteresis.enabled = true;
- evdev_log_debug(tp->device, "hysteresis enabled\n");
+ evdev_log_debug(tp->device,
+ "hysteresis enabled. "
+ "See %stouchpad_jitter.html for details\n",
+ HTTP_DOC_LINK);
}
}
}
@@ -3127,7 +3130,10 @@ tp_init_hysteresis(struct tp_dispatch *tp)
tp->hysteresis.margin.y = ymargin;
tp->hysteresis.enabled = (ax->fuzz || ay->fuzz);
if (tp->hysteresis.enabled)
- evdev_log_debug(tp->device, "hysteresis enabled\n");
+ evdev_log_debug(tp->device,
+ "hysteresis enabled. "
+ "See %stouchpad_jitter.html for details\n",
+ HTTP_DOC_LINK);
}
static void
diff --git a/tools/libinput-measure-fuzz b/tools/libinput-measure-fuzz
new file mode 100755
index 00000000..23386fe2
--- /dev/null
+++ b/tools/libinput-measure-fuzz
@@ -0,0 +1,464 @@
+#!/usr/bin/env python3
+# 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.
+#
+
+import os
+import sys
+import argparse
+import subprocess
+try:
+ import evdev
+ import evdev.ecodes
+ import pyudev
+except ModuleNotFoundError as e:
+ print('Error: {}'.format(str(e)), file=sys.stderr)
+ print('One or more python modules are missing. Please install those '
+ 'modules and re-run this tool.')
+ sys.exit(1)
+
+
+DEFAULT_HWDB_FILE = '/usr/lib/udev/hwdb.d/60-evdev.hwdb'
+OVERRIDE_HWDB_FILE = '/etc/udev/hwdb.d/99-touchpad-fuzz-override.hwdb'
+
+
+class tcolors:
+ GREEN = '\033[92m'
+ RED = '\033[91m'
+ BOLD = '\033[1m'
+ NORMAL = '\033[0m'
+
+
+def print_bold(msg, **kwargs):
+ print(tcolors.BOLD + msg + tcolors.NORMAL, **kwargs)
+
+
+def print_green(msg, **kwargs):
+ print(tcolors.BOLD + tcolors.GREEN + msg + tcolors.NORMAL, **kwargs)
+
+
+def print_red(msg, **kwargs):
+ print(tcolors.BOLD + tcolors.RED + msg + tcolors.NORMAL, **kwargs)
+
+
+class InvalidConfigurationError(Exception):
+ pass
+
+
+class InvalidDeviceError(Exception):
+ pass
+
+
+class Device(object):
+ def __init__(self, path):
+ if path is None:
+ self.path = self.find_touch_device()
+ else:
+ self.path = path
+
+ self.device = evdev.InputDevice(self.path)
+ self.name = self.device.name
+ context = pyudev.Context()
+ self.udev_device = pyudev.Devices.from_device_file(context, self.path)
+
+ def find_touch_device(self):
+ context = pyudev.Context()
+ for device in context.list_devices(subsystem='input'):
+ if not device.get('ID_INPUT_TOUCHPAD', 0):
+ continue
+
+ if not device.device_node or \
+ not device.device_node.startswith('/dev/input/event'):
+ continue
+
+ return device.device_node
+
+ print('Unable to find a touch device.', file=sys.stderr)
+ sys.exit(1)
+
+ def check_property(self):
+ '''Return a tuple of (xfuzz, yfuzz) with the fuzz as set in the libinput
+ property. Returns None if the property doesn't exist'''
+
+ axes = {
+ 0x00: self.udev_device.get('LIBINPUT_FUZZ_00'),
+ 0x01: self.udev_device.get('LIBINPUT_FUZZ_01'),
+ 0x35: self.udev_device.get('LIBINPUT_FUZZ_35'),
+ 0x36: self.udev_device.get('LIBINPUT_FUZZ_36'),
+ }
+
+ if axes[0x35] is not None:
+ if axes[0x35] != axes[0x00]:
+ raise InvalidConfigurationError('fuzz for ABS_X differs from ABS_MT_POSITION_X')
+
+ if axes[0x36] is not None:
+ if axes[0x36] != axes[0x01]:
+ raise InvalidConfigurationError('fuzz for ABS_Y differs from ABS_MT_POSITION_Y')
+
+ xfuzz = axes[0x35] or axes[0x00]
+ yfuzz = axes[0x36] or axes[0x01]
+
+ if xfuzz is None and yfuzz is None:
+ return None
+
+ if ((xfuzz is not None and yfuzz is None) or
+ (xfuzz is None and yfuzz is not None)):
+ raise InvalidConfigurationError('fuzz should be set for both axes')
+
+ return (xfuzz, yfuzz)
+
+ def check_axis(self):
+ '''
+ Returns a duple of (xfuzz, yfuzz) with the fuzz as set on the device
+ axis. Returns None if no fuzz is set.
+ '''
+ # capabilities returns a dict with the EV_* codes as key,
+ # each of which is a list of tuples of (code, AbsInfo)
+ #
+ # Get the abs list first (or empty list if missing),
+ # then extract the touch major absinfo from that
+ caps = self.device.capabilities(absinfo=True).get(
+ evdev.ecodes.EV_ABS, []
+ )
+ codes = [cap[0] for cap in caps]
+
+ if evdev.ecodes.ABS_X not in codes or evdev.ecodes.ABS_Y not in codes:
+ raise InvalidDeviceError('device does not have x/y axes')
+
+ if (evdev.ecodes.ABS_MT_POSITION_X in codes) != (evdev.ecodes.ABS_MT_POSITION_Y in codes):
+ raise InvalidDeviceError('device does not have both multitouch axes')
+
+ axes = {
+ 0x00: None,
+ 0x01: None,
+ 0x35: None,
+ 0x36: None,
+ }
+
+ for c in caps:
+ if c[0] in axes.keys():
+ axes[c[0]] = c[1].fuzz
+
+ xfuzz = axes[0x35] or axes[0x00]
+ yfuzz = axes[0x36] or axes[0x01]
+
+ if xfuzz is None and yfuzz is None:
+ return None
+
+ return (xfuzz, yfuzz)
+
+
+def print_fuzz(what, fuzz):
+ print(' Checking {}... '.format(what), end='')
+ if fuzz is None:
+ print('not set')
+ elif fuzz == (0, 0):
+ print('is zero')
+ else:
+ print('x={} y={}'.format(*fuzz))
+
+
+def handle_existing_entry(device, fuzz):
+ # This is getting messy because we don't really know where the entry
+ # could be or how the match rule looks like. So we just check the
+ # default location only.
+ # For the match comparison, we search for the property value in the
+ # file. If there is more than one entry that uses the same
+ # overrides this will generate false positives.
+ # If the lines aren't in the same order in the file, it'll be a false
+ # negative.
+ overrides = {
+ 0x00: device.udev_device.get('EVDEV_ABS_00'),
+ 0x01: device.udev_device.get('EVDEV_ABS_01'),
+ 0x35: device.udev_device.get('EVDEV_ABS_35'),
+ 0x36: device.udev_device.get('EVDEV_ABS_36'),
+ }
+
+ has_existing_rules = False
+ for key, value in overrides.items():
+ if value is not None:
+ has_existing_rules = True
+ break
+ if not has_existing_rules:
+ return False
+
+ print_red('Error! ', end='')
+ print('This device already has axis overrides defined')
+ print('')
+ print_bold('Searching for existing override...')
+
+ # Construct a template that looks like a hwdb entry (values only) from
+ # the udev property values
+ template = [' EVDEV_ABS_00={}'.format(overrides[0x00]),
+ ' EVDEV_ABS_01={}'.format(overrides[0x01])]
+ if overrides[0x35] is not None:
+ template += [' EVDEV_ABS_35={}'.format(overrides[0x35]),
+ ' EVDEV_ABS_36={}'.format(overrides[0x36])]
+
+ found = False
+ print('Checking in {}... '.format(OVERRIDE_HWDB_FILE), end='')
+ entry, prefix, lineno = check_file_for_lines(OVERRIDE_HWDB_FILE, template)
+ if entry is not None:
+ print_green('found')
+ print('The existing hwdb entry can be overwritten');
+ return False
+ else:
+ print_red('not found')
+ print('Checking in {}... '.format(DEFAULT_HWDB_FILE, template), end='')
+ entry, prefix, lineno = check_file_for_lines(DEFAULT_HWDB_FILE, template)
+ if entry is not None:
+ print_green('found')
+ else:
+ print_red('not found')
+ print('The device has a hwdb override defined but it\'s not where I expected it to be.');
+ print('Please look at the libinput documentation for more details.')
+ print('Exiting now.')
+ return True
+
+ print_bold('Probable entry for this device found in line {}:'.format(lineno))
+ print('\n'.join(prefix + entry))
+ print('')
+
+ print_bold('Suggested new entry for this device:')
+ new_entry = []
+ for i in range(0, len(template)):
+ parts = entry[i].split(':')
+ while len(parts) < 4:
+ parts.append('')
+ parts[3] = str(fuzz)
+ new_entry.append(':'.join(parts))
+ print('\n'.join(prefix + new_entry))
+ print('')
+
+ # Not going to overwrite the 60-evdev.hwdb entry with this program, too
+ # risky. And it may not be our device match anyway.
+ print_bold('You must now:')
+ print('\n'.join((
+ '1. Check the above suggestion for sanity. Does it match your device?',
+ '2. Open {} and amend the existing entry'.format(DEFAULT_HWDB_FILE),
+ ' as recommended above',
+ '',
+ ' The property format is:',
+ ' EVDEV_ABS_00=min:max:resolution:fuzz',
+ '',
+ ' Leave the entry as-is and only add or amend the fuzz value.',
+ ' A non-existent value can be skipped, e.g. this entry sets the ',
+ ' resolution to 32 and the fuzz to 8',
+ ' EVDEV_ABS_00=::32:8',
+ '',
+ '3. Save the edited file',
+ '4. Say Y to the next prompt')))
+
+ cont = input('Continue? [Y/n] ')
+ if cont == 'n':
+ raise KeyboardInterrupt
+
+ if test_hwdb_entry(device, fuzz):
+ print_bold('Please test the new fuzz setting by restarting libinput')
+ print_bold('Then submit a pull request for this hwdb entry change to '
+ 'to systemd at http://github.com/systemd/systemd')
+ else:
+ print_bold('The new fuzz setting did not take effect.')
+ print_bold('Did you edit the correct file?')
+ print('Please look at the libinput documentation for more details.')
+ print('Exiting now.')
+
+ return True
+
+
+def reload_and_trigger_udev(device):
+ import time
+
+ print('Running udevadm hwdb --update')
+ subprocess.run(['udevadm', 'hwdb', '--update'], check=True)
+ syspath = device.path.replace('/dev/input/', '/sys/class/input/')
+ time.sleep(1)
+ print('Running udevadm trigger {}'.format(syspath))
+ subprocess.run(['udevadm', 'trigger', syspath], check=True)
+ time.sleep(1)
+
+
+def test_hwdb_entry(device, fuzz):
+ reload_and_trigger_udev(device)
+ print_bold('Testing... ', end='')
+
+ d = Device(device.path)
+ f = d.check_axis()
+ if fuzz == f[0] and fuzz == f[1]:
+ print_green('Success')
+ return True
+ else:
+ print_red('Error')
+ return False
+
+
+def check_file_for_lines(path, template):
+ '''
+ Checks file at path for the lines given in template. If found, the
+ return value is a tuple of the matching lines and the prefix (i.e. the
+ two lines before the matching lines)
+ '''
+ try:
+ lines = [l[:-1] for l in open(path).readlines()]
+ idx = -1
+ try:
+ while idx < len(lines) - 1:
+ idx += 1
+ line = lines[idx]
+ if not line.startswith(' EVDEV_ABS_00'):
+ continue
+ if lines[idx:idx + len(template)] != template:
+ continue
+
+ return (lines[idx:idx + len(template)], lines[idx - 2:idx], idx)
+
+ except IndexError:
+ pass
+ except FileNotFoundError:
+ pass
+
+ return (None, None, None)
+
+
+def write_udev_rule(device, fuzz):
+ '''Write out a udev rule that may match the device, run udevadm trigger and
+ check if the udev rule worked. Of course, there's plenty to go wrong...
+ '''
+ print('')
+ print_bold('Guessing a udev rule to overwrite the fuzz')
+
+ # Some devices match better on pvr, others on pn, so we get to try both. yay
+ modalias = open('/sys/class/dmi/id/modalias').readlines()[0]
+ ms = modalias.split(':')
+ svn, pn, pvr = None, None, None
+ for m in ms:
+ if m.startswith('svn'):
+ svn = m
+ elif m.startswith('pn'):
+ pn = m
+ elif m.startswith('pvr'):
+ pvr = m
+
+ # Let's print out both to inform and/or confuse the user
+ template = '\n'.join(('# {} {}',
+ 'evdev:name:{}:dmi:*:{}*:{}*:',
+ ' EVDEV_ABS_00=:::{}',
+ ' EVDEV_ABS_01=:::{}',
+ ' EVDEV_ABS_35=:::{}',
+ ' EVDEV_ABS_36=:::{}',
+ ''))
+ rule1 = template.format(svn[3:], device.name, device.name, svn, pvr, fuzz, fuzz, fuzz, fuzz)
+ rule2 = template.format(svn[3:], device.name, device.name, svn, pn, fuzz, fuzz, fuzz, fuzz)
+
+ print('Full modalias is: {}'.format(modalias))
+ print()
+ print_bold('Suggested udev rule, option 1:')
+ print(rule1)
+ print()
+ print_bold('Suggested udev rule, option 2:')
+ print(rule2)
+ print('')
+
+ # The weird hwdb matching behavior means we match on the least specific
+ # rule (i.e. most wildcards) first although that was supposed to be fixed in
+ # systemd 3a04b789c6f1.
+ # Our rule uses dmi strings and will be more specific than what 60-evdev.hwdb
+ # already has. So we basically throw up our hands because we can't do anything
+ # then.
+ if handle_existing_entry(device, fuzz):
+ return
+
+ while True:
+ print_bold('Wich rule do you want to to test? 1 or 2? ', end='')
+ yesno = input('Ctrl+C to exit ')
+
+ if yesno == '1':
+ rule = rule1
+ break
+ elif yesno == '2':
+ rule = rule2
+ break
+
+ fname = OVERRIDE_HWDB_FILE
+ try:
+ fd = open(fname, 'x')
+ except FileExistsError as e:
+ yesno = input('File {} exists, overwrite? [Y/n] '.format(fname))
+ if yesno.lower == 'n':
+ return
+
+ fd = open(fname, 'w')
+
+ fd.write('# File generated by libinput measure fuzz\n\n')
+ fd.write(rule)
+ fd.close()
+
+ if test_hwdb_entry(device, fuzz):
+ print('Your hwdb override file is in {}'.format(fname))
+ print_bold('Please test the new fuzz setting by restarting libinput')
+ print_bold('Then submit a pull request for this hwdb entry to '
+ 'systemd at http://github.com/systemd/systemd')
+ else:
+ print('The hwdb entry failed to apply to the device.')
+ print('Removing hwdb file again.')
+ os.remove(fname)
+ reload_and_trigger_udev(device)
+ print_bold('What now?')
+ print('1. Re-run this program and try the other suggested udev rule. If that fails,')
+ print('2. File a bug with the suggested udev rule at http://github.com/systemd/systemd')
+
+
+def main(args):
+ parser = argparse.ArgumentParser(
+ description='Print fuzz settings'
+ )
+ parser.add_argument('path', metavar='/dev/input/event0',
+ nargs='?', type=str, help='Path to device (optional)')
+ parser.add_argument('--fuzz', type=int, help='Suggsted fuzz')
+ args = parser.parse_args()
+
+ try:
+ device = Device(args.path)
+ print_bold('Using {}: {}'.format(device.name, device.path))
+
+ fuzz = device.check_property()
+ print_fuzz('udev property', fuzz)
+
+ fuzz = device.check_axis()
+ print_fuzz('axes', fuzz)
+
+ userfuzz = args.fuzz
+ if userfuzz is not None:
+ write_udev_rule(device, userfuzz)
+
+ except PermissionError as e:
+ print('Permission denied, please re-run as root')
+ except InvalidConfigurationError as e:
+ print('Error: {}'.format(e.message))
+ except KeyboardInterrupt as e:
+ print('Exited on user request')
+
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/tools/libinput-measure-fuzz.man b/tools/libinput-measure-fuzz.man
new file mode 100644
index 00000000..74b319cc
--- /dev/null
+++ b/tools/libinput-measure-fuzz.man
@@ -0,0 +1,30 @@
+.TH libinput-measure-fuzz "1"
+.SH NAME
+libinput\-measure\-fuzz \- measure absolute axis fuzz
+.SH SYNOPSIS
+.B libinput measure fuzz [\-\-help] [options]
+[\fI/dev/input/event0\fI]
+.SH DESCRIPTION
+.PP
+The
+.B "libinput measure fuzz"
+tool measures the fuzz for an absolute axis on a kernel device. The current
+implementation does not actually measure anything, it only prints the
+relevant information available and suggests a udev rule.
+.PP
+This is a debugging tool only, its output may change at any time. Do not
+rely on the output.
+.PP
+This tool usually needs to be run as root to have access to the
+/dev/input/eventX nodes.
+.SH OPTIONS
+If a device node is given, this tool opens that device node. Otherwise, this
+tool searches for the first node that looks like a touchpad device and
+uses that node.
+.TP 8
+.B \-\-help
+Print help
+.SH LIBINPUT
+Part of the
+.B libinput(1)
+suite
diff --git a/tools/libinput-measure.man b/tools/libinput-measure.man
index 9e52e5e1..09267a97 100644
--- a/tools/libinput-measure.man
+++ b/tools/libinput-measure.man
@@ -22,6 +22,9 @@ Print help
.SH FEATURES
Features that can be measured include
.TP 8
+.B libinput\-measure\-fuzz(1)
+Measure touch fuzz to avoid pointer jitter
+.TP 8
.B libinput\-measure\-touch\-size(1)
Measure touch size and orientation
.TP 8