Add per-tool settings and gesture customization
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user