Add per-tool settings and gesture customization

This commit is contained in:
ajp_anton
2026-09-05 05:02:34 +00:00
parent d6f30c5735
commit 38142b9499
26 changed files with 1383 additions and 83 deletions
+70 -35
View File
@@ -1,18 +1,23 @@
-- SPDX-License-Identifier: MIT
--
-- Recognize one touchpad contact that has paused before a quick second-finger
-- tap. Hide the tapping contact from libinput and emit a middle click instead.
-- Recognize one established touchpad contact followed by a quick second-finger
-- tap. Hide the tapping contact from libinput and emit a configured action.
libinput:register({1})
local MINIMUM_STATIONARY_US = 100 * 1000
local MAXIMUM_TAP_US = 150 * 1000
local settings = TOUCHPAD_HOLD_TAP_SETTINGS or {}
local MINIMUM_ANCHOR_AGE_US = (settings.minimum_anchor_age_ms or 100) * 1000
local MINIMUM_STATIONARY_US = (settings.minimum_pause_ms or 100) * 1000
local MINIMUM_TAP_US = (settings.minimum_tap_ms or 10) * 1000
local MAXIMUM_TAP_US = (settings.maximum_tap_ms or 150) * 1000
local MAXIMUM_ANCHOR_MOVEMENT_MM = 1.5
local MAXIMUM_TAPPER_MOVEMENT_MM = 1.0
local OUTPUT_EVENT_NAME = settings.output_event or "BTN_MIDDLE"
local OUTPUT_USAGE = evdev[OUTPUT_EVENT_NAME]
local states = {}
local button_devices = {}
local button_device = nil
local output_devices = {}
local output_device = nil
local tool_usages = {
{ usage = evdev.BTN_TOOL_FINGER, count = 1 },
@@ -131,18 +136,19 @@ local function restore_candidate(device, state, reason)
"touchpad-hold-tap: restored candidate (" .. reason .. ")")
end
local function emit_middle_click()
if not button_device then
local function emit_action()
if not output_device then
libinput:log_error(
"touchpad-hold-tap: no pointer device available for button output")
"touchpad-hold-tap: no device available for " ..
OUTPUT_EVENT_NAME .. " output")
return false
end
button_device:append_frame({
{ usage = evdev.BTN_MIDDLE, value = 1 },
output_device:append_frame({
{ usage = OUTPUT_USAGE, value = 1 },
})
button_device:append_frame({
{ usage = evdev.BTN_MIDDLE, value = 0 },
output_device:append_frame({
{ usage = OUTPUT_USAGE, value = 0 },
})
return true
end
@@ -185,8 +191,14 @@ local function handle_tracking_begin(device, state, tracking_id, timestamp)
state.anchor_slot = contact.slot
elseif state.mode == "anchor" and previous_count == 1 then
local anchor = state.contacts[state.anchor_slot]
if anchor and anchor.rest_x and anchor.rest_y and
timestamp - anchor.stationary_since >= MINIMUM_STATIONARY_US then
local old_enough = anchor and
timestamp - anchor.down_time >=
MINIMUM_ANCHOR_AGE_US
local stationary = MINIMUM_STATIONARY_US == 0 or
(anchor and anchor.rest_x and anchor.rest_y and
timestamp - anchor.stationary_since >=
MINIMUM_STATIONARY_US)
if old_enough and stationary then
contact.hidden = true
state.mode = "candidate"
state.candidate_slot = contact.slot
@@ -211,7 +223,9 @@ local function handle_tracking_end(device, state, timestamp)
if contact.hidden and state.source_slot == state.candidate_slot then
local anchor = state.contacts[state.anchor_slot]
local duration = timestamp - contact.down_time
local accepted = anchor and duration <= MAXIMUM_TAP_US
local accepted = anchor and
duration >= MINIMUM_TAP_US and
duration <= MAXIMUM_TAP_US
state.contacts[state.source_slot] = nil
state.candidate_slot = nil
@@ -224,11 +238,12 @@ local function handle_tracking_end(device, state, timestamp)
end
if accepted then
if emit_middle_click() then
libinput:log_debug("touchpad-hold-tap: middle click")
if emit_action() then
libinput:log_debug(
"touchpad-hold-tap: emitted " .. OUTPUT_EVENT_NAME)
end
else
libinput:log_debug("touchpad-hold-tap: ignored long tap")
libinput:log_debug("touchpad-hold-tap: ignored tap duration")
end
return false, true
end
@@ -243,6 +258,12 @@ local function handle_tracking_end(device, state, timestamp)
if contact_count(state, false) > 0 then
state.mode = "passthrough"
end
elseif state.mode == "passthrough" and state.anchor_slot and
state.contacts[state.anchor_slot] and
contact_count(state, false) == 1 then
-- A rejected second contact must not poison the original anchor.
-- It can become eligible again without first being lifted.
state.mode = "anchor"
end
reset_if_clear(state)
return true, false
@@ -272,7 +293,7 @@ local function handle_mt_axis(device, state, event, timestamp)
remember_axis(contact, event.usage, event.value)
if contact.slot == state.anchor_slot then
if state.mode == "candidate" and
if state.mode == "candidate" and MINIMUM_STATIONARY_US > 0 and
movement_exceeded_from(
state, contact, state.candidate_anchor_x,
state.candidate_anchor_y, MAXIMUM_ANCHOR_MOVEMENT_MM) then
@@ -373,31 +394,45 @@ end
local function device_removed(device)
states[device] = nil
button_devices[device] = nil
if button_device == device then
button_device = nil
for candidate in pairs(button_devices) do
button_device = candidate
output_devices[device] = nil
if output_device == device then
output_device = nil
for candidate in pairs(output_devices) do
output_device = candidate
break
end
end
end
local function can_emit_output(properties, usages)
if OUTPUT_EVENT_NAME:sub(1, 4) == "BTN_" then
return (properties.ID_INPUT_MOUSE or
properties.ID_INPUT_POINTINGSTICK) and
not properties.ID_INPUT_TOUCHPAD and
usages[evdev.REL_X] and usages[evdev.REL_Y]
end
return properties.ID_INPUT_KEYBOARD
end
local function device_new(device)
if not OUTPUT_USAGE then
libinput:log_error(
"touchpad-hold-tap: unknown output event " .. OUTPUT_EVENT_NAME)
return
end
local properties = device:udev_properties()
local usages = device:usages()
-- Clickpads intentionally ignore BTN_MIDDLE from their own event node.
-- Route synthesized buttons through an existing pointer device instead.
if (properties.ID_INPUT_MOUSE or properties.ID_INPUT_POINTINGSTICK) and
not properties.ID_INPUT_TOUCHPAD and
usages[evdev.REL_X] and usages[evdev.REL_Y] then
if not usages[evdev.BTN_MIDDLE] then
device:enable_evdev_usage(evdev.BTN_MIDDLE)
-- Clickpads ignore synthesized buttons on their own event node. Keyboard
-- events likewise belong on a keyboard, so use a suitable existing device.
if can_emit_output(properties, usages) then
if not usages[OUTPUT_USAGE] then
device:enable_evdev_usage(OUTPUT_USAGE)
end
button_devices[device] = true
if not button_device then
button_device = device
output_devices[device] = true
if not output_device then
output_device = device
end
device:connect("device-removed", device_removed)
end
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
set -euo pipefail
plugin=$1
configurator=$2
test_root=$(mktemp -d)
trap 'rm -rf -- "$test_root"' EXIT
install -Dpm 0644 "$plugin" \
"$test_root/usr/lib64/libinput/plugins/90-touchpad-hold-tap.lua"
TOUCHPAD_HOLD_TAP_TEST_ROOT=$test_root bash "$configurator" set \
--minimum-anchor-age-ms 125 \
--minimum-pause-ms 175 \
--minimum-tap-ms 15 \
--maximum-tap-ms 225 \
--output-event KEY_F13
config="$test_root/etc/touchpad-hold-tap.conf"
override="$test_root/etc/libinput/plugins/90-touchpad-hold-tap.lua"
grep -qx 'MINIMUM_PAUSE_MS=175' "$config"
grep -qx 'MINIMUM_ANCHOR_AGE_MS=125' "$config"
grep -qx 'MINIMUM_TAP_MS=15' "$config"
grep -qx 'MAXIMUM_TAP_MS=225' "$config"
grep -qx 'OUTPUT_EVENT=KEY_F13' "$config"
grep -q 'minimum_pause_ms = 175' "$override"
grep -q 'minimum_anchor_age_ms = 125' "$override"
grep -q 'minimum_tap_ms = 15' "$override"
grep -q 'maximum_tap_ms = 225' "$override"
grep -q 'output_event = "KEY_F13"' "$override"
TOUCHPAD_HOLD_TAP_TEST_ROOT=$test_root bash "$configurator" reset
[[ ! -e $config ]]
[[ ! -e $override ]]
printf 'touchpad configurator tests passed\n'
@@ -5,6 +5,8 @@ local function usage(event_type, code)
end
evdev = {
BTN_LEFT = usage(1, 272),
BTN_RIGHT = usage(1, 273),
BTN_MIDDLE = usage(1, 274),
BTN_TOUCH = usage(1, 330),
BTN_TOOL_FINGER = usage(1, 325),
@@ -20,8 +22,19 @@ evdev = {
ABS_MT_POSITION_Y = usage(3, 54),
ABS_MT_TOOL_Y = usage(3, 61),
ABS_MT_TRACKING_ID = usage(3, 57),
KEY_F13 = usage(1, 183),
}
local output_event = arg[2] or "BTN_MIDDLE"
local allow_moving_anchor = arg[3] == "allow-moving-anchor"
local expected_output = evdev[output_event]
if arg[2] or allow_moving_anchor then
TOUCHPAD_HOLD_TAP_SETTINGS = {
output_event = output_event,
minimum_pause_ms = allow_moving_anchor and 0 or nil,
}
end
local plugin_callbacks = {}
libinput = {
register = function(_, versions)
@@ -66,6 +79,8 @@ local function new_device(device_type)
return { ID_INPUT_TOUCHPAD = true }
elseif device_type == "mouse" then
return { ID_INPUT_MOUSE = true }
elseif device_type == "keyboard" then
return { ID_INPUT_KEYBOARD = true }
end
return {}
end
@@ -159,12 +174,13 @@ local function up(device, timestamp, slot, count_after)
return feed(device, timestamp, frame)
end
local button_device = new_device("mouse")
local output_device = new_device(output_event:sub(1, 4) == "KEY_" and
"keyboard" or "mouse")
local function click_count()
local function action_count()
local count = 0
for _, frame in ipairs(button_device.appended) do
if contains(frame, evdev.BTN_MIDDLE, 1) then
for _, frame in ipairs(output_device.appended) do
if contains(frame, expected_output, 1) then
count = count + 1
end
end
@@ -181,7 +197,7 @@ function tests.hold_tap_emits_middle_and_hides_tapper()
assert(not contains(second_down, evdev.BTN_TOOL_DOUBLETAP, 1))
local second_up = up(device, 260000, 1, 1)
assert(not contains(second_up, evdev.ABS_MT_TRACKING_ID, -1))
assert(click_count() == 1)
assert(action_count() == 1)
assert(#device.appended == 0)
up(device, 440000, 0, 0)
end
@@ -193,7 +209,7 @@ function tests.hold_doubletap_emits_two_middle_clicks()
up(device, 250000, 1, 1)
down(device, 360000, 1, 22, 1400, 1000, 2)
up(device, 420000, 1, 1)
assert(click_count() == 2)
assert(action_count() == 2)
up(device, 600000, 0, 0)
end
@@ -205,7 +221,7 @@ function tests.ordinary_two_finger_tap_passes_through()
assert(contains(second, evdev.BTN_TOOL_DOUBLETAP, 1))
up(device, 70000, 1, 1)
up(device, 80000, 0, 0)
assert(click_count() == 0)
assert(action_count() == 0)
end
function tests.long_stationary_tap_is_consumed_without_click()
@@ -214,10 +230,19 @@ function tests.long_stationary_tap_is_consumed_without_click()
down(device, 200000, 1, 41, 1400, 1000, 2)
local second_up = up(device, 360001, 1, 1)
assert(not contains(second_up, evdev.ABS_MT_TRACKING_ID, -1))
assert(click_count() == 0)
assert(action_count() == 0)
up(device, 500000, 0, 0)
end
function tests.implausibly_short_contact_is_consumed_without_click()
local device = new_device("touchpad")
down(device, 0, 0, 45, 1000, 1000, 1)
down(device, 200000, 1, 46, 1400, 1000, 2)
up(device, 201000, 1, 1)
assert(action_count() == 0)
up(device, 400000, 0, 0)
end
function tests.tapper_movement_restores_normal_input()
local device = new_device("touchpad")
down(device, 0, 0, 50, 1000, 1000, 1)
@@ -231,7 +256,7 @@ function tests.tapper_movement_restores_normal_input()
assert(not contains(moved, evdev.ABS_MT_POSITION_X, 1430))
up(device, 260000, 1, 1)
up(device, 400000, 0, 0)
assert(click_count() == 0)
assert(action_count() == 0)
end
function tests.anchor_movement_prevents_recognition()
@@ -239,10 +264,11 @@ function tests.anchor_movement_prevents_recognition()
down(device, 0, 0, 60, 1000, 1000, 1)
feed(device, 150000, { event(evdev.ABS_MT_POSITION_X, 1040) })
local second = down(device, 200000, 1, 61, 1400, 1000, 2)
assert(contains(second, evdev.ABS_MT_TRACKING_ID, 61))
assert(contains(second, evdev.ABS_MT_TRACKING_ID, 61) ~=
allow_moving_anchor)
up(device, 250000, 1, 1)
up(device, 400000, 0, 0)
assert(click_count() == 0)
assert(action_count() == (allow_moving_anchor and 1 or 0))
end
function tests.anchor_movement_during_tap_restores_normal_input()
@@ -253,12 +279,54 @@ function tests.anchor_movement_during_tap_restores_normal_input()
event(evdev.ABS_MT_SLOT, 0),
event(evdev.ABS_MT_POSITION_X, 1040),
})
assert(#device.prepended == 1)
assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 63))
assert(#device.prepended == (allow_moving_anchor and 0 or 1))
if not allow_moving_anchor then
assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 63))
end
assert(contains(moved, evdev.ABS_MT_POSITION_X, 1040))
up(device, 260000, 1, 1)
up(device, 400000, 0, 0)
assert(click_count() == 0)
assert(action_count() == (allow_moving_anchor and 1 or 0))
end
function tests.rejected_tap_does_not_poison_anchor()
local device = new_device("touchpad")
down(device, 0, 0, 64, 1000, 1000, 1)
-- This near-simultaneous contact is passed through as an ordinary
-- two-finger interaction.
local early = down(device, 10000, 1, 65, 1400, 1000, 2)
assert(contains(early, evdev.ABS_MT_TRACKING_ID, 65))
up(device, 60000, 1, 1)
-- The same anchor can recognize a later tap without being lifted first.
local later = down(device, 200000, 1, 66, 1400, 1000, 2)
assert(not contains(later, evdev.ABS_MT_TRACKING_ID, 66))
up(device, 250000, 1, 1)
assert(action_count() == 1)
up(device, 300000, 0, 0)
end
function tests.tap_while_moving_does_not_poison_anchor()
if allow_moving_anchor then
return
end
local device = new_device("touchpad")
down(device, 0, 0, 67, 1000, 1000, 1)
feed(device, 150000, {
event(evdev.ABS_MT_SLOT, 0),
event(evdev.ABS_MT_POSITION_X, 1040),
})
local moving_tap = down(device, 160000, 1, 68, 1400, 1000, 2)
assert(contains(moving_tap, evdev.ABS_MT_TRACKING_ID, 68))
up(device, 210000, 1, 1)
local later = down(device, 320000, 1, 69, 1400, 1000, 2)
assert(not contains(later, evdev.ABS_MT_TRACKING_ID, 69))
up(device, 370000, 1, 1)
assert(action_count() == 1)
up(device, 400000, 0, 0)
end
function tests.anchor_can_move_pause_and_tap_repeatedly()
@@ -272,7 +340,7 @@ function tests.anchor_can_move_pause_and_tap_repeatedly()
})
down(device, 650000, 1, 66, 1500, 1100, 2)
up(device, 700000, 1, 1)
assert(click_count() == 1)
assert(action_count() == 1)
feed(device, 800000, {
event(evdev.ABS_MT_SLOT, 0),
@@ -281,7 +349,7 @@ function tests.anchor_can_move_pause_and_tap_repeatedly()
})
down(device, 950000, 1, 67, 2000, 1200, 2)
up(device, 1000000, 1, 1)
assert(click_count() == 2)
assert(action_count() == 2)
up(device, 1100000, 0, 0)
end
@@ -297,7 +365,7 @@ function tests.third_contact_restores_candidate()
up(device, 260000, 2, 2)
up(device, 280000, 1, 1)
up(device, 400000, 0, 0)
assert(click_count() == 0)
assert(action_count() == 0)
end
function tests.two_finger_hold_tap_remains_passthrough()
@@ -309,7 +377,7 @@ function tests.two_finger_hold_tap_remains_passthrough()
up(device, 360000, 2, 2)
up(device, 500000, 1, 1)
up(device, 510000, 0, 0)
assert(click_count() == 0)
assert(action_count() == 0)
assert(#device.prepended == 0)
end
@@ -320,8 +388,8 @@ function tests.non_touchpad_is_ignored()
end
function tests.pointer_device_is_used_for_button_output()
assert(button_device.enabled[evdev.BTN_MIDDLE])
assert(button_device.callbacks["evdev-frame"] == nil)
assert(output_device.enabled[expected_output])
assert(output_device.callbacks["evdev-frame"] == nil)
end
local names = {}
@@ -330,7 +398,7 @@ for name in pairs(tests) do
end
table.sort(names)
for _, name in ipairs(names) do
button_device.appended = {}
output_device.appended = {}
tests[name]()
print("ok - " .. name)
end
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
set -euo pipefail
root=${TOUCHPAD_HOLD_TAP_TEST_ROOT:-}
config_file="$root/etc/touchpad-hold-tap.conf"
override_file="$root/etc/libinput/plugins/90-touchpad-hold-tap.lua"
template_file="$root/usr/lib64/libinput/plugins/90-touchpad-hold-tap.lua"
if [[ ! -e $template_file ]]; then
template_file="$root/usr/lib/libinput/plugins/90-touchpad-hold-tap.lua"
fi
minimum_anchor_age_ms=100
minimum_pause_ms=100
minimum_tap_ms=10
maximum_tap_ms=150
output_event=BTN_MIDDLE
die()
{
printf 'touchpad-hold-tap-config: %s\n' "$*" >&2
exit 1
}
require_root()
{
if [[ -z $root && $EUID -ne 0 ]]; then
die "this operation must be run as root"
fi
}
validate()
{
[[ $minimum_anchor_age_ms =~ ^[0-9]+$ ]] || die "minimum anchor age must be an integer"
[[ $minimum_pause_ms =~ ^[0-9]+$ ]] || die "minimum pause must be an integer"
[[ $minimum_tap_ms =~ ^[0-9]+$ ]] || die "minimum tap duration must be an integer"
[[ $maximum_tap_ms =~ ^[0-9]+$ ]] || die "maximum tap duration must be an integer"
(( minimum_anchor_age_ms <= 2000 )) || die "minimum anchor age must not exceed 2000 ms"
(( minimum_pause_ms <= 2000 )) || die "minimum pause must not exceed 2000 ms"
(( maximum_tap_ms >= 1 && minimum_tap_ms <= maximum_tap_ms && maximum_tap_ms <= 2000 )) || \
die "tap durations must be between 0 and 2000 ms, with a positive maximum and minimum not exceeding maximum"
[[ $output_event =~ ^(BTN|KEY)_[A-Z0-9_]+$ ]] || \
die "output event must be an evdev BTN_* or KEY_* name"
}
read_config()
{
if [[ ! -r $config_file ]]; then
return 0
fi
while IFS='=' read -r key value; do
case $key in
MINIMUM_ANCHOR_AGE_MS) minimum_anchor_age_ms=$value ;;
MINIMUM_PAUSE_MS) minimum_pause_ms=$value ;;
MINIMUM_TAP_MS) minimum_tap_ms=$value ;;
MAXIMUM_TAP_MS) maximum_tap_ms=$value ;;
OUTPUT_EVENT) output_event=$value ;;
''|'#'*) ;;
*) die "unknown setting: $key" ;;
esac
done < "$config_file"
validate
}
write_config()
{
mkdir -p "$(dirname -- "$config_file")"
local temporary
temporary=$(mktemp "${config_file}.XXXXXX")
trap 'rm -f -- "$temporary"' RETURN
printf 'MINIMUM_ANCHOR_AGE_MS=%s\nMINIMUM_PAUSE_MS=%s\nMINIMUM_TAP_MS=%s\nMAXIMUM_TAP_MS=%s\nOUTPUT_EVENT=%s\n' \
"$minimum_anchor_age_ms" "$minimum_pause_ms" "$minimum_tap_ms" "$maximum_tap_ms" \
"$output_event" > "$temporary"
chmod 0644 "$temporary"
mv -f -- "$temporary" "$config_file"
trap - RETURN
}
write_override()
{
[[ -r $template_file ]] || die "installed plugin was not found"
mkdir -p "$(dirname -- "$override_file")"
local temporary
temporary=$(mktemp "${override_file}.XXXXXX")
trap 'rm -f -- "$temporary"' RETURN
awk -v age="$minimum_anchor_age_ms" -v pause="$minimum_pause_ms" \
-v mintap="$minimum_tap_ms" \
-v maxtap="$maximum_tap_ms" \
-v output="$output_event" '
/^local settings = / {
print "local settings = {"
print " minimum_anchor_age_ms = " age ","
print " minimum_pause_ms = " pause ","
print " minimum_tap_ms = " mintap ","
print " maximum_tap_ms = " maxtap ","
print " output_event = \"" output "\","
print "}"
replaced = 1
next
}
{ print }
END { if (!replaced) exit 1 }
' "$template_file" > "$temporary" || die "installed plugin has an unsupported format"
chmod 0644 "$temporary"
mv -f -- "$temporary" "$override_file"
trap - RETURN
}
usage()
{
cat <<'EOF'
Usage:
touchpad-hold-tap-config show
touchpad-hold-tap-config set [--minimum-anchor-age-ms N] [--minimum-pause-ms N] [--minimum-tap-ms N] [--maximum-tap-ms N] [--output-event NAME]
touchpad-hold-tap-config reset
touchpad-hold-tap-config refresh
EOF
}
command=${1:-}
case $command in
show)
[[ $# -eq 1 ]] || die "show takes no arguments"
read_config
printf 'MINIMUM_ANCHOR_AGE_MS=%s\nMINIMUM_PAUSE_MS=%s\nMINIMUM_TAP_MS=%s\nMAXIMUM_TAP_MS=%s\nOUTPUT_EVENT=%s\n' \
"$minimum_anchor_age_ms" "$minimum_pause_ms" "$minimum_tap_ms" "$maximum_tap_ms" \
"$output_event"
;;
set)
shift
read_config
while [[ $# -gt 0 ]]; do
case $1 in
--minimum-anchor-age-ms)
[[ $# -ge 2 ]] || die "$1 requires a value"
minimum_anchor_age_ms=$2
shift 2
;;
--minimum-pause-ms)
[[ $# -ge 2 ]] || die "$1 requires a value"
minimum_pause_ms=$2
shift 2
;;
--minimum-tap-ms)
[[ $# -ge 2 ]] || die "$1 requires a value"
minimum_tap_ms=$2
shift 2
;;
--maximum-tap-ms)
[[ $# -ge 2 ]] || die "$1 requires a value"
maximum_tap_ms=$2
shift 2
;;
--output-event)
[[ $# -ge 2 ]] || die "$1 requires a value"
output_event=$2
shift 2
;;
*) die "unknown option: $1" ;;
esac
done
require_root
validate
write_config
write_override
;;
reset)
[[ $# -eq 1 ]] || die "reset takes no arguments"
require_root
rm -f -- "$config_file" "$override_file"
;;
refresh)
[[ $# -eq 1 ]] || die "refresh takes no arguments"
require_root
if [[ -e $config_file ]]; then
read_config
write_override
else
rm -f -- "$override_file"
fi
;;
-h|--help)
usage
;;
*)
usage >&2
exit 2
;;
esac
+28 -1
View File
@@ -1,6 +1,6 @@
Name: touchpad-hold-tap
Version: 0.1.0
Release: 6%{?dist}
Release: 9%{?dist}
Summary: Hold-tap middle-click gesture for touchpads
License: MIT
@@ -9,6 +9,8 @@ Source0: 90-touchpad-hold-tap.lua
Source1: LICENSE
Source2: test-touchpad-hold-tap.lua
Source3: README.md
Source4: touchpad-hold-tap-config
Source5: test-touchpad-hold-tap-config
BuildRequires: lua
Requires: libinput >= 1.30
@@ -25,10 +27,15 @@ gestures. Other touchpad input continues through libinput normally.
%check
lua %{SOURCE2} %{SOURCE0}
lua %{SOURCE2} %{SOURCE0} KEY_F13
lua %{SOURCE2} %{SOURCE0} BTN_MIDDLE allow-moving-anchor
bash %{SOURCE5} %{SOURCE0} %{SOURCE4}
%install
install -Dpm 0644 %{SOURCE0} \
%{buildroot}%{_libdir}/libinput/plugins/90-touchpad-hold-tap.lua
install -Dpm 0755 %{SOURCE4} \
%{buildroot}%{_bindir}/touchpad-hold-tap-config
install -Dpm 0644 %{SOURCE1} \
%{buildroot}%{_licensedir}/%{name}/LICENSE
install -Dpm 0644 %{SOURCE3} \
@@ -37,9 +44,29 @@ install -Dpm 0644 %{SOURCE3} \
%files
%license %{_licensedir}/%{name}/LICENSE
%doc %{_docdir}/%{name}/README.md
%{_bindir}/touchpad-hold-tap-config
%{_libdir}/libinput/plugins/90-touchpad-hold-tap.lua
%posttrans
%{_bindir}/touchpad-hold-tap-config refresh || :
%postun
if [ "$1" -eq 0 ]; then
rm -f %{_sysconfdir}/touchpad-hold-tap.conf \
%{_sysconfdir}/libinput/plugins/90-touchpad-hold-tap.lua
fi
%changelog
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-9
- Separate minimum anchor age from the optional stationary period
- Allow a held anchor to recover after rejected taps
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-8
- Add a configurable minimum tap duration
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-7
- Add persistent timing and output-event configuration
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-6
- Advertise the tool to Fedora Tools settings