-- SPDX-License-Identifier: MIT local function usage(event_type, code) return event_type * 65536 + code end evdev = { BTN_MIDDLE = usage(1, 274), BTN_TOUCH = usage(1, 330), BTN_TOOL_FINGER = usage(1, 325), BTN_TOOL_QUINTTAP = usage(1, 328), BTN_TOOL_DOUBLETAP = usage(1, 333), BTN_TOOL_TRIPLETAP = usage(1, 334), BTN_TOOL_QUADTAP = usage(1, 335), REL_X = usage(2, 0), REL_Y = usage(2, 1), ABS_MT_SLOT = usage(3, 47), ABS_MT_TOUCH_MAJOR = usage(3, 48), ABS_MT_POSITION_X = usage(3, 53), ABS_MT_POSITION_Y = usage(3, 54), ABS_MT_TOOL_Y = usage(3, 61), ABS_MT_TRACKING_ID = usage(3, 57), } local plugin_callbacks = {} libinput = { register = function(_, versions) assert(versions[1] == 1) return 1 end, connect = function(_, name, callback) plugin_callbacks[name] = callback end, log_debug = function() end, log_error = function() end, } dofile(arg[1]) local function new_device(device_type) local callbacks = {} local device = { callbacks = callbacks, prepended = {}, appended = {}, enabled = {}, } local usages = { [evdev.ABS_MT_SLOT] = true, [evdev.ABS_MT_TRACKING_ID] = true, [evdev.ABS_MT_POSITION_X] = true, [evdev.ABS_MT_POSITION_Y] = true, [evdev.BTN_TOUCH] = true, [evdev.BTN_TOOL_FINGER] = true, [evdev.BTN_TOOL_DOUBLETAP] = true, [evdev.BTN_TOOL_TRIPLETAP] = true, [evdev.BTN_TOOL_QUADTAP] = true, [evdev.BTN_TOOL_QUINTTAP] = true, } if device_type == "mouse" then usages[evdev.REL_X] = true usages[evdev.REL_Y] = true end function device:udev_properties() if device_type == "touchpad" then return { ID_INPUT_TOUCHPAD = true } elseif device_type == "mouse" then return { ID_INPUT_MOUSE = true } end return {} end function device:usages() return usages end function device:absinfos() return { [evdev.ABS_MT_POSITION_X] = { minimum = 0, maximum = 2833, resolution = 24, }, [evdev.ABS_MT_POSITION_Y] = { minimum = 0, maximum = 1723, resolution = 24, }, } end function device:enable_evdev_usage(code) self.enabled[code] = true usages[code] = true end function device:connect(name, callback) callbacks[name] = callback end function device:prepend_frame(frame) table.insert(self.prepended, frame) end function device:append_frame(frame) table.insert(self.appended, frame) end plugin_callbacks["new-evdev-device"](device) return device end local function event(code, value) return { usage = code, value = value } end local function contains(frame, code, value) if not frame then return false end for _, item in ipairs(frame) do if item.usage == code and (value == nil or item.value == value) then return true end end return false end local function feed(device, timestamp, frame) local output = device.callbacks["evdev-frame"](device, frame, timestamp) if output == nil then return frame end return output end local function down(device, timestamp, slot, tracking_id, x, y, count) local frame = { event(evdev.ABS_MT_SLOT, slot), event(evdev.ABS_MT_TRACKING_ID, tracking_id), event(evdev.ABS_MT_POSITION_X, x), event(evdev.ABS_MT_POSITION_Y, y), } if count == 1 then table.insert(frame, event(evdev.BTN_TOUCH, 1)) table.insert(frame, event(evdev.BTN_TOOL_FINGER, 1)) elseif count == 2 then table.insert(frame, event(evdev.BTN_TOOL_FINGER, 0)) table.insert(frame, event(evdev.BTN_TOOL_DOUBLETAP, 1)) elseif count == 3 then table.insert(frame, event(evdev.BTN_TOOL_DOUBLETAP, 0)) table.insert(frame, event(evdev.BTN_TOOL_TRIPLETAP, 1)) end return feed(device, timestamp, frame) end local function up(device, timestamp, slot, count_after) local frame = { event(evdev.ABS_MT_SLOT, slot), event(evdev.ABS_MT_TRACKING_ID, -1), } if count_after == 1 then table.insert(frame, event(evdev.BTN_TOOL_FINGER, 1)) table.insert(frame, event(evdev.BTN_TOOL_DOUBLETAP, 0)) elseif count_after == 0 then table.insert(frame, event(evdev.BTN_TOUCH, 0)) table.insert(frame, event(evdev.BTN_TOOL_FINGER, 0)) end return feed(device, timestamp, frame) end local button_device = new_device("mouse") local function click_count() local count = 0 for _, frame in ipairs(button_device.appended) do if contains(frame, evdev.BTN_MIDDLE, 1) then count = count + 1 end end return count end local tests = {} function tests.hold_tap_emits_middle_and_hides_tapper() local device = new_device("touchpad") down(device, 0, 0, 10, 1000, 1000, 1) local second_down = down(device, 200000, 1, 11, 1400, 1000, 2) assert(not contains(second_down, evdev.ABS_MT_TRACKING_ID, 11)) 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(#device.appended == 0) up(device, 440000, 0, 0) end function tests.hold_doubletap_emits_two_middle_clicks() local device = new_device("touchpad") down(device, 0, 0, 20, 1000, 1000, 1) down(device, 200000, 1, 21, 1400, 1000, 2) up(device, 250000, 1, 1) down(device, 360000, 1, 22, 1400, 1000, 2) up(device, 420000, 1, 1) assert(click_count() == 2) up(device, 600000, 0, 0) end function tests.ordinary_two_finger_tap_passes_through() local device = new_device("touchpad") down(device, 0, 0, 30, 1000, 1000, 1) local second = down(device, 10000, 1, 31, 1400, 1000, 2) assert(contains(second, evdev.ABS_MT_TRACKING_ID, 31)) assert(contains(second, evdev.BTN_TOOL_DOUBLETAP, 1)) up(device, 70000, 1, 1) up(device, 80000, 0, 0) assert(click_count() == 0) end function tests.long_stationary_tap_is_consumed_without_click() local device = new_device("touchpad") down(device, 0, 0, 40, 1000, 1000, 1) 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) up(device, 500000, 0, 0) end function tests.tapper_movement_restores_normal_input() local device = new_device("touchpad") down(device, 0, 0, 50, 1000, 1000, 1) down(device, 200000, 1, 51, 1400, 1000, 2) local moved = feed(device, 230000, { event(evdev.ABS_MT_POSITION_X, 1430), }) assert(#device.prepended == 1) assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 51)) assert(contains(device.prepended[1], evdev.ABS_MT_POSITION_X, 1430)) assert(not contains(moved, evdev.ABS_MT_POSITION_X, 1430)) up(device, 260000, 1, 1) up(device, 400000, 0, 0) assert(click_count() == 0) end function tests.anchor_movement_prevents_recognition() local device = new_device("touchpad") 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)) up(device, 250000, 1, 1) up(device, 400000, 0, 0) assert(click_count() == 0) end function tests.anchor_movement_during_tap_restores_normal_input() local device = new_device("touchpad") down(device, 0, 0, 62, 1000, 1000, 1) down(device, 200000, 1, 63, 1400, 1000, 2) local moved = feed(device, 230000, { 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(contains(moved, evdev.ABS_MT_POSITION_X, 1040)) up(device, 260000, 1, 1) up(device, 400000, 0, 0) assert(click_count() == 0) end function tests.anchor_can_move_pause_and_tap_repeatedly() local device = new_device("touchpad") down(device, 0, 0, 65, 1000, 1000, 1) feed(device, 500000, { event(evdev.ABS_MT_SLOT, 0), event(evdev.ABS_MT_POSITION_X, 1300), event(evdev.ABS_MT_POSITION_Y, 1100), }) down(device, 650000, 1, 66, 1500, 1100, 2) up(device, 700000, 1, 1) assert(click_count() == 1) feed(device, 800000, { event(evdev.ABS_MT_SLOT, 0), event(evdev.ABS_MT_POSITION_X, 1800), event(evdev.ABS_MT_POSITION_Y, 1200), }) down(device, 950000, 1, 67, 2000, 1200, 2) up(device, 1000000, 1, 1) assert(click_count() == 2) up(device, 1100000, 0, 0) end function tests.third_contact_restores_candidate() local device = new_device("touchpad") down(device, 0, 0, 70, 1000, 1000, 1) down(device, 200000, 1, 71, 1400, 1000, 2) local third = down(device, 230000, 2, 72, 1800, 1000, 3) assert(#device.prepended == 1) assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 71)) assert(contains(third, evdev.ABS_MT_TRACKING_ID, 72)) up(device, 260000, 2, 2) up(device, 280000, 1, 1) up(device, 400000, 0, 0) assert(click_count() == 0) end function tests.two_finger_hold_tap_remains_passthrough() local device = new_device("touchpad") down(device, 0, 0, 80, 1000, 1000, 1) down(device, 10000, 1, 81, 1400, 1000, 2) local third = down(device, 300000, 2, 82, 1800, 1000, 3) assert(contains(third, evdev.ABS_MT_TRACKING_ID, 82)) up(device, 360000, 2, 2) up(device, 500000, 1, 1) up(device, 510000, 0, 0) assert(click_count() == 0) assert(#device.prepended == 0) end function tests.non_touchpad_is_ignored() local device = new_device("other") assert(device.callbacks["evdev-frame"] == nil) assert(not device.enabled[evdev.BTN_MIDDLE]) end function tests.pointer_device_is_used_for_button_output() assert(button_device.enabled[evdev.BTN_MIDDLE]) assert(button_device.callbacks["evdev-frame"] == nil) end local names = {} for name in pairs(tests) do table.insert(names, name) end table.sort(names) for _, name in ipairs(names) do button_device.appended = {} tests[name]() print("ok - " .. name) end print(string.format("%d tests passed", #names))