Last active
February 16, 2024 19:53
-
-
Save arpruss/a2a813c5d0020cd2ebbd3f097dcdb750 to your computer and use it in GitHub Desktop.
Remap precision touchpad two-finger right click to a left click
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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