Skip to content

Instantly share code, notes, and snippets.

@arpruss
Last active February 16, 2024 19:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arpruss/a2a813c5d0020cd2ebbd3f097dcdb750 to your computer and use it in GitHub Desktop.
Save arpruss/a2a813c5d0020cd2ebbd3f097dcdb750 to your computer and use it in GitHub Desktop.
Remap precision touchpad two-finger right click to a left click
#!/usr/bin/env python
import ctypes as cts
import ctypes.wintypes as wts
from ctypes import *
from ctypes.wintypes import *
import sys
import time
import threading
import ctypes_wrappers as cws
ALLOW_DOUBLE_TAP_RIGHT_CLICK = True
CLICK_DETECT_DELAY = 0.05
TWOFINGER_DETECT_DELAY = 0.05
user32 = ctypes.WinDLL('user32', use_last_error = True)
NULL = c_int(0)
HWND_MESSAGE = -3
WM_QUIT = 0x0012
WM_INPUT = 0x00FF
WM_KEYUP = 0x0101
WM_CHAR = 0x0102
HID_USAGE_PAGE_GENERIC = 0x01
RIDEV_NOLEGACY = 0x00000030
RIDEV_INPUTSINK = 0x00000100
RIDEV_CAPTUREMOUSE = 0x00000200
RID_HEADER = 0x10000005
RID_INPUT = 0x10000003
RIM_TYPEMOUSE = 0
RIM_TYPEKEYBOARD = 1
RIM_TYPEHID = 2
PM_NOREMOVE = 0x0000
WM_RBUTTONDOWN = 0x204
WM_RBUTTONUP = 0x205
ULONG_PTR = c_ulong if sizeof(c_void_p) == 4 else c_ulonglong
class MOUSEINPUT(Structure):
_fields_ = [('dx' ,c_long),
('dy',c_long),
('mouseData',DWORD),
('dwFlags',DWORD),
('time',DWORD),
('dwExtraInfo',ULONG_PTR)]
class HARDWAREINPUT(Structure):
_fields_ = [('uMsg' ,DWORD),
('wParamL',WORD),
('wParamH',WORD)]
class KEYBDINPUT(Structure):
_fields_ = [('wVk' ,WORD),
('wScan',WORD),
('dwFlags',DWORD),
('time',DWORD),
('dwExtraInfo',ULONG_PTR)]
class DUMMYUNIONNAME(Union):
_fields_ = [('mi',MOUSEINPUT),
('ki',KEYBDINPUT),
('hi',HARDWAREINPUT)]
class INPUT(Structure):
_anonymous_ = ['u']
_fields_ = [('type',DWORD),
('u',DUMMYUNIONNAME)]
class MSLLHOOKSTRUCT(Structure):
_fields_ = [("x", c_long),
("y", c_long),
('data', c_int32),
('reserved', c_int32),
("flags", DWORD),
("time", c_int),
]
INPUT_MOUSE = 0
MOUSEEVENTF_LEFTDOWN = 0x2
MOUSEEVENTF_LEFTUP = 0x4
LowLevelMouseProc = CFUNCTYPE(c_int, WPARAM, LPARAM, POINTER(MSLLHOOKSTRUCT))
SetWindowsHookEx = user32.SetWindowsHookExA
#SetWindowsHookEx.argtypes = [c_int, LowLevelMouseProc, c_int, c_int]
SetWindowsHookEx.restype = HHOOK
CallNextHookEx = user32.CallNextHookEx
#CallNextHookEx.argtypes = [c_int , c_int, c_int, POINTER(MSLLHOOKSTRUCT)]
CallNextHookEx.restype = c_int
UnhookWindowsHookEx = user32.UnhookWindowsHookEx
UnhookWindowsHookEx.argtypes = [HHOOK]
UnhookWindowsHookEx.restype = BOOL
queue_flag = threading.Event()
num_fingers = 0
last_click = 0
remapped_down = False
last_two_finger_time = 0
def wnd_proc(hwnd, msg, wparam, lparam):
global num_fingers,last_click,last_two_finger_time
if msg == WM_INPUT:
size = wts.UINT(0)
res = cws.GetRawInputData(cast(lparam, cws.PRAWINPUT), RID_INPUT, None, byref(size), sizeof(cws.RAWINPUTHEADER))
if res == wts.UINT(-1) or size == 0:
return 0
buf = create_string_buffer(size.value)
res = cws.GetRawInputData(cast(lparam, cws.PRAWINPUT), RID_INPUT, buf, byref(size), sizeof(cws.RAWINPUTHEADER))
if res != size.value:
return 0
ri = cast(buf, cws.PRAWINPUT).contents
ptr = cast(ri.data.hid.bRawData,POINTER(c_ubyte))
if ri.header.dwType == RIM_TYPEHID and ri.data.hid.dwSizeHid:
#print(' '.join('%02x' % ptr[i] for i in range(ri.data.hid.dwSizeHid)))
if ri.data.hid.dwSizeHid > 29:
num_fingers = ptr[28]
if num_fingers > 1:
last_two_finger_time = time.time()
if ptr[29]:
last_click = time.time()
return cws.DefWindowProc(hwnd, msg, wparam, lparam)
queue = []
active = True
def run_queue():
while active:
for i in queue:
user32.SendInput(1,byref(i),sizeof(INPUT))
queue.clear()
queue_flag.clear()
queue_flag.wait()
def low_level_mouse_handler(nCode, wParam, lParam):
global num_fingers,last_click,remapped_down
struct = lParam.contents
if nCode < 0:
# normally you just for flags & LLMHF_INJECTED, but here we don't, because
# the double clicks we are intercepting are injected by the Windows Precision TouchPad driver.
return CallNextHookEx(NULL, nCode, wParam, lParam)
if wParam == WM_RBUTTONDOWN:
t = time.time()
if (
(num_fingers > 1 or t-last_two_finger_time<TWOFINGER_DETECT_DELAY) and
(ALLOW_DOUBLE_TAP_RIGHT_CLICK or t-last_click<CLICK_DETECT_DELAY) ): #TODO: check whether maybe this is in the right-click area
i = INPUT()
i.type = INPUT_MOUSE
i.mi = MOUSEINPUT(struct.x, struct.y, 0, MOUSEEVENTF_LEFTDOWN, 0, 0)
remapped_down = True
queue.append(i)
queue_flag.set()
return 1
elif wParam == WM_RBUTTONUP and remapped_down:
i = INPUT()
i.type = INPUT_MOUSE
i.mi = MOUSEINPUT(struct.x, struct.y, 0, MOUSEEVENTF_LEFTUP, 0, 0)
remapped_down = False
queue.append(i)
queue_flag.set()
return 1
return CallNextHookEx(NULL, nCode, wParam, lParam)
def register_devices(hwnd=None):
device = cws.RawInputDevice(0x0D, 0x05, RIDEV_INPUTSINK, hwnd)
if cws.RegisterRawInputDevices(pointer(device), 1, sizeof(cws.RawInputDevice)):
return True
else:
print("Registration failed")
return False
wnd_cls = "SO049572093_RawInputWndClass"
wcx = cws.WNDCLASSEX()
wcx.cbSize = sizeof(cws.WNDCLASSEX)
wcx.lpfnWndProc = cws.WNDPROC(wnd_proc)
wcx.hInstance = cws.GetModuleHandle(None)
wcx.lpszClassName = wnd_cls
res = cws.RegisterClassEx(byref(wcx))
if not res:
print("Error registering")
sys.exit(1)
hwnd = cws.CreateWindowEx(0, wnd_cls, None, 0, 0, 0, 0, 0, 0, None, wcx.hInstance, None)
if not hwnd:
print("Error creating hidden window")
sys.exit(1)
if not register_devices(hwnd):
sys.exit(1)
WH_MOUSE_LL = c_int(14)
mouse_callback = LowLevelMouseProc(low_level_mouse_handler)
mouse_hook = SetWindowsHookEx(WH_MOUSE_LL, mouse_callback, NULL, 0)
t = threading.Thread(target=run_queue)
t.start()
msg = wts.MSG()
pmsg = byref(msg)
try:
while res := cws.GetMessage(pmsg, None, 0, 0):
if res < 0:
break
cws.TranslateMessage(pmsg)
cws.DispatchMessage(pmsg)
except:
print("error")
print("exiting...")
UnhookWindowsHookEx(mouse_hook)
queue_flag.set()
active = False
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment