diff --git a/.github/scripts/update-readme.py b/.github/scripts/update-readme.py new file mode 100755 index 0000000..f57402c --- /dev/null +++ b/.github/scripts/update-readme.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import copy +import json +import re + +README_FILES = [ + "README.md", +] + +VARIABLE_JSON_FILES = [ + "src/libs/Karabiner-DriverKit-VirtualHIDDevice/version.json", + "src/libs/Karabiner-DriverKit-VirtualHIDDevice/elements-version.json", +] + +REPLACEMENTS = { + "package_version": r"\d+\.\d+\.\d+", + "elements_version": r"\d+\.\d+\.\d+", +} + +TAG_REGEX_FORMAT = r"()(`[^`]+`|\[[^]]+\]\([^)]+\))" + +variables = {} +for var_file in VARIABLE_JSON_FILES: + with open(var_file) as f: + variables.update(json.load(f)) + +for readme_file in README_FILES: + with open(readme_file) as f: + content = f.read() + original_content = copy.copy(content) + + for var_name, var_pattern in REPLACEMENTS.items(): + pattern = TAG_REGEX_FORMAT.format(name=re.escape(var_name)) + def replacer(m: re.Match) -> str: + tag = m[1] + tag_content = m[2] + try: + tag_content = re.sub(var_pattern, variables[var_name], tag_content) + except KeyError as e: + raise ValueError(f"REPLACEMENTS variable {var_name} does not appear in any of the JSON files: {VARIABLE_JSON_FILES!r}") from e + return tag + tag_content + + content = re.sub(pattern, replacer, content) + + if content != original_content: + with open(readme_file, "w") as f: + f.write(content) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..7c3ba50 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,42 @@ +name: Push + +on: + push: + branches: + - main + - ci_test + paths: + - 'README.md' + - 'src/libs/Karabiner-DriverKit-VirtualHIDDevice/version.json' + - 'src/libs/Karabiner-DriverKit-VirtualHIDDevice/elements-version.json' + +jobs: + update_readme: + runs-on: ubuntu-slim + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Update README + id: update_readme + run: | + ./.github/scripts/update-readme.py + if git diff --quiet; then echo worktree=UNCHANGED; else echo worktree=CHANGED; fi >> "$GITHUB_OUTPUT" + + - name: Create PR if README is changed + if: steps.update_readme.outputs.worktree == 'CHANGED' + # email and name via https://api.github.com/users/github-actions%5Bbot%5D + # See https://github.com/actions/checkout?tab=readme-ov-file#push-a-commit-using-the-built-in-token + run: | + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + # Comment out `git switch` and `gh pr create` to push a commit + # directly to the branch that triggered this job (usually main) + git switch --create "$GITHUB_JOB/$GITHUB_RUN_ID" + git commit --all --message "Update variables in README" + git push --set-upstream origin + gh pr create --fill + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CMakeLists.txt b/CMakeLists.txt index e0b3cd5..3eb5d41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,8 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "Windows") set(SOURCES_SERVER ${SOURCES_SERVER} src/server/windows/Devices.cpp src/server/windows/Devices.h + src/server/windows/HookThread.cpp + src/server/windows/HookThread.h src/server/windows/main.cpp src/server/windows/interception.h ) diff --git a/README.md b/README.md index 2f5782c..5c6b746 100644 --- a/README.md +++ b/README.md @@ -577,7 +577,13 @@ The values for the `device-id` context filters are obtained by looking for symli ### MacOS The MacOS build depends on [Karabiner-Element's](https://karabiner-elements.pqrs.org) virtual device driver. -One can install it either directly from [Karabiner-DriverKit-VirtualHIDDevice](https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice/releases), or along with [Karabiner Elements](https://github.com/pqrs-org/Karabiner-Elements/releases). +One can install it either directly from [Karabiner-DriverKit-VirtualHIDDevice](https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice/releases#release-v99.99.99), or along with [Karabiner Elements](https://github.com/pqrs-org/Karabiner-Elements/releases#release-v99.99.99). + +The maximum supported versions of Karabiner-Element currently are: +| Package | Version | +|--------------------------------------|---------------------------------------------| +| Karabiner-DriverKit-VirtualHIDDevice | `99.99.99` | +| Karabiner-Elements | `99.99.99` | A [Homebrew](https://brew.sh) formula is provided for building and installing keymapper: ```ini diff --git a/keymapper.conf b/keymapper.conf index d583018..26d1249 100644 --- a/keymapper.conf +++ b/keymapper.conf @@ -114,8 +114,10 @@ T >> G Y >> J U >> L I >> U -O >> Z -Z >> Y +Shift{O} >> 'Y' +O >> 'y' +Shift{Z} >> 'Z' +Z >> 'z' Shift{P} >> ':' P >> ';' S >> R @@ -136,11 +138,11 @@ Shift{BracketLeft} >> '{' BracketLeft >> '[' Shift{BracketRight} >> '}' BracketRight >> ']' -Shift{Backquote} >> Shift{Equal} Space # dead key '`' +Shift{Backquote} >> '`' Backquote >> '~' Shift{2} >> '@' Shift{3} >> '#' -Shift{6} >> Backquote Space # dead key '^' +Shift{6} >> '^' Shift{7} >> '&' Shift{8} >> '*' Shift{9} >> '(' @@ -171,8 +173,8 @@ prev_tab >> Control{PageDown} edit_copy >> Control{C} edit_cut >> Control{X} edit_paste >> Control{V} -edit_undo >> Control{Y} -edit_redo >> Control{Z} +edit_undo >> Control{'z'} +edit_redo >> (Control Shift){'z'} find >> Control{F} build >> Control{B} debug_step_over >> F10 @@ -191,10 +193,16 @@ edit_rename >> F2 edit_cut >> Command{X} edit_copy >> Command{C} edit_paste >> Command{V} - edit_undo >> Command{Y} - edit_redo >> (Command Shift){Y} + edit_undo >> Command{'z'} + edit_redo >> (Command Shift){'z'} find >> Command{F} +[system="Linux", title = "VncViewer"] + edit_paste >> $(keymapperctl --type "$(xclip -o -selection clipboard)") + +[system="Linux", title = "Terminator"] + edit_paste >> Shift{Insert} + [system="Linux"] lower_window >> (Alt Shift){PageDown} @@ -230,25 +238,24 @@ edit_rename >> F2 [title="Qt Creator"] open_file >> Control{K} go_to_definition >> F2 - edit_redo >> (Control Shift){Y} edit_rename >> (Control Shift){R} -[title=/Code - OSS|Visual Studio Code/] - go_to_definition >> F12 - navigate_back >> (Control Alt){Slash} - navigate_fore >> (Control Shift){Slash} - build >> (Control Shift){B} - [system="MacOS" class="Code"] go_to_definition >> F12 - navigate_back >> (Control){Slash} - navigate_fore >> (Control Shift){Slash} + navigate_back >> (Control){'-'} + navigate_fore >> (Control Shift){'-'} build >> (Control Shift){B} [title="Visual Studio"] go_to_definition >> F12 navigate_back >> Control{Slash} - navigate_fore >> (Control Shift){Slash} + navigate_fore >> (Control Shift){'-'} + build >> (Control Shift){B} + +[title=/Code - OSS|Visual Studio Code/] + go_to_definition >> F12 + navigate_back >> (Control Alt){'-'} + navigate_fore >> (Control Shift){'-'} build >> (Control Shift){B} [title="Geany"] diff --git a/src/client/windows/main.cpp b/src/client/windows/main.cpp index 0493126..2fcb4fe 100644 --- a/src/client/windows/main.cpp +++ b/src/client/windows/main.cpp @@ -143,7 +143,7 @@ namespace { socket && WSAAsyncSelect(*socket, g_window, WM_APP_CONTROL_MESSAGE, (FD_READ | FD_CLOSE)) == 0) return true; - error("Accepting keymapperctl connection failed"); + verbose("Accepting keymapperctl connection failed"); return false; } diff --git a/src/libs/Karabiner-DriverKit-VirtualHIDDevice/elements-version.json b/src/libs/Karabiner-DriverKit-VirtualHIDDevice/elements-version.json new file mode 100644 index 0000000..76b456e --- /dev/null +++ b/src/libs/Karabiner-DriverKit-VirtualHIDDevice/elements-version.json @@ -0,0 +1,3 @@ +{ + "elements_version": "15.9.0" +} diff --git a/src/server/ServerState.cpp b/src/server/ServerState.cpp index 52b8b32..267b325 100644 --- a/src/server/ServerState.cpp +++ b/src/server/ServerState.cpp @@ -304,13 +304,6 @@ bool is_control_up(const KeyEvent& event) { event.key == Key::ControlRight)); } -bool ServerState::send_buffer_has_mouse_events() const { - return std::any_of(m_send_buffer.begin(), m_send_buffer.end(), - [](const KeyEvent& event) { - return is_mouse_button(event.key) || is_mouse_wheel(event.key); - }); -} - bool ServerState::flush_send_buffer() { if (m_sending_key) return true; diff --git a/src/server/ServerState.h b/src/server/ServerState.h index 5259e84..0412c22 100644 --- a/src/server/ServerState.h +++ b/src/server/ServerState.h @@ -20,7 +20,6 @@ class ServerState : public ClientPort::MessageHandler { void set_device_descs(std::vector device_descs); bool should_exit() const; bool translate_input(KeyEvent input, int device_index); - bool send_buffer_has_mouse_events() const; bool flush_send_buffer(); bool sending_key() const { return m_sending_key; } const std::vector& stages() const { return m_stage->stages(); } diff --git a/src/server/windows/HookThread.cpp b/src/server/windows/HookThread.cpp new file mode 100644 index 0000000..88548ee --- /dev/null +++ b/src/server/windows/HookThread.cpp @@ -0,0 +1,141 @@ + +#include "HookThread.h" +#include "common/output.h" +#include +#include +#include +#include + +namespace { + constexpr UINT WM_APP_CONFIGURE_HOOKS = WM_APP + 0; + + KeyboardHookCallback g_keyboard_hook_callback; + MouseHookCallback g_mouse_hook_callback; + HHOOK g_keyboard_hook; + HHOOK g_mouse_hook; + std::mutex g_hook_thread_mutex; + std::thread g_hook_thread; + DWORD g_hook_thread_id; + + struct HookConfig { + HINSTANCE instance; + KeyboardHookCallback keyboard_callback; + MouseHookCallback mouse_callback; + std::promise completed; + }; + + LRESULT CALLBACK keyboard_hook_proc(int code, WPARAM wparam, LPARAM lparam) { + if (code == HC_ACTION) { + const auto& kbd = *reinterpret_cast(lparam); + if (g_keyboard_hook_callback && g_keyboard_hook_callback(wparam, kbd)) + return 1; + } + return CallNextHookEx(g_keyboard_hook, code, wparam, lparam); + } + + LRESULT CALLBACK mouse_hook_proc(int code, WPARAM wparam, LPARAM lparam) { + if (code == HC_ACTION) { + const auto& ms = *reinterpret_cast(lparam); + if (g_mouse_hook_callback && g_mouse_hook_callback(wparam, ms)) + return 1; + } + return CallNextHookEx(g_mouse_hook, code, wparam, lparam); + } + + void unhook_devices_on_hook_thread() { + if (g_keyboard_hook) + UnhookWindowsHookEx(g_keyboard_hook); + g_keyboard_hook = nullptr; + g_keyboard_hook_callback = nullptr; + + if (g_mouse_hook) + UnhookWindowsHookEx(g_mouse_hook); + g_mouse_hook = nullptr; + g_mouse_hook_callback = nullptr; + } + + void hook_devices_on_hook_thread(const HookConfig& config) { + const auto keyboard_was_hooked = (g_keyboard_hook_callback != nullptr); + const auto mouse_was_hooked = (g_mouse_hook_callback != nullptr); + + unhook_devices_on_hook_thread(); + + g_keyboard_hook_callback = config.keyboard_callback; + if (g_keyboard_hook_callback) + g_keyboard_hook = SetWindowsHookExW( + WH_KEYBOARD_LL, keyboard_hook_proc, config.instance, 0); + + g_mouse_hook_callback = config.mouse_callback; + if (g_mouse_hook_callback) + g_mouse_hook = SetWindowsHookExW( + WH_MOUSE_LL, mouse_hook_proc, config.instance, 0); + + const auto keyboard_is_hooked = (g_keyboard_hook != nullptr); + const auto mouse_is_hooked = (g_mouse_hook != nullptr); + if (keyboard_is_hooked != keyboard_was_hooked) + verbose(keyboard_is_hooked ? "Hooked keyboard" : "Unhooked keyboard"); + if (mouse_is_hooked != mouse_was_hooked) + verbose(mouse_is_hooked ? "Hooked mouse" : "Unhooked mouse"); + } + + void hook_thread_main(std::promise ready) { + g_hook_thread_id = GetCurrentThreadId(); + + // create message queue + auto message = MSG{ }; + PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE); + ready.set_value(); + + while (GetMessageW(&message, nullptr, 0, 0) > 0) + if (message.message == WM_APP_CONFIGURE_HOOKS) { + auto& config = *reinterpret_cast(message.lParam); + hook_devices_on_hook_thread(config); + config.completed.set_value(); + } + + unhook_devices_on_hook_thread(); + } + + void start_hook_thread() { + if (g_hook_thread.joinable()) + return; + + auto ready = std::promise(); + auto ready_future = ready.get_future(); + g_hook_thread = std::thread(hook_thread_main, std::move(ready)); + ready_future.get(); + } + + void configure_devices(HookConfig config) { + auto completed = config.completed.get_future(); + if (!PostThreadMessageW(g_hook_thread_id, WM_APP_CONFIGURE_HOOKS, 0, + reinterpret_cast(&config))) + return; + + completed.get(); + } +} // namespace + +void unhook_devices() { + const auto lock = std::lock_guard(g_hook_thread_mutex); + if (g_hook_thread.joinable()) + configure_devices({ }); +} + +void hook_devices(HINSTANCE instance, + KeyboardHookCallback keyboard_hook_callback, + MouseHookCallback mouse_hook_callback) { + const auto lock = std::lock_guard(g_hook_thread_mutex); + start_hook_thread(); + configure_devices({ instance, keyboard_hook_callback, mouse_hook_callback }); +} + +void shutdown_hook_thread() { + const auto lock = std::lock_guard(g_hook_thread_mutex); + if (!g_hook_thread.joinable()) + return; + + PostThreadMessageW(g_hook_thread_id, WM_QUIT, 0, 0); + g_hook_thread.join(); + g_hook_thread_id = 0; +} diff --git a/src/server/windows/HookThread.h b/src/server/windows/HookThread.h new file mode 100644 index 0000000..e9f122c --- /dev/null +++ b/src/server/windows/HookThread.h @@ -0,0 +1,11 @@ + +#include "common/windows/win.h" + +using KeyboardHookCallback = bool(*)(WPARAM, const KBDLLHOOKSTRUCT& kbd); +using MouseHookCallback = bool(*)(WPARAM, const MSLLHOOKSTRUCT& ms); + +void hook_devices(HINSTANCE instance, + KeyboardHookCallback keyboard_hook_callback, + MouseHookCallback mouse_hook_callback); +void unhook_devices(); +void shutdown_hook_thread(); diff --git a/src/server/windows/main.cpp b/src/server/windows/main.cpp index 23bec61..df3bf07 100644 --- a/src/server/windows/main.cpp +++ b/src/server/windows/main.cpp @@ -4,8 +4,10 @@ #include "runtime/Timeout.h" #include "common/windows/LimitSingleInstance.h" #include "common/output.h" +#include "HookThread.h" #include "Devices.h" #include +#include // enable visual styles for message boxes #pragma comment(linker,"\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") @@ -26,9 +28,6 @@ namespace { std::string get_devices_error_message() override; }; - // Calling SendInput directly from mouse hook proc seems to trigger a - // timeout, therefore it is called after returning from the hook proc. - // But for keyboard input it is still more reliable to call it directly! const auto TIMER_FLUSH_SEND_BUFFER = 1; const auto TIMER_TIMEOUT = 2; const auto WM_APP_CLIENT_MESSAGE = WM_APP + 0; @@ -39,12 +38,27 @@ namespace { HINSTANCE g_instance; HWND g_window; - HHOOK g_keyboard_hook; - HHOOK g_mouse_hook; std::vector g_buttons_down; Devices g_devices; ServerStateImpl g_state; std::vector g_input_buffer; + std::vector g_flush_input_buffer; + std::mutex g_hook_callback_mutex; + + class unlock_guard { + public: + explicit unlock_guard(std::mutex& mutex) + : m_mutex(mutex) { + m_mutex.unlock(); + } + + ~unlock_guard() { + m_mutex.lock(); + } + + private: + std::mutex& m_mutex; + }; KeyEvent get_key_event(WPARAM wparam, const KBDLLHOOKSTRUCT& kbd) { // ignore unknown events @@ -200,35 +214,21 @@ namespace { return ++result; } - bool merge_wheel_event(INPUT& a, const INPUT& b) { - if (a.type == INPUT_MOUSE && - b.type == INPUT_MOUSE && - a.mi.dwFlags == b.mi.dwFlags) { - const auto value_a = static_cast(a.mi.mouseData); - const auto value_b = static_cast(b.mi.mouseData); - if (std::signbit(value_a) == std::signbit(value_b)) { - a.mi.mouseData = static_cast(value_a + value_b); - return true; - } - } - return false; - } - - void merge_wheel_events(std::vector& input) { - input.erase(unique_mutable(begin(input), end(input), - &merge_wheel_event), end(input)); - } - bool ServerStateImpl::on_flushed_send_buffer() { - // merge wheel events, since sending many can lag severly - merge_wheel_events(g_input_buffer); - - // sending each input separately, since Windows starts beeping - // when sending multiple mouse click events at once - for (auto& input : g_input_buffer) - ::SendInput(1, &input, sizeof(input)); - - g_input_buffer.clear(); + while (!g_input_buffer.empty()) { + std::swap(g_input_buffer, g_flush_input_buffer); + + // allow hook callbacks to run concurrently + // when they are called as a consequence of sending input + const auto unlock = unlock_guard(g_hook_callback_mutex); + + // sending each input separately, since Windows starts beeping + // when sending multiple mouse click events at once + for (auto& input : g_flush_input_buffer) + ::SendInput(1, &input, sizeof(INPUT)); + + g_flush_input_buffer.clear(); + } return true; } @@ -284,22 +284,13 @@ namespace { return g_state.translate_input(input, keyboard_device_index); } - LRESULT CALLBACK keyboard_hook_proc(int code, WPARAM wparam, LPARAM lparam) { - if (code == HC_ACTION) { - const auto& kbd = *reinterpret_cast(lparam); - if (translate_keyboard_input(wparam, kbd)) { - // never send mouse events directly from the keyboard hook proc since it can block - if (g_state.send_buffer_has_mouse_events()) { - g_state.schedule_flush(); - } - else { - if (!g_state.flush_scheduled_at()) - g_state.flush_send_buffer(); - } - return -1; - } + bool CALLBACK keyboard_hook_callback(WPARAM wparam, const KBDLLHOOKSTRUCT& kbd) { + const auto lock = std::lock_guard(g_hook_callback_mutex); + if (translate_keyboard_input(wparam, kbd)) { + g_state.schedule_flush(); + return true; } - return CallNextHookEx(g_keyboard_hook, code, wparam, lparam); + return false; } std::optional get_button_event(WPARAM wparam, const MSLLHOOKSTRUCT& ms) { @@ -349,30 +340,16 @@ namespace { return prevent_button_repeat(*input); } - LRESULT CALLBACK mouse_hook_proc(int code, WPARAM wparam, LPARAM lparam) { - if (code == HC_ACTION) { - const auto& ms = *reinterpret_cast(lparam); - if (translate_mouse_input(wparam, ms)) { - g_state.schedule_flush(); - return -1; - } + bool mouse_hook_callback(WPARAM wparam, const MSLLHOOKSTRUCT& ms) { + const auto lock = std::lock_guard(g_hook_callback_mutex); + if (translate_mouse_input(wparam, ms)) { + g_state.schedule_flush(); + return true; } - return CallNextHookEx(g_mouse_hook, code, wparam, lparam); - } - - void unhook_devices() { - if (auto hook = std::exchange(g_keyboard_hook, nullptr)) - UnhookWindowsHookEx(hook); - if (auto hook = std::exchange(g_mouse_hook, nullptr)) - UnhookWindowsHookEx(hook); + return false; } void hook_devices() { - const auto keyboard_was_hooked = (g_keyboard_hook != nullptr); - const auto mouse_was_hooked = (g_mouse_hook != nullptr); - - unhook_devices(); - if (g_devices.initialized()) return; @@ -390,24 +367,10 @@ namespace { hook_mouse = evaluate_grab_filters(g_devices.grab_filters(), "mouse", "mouse", hook_mouse); - if (hook_keyboard) { - g_keyboard_hook = SetWindowsHookExW( - WH_KEYBOARD_LL, &keyboard_hook_proc, g_instance, 0); - if (!g_keyboard_hook) - error("Hooking keyboard failed"); - } - - if (hook_mouse) { - g_mouse_hook = SetWindowsHookExW( - WH_MOUSE_LL, &mouse_hook_proc, g_instance, 0); - if (!g_mouse_hook) - error("Hooking mouse failed"); - } - - if (hook_keyboard != keyboard_was_hooked) - verbose(hook_keyboard ? "Hooked keyboard" : "Unhooked keyboard"); - if (hook_mouse != mouse_was_hooked) - verbose(hook_mouse ? "Hooked mouse" : "Unhooked mouse"); + const auto unlock = unlock_guard(g_hook_callback_mutex); + hook_devices(g_instance, + hook_keyboard ? &keyboard_hook_callback : nullptr, + hook_mouse ? &mouse_hook_callback : nullptr); } bool is_unconditionally_forwarding() { @@ -481,6 +444,9 @@ namespace { LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) { + + const auto lock = std::lock_guard(g_hook_callback_mutex); + switch(message) { case WM_DESTROY: g_devices.shutdown(); @@ -499,6 +465,8 @@ namespace { verbose("Connection to keymapper lost"); verbose("---------------"); g_state.reset_configuration(); + + const auto unlock = unlock_guard(g_hook_callback_mutex); unhook_devices(); } return 0; @@ -599,5 +567,6 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, LPWSTR, int) { DispatchMessageW(&message); } verbose("Exiting"); + shutdown_hook_thread(); return 0; }