Skip to content

Instantly share code, notes, and snippets.

@bielarusajed
Last active June 5, 2026 11:05
Show Gist options
  • Select an option

  • Save bielarusajed/4f6f6c1d38f2c4e60987f0209e33f97e to your computer and use it in GitHub Desktop.

Select an option

Save bielarusajed/4f6f6c1d38f2c4e60987f0209e33f97e to your computer and use it in GitHub Desktop.
libinput plugin for touchpad / trackpad scrolling speed control. Vibe coded.
-- Reduce Magic Trackpad two-finger scroll speed by scaling multitouch motion.
-- The curve is { touchpad-coordinate-units-per-ms, multiplier }.
-- Low speeds stay precise; fast swipes are slowed less.
local SCROLL_SPEED_CURVE = {
{ 0.0, 0.25 },
{ 1.0, 0.25 },
{ 4.0, 0.40 },
{ 10.0, 0.65 },
{ 25.0, 0.90 },
}
local APPLE_VENDOR_ID = 0x004c
local MAGIC_TRACKPAD_BLUETOOTH_PRODUCT_ID = 0x0265
libinput:register({1})
local function round(value)
if value >= 0 then
return math.floor(value + 0.5)
end
return math.ceil(value - 0.5)
end
local function scroll_multiplier(speed)
if speed <= SCROLL_SPEED_CURVE[1][1] then
return SCROLL_SPEED_CURVE[1][2]
end
for i = 1, #SCROLL_SPEED_CURVE - 1 do
local p1 = SCROLL_SPEED_CURVE[i]
local p2 = SCROLL_SPEED_CURVE[i + 1]
if speed >= p1[1] and speed <= p2[1] then
local t = (speed - p1[1]) / (p2[1] - p1[1])
return p1[2] + t * (p2[2] - p1[2])
end
end
return SCROLL_SPEED_CURVE[#SCROLL_SPEED_CURVE][2]
end
local function slot_count(slots)
local count = 0
for _, slot in pairs(slots) do
if slot.active then
count = count + 1
end
end
return count
end
local function reset_virtual_positions(slots)
for _, slot in pairs(slots) do
slot.x_virtual = slot.x
slot.y_virtual = slot.y
slot.x_initialized = false
slot.y_initialized = false
end
end
local function connect_scroll_scaler(device)
local slots = {}
local current_slot = 0
local scaling_active = false
local last_timestamp = 0
device:connect("evdev-frame", function (_, frame, timestamp)
local changed = false
local dt_ms = 16.0
if last_timestamp ~= 0 then
dt_ms = (timestamp - last_timestamp) / 1000.0
if dt_ms < 1.0 then
dt_ms = 1.0
elseif dt_ms > 200.0 then
dt_ms = 200.0
end
end
last_timestamp = timestamp
for _, event in ipairs(frame) do
if event.usage == evdev.ABS_MT_SLOT then
current_slot = event.value
slots[current_slot] = slots[current_slot] or {}
elseif event.usage == evdev.ABS_MT_TRACKING_ID then
local slot = slots[current_slot] or {}
slots[current_slot] = slot
if event.value == -1 then
slot.active = false
slot.x_initialized = false
slot.y_initialized = false
else
slot.active = true
slot.x_initialized = false
slot.y_initialized = false
end
local fingers = slot_count(slots)
local should_scale = fingers == 2
if scaling_active ~= should_scale then
reset_virtual_positions(slots)
scaling_active = should_scale
end
elseif event.usage == evdev.ABS_MT_POSITION_X or event.usage == evdev.ABS_MT_POSITION_Y then
local slot = slots[current_slot] or {}
slots[current_slot] = slot
local axis = event.usage == evdev.ABS_MT_POSITION_X and "x" or "y"
local initialized = axis .. "_initialized"
if slot.active and slot[initialized] and scaling_active then
local physical = slot[axis]
local virtual = slot[axis .. "_virtual"]
local delta = event.value - physical
local speed = math.abs(delta) / dt_ms
local scaled = virtual + delta * scroll_multiplier(speed)
slot[axis] = event.value
slot[axis .. "_virtual"] = scaled
event.value = round(scaled)
changed = true
else
slot[axis] = event.value
slot[axis .. "_virtual"] = event.value
slot[initialized] = true
end
end
end
if changed then
return frame
end
return nil
end)
device:connect("device-removed", function (removed_device)
removed_device:disconnect("evdev-frame")
removed_device:disconnect("device-removed")
end)
end
libinput:connect("new-evdev-device", function (device)
local info = device:info()
if info.vid ~= APPLE_VENDOR_ID or info.pid ~= MAGIC_TRACKPAD_BLUETOOTH_PRODUCT_ID then
return
end
local usages = device:usages()
if usages[evdev.ABS_MT_SLOT]
and usages[evdev.ABS_MT_TRACKING_ID]
and usages[evdev.ABS_MT_POSITION_X]
and usages[evdev.ABS_MT_POSITION_Y] then
connect_scroll_scaler(device)
libinput:log_info("Magic Trackpad scroll speed curve enabled")
end
end)
@bielarusajed

Copy link
Copy Markdown
Author

Disclaimer: I'm not familiar with libinput, its plugin system, or Lua. I've just updated to Gnome 50, and since they added the support for the libinput plugins, I've asked ChatGPT to fix the scrolling speed for me. It gave me this plugin.

Some instructions for Gnome 50+:

  1. Put the file into ~/.config/libinput/plugins/ (create if it doesn't exist)
  2. Check your touchpad/trackpad ID, and replace those in the script if needed.
  3. Set the curve how you like it.
  4. Log out and log back in.

To check the trackpad ID, use libinput list-devices. For me it looks like this:

Device:                  Apple Inc. Magic Trackpad
Kernel:                  /dev/input/event23
Id:                      bluetooth:004c:0265

And bluetooth:004c:0265 becomes this:

local APPLE_VENDOR_ID = 0x004c
local MAGIC_TRACKPAD_BLUETOOTH_PRODUCT_ID = 0x0265

Libinput 1.30.0 or higher is required, as this is the version the plugin system was introduced. I guess if you want to apply the plugin globally, you put the file into /etc/libinput/plugins/.

If I understand correctly from the MR#4739, the ~/.config/libinput/plugins/ directory is enabled by Gnome itself and can take no effect in other Wayland compositors, so check the docs for your environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment