Skip to content

Instantly share code, notes, and snippets.

@Egor-Skriptunoff
Created November 1, 2021 11:57
Show Gist options
  • Save Egor-Skriptunoff/765036ce4a3dc23d94c53267e682e94e to your computer and use it in GitHub Desktop.
Save Egor-Skriptunoff/765036ce4a3dc23d94c53267e682e94e to your computer and use it in GitHub Desktop.
Scroll for Logitech Trackman Marble trackball
--------------------------------------
-- Scroll for Logitech Trackman Marble
--------------------------------------
-- This script is based on the project "MarbleScroll for Logitech Trackman Marble" by Primož.
-- https://www.fewprojects.com/marblescroll-for-logitech-trackman-marble/
-- The original code was rewritten to LuaJIT FFI and slightly modified by Egor Skriptunoff.
-- Logitech Trackman Marble trackball has only 4 buttons: LMB, RMB, Back, Forward.
-- It lacks scroll wheel and middle button.
-- This script adds the following possibilities:
--
-- buttons combination function
-- ------------------- -------------
-- Back alone = Back
-- Forward alone = Forward
-- Back + Forward = Middle Mouse Button
-- Back + Marble turn = Scroll (horizontal, vertical, diagonal)
-- Forward + Marble turn = Fast Scroll (speed x10)
--
-- How to run the script:
-- wluajit.exe marble_scroll.lua
--
-- The script runs in the background, it creates neither console nor GUI window.
--
-- How to install:
-- 1) If you use SetPoint, you need to set "Generic Button" as a task for the buttons 3 and 4:
-- https://www.fewprojects.com/files/marblescroll/setpoint.jpg
-- 2) Get windowless LuaJIT:
-- either download binary files (wluajit.exe + lua51.dll) from this repo:
-- https://github.com/Egor-Skriptunoff/LGS_extension
-- or build LuaJIT from sources using instructions at:
-- https://gist.github.com/Egor-Skriptunoff/22bf55c1abe44d7825605e132e48c084
-- 3) Place the 3 files
-- * wluajit.exe
-- * lua51.dll
-- * marble_scroll.lua
-- in any folder on your hard drive and create a shortcut for
-- wluajit.exe marble_scroll.lua
-- 4) Put the shortcut in Startup folder for auto start at reboot.
---------------------------- SETTINGS -----------------------------
local sens_x = 20 -- mouse move required for one horizontal scroll
local sens_y = 30 -- mouse move required for one vertical scroll
local fast_scroll_factor = 10
-------------------------------------------------------------------
-- Script version: November 1st 2021
local ffi = require'ffi'
local C, AND, HEX, tonumber = ffi.C, bit.band, bit.tohex, tonumber
ffi.cdef[[
// common WinAPI definitions
typedef int BOOL;
typedef uint32_t DWORD;
typedef intptr_t LRESULT;
typedef intptr_t LPARAM;
typedef uintptr_t WPARAM;
typedef intptr_t HANDLE;
typedef HANDLE HINSTANCE;
typedef HANDLE HWND;
typedef HANDLE HHOOK;
// Low Level Mouse Hook
typedef struct {
int x;
int y;
int16_t reserved;
int16_t mouseData_high;
DWORD flags;
DWORD time;
uintptr_t dwExtraInfo;
} MSLLHOOKSTRUCT;
typedef LRESULT (__stdcall * MSHOOKPROC)(
int nCode,
WPARAM wParam,
MSLLHOOKSTRUCT * lParam
);
// Hook Functions
HHOOK SetWindowsHookExA(
int idHook,
MSHOOKPROC lpfn,
HINSTANCE hmod,
DWORD dwThreadId
);
BOOL UnhookWindowsHookEx(HHOOK hhk);
LRESULT CallNextHookEx(
HHOOK hhk,
int nCode,
WPARAM wParam,
void * lParam
);
// Misc Functions
void mouse_event(
DWORD dwFlags,
int dx,
int dy,
DWORD dwData,
uintptr_t dwExtraInfo
);
typedef struct { int x, y; } POINT;
HWND WindowFromPoint(POINT Point);
HWND GetForegroundWindow();
BOOL SetForegroundWindow(HWND hWnd);
HWND GetAncestor(HWND hwnd, DWORD gaFlags);
BOOL GetMessageA(
void * lpMsg,
HWND hWnd,
DWORD wMsgFilterMin,
DWORD wMsgFilterMax
);
]]
local POINT = ffi.typeof"POINT"
local back_is_down, forward_is_down, pure_x_btn, middle_is_down, last_x, last_y, dx, dy
local function LowLevelMouseProc(nCode, wParam, lParam)
if nCode == 0 then -- HC_ACTION
local msg_id = tonumber(wParam)
if msg_id == 0x020B then -- WM_XBUTTONDOWN
if not (back_is_down or forward_is_down) then
pure_x_btn = true
last_x, last_y, dx, dy = lParam.x, lParam.y, 0, 0
-- for scrolling in window under mouse pointer
local pt = POINT(last_x, last_y)
local focus_window = C.WindowFromPoint(pt)
local foreground_window = C.GetForegroundWindow()
-- only focus if window is not already focused
if C.GetAncestor(focus_window, 3) ~= C.GetAncestor(foreground_window, 3) then -- GA_ROOTOWNER
C.SetForegroundWindow(focus_window)
end
end
local x_btn_mask = lParam.mouseData_high
back_is_down = back_is_down or AND(x_btn_mask, 1) ~= 0
forward_is_down = forward_is_down or AND(x_btn_mask, 2) ~= 0
if back_is_down and forward_is_down then
C.mouse_event(0x0020, lParam.x, lParam.y, 0, 0) -- MOUSEEVENTF_MIDDLEDOWN
middle_is_down = true
pure_x_btn = false
end
return 1
elseif msg_id == 0x020C then -- WM_XBUTTONUP
if middle_is_down then
C.mouse_event(0x0040, lParam.x, lParam.y, 0, 0) -- MOUSEEVENTF_MIDDLEUP
middle_is_down = false
end
local x_btn_mask = lParam.mouseData_high
back_is_down = back_is_down and AND(x_btn_mask, 1) == 0
forward_is_down = forward_is_down and AND(x_btn_mask, 2) == 0
if back_is_down or forward_is_down or not pure_x_btn then
return 1
end
elseif msg_id == 0x0200 and (back_is_down or forward_is_down) then -- WM_MOUSEMOVE
local speed_factor = forward_is_down and fast_scroll_factor or 1
dx = dx + (lParam.x - last_x) * speed_factor
dy = dy + (lParam.y - last_y) * speed_factor
last_x, last_y = lParam.x, lParam.y
repeat
local wheel_dist
if dx >= sens_x then
dx = dx - sens_x
wheel_dist = 120
elseif dx <= -sens_x then
dx = dx + sens_x
wheel_dist = -120
end
if wheel_dist then
C.mouse_event(0x1000, 0, 0, wheel_dist, 0) -- MOUSEEVENTF_HWHEEL
pure_x_btn = false
wheel_dist = false
end
if dy >= sens_y then
dy = dy - sens_y
wheel_dist = -120
elseif dy <= -sens_y then
dy = dy + sens_y
wheel_dist = 120
end
if wheel_dist then
C.mouse_event(0x0800, 0, 0, wheel_dist, 0) -- MOUSEEVENTF_WHEEL
pure_x_btn = false
end
until wheel_dist == nil
end
end
return C.CallNextHookEx(0, nCode, wParam, lParam)
end
local ms_hook = C.SetWindowsHookExA(14, LowLevelMouseProc, 0, 0) -- WH_MOUSE_LL
if ms_hook ~= 0 then
local msg = ffi.new("intptr_t[?]", 8) -- this memory block must be large enough to hold the MSG struct
repeat
local res = C.GetMessageA(msg, 0, 0, 0)
until res == 0 or res == -1
C.UnhookWindowsHookEx(ms_hook)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment