From 1c3a562c6e1ac9b19349ed1ee632ea2dc45e75d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:17:08 +0000 Subject: [PATCH 01/15] Initial plan From b0fa02875f67eab6361af17f95da605a3ea397c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:29:56 +0000 Subject: [PATCH 02/15] Implement hotkey on active screen only feature - Add m_activeScreenOnly field to IKeyState::KeyInfo - Add m_disableGlobalHotkeyRegister to InputFilter::KeystrokeCondition - Update all registerHotKey/unregisterHotKey signatures with boolean flags - Implement platform-specific hotkey registration changes (Windows, macOS, Linux, Ei) - Update Server event handling for activeScreenOnly logic - Add config parsing for "activeScreenOnly" action option and "disableGlobalHotkeyRegister" condition option - Enable keystroke forwarding to primary client Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- src/lib/deskflow/IKeyState.cpp | 11 ++++++ src/lib/deskflow/IKeyState.h | 5 +++ src/lib/deskflow/IPrimaryScreen.h | 4 +-- src/lib/deskflow/PlatformScreen.h | 4 +-- src/lib/deskflow/Screen.cpp | 8 ++--- src/lib/deskflow/Screen.h | 4 +-- src/lib/platform/EiScreen.cpp | 6 ++-- src/lib/platform/EiScreen.h | 4 +-- src/lib/platform/MSWindowsScreen.cpp | 14 ++++---- src/lib/platform/MSWindowsScreen.h | 4 +-- src/lib/platform/OSXScreen.h | 4 +-- src/lib/platform/OSXScreen.mm | 20 ++++++----- src/lib/platform/XWindowsScreen.cpp | 20 +++++++---- src/lib/platform/XWindowsScreen.h | 4 +-- src/lib/server/Config.cpp | 51 ++++++++++++++++++++++++---- src/lib/server/Config.h | 6 ++++ src/lib/server/InputFilter.cpp | 34 ++++++++++++++++--- src/lib/server/InputFilter.h | 3 ++ src/lib/server/PrimaryClient.cpp | 20 ++++------- src/lib/server/PrimaryClient.h | 4 +-- src/lib/server/Server.cpp | 28 ++++++++++++--- src/lib/server/Server.h | 4 +-- 22 files changed, 185 insertions(+), 77 deletions(-) diff --git a/src/lib/deskflow/IKeyState.cpp b/src/lib/deskflow/IKeyState.cpp index 553c1980941b..c5c4a0eaaa17 100644 --- a/src/lib/deskflow/IKeyState.cpp +++ b/src/lib/deskflow/IKeyState.cpp @@ -30,6 +30,7 @@ IKeyState::KeyInfo *IKeyState::KeyInfo::alloc(KeyID id, KeyModifierMask mask, Ke info->m_mask = mask; info->m_button = button; info->m_count = count; + info->m_activeScreenOnly = false; info->m_screens = nullptr; info->m_screensBuffer[0] = '\0'; return info; @@ -38,6 +39,14 @@ IKeyState::KeyInfo *IKeyState::KeyInfo::alloc(KeyID id, KeyModifierMask mask, Ke IKeyState::KeyInfo *IKeyState::KeyInfo::alloc( KeyID id, KeyModifierMask mask, KeyButton button, int32_t count, const std::set &destinations ) +{ + return alloc(id, mask, button, count, destinations, false); +} + +IKeyState::KeyInfo *IKeyState::KeyInfo::alloc( + KeyID id, KeyModifierMask mask, KeyButton button, int32_t count, const std::set &destinations, + bool activeScreenOnly +) { std::string screens = join(destinations); const char *buffer = screens.c_str(); @@ -54,6 +63,7 @@ IKeyState::KeyInfo *IKeyState::KeyInfo::alloc( info->m_mask = mask; info->m_button = button; info->m_count = count; + info->m_activeScreenOnly = activeScreenOnly; info->m_screens = info->m_screensBuffer; std::copy(buffer, buffer + screens.size() + 1, info->m_screensBuffer); return info; @@ -74,6 +84,7 @@ IKeyState::KeyInfo *IKeyState::KeyInfo::alloc(const KeyInfo &x) info->m_mask = x.m_mask; info->m_button = x.m_button; info->m_count = x.m_count; + info->m_activeScreenOnly = x.m_activeScreenOnly; info->m_screens = x.m_screens ? info->m_screensBuffer : nullptr; memcpy(info->m_screensBuffer, x.m_screensBuffer, bufferLen + 1); return info; diff --git a/src/lib/deskflow/IKeyState.h b/src/lib/deskflow/IKeyState.h index cad575a64f39..e8e2cb14fca0 100644 --- a/src/lib/deskflow/IKeyState.h +++ b/src/lib/deskflow/IKeyState.h @@ -32,6 +32,10 @@ class IKeyState public: static KeyInfo *alloc(KeyID, KeyModifierMask, KeyButton, int32_t count); static KeyInfo *alloc(KeyID, KeyModifierMask, KeyButton, int32_t count, const std::set &destinations); + static KeyInfo *alloc( + KeyID, KeyModifierMask, KeyButton, int32_t count, const std::set &destinations, + bool activeScreenOnly + ); static KeyInfo *alloc(const KeyInfo &); static bool isDefault(const char *screens); @@ -45,6 +49,7 @@ class IKeyState KeyModifierMask m_mask; KeyButton m_button; int32_t m_count; + bool m_activeScreenOnly; char *m_screens; char m_screensBuffer[1]; }; diff --git a/src/lib/deskflow/IPrimaryScreen.h b/src/lib/deskflow/IPrimaryScreen.h index 406deaeffab6..76216fa78fda 100644 --- a/src/lib/deskflow/IPrimaryScreen.h +++ b/src/lib/deskflow/IPrimaryScreen.h @@ -123,13 +123,13 @@ class IPrimaryScreen the modifiers in any order or to require the user to press the given key last. */ - virtual uint32_t registerHotKey(KeyID key, KeyModifierMask mask) = 0; + virtual uint32_t registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) = 0; //! Unregister a system hotkey /*! Unregisters a previously registered hot key. */ - virtual void unregisterHotKey(uint32_t id) = 0; + virtual void unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) = 0; //! Prepare to synthesize input on primary screen /*! diff --git a/src/lib/deskflow/PlatformScreen.h b/src/lib/deskflow/PlatformScreen.h index 512682f7175f..a6b435956273 100644 --- a/src/lib/deskflow/PlatformScreen.h +++ b/src/lib/deskflow/PlatformScreen.h @@ -31,8 +31,8 @@ class PlatformScreen : public IPlatformScreen void reconfigure(uint32_t activeSides) override = 0; uint32_t activeSides() override = 0; void warpCursor(int32_t x, int32_t y) override = 0; - uint32_t registerHotKey(KeyID key, KeyModifierMask mask) override = 0; - void unregisterHotKey(uint32_t id) override = 0; + uint32_t registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) override = 0; + void unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) override = 0; void fakeInputBegin() override = 0; void fakeInputEnd() override = 0; int32_t getJumpZoneSize() const override = 0; diff --git a/src/lib/deskflow/Screen.cpp b/src/lib/deskflow/Screen.cpp index 3258acbb3475..da47ad537be4 100644 --- a/src/lib/deskflow/Screen.cpp +++ b/src/lib/deskflow/Screen.cpp @@ -277,14 +277,14 @@ void Screen::setSequenceNumber(uint32_t seqNum) m_screen->setSequenceNumber(seqNum); } -uint32_t Screen::registerHotKey(KeyID key, KeyModifierMask mask) +uint32_t Screen::registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) { - return m_screen->registerHotKey(key, mask); + return m_screen->registerHotKey(key, mask, registerGlobalHotkey); } -void Screen::unregisterHotKey(uint32_t id) +void Screen::unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) { - m_screen->unregisterHotKey(id); + m_screen->unregisterHotKey(id, unregisterGlobalHotkey); } void Screen::fakeInputBegin() diff --git a/src/lib/deskflow/Screen.h b/src/lib/deskflow/Screen.h index 8859e851ccef..978e3d25ee36 100644 --- a/src/lib/deskflow/Screen.h +++ b/src/lib/deskflow/Screen.h @@ -188,13 +188,13 @@ class Screen : public IScreen Registers a system-wide hotkey for key \p key with modifiers \p mask. Returns an id used to unregister the hotkey. */ - uint32_t registerHotKey(KeyID key, KeyModifierMask mask); + uint32_t registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey); //! Unregister a system hotkey /*! Unregisters a previously registered hot key. */ - void unregisterHotKey(uint32_t id); + void unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey); //! Prepare to synthesize input on primary screen /*! diff --git a/src/lib/platform/EiScreen.cpp b/src/lib/platform/EiScreen.cpp index ce42a2f5e093..64425be57718 100644 --- a/src/lib/platform/EiScreen.cpp +++ b/src/lib/platform/EiScreen.cpp @@ -209,8 +209,9 @@ void EiScreen::warpCursor(int32_t x, int32_t y) m_cursorY = y; } -std::uint32_t EiScreen::registerHotKey(KeyID key, KeyModifierMask mask) +std::uint32_t EiScreen::registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) { + (void)registerGlobalHotkey; // Unused for Ei implementation static std::uint32_t next_id; std::uint32_t id = std::min(++next_id, 1u); @@ -226,8 +227,9 @@ std::uint32_t EiScreen::registerHotKey(KeyID key, KeyModifierMask mask) return id; } -void EiScreen::unregisterHotKey(uint32_t id) +void EiScreen::unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) { + (void)unregisterGlobalHotkey; // Unused for Ei implementation for (auto &[key, set] : m_hotkeys) { (void)key; if (set.removeById(id)) { diff --git a/src/lib/platform/EiScreen.h b/src/lib/platform/EiScreen.h index e75108db0c81..95458e405cf8 100644 --- a/src/lib/platform/EiScreen.h +++ b/src/lib/platform/EiScreen.h @@ -47,8 +47,8 @@ class EiScreen : public PlatformScreen void reconfigure(std::uint32_t activeSides) override; std::uint32_t activeSides() override; void warpCursor(std::int32_t x, std::int32_t y) override; - std::uint32_t registerHotKey(KeyID key, KeyModifierMask mask) override; - void unregisterHotKey(std::uint32_t id) override; + std::uint32_t registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) override; + void unregisterHotKey(std::uint32_t id, bool unregisterGlobalHotkey) override; void fakeInputBegin() override; void fakeInputEnd() override; std::int32_t getJumpZoneSize() const override; diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index a606da7a9a7c..80a0630434e0 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -538,7 +538,7 @@ void MSWindowsScreen::saveMousePosition(int32_t x, int32_t y) LOG_DEBUG5("saved mouse position for next delta: %+d,%+d", x, y); } -uint32_t MSWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) +uint32_t MSWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) { // only allow certain modifiers if ((mask & ~(KeyModifierShift | KeyModifierControl | KeyModifierAlt | KeyModifierSuper)) != 0) { @@ -587,11 +587,11 @@ uint32_t MSWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) } // if this hot key has modifiers only then we'll handle it specially - bool err; + bool err = false; if (key == kKeyNone) { // check if already registered err = (m_hotKeyToIDMap.count(HotKeyItem(vk, modifiers)) > 0); - } else { + } else if (registerGlobalHotkey) { // register with OS err = (RegisterHotKey(nullptr, id, modifiers, vk) == 0); } @@ -615,7 +615,7 @@ uint32_t MSWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) return id; } -void MSWindowsScreen::unregisterHotKey(uint32_t id) +void MSWindowsScreen::unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) { // look up hotkey HotKeyMap::iterator i = m_hotKeys.find(id); @@ -624,11 +624,9 @@ void MSWindowsScreen::unregisterHotKey(uint32_t id) } // unregister with OS - bool err; - if (i->second.getVirtualKey() != 0) { + bool err = false; + if (unregisterGlobalHotkey && i->second.getVirtualKey() != 0) { err = !UnregisterHotKey(nullptr, id); - } else { - err = false; } if (err) { LOG_WARN("failed to unregister hotkey id=%d", id); diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 5430c3dba3f3..7e7a86051356 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -86,8 +86,8 @@ class MSWindowsScreen : public PlatformScreen void reconfigure(uint32_t activeSides) override; uint32_t activeSides() override; void warpCursor(int32_t x, int32_t y) override; - uint32_t registerHotKey(KeyID key, KeyModifierMask mask) override; - void unregisterHotKey(uint32_t id) override; + uint32_t registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) override; + void unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) override; void fakeInputBegin() override; void fakeInputEnd() override; int32_t getJumpZoneSize() const override; diff --git a/src/lib/platform/OSXScreen.h b/src/lib/platform/OSXScreen.h index 8a24bcfdbdca..73f43f78d3fe 100644 --- a/src/lib/platform/OSXScreen.h +++ b/src/lib/platform/OSXScreen.h @@ -62,8 +62,8 @@ class OSXScreen : public PlatformScreen void reconfigure(uint32_t activeSides) override; uint32_t activeSides() override; void warpCursor(int32_t x, int32_t y) override; - uint32_t registerHotKey(KeyID key, KeyModifierMask mask) override; - void unregisterHotKey(uint32_t id) override; + uint32_t registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) override; + void unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) override; void fakeInputBegin() override; void fakeInputEnd() override; int32_t getJumpZoneSize() const override; diff --git a/src/lib/platform/OSXScreen.mm b/src/lib/platform/OSXScreen.mm index 19afc25046b6..18aa30002d93 100644 --- a/src/lib/platform/OSXScreen.mm +++ b/src/lib/platform/OSXScreen.mm @@ -301,7 +301,7 @@ y = m_yCenter; } -uint32_t OSXScreen::registerHotKey(KeyID key, KeyModifierMask mask) +uint32_t OSXScreen::registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) { // get mac virtual key and modifier mask matching deskflow key and mask uint32_t macKey, macMask; @@ -321,7 +321,7 @@ // if this hot key has modifiers only then we'll handle it specially EventHotKeyRef ref = nullptr; - bool okay; + bool okay = true; if (key == kKeyNone) { if (m_modifierHotKeys.count(mask) > 0) { // already registered @@ -331,9 +331,11 @@ okay = true; } } else { - EventHotKeyID hkid = {'SNRG', (uint32_t)id}; - OSStatus status = RegisterEventHotKey(macKey, macMask, hkid, GetApplicationEventTarget(), 0, &ref); - okay = (status == noErr); + if (registerGlobalHotkey) { + EventHotKeyID hkid = {'SNRG', (uint32_t)id}; + OSStatus status = RegisterEventHotKey(macKey, macMask, hkid, GetApplicationEventTarget(), 0, &ref); + okay = (status == noErr); + } m_hotKeyToIDMap[HotKeyItem(macKey, macMask)] = id; } @@ -354,7 +356,7 @@ return id; } -void OSXScreen::unregisterHotKey(uint32_t id) +void OSXScreen::unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) { // look up hotkey HotKeyMap::iterator i = m_hotKeys.find(id); @@ -363,9 +365,11 @@ } // unregister with OS - bool okay; + bool okay = true; if (i->second.getRef() != nullptr) { - okay = (UnregisterEventHotKey(i->second.getRef()) == noErr); + if (unregisterGlobalHotkey) { + okay = (UnregisterEventHotKey(i->second.getRef()) == noErr); + } } else { okay = false; // XXX -- this is inefficient diff --git a/src/lib/platform/XWindowsScreen.cpp b/src/lib/platform/XWindowsScreen.cpp index c57ebf8470e3..e22dd3a92c12 100644 --- a/src/lib/platform/XWindowsScreen.cpp +++ b/src/lib/platform/XWindowsScreen.cpp @@ -511,7 +511,7 @@ void XWindowsScreen::warpCursor(int32_t x, int32_t y) m_yCursor = y; } -uint32_t XWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) +uint32_t XWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) { // only allow certain modifiers if ((mask & ~(KeyModifierShift | KeyModifierControl | KeyModifierAlt | KeyModifierSuper)) != 0) { @@ -620,7 +620,9 @@ uint32_t XWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) for (int k = 0; k < modKeymap->max_keypermod && !err; ++k) { KeyCode code = modifiermap[k]; if (modifiermap[k] != 0) { - XGrabKey(m_display, code, modifiers2, m_root, False, GrabModeAsync, GrabModeAsync); + if (registerGlobalHotkey) { + XGrabKey(m_display, code, modifiers2, m_root, False, GrabModeAsync, GrabModeAsync); + } if (!err) { hotKeys.push_back(std::make_pair(code, modifiers2)); m_hotKeyToIDMap[HotKeyItem(code, modifiers2)] = id; @@ -664,7 +666,9 @@ uint32_t XWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) } // add grab - XGrabKey(m_display, *j, tmpModifiers, m_root, False, GrabModeAsync, GrabModeAsync); + if (registerGlobalHotkey) { + XGrabKey(m_display, *j, tmpModifiers, m_root, False, GrabModeAsync, GrabModeAsync); + } if (!err) { hotKeys.push_back(std::make_pair(*j, tmpModifiers)); m_hotKeyToIDMap[HotKeyItem(*j, tmpModifiers)] = id; @@ -677,7 +681,9 @@ uint32_t XWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) if (err) { // if any failed then unregister any we did get for (const auto &[keyCode, keyMask] : hotKeys) { - XUngrabKey(m_display, keyCode, keyMask, m_root); + if (registerGlobalHotkey) { + XUngrabKey(m_display, keyCode, keyMask, m_root); + } m_hotKeyToIDMap.erase(HotKeyItem(keyCode, keyMask)); } @@ -697,7 +703,7 @@ uint32_t XWindowsScreen::registerHotKey(KeyID key, KeyModifierMask mask) return id; } -void XWindowsScreen::unregisterHotKey(uint32_t id) +void XWindowsScreen::unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) { // look up hotkey HotKeyMap::iterator i = m_hotKeys.find(id); @@ -711,7 +717,9 @@ void XWindowsScreen::unregisterHotKey(uint32_t id) XWindowsUtil::ErrorLock lock(m_display, &err); const HotKeyList &hotKeys = i->second; for (const auto &[keyCode, keyMask] : hotKeys) { - XUngrabKey(m_display, keyCode, keyMask, m_root); + if (unregisterGlobalHotkey) { + XUngrabKey(m_display, keyCode, keyMask, m_root); + } m_hotKeyToIDMap.erase(HotKeyItem(keyCode, keyMask)); } } diff --git a/src/lib/platform/XWindowsScreen.h b/src/lib/platform/XWindowsScreen.h index 4c436028e640..d0f5dbf6bb78 100644 --- a/src/lib/platform/XWindowsScreen.h +++ b/src/lib/platform/XWindowsScreen.h @@ -48,8 +48,8 @@ class XWindowsScreen : public PlatformScreen void reconfigure(uint32_t activeSides) override; uint32_t activeSides() override; void warpCursor(int32_t x, int32_t y) override; - uint32_t registerHotKey(KeyID key, KeyModifierMask mask) override; - void unregisterHotKey(uint32_t id) override; + uint32_t registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) override; + void unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) override; void fakeInputBegin() override; void fakeInputEnd() override; int32_t getJumpZoneSize() const override; diff --git a/src/lib/server/Config.cpp b/src/lib/server/Config.cpp index ed60e3159219..648584474890 100644 --- a/src/lib/server/Config.cpp +++ b/src/lib/server/Config.cpp @@ -924,13 +924,18 @@ InputFilter::Condition * Config::parseCondition(const ConfigReadContext &s, const std::string &name, const std::vector &args) { if (name == "keystroke") { - if (args.size() != 1) { - throw ServerConfigReadException(s, "syntax for condition: keystroke(modifiers+key)"); + if (args.size() < 1 || args.size() > 2) { + throw ServerConfigReadException(s, "syntax for condition: keystroke(modifiers+key[,options])"); } IPlatformScreen::KeyInfo *keyInfo = s.parseKeystroke(args[0]); + bool disableGlobalHotkeyRegister = false; + + if (args.size() > 1) { + parseKeystrokeConditionOptions(s, args[1], disableGlobalHotkeyRegister); + } - return new InputFilter::KeystrokeCondition(m_events, keyInfo); + return new InputFilter::KeystrokeCondition(m_events, keyInfo, disableGlobalHotkeyRegister); } if (name == "mousebutton") { @@ -969,8 +974,8 @@ void Config::parseAction( InputFilter::Action *action; if (name == "keystroke" || name == "keyDown" || name == "keyUp") { - if (args.size() < 1 || args.size() > 2) { - throw ServerConfigReadException(s, "syntax for action: keystroke(modifiers+key[,screens])"); + if (args.size() < 1 || args.size() > 3) { + throw ServerConfigReadException(s, "syntax for action: keystroke(modifiers+key[,screens[,options]])"); } IPlatformScreen::KeyInfo *keyInfo; @@ -979,7 +984,11 @@ void Config::parseAction( } else { std::set screens; parseScreens(s, args[1], screens); - keyInfo = s.parseKeystroke(args[0], screens); + bool activeScreenOnly = false; + if (args.size() > 2) { + parseKeystrokeActionOptions(s, args[2], activeScreenOnly); + } + keyInfo = s.parseKeystroke(args[0], screens, activeScreenOnly); } if (name == "keystroke") { @@ -1186,6 +1195,26 @@ void Config::parseScreens(const ConfigReadContext &c, const std::string_view &s, } } +void Config::parseKeystrokeConditionOptions( + const ConfigReadContext &c, const std::string &s, bool &disableGlobalHotkeyRegister +) const +{ + if (s == "disableGlobalHotkeyRegister") { + disableGlobalHotkeyRegister = true; + } else { + disableGlobalHotkeyRegister = false; + } +} + +void Config::parseKeystrokeActionOptions(const ConfigReadContext &c, const std::string &s, bool &activeScreenOnly) const +{ + if (s == "activeScreenOnly") { + activeScreenOnly = true; + } else { + activeScreenOnly = false; + } +} + const char *Config::getOptionName(OptionID id) { if (id == kOptionHalfDuplexCapsLock) { @@ -2000,6 +2029,14 @@ IPlatformScreen::KeyInfo *ConfigReadContext::parseKeystroke(const std::string &k IPlatformScreen::KeyInfo * ConfigReadContext::parseKeystroke(const std::string &keystroke, const std::set &screens) const +{ + return parseKeystroke(keystroke, screens, false); +} + +IPlatformScreen::KeyInfo * +ConfigReadContext::parseKeystroke( + const std::string &keystroke, const std::set &screens, bool activeScreenOnly +) const { std::string s = keystroke; @@ -2017,7 +2054,7 @@ ConfigReadContext::parseKeystroke(const std::string &keystroke, const std::set &screens) const; + void parseKeystrokeConditionOptions(const ConfigReadContext &c, const std::string &s, bool &disableGlobalHotkeyRegister) + const; + void parseKeystrokeActionOptions(const ConfigReadContext &c, const std::string &s, bool &activeScreenOnly) const; static const char *getOptionName(OptionID); static std::string getOptionValue(OptionID, OptionValue); @@ -506,6 +509,9 @@ class ConfigReadContext ) const; IPlatformScreen::KeyInfo *parseKeystroke(const std::string &keystroke) const; IPlatformScreen::KeyInfo *parseKeystroke(const std::string &keystroke, const std::set &screens) const; + IPlatformScreen::KeyInfo *parseKeystroke( + const std::string &keystroke, const std::set &screens, bool activeScreenOnly + ) const; IPlatformScreen::ButtonInfo parseMouse(const std::string &mouse) const; KeyModifierMask parseModifier(const std::string &modifiers) const; std::istream &getStream() const diff --git a/src/lib/server/InputFilter.cpp b/src/lib/server/InputFilter.cpp index e3e7e6da9a76..08be4307a0e4 100644 --- a/src/lib/server/InputFilter.cpp +++ b/src/lib/server/InputFilter.cpp @@ -34,7 +34,19 @@ void InputFilter::Condition::disablePrimary(PrimaryClient *) InputFilter::KeystrokeCondition::KeystrokeCondition(IEventQueue *events, IPlatformScreen::KeyInfo *info) : m_key(info->m_key), m_mask(info->m_mask), - m_events(events) + m_events(events), + m_disableGlobalHotkeyRegister(false) +{ + free(info); +} + +InputFilter::KeystrokeCondition::KeystrokeCondition( + IEventQueue *events, IPlatformScreen::KeyInfo *info, bool disableGlobalHotkeyRegister +) + : m_key(info->m_key), + m_mask(info->m_mask), + m_events(events), + m_disableGlobalHotkeyRegister(disableGlobalHotkeyRegister) { free(info); } @@ -42,7 +54,19 @@ InputFilter::KeystrokeCondition::KeystrokeCondition(IEventQueue *events, IPlatfo InputFilter::KeystrokeCondition::KeystrokeCondition(IEventQueue *events, KeyID key, KeyModifierMask mask) : m_key(key), m_mask(mask), - m_events(events) + m_events(events), + m_disableGlobalHotkeyRegister(false) +{ + // do nothing +} + +InputFilter::KeystrokeCondition::KeystrokeCondition( + IEventQueue *events, KeyID key, KeyModifierMask mask, bool disableGlobalHotkeyRegister +) + : m_key(key), + m_mask(mask), + m_events(events), + m_disableGlobalHotkeyRegister(disableGlobalHotkeyRegister) { // do nothing } @@ -59,7 +83,7 @@ KeyModifierMask InputFilter::KeystrokeCondition::getMask() const InputFilter::Condition *InputFilter::KeystrokeCondition::clone() const { - return new KeystrokeCondition(m_events, m_key, m_mask); + return new KeystrokeCondition(m_events, m_key, m_mask, m_disableGlobalHotkeyRegister); } std::string InputFilter::KeystrokeCondition::format() const @@ -91,12 +115,12 @@ InputFilter::FilterStatus InputFilter::KeystrokeCondition::match(const Event &ev void InputFilter::KeystrokeCondition::enablePrimary(PrimaryClient *primary) { - m_id = primary->registerHotKey(m_key, m_mask); + m_id = primary->registerHotKey(m_key, m_mask, !m_disableGlobalHotkeyRegister); } void InputFilter::KeystrokeCondition::disablePrimary(PrimaryClient *primary) { - primary->unregisterHotKey(m_id); + primary->unregisterHotKey(m_id, !m_disableGlobalHotkeyRegister); m_id = 0; } diff --git a/src/lib/server/InputFilter.h b/src/lib/server/InputFilter.h index 322f119a0ed5..13b695bf94fb 100644 --- a/src/lib/server/InputFilter.h +++ b/src/lib/server/InputFilter.h @@ -52,7 +52,9 @@ class InputFilter { public: KeystrokeCondition(IEventQueue *events, IPlatformScreen::KeyInfo *); + KeystrokeCondition(IEventQueue *events, IPlatformScreen::KeyInfo *, bool disableGlobalHotkeyRegister); KeystrokeCondition(IEventQueue *events, KeyID key, KeyModifierMask mask); + KeystrokeCondition(IEventQueue *events, KeyID key, KeyModifierMask mask, bool disableGlobalHotkeyRegister); ~KeystrokeCondition() override = default; KeyID getKey() const; @@ -70,6 +72,7 @@ class InputFilter KeyID m_key; KeyModifierMask m_mask; IEventQueue *m_events; + bool m_disableGlobalHotkeyRegister = false; }; // MouseButtonCondition diff --git a/src/lib/server/PrimaryClient.cpp b/src/lib/server/PrimaryClient.cpp index a98cd9bd0e01..94c166411470 100644 --- a/src/lib/server/PrimaryClient.cpp +++ b/src/lib/server/PrimaryClient.cpp @@ -25,14 +25,14 @@ void PrimaryClient::reconfigure(uint32_t activeSides) m_screen->reconfigure(activeSides); } -uint32_t PrimaryClient::registerHotKey(KeyID key, KeyModifierMask mask) +uint32_t PrimaryClient::registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey) { - return m_screen->registerHotKey(key, mask); + return m_screen->registerHotKey(key, mask, registerGlobalHotkey); } -void PrimaryClient::unregisterHotKey(uint32_t id) +void PrimaryClient::unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey) { - m_screen->unregisterHotKey(id); + m_screen->unregisterHotKey(id, unregisterGlobalHotkey); } void PrimaryClient::fakeInputBegin() @@ -142,11 +142,7 @@ void PrimaryClient::setClipboardDirty(ClipboardID id, bool dirty) void PrimaryClient::keyDown(KeyID key, KeyModifierMask mask, KeyButton button, const std::string &) { if (m_fakeInputCount > 0) { - // XXX -- don't forward keystrokes to primary screen for now - (void)key; - (void)mask; - (void)button; - // m_screen->keyDown(key, mask, button); + m_screen->keyDown(key, mask, button); } } @@ -158,11 +154,7 @@ void PrimaryClient::keyRepeat(KeyID, KeyModifierMask, int32_t, KeyButton, const void PrimaryClient::keyUp(KeyID key, KeyModifierMask mask, KeyButton button) { if (m_fakeInputCount > 0) { - // XXX -- don't forward keystrokes to primary screen for now - (void)key; - (void)mask; - (void)button; - // m_screen->keyUp(key, mask, button); + m_screen->keyUp(key, mask, button); } } diff --git a/src/lib/server/PrimaryClient.h b/src/lib/server/PrimaryClient.h index 68d1c61e8d0c..ffae824609e9 100644 --- a/src/lib/server/PrimaryClient.h +++ b/src/lib/server/PrimaryClient.h @@ -42,13 +42,13 @@ class PrimaryClient : public BaseClientProxy Registers a system-wide hotkey for key \p key with modifiers \p mask. Returns an id used to unregister the hotkey. */ - virtual uint32_t registerHotKey(KeyID key, KeyModifierMask mask); + virtual uint32_t registerHotKey(KeyID key, KeyModifierMask mask, bool registerGlobalHotkey); //! Unregister a system hotkey /*! Unregisters a previously registered hot key. */ - virtual void unregisterHotKey(uint32_t id); + virtual void unregisterHotKey(uint32_t id, bool unregisterGlobalHotkey); //! Prepare to synthesize input on primary screen /*! diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index ade4dd8054e2..e5f7a4602f92 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -205,7 +205,7 @@ bool Server::setConfig(const ServerConfig &config) // ScrollLock as a hotkey. if (!m_disableLockToScreen && !m_config->hasLockToScreenAction()) { IPlatformScreen::KeyInfo *key = IPlatformScreen::KeyInfo::alloc(kKeyScrollLock, 0, 0, 0); - InputFilter::Rule rule(new InputFilter::KeystrokeCondition(m_events, key)); + InputFilter::Rule rule(new InputFilter::KeystrokeCondition(m_events, key, true)); rule.adoptAction(new InputFilter::LockCursorToScreenAction(m_events), true); m_inputFilter->addFilterRule(rule); } @@ -1228,13 +1228,13 @@ void Server::handleKeyDownEvent(const Event &event) { const auto *info = static_cast(event.getData()); auto lang = AppUtil::instance().getCurrentLanguageCode(); - onKeyDown(info->m_key, info->m_mask, info->m_button, lang, info->m_screens); + onKeyDown(info->m_key, info->m_mask, info->m_button, lang, info->m_screens, info->m_activeScreenOnly); } void Server::handleKeyUpEvent(const Event &event) { auto *info = static_cast(event.getData()); - onKeyUp(info->m_key, info->m_mask, info->m_button, info->m_screens); + onKeyUp(info->m_key, info->m_mask, info->m_button, info->m_screens, info->m_activeScreenOnly); } void Server::handleKeyRepeatEvent(const Event &event) @@ -1531,7 +1531,9 @@ void Server::onScreensaver(bool activated) } } -void Server::onKeyDown(KeyID id, KeyModifierMask mask, KeyButton button, const std::string &lang, const char *screens) +void Server::onKeyDown( + KeyID id, KeyModifierMask mask, KeyButton button, const std::string &lang, const char *screens, bool activeScreenOnly +) { LOG_DEBUG1("onKeyDown id=%d mask=0x%04x button=0x%04x lang=%s", id, mask, button, lang.c_str()); assert(m_active != nullptr); @@ -1539,6 +1541,14 @@ void Server::onKeyDown(KeyID id, KeyModifierMask mask, KeyButton button, const s // relay if (!m_keyboardBroadcasting && IKeyState::KeyInfo::isDefault(screens)) { m_active->keyDown(id, mask, button, lang); + } else if (activeScreenOnly) { + auto activeName = m_active->getName(); + if (IKeyState::KeyInfo::contains(screens, activeName)) { + // This won't work on the primary client if the action has the same keystroke as the condition. + // Unlike other clients, the primary client registers the original keystroke with the OS as a hotkey to block + // other apps from handling them, which also stops Deskflow from being able to create a fake event for them. + m_active->keyDown(id, mask, button, lang); + } } else { if (!screens && m_keyboardBroadcasting) { screens = m_keyboardBroadcastingScreens.c_str(); @@ -1554,7 +1564,7 @@ void Server::onKeyDown(KeyID id, KeyModifierMask mask, KeyButton button, const s } } -void Server::onKeyUp(KeyID id, KeyModifierMask mask, KeyButton button, const char *screens) +void Server::onKeyUp(KeyID id, KeyModifierMask mask, KeyButton button, const char *screens, bool activeScreenOnly) { LOG_DEBUG1("onKeyUp id=%d mask=0x%04x button=0x%04x", id, mask, button); assert(m_active != nullptr); @@ -1562,6 +1572,14 @@ void Server::onKeyUp(KeyID id, KeyModifierMask mask, KeyButton button, const cha // relay if (!m_keyboardBroadcasting && IKeyState::KeyInfo::isDefault(screens)) { m_active->keyUp(id, mask, button); + } else if (activeScreenOnly) { + auto activeName = m_active->getName(); + if (IKeyState::KeyInfo::contains(screens, activeName)) { + // This won't work on the primary client if the action has the same keystroke as the condition. + // Unlike other clients, the primary client registers the original keystroke with the OS as a hotkey to block + // other apps from handling them, which also stops Deskflow from being able to create a fake event for them. + m_active->keyUp(id, mask, button); + } } else { if (!screens && m_keyboardBroadcasting) { screens = m_keyboardBroadcastingScreens.c_str(); diff --git a/src/lib/server/Server.h b/src/lib/server/Server.h index d4e8fd1f608c..7bc6595524e3 100644 --- a/src/lib/server/Server.h +++ b/src/lib/server/Server.h @@ -325,8 +325,8 @@ class Server // event processing void onClipboardChanged(const BaseClientProxy *sender, ClipboardID id, uint32_t seqNum); void onScreensaver(bool activated); - void onKeyDown(KeyID, KeyModifierMask, KeyButton, const std::string &, const char *screens); - void onKeyUp(KeyID, KeyModifierMask, KeyButton, const char *screens); + void onKeyDown(KeyID, KeyModifierMask, KeyButton, const std::string &, const char *screens, bool activeScreenOnly); + void onKeyUp(KeyID, KeyModifierMask, KeyButton, const char *screens, bool activeScreenOnly); void onKeyRepeat(KeyID, KeyModifierMask, int32_t, KeyButton, const std::string &); void onMouseDown(ButtonID); void onMouseUp(ButtonID); From 16ad1e8147b45170042a16a280e358855091cc97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:32:30 +0000 Subject: [PATCH 03/15] Add documentation for activeScreenOnly and disableGlobalHotkeyRegister features Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- doc/user/configuration.md | 43 +++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/doc/user/configuration.md b/doc/user/configuration.md index ebbe0896e488..0b4c697eb98d 100644 --- a/doc/user/configuration.md +++ b/doc/user/configuration.md @@ -403,7 +403,7 @@ end |clipboardSharing| `true` or `false`|If set to ''true'' then clipboard sharing will be enabled and the ''clipboardSharingSize'' setting will be used. If set to false, then clipboard sharing will be disabled and the the ''clipboardSharingSize'' setting will be ignored.| |clipboardSharingSize| integer (N)| Deskflow will send a maximum of `N` kilobytes of clipboard data to another computer when the mouse transitions to that computer.| |win32KeepForeground | `true` or `false`| If set to ''true'' (the default), Deskflow will grab the foreground focus on a Windows server (thereby putting all other windows in the background) upon switching to a client. If set to ''false'', it will leave the currently foreground window in the foreground. Deskflow grabs the focus to avoid issues with other apps interfering with Deskflow's ability to read the hardware inputs. | -|keystroke(key) | actions | Binds the ''key'' combination key to the given ''actions''. ''key'' is an optional list of modifiers (''shift'', ''control'', ''alt'', ''meta'' or ''super'') optionally followed by a character or a key name, all separated by + (plus signs). You must have either modifiers or a character/key name or both. See below for `valid key names` and `actions`. Keyboard hot keys are handled while the cursor is on the primary screen and secondary screens. Separate actions can be assigned to press and release.| +|keystroke(key[,options]) | actions | Binds the ''key'' combination key to the given ''actions''. ''key'' is an optional list of modifiers (''shift'', ''control'', ''alt'', ''meta'' or ''super'') optionally followed by a character or a key name, all separated by + (plus signs). You must have either modifiers or a character/key name or both. See below for `valid key names` and `actions`. Keyboard hot keys are handled while the cursor is on the primary screen and secondary screens. Separate actions can be assigned to press and release. The optional ''options'' parameter can be ''disableGlobalHotkeyRegister'' to allow apps on the server to respond to the original keystroke without OS blocking.| |mousebutton(button) | actions| Binds the modifier and mouse button combination ''button'' to the given ''actions''. ''button'' is an optional list of modifiers (''shift'', ''control'', ''alt'', ''meta'' or ''super'') followed by a button number. The primary button (the left button for right handed users) is button 1, the middle button is 2, etc. Actions can be found below. Mouse button actions are not handled while the cursor is on the primary screen. You cannot use these to perform an action while on the primary screen. Separate actions can be assigned to press and release.| @@ -413,16 +413,17 @@ You can use both the ''switchDelay'' and ''switchDoubleTap'' options at the same Actions are two lists of individual actions separated by commas. The two lists are separated by a '';'' (semicolon). Either list can be empty and if the second list is empty then the semicolon is optional. The first list lists actions to take when the condition becomes true (e.g. the hot key or mouse button is pressed) and the second lists actions to take when the condition becomes false (e.g. the hot key or button is released). The condition becoming true is called activation and becoming false is called deactivation. Allowed individual actions are: -* `keystroke(key[,screens])` +* `keystroke(key[,screens[,options]])` -* `keyDown(key[,screens])` +* `keyDown(key[,screens[,options]])` -* `keyUp(key[,screens])` +* `keyUp(key[,screens[,options]])` : Synthesizes the modifiers and key given in ''key'' which has the same form as described in the ''keystroke'' option. If given, ''screens'' lists the screen or screens to direct the event to, regardless of the active screen. If not given then the event is directed to the active screen only. : ''keyDown'' synthesizes a key press and ''keyUp'' synthesizes a key release. ''keystroke'' synthesizes a key press on activation and a release on deactivation and is equivalent to a ''keyDown'' on activation and ''keyUp'' on deactivation. : ''screens'' is either ''*'' (asterisk) to indicate all screens or a '':'' (colon) separated list of screen names. (Note that the screen name must have already been encountered in the configuration file so you'll probably want to put ''actions'' at the bottom of the file.) +: The optional ''options'' parameter can be ''activeScreenOnly'' to perform the action only when the specified screen is currently active. This is useful for creating screen-specific hotkeys that only work when that screen is in focus. * `mousebutton(button)` * `mouseDown(button)` @@ -682,6 +683,40 @@ section: options end ``` +### Active Screen Only Hotkeys + +The following example shows how to use the `activeScreenOnly` option to create screen-specific hotkeys. This is useful when you want different hotkey behaviors on different screens. + +``` +# Physical monitor arrangement +# +----------+----------+ +# | Server | Mac-Mini | +# | | | +# +----------+----------+ + +section: screens + Server: + Mac-Mini: +end + +section: links + Server: + right = Mac-Mini + Mac-Mini: + left = Server +end + +section: options + # Map Control key combinations to Super (Command) key on Mac, only when Mac is active + # Using disableGlobalHotkeyRegister allows the server to still use Control shortcuts normally + keystroke(Control+Left,disableGlobalHotkeyRegister) = keystroke(Super+Left,Mac-Mini,activeScreenOnly) + keystroke(Control+Right,disableGlobalHotkeyRegister) = keystroke(Super+Right,Mac-Mini,activeScreenOnly) + keystroke(Control+Up,disableGlobalHotkeyRegister) = keystroke(Super+Up,Mac-Mini,activeScreenOnly) + keystroke(Control+c,disableGlobalHotkeyRegister) = keystroke(Super+c,Mac-Mini,activeScreenOnly) + keystroke(Control+v,disableGlobalHotkeyRegister) = keystroke(Super+v,Mac-Mini,activeScreenOnly) +end +``` + ### AltGr key The following screen config allows the mapping for ''Alt'' to ''AltGr''. Although this may not work, see [https://github.com/deskflow/deskflow-core/issues/4411 bug #4411]. From 5ca471bdaff82dfeef01c6394ecdacfa310359d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:33:56 +0000 Subject: [PATCH 04/15] Simplify option parsing functions Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- src/lib/server/Config.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/lib/server/Config.cpp b/src/lib/server/Config.cpp index 648584474890..235b564d2e01 100644 --- a/src/lib/server/Config.cpp +++ b/src/lib/server/Config.cpp @@ -1199,20 +1199,12 @@ void Config::parseKeystrokeConditionOptions( const ConfigReadContext &c, const std::string &s, bool &disableGlobalHotkeyRegister ) const { - if (s == "disableGlobalHotkeyRegister") { - disableGlobalHotkeyRegister = true; - } else { - disableGlobalHotkeyRegister = false; - } + disableGlobalHotkeyRegister = (s == "disableGlobalHotkeyRegister"); } void Config::parseKeystrokeActionOptions(const ConfigReadContext &c, const std::string &s, bool &activeScreenOnly) const { - if (s == "activeScreenOnly") { - activeScreenOnly = true; - } else { - activeScreenOnly = false; - } + activeScreenOnly = (s == "activeScreenOnly"); } const char *Config::getOptionName(OptionID id) From bb5c8dfa2330d305aa7eb6a019c75d63ee693062 Mon Sep 17 00:00:00 2001 From: axm Date: Sun, 18 Jan 2026 16:57:30 -0500 Subject: [PATCH 05/15] lint --- src/lib/server/Config.cpp | 3 +-- src/lib/server/Config.h | 10 +++++----- src/lib/server/Server.cpp | 3 ++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/server/Config.cpp b/src/lib/server/Config.cpp index 235b564d2e01..19df363cf413 100644 --- a/src/lib/server/Config.cpp +++ b/src/lib/server/Config.cpp @@ -2025,8 +2025,7 @@ ConfigReadContext::parseKeystroke(const std::string &keystroke, const std::set &screens, bool activeScreenOnly ) const { diff --git a/src/lib/server/Config.h b/src/lib/server/Config.h index ae1e1b254b35..51a97d8d2e5f 100644 --- a/src/lib/server/Config.h +++ b/src/lib/server/Config.h @@ -463,8 +463,9 @@ class Config ); void parseScreens(const ConfigReadContext &, const std::string_view &, std::set &screens) const; - void parseKeystrokeConditionOptions(const ConfigReadContext &c, const std::string &s, bool &disableGlobalHotkeyRegister) - const; + void parseKeystrokeConditionOptions( + const ConfigReadContext &c, const std::string &s, bool &disableGlobalHotkeyRegister + ) const; void parseKeystrokeActionOptions(const ConfigReadContext &c, const std::string &s, bool &activeScreenOnly) const; static const char *getOptionName(OptionID); static std::string getOptionValue(OptionID, OptionValue); @@ -509,9 +510,8 @@ class ConfigReadContext ) const; IPlatformScreen::KeyInfo *parseKeystroke(const std::string &keystroke) const; IPlatformScreen::KeyInfo *parseKeystroke(const std::string &keystroke, const std::set &screens) const; - IPlatformScreen::KeyInfo *parseKeystroke( - const std::string &keystroke, const std::set &screens, bool activeScreenOnly - ) const; + IPlatformScreen::KeyInfo * + parseKeystroke(const std::string &keystroke, const std::set &screens, bool activeScreenOnly) const; IPlatformScreen::ButtonInfo parseMouse(const std::string &mouse) const; KeyModifierMask parseModifier(const std::string &modifiers) const; std::istream &getStream() const diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index e5f7a4602f92..a325c121d2bb 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -1532,7 +1532,8 @@ void Server::onScreensaver(bool activated) } void Server::onKeyDown( - KeyID id, KeyModifierMask mask, KeyButton button, const std::string &lang, const char *screens, bool activeScreenOnly + KeyID id, KeyModifierMask mask, KeyButton button, const std::string &lang, const char *screens, + bool activeScreenOnly ) { LOG_DEBUG1("onKeyDown id=%d mask=0x%04x button=0x%04x lang=%s", id, mask, button, lang.c_str()); From 1d34a8117f81f9f492848ab0e1c02a2215772023 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:03:26 +0000 Subject: [PATCH 06/15] Fix compilation error: pass language parameter to Screen::keyDown() Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- src/lib/server/PrimaryClient.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/server/PrimaryClient.cpp b/src/lib/server/PrimaryClient.cpp index 94c166411470..b81cc982c6c6 100644 --- a/src/lib/server/PrimaryClient.cpp +++ b/src/lib/server/PrimaryClient.cpp @@ -139,10 +139,10 @@ void PrimaryClient::setClipboardDirty(ClipboardID id, bool dirty) m_clipboardDirty[id] = dirty; } -void PrimaryClient::keyDown(KeyID key, KeyModifierMask mask, KeyButton button, const std::string &) +void PrimaryClient::keyDown(KeyID key, KeyModifierMask mask, KeyButton button, const std::string &lang) { if (m_fakeInputCount > 0) { - m_screen->keyDown(key, mask, button); + m_screen->keyDown(key, mask, button, lang); } } From 6a45d64edff54918f631925849abafdf72d28dbf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:13:40 +0000 Subject: [PATCH 07/15] Fix segfault in IKeyStateTests: allocate proper memory for flexible array member The KeyInfo struct uses a flexible array member for m_screensBuffer. On non-Windows systems, the code was using 'new KeyInfo()' which only allocated the base struct size, causing buffer overruns when copying screen names. This pre-existing bug was exposed by the new test. Now using malloc() on all platforms to allocate sizeof(KeyInfo) + string length, ensuring sufficient space for the screen names string. Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- src/lib/deskflow/IKeyState.cpp | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/lib/deskflow/IKeyState.cpp b/src/lib/deskflow/IKeyState.cpp index c5c4a0eaaa17..3b8d73db92e1 100644 --- a/src/lib/deskflow/IKeyState.cpp +++ b/src/lib/deskflow/IKeyState.cpp @@ -51,13 +51,8 @@ IKeyState::KeyInfo *IKeyState::KeyInfo::alloc( std::string screens = join(destinations); const char *buffer = screens.c_str(); - // build structure -#if SYSAPI_WIN32 - // On windows we use malloc to avoid random test failures + // build structure - allocate extra space for the screens string auto *info = (KeyInfo *)malloc(sizeof(KeyInfo) + screens.size()); -#else - auto *info = new KeyInfo(); -#endif info->m_key = id; info->m_mask = mask; @@ -73,12 +68,8 @@ IKeyState::KeyInfo *IKeyState::KeyInfo::alloc(const KeyInfo &x) { auto bufferLen = strnlen(x.m_screensBuffer, SIZE_MAX); -#if SYSAPI_WIN32 - // On windows we use malloc to avoid random test failures - auto info = (KeyInfo *)malloc(sizeof(KeyInfo) + bufferLen); -#else - auto *info = new KeyInfo(); -#endif + // allocate extra space for the screens string + auto *info = (KeyInfo *)malloc(sizeof(KeyInfo) + bufferLen); info->m_key = x.m_key; info->m_mask = x.m_mask; From 9e750caedd6970c379c2e09bde233c8622c2f80c Mon Sep 17 00:00:00 2001 From: axm2 <24306299+axm2@users.noreply.github.com> Date: Sun, 18 Jan 2026 17:25:52 -0500 Subject: [PATCH 08/15] Potential fix for code scanning alert no. 5: Mismatching new/free or malloc/delete Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/lib/server/Server.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index a325c121d2bb..221113126953 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -205,7 +205,11 @@ bool Server::setConfig(const ServerConfig &config) // ScrollLock as a hotkey. if (!m_disableLockToScreen && !m_config->hasLockToScreenAction()) { IPlatformScreen::KeyInfo *key = IPlatformScreen::KeyInfo::alloc(kKeyScrollLock, 0, 0, 0); - InputFilter::Rule rule(new InputFilter::KeystrokeCondition(m_events, key, true)); + // Create a copy using new so that KeystrokeCondition can safely delete it + IPlatformScreen::KeyInfo *keyCopy = new IPlatformScreen::KeyInfo(*key); + // Free the original KeyInfo using the appropriate deallocator + std::free(key); + InputFilter::Rule rule(new InputFilter::KeystrokeCondition(m_events, keyCopy, true)); rule.adoptAction(new InputFilter::LockCursorToScreenAction(m_events), true); m_inputFilter->addFilterRule(rule); } From 1242e7cc0fe7b5a50e276ad25b35f2704163f038 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:51:51 +0000 Subject: [PATCH 09/15] Add support for multiple clients with activeScreenOnly feature Users can now specify multiple screens for activeScreenOnly in two ways: 1. Colon-separated screen list: keystroke(Control+c) = keystroke(Super+c,Mac-Mini:Macbook,activeScreenOnly) 2. Chained actions with commas: keystroke(Control+v) = keystroke(Super+v,Mac-Mini,activeScreenOnly),keystroke(Super+v,Macbook,activeScreenOnly) Updated documentation with examples for both approaches. Added test case to verify multiple screens configuration works correctly. Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- doc/user/configuration.md | 34 +++++++++++++++++ src/unittests/server/ServerConfigTests.cpp | 44 ++++++++++++++++++++++ src/unittests/server/ServerConfigTests.h | 1 + 3 files changed, 79 insertions(+) diff --git a/doc/user/configuration.md b/doc/user/configuration.md index 0b4c697eb98d..46b4a7259e10 100644 --- a/doc/user/configuration.md +++ b/doc/user/configuration.md @@ -717,6 +717,40 @@ section: options end ``` +For multiple clients, you can use a colon-separated list of screen names or chain actions with commas: + +``` +# Physical monitor arrangement +# +----------+----------+----------+ +# | Server | Mac-Mini | Macbook | +# | | | | +# +----------+----------+----------+ + +section: screens + Server: + Mac-Mini: + Macbook: +end + +section: links + Server: + right = Mac-Mini + Mac-Mini: + left = Server + right = Macbook + Macbook: + left = Mac-Mini +end + +section: options + # Option 1: Use colon-separated screen list (applies same action to multiple screens) + keystroke(Control+c,disableGlobalHotkeyRegister) = keystroke(Super+c,Mac-Mini:Macbook,activeScreenOnly) + + # Option 2: Chain multiple actions with commas (different actions per screen) + keystroke(Control+Left,disableGlobalHotkeyRegister) = keystroke(Super+Left,Mac-Mini,activeScreenOnly),keystroke(Super+Left,Macbook,activeScreenOnly) +end +``` + ### AltGr key The following screen config allows the mapping for ''Alt'' to ''AltGr''. Although this may not work, see [https://github.com/deskflow/deskflow-core/issues/4411 bug #4411]. diff --git a/src/unittests/server/ServerConfigTests.cpp b/src/unittests/server/ServerConfigTests.cpp index aa870a9524c3..7e50c4045fb6 100644 --- a/src/unittests/server/ServerConfigTests.cpp +++ b/src/unittests/server/ServerConfigTests.cpp @@ -161,4 +161,48 @@ void ServerConfigTests::equalityCheck_diff_neighbours3() QVERIFY(a != b); } +void ServerConfigTests::activeScreenOnly_multipleScreens() +{ + // Test that activeScreenOnly works with multiple screens using colon-separated list + const char *configStr = R"( +section: screens + Server: + Mac-Mini: + Macbook: +end + +section: links + Server: + right = Mac-Mini + Mac-Mini: + left = Server + right = Macbook + Macbook: + left = Mac-Mini +end + +section: options + # Test colon-separated screen list + keystroke(Control+c,disableGlobalHotkeyRegister) = keystroke(Super+c,Mac-Mini:Macbook,activeScreenOnly) + + # Test chained actions + keystroke(Control+v,disableGlobalHotkeyRegister) = keystroke(Super+v,Mac-Mini,activeScreenOnly),keystroke(Super+v,Macbook,activeScreenOnly) +end +)"; + + std::istringstream configStream(configStr); + Config config(nullptr); + + // Parse the config - this should succeed without throwing + try { + config.read(configStream); + // Verify screens were added + QVERIFY(config.isScreen("Server")); + QVERIFY(config.isScreen("Mac-Mini")); + QVERIFY(config.isScreen("Macbook")); + } catch (const std::exception &e) { + QFAIL(e.what()); + } +} + QTEST_MAIN(ServerConfigTests) diff --git a/src/unittests/server/ServerConfigTests.h b/src/unittests/server/ServerConfigTests.h index 4bb2cf3a2546..7a0cee833f8c 100644 --- a/src/unittests/server/ServerConfigTests.h +++ b/src/unittests/server/ServerConfigTests.h @@ -18,4 +18,5 @@ private Q_SLOTS: void equalityCheck_diff_neighbours1(); void equalityCheck_diff_neighbours2(); void equalityCheck_diff_neighbours3(); + void activeScreenOnly_multipleScreens(); }; From 1f96815c4feb84e9c065ecdb053891ac4d128a86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:03:16 +0000 Subject: [PATCH 10/15] Add GUI support for activeScreenOnly and disableGlobalHotkeyRegister options Added UI elements to HotkeyDialog and ActionDialog: - HotkeyDialog: Added checkbox for disableGlobalHotkeyRegister option - ActionDialog: Added checkbox for activeScreenOnly option (only visible for key actions) Updated Action and Hotkey classes to persist new options: - Added m_activeScreenOnly and m_disableGlobalHotkeyRegister fields - Updated save/load methods to persist settings - Updated text() methods to output correct config syntax with options UI changes: - Checkboxes appear in appropriate dialogs with tooltips explaining usage - activeScreenOnly checkbox only visible for keyboard actions (not mouse) - Both options are saved/loaded with hotkey configurations Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- src/lib/gui/Action.cpp | 9 +++++++++ src/lib/gui/Action.h | 22 +++++++++++++++++++++- src/lib/gui/Hotkey.cpp | 19 ++++++++++++++++--- src/lib/gui/Hotkey.h | 10 ++++++++++ src/lib/gui/dialogs/ActionDialog.cpp | 5 +++++ src/lib/gui/dialogs/ActionDialog.ui | 12 +++++++++++- src/lib/gui/dialogs/HotkeyDialog.cpp | 2 ++ src/lib/gui/dialogs/HotkeyDialog.ui | 12 +++++++++++- 8 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/lib/gui/Action.cpp b/src/lib/gui/Action.cpp index 65717c96e8e8..7c2b127dd978 100644 --- a/src/lib/gui/Action.cpp +++ b/src/lib/gui/Action.cpp @@ -34,6 +34,11 @@ QString Action::text() const commandArgs.append(QStringLiteral(",%1").arg(screenList)); } else commandArgs.append(QStringLiteral(",*")); + + // Add activeScreenOnly option if set + if (activeScreenOnly()) { + commandArgs.append(QStringLiteral(",activeScreenOnly")); + } } text.append(m_commandTemplate.arg(commandArgs)); } break; @@ -76,6 +81,8 @@ void Action::loadSettings(QSettings &settings) setActiveOnRelease(settings.value(SettingsKeys::ActiveOnRelease, false).toBool()); setHaveScreens(settings.value(SettingsKeys::HasScreens, false).toBool()); setRestartServer(settings.value(SettingsKeys::RestartServer, false).toBool()); + setActiveScreenOnly(settings.value(SettingsKeys::ActiveScreenOnly, false).toBool()); + setDisableGlobalHotkeyRegister(settings.value(SettingsKeys::DisableGlobalHotkeyRegister, false).toBool()); } void Action::saveSettings(QSettings &settings) const @@ -96,6 +103,8 @@ void Action::saveSettings(QSettings &settings) const settings.setValue(SettingsKeys::ActiveOnRelease, activeOnRelease()); settings.setValue(SettingsKeys::HasScreens, haveScreens()); settings.setValue(SettingsKeys::RestartServer, restartServer()); + settings.setValue(SettingsKeys::ActiveScreenOnly, activeScreenOnly()); + settings.setValue(SettingsKeys::DisableGlobalHotkeyRegister, disableGlobalHotkeyRegister()); } QTextStream &operator<<(QTextStream &outStream, const Action &action) diff --git a/src/lib/gui/Action.h b/src/lib/gui/Action.h index 279d3059a972..69d2d4c1c1bb 100644 --- a/src/lib/gui/Action.h +++ b/src/lib/gui/Action.h @@ -28,6 +28,8 @@ inline static const QString LockToScreen = QStringLiteral("lockCursorToScreen"); inline static const QString ActiveOnRelease = QStringLiteral("activeOnRelease"); inline static const QString HasScreens = QStringLiteral("hasScreens"); inline static const QString RestartServer = QStringLiteral("restartServer"); +inline static const QString ActiveScreenOnly = QStringLiteral("activeScreenOnly"); +inline static const QString DisableGlobalHotkeyRegister = QStringLiteral("disableGlobalHotkeyRegister"); } // namespace SettingsKeys class Action @@ -106,6 +108,14 @@ class Action { return m_restartServer; } + bool activeScreenOnly() const + { + return m_activeScreenOnly; + } + bool disableGlobalHotkeyRegister() const + { + return m_disableGlobalHotkeyRegister; + } bool operator==(const Action &a) const = default; @@ -150,6 +160,14 @@ class Action { m_restartServer = b; } + void setActiveScreenOnly(bool b) + { + m_activeScreenOnly = b; + } + void setDisableGlobalHotkeyRegister(bool b) + { + m_disableGlobalHotkeyRegister = b; + } private: KeySequence m_keySequence; @@ -160,7 +178,9 @@ class Action int m_lockCursorMode = static_cast(LockCursorMode::toggle); bool m_activeOnRelease = false; bool m_hasScreens = false; - bool m_restartServer; + bool m_restartServer = false; + bool m_activeScreenOnly = false; + bool m_disableGlobalHotkeyRegister = false; inline static const QString m_commandTemplate = QStringLiteral("(%1)"); inline static const QStringList m_actionTypeNames{ diff --git a/src/lib/gui/Hotkey.cpp b/src/lib/gui/Hotkey.cpp index 3ec948b11314..2172576baf3a 100644 --- a/src/lib/gui/Hotkey.cpp +++ b/src/lib/gui/Hotkey.cpp @@ -12,8 +12,16 @@ QString Hotkey::text() const { - return m_keySequence.isMouseButton() ? kMousebutton.arg(m_keySequence.toString()) - : kKeystroke.arg(m_keySequence.toString()); + QString hotkeyText = m_keySequence.isMouseButton() ? kMousebutton.arg(m_keySequence.toString()) + : kKeystroke.arg(m_keySequence.toString()); + + // Add disableGlobalHotkeyRegister option if set (only for keystroke, not mousebutton) + if (!m_keySequence.isMouseButton() && m_disableGlobalHotkeyRegister) { + // Insert the option before the closing parenthesis + hotkeyText.insert(hotkeyText.length() - 1, QStringLiteral(",disableGlobalHotkeyRegister")); + } + + return hotkeyText; } void Hotkey::loadSettings(QSettings &settings) @@ -30,6 +38,8 @@ void Hotkey::loadSettings(QSettings &settings) } settings.endArray(); + + m_disableGlobalHotkeyRegister = settings.value(kDisableGlobalHotkeyRegister, false).toBool(); } void Hotkey::saveSettings(QSettings &settings) const @@ -42,11 +52,14 @@ void Hotkey::saveSettings(QSettings &settings) const m_actions.at(i).saveSettings(settings); } settings.endArray(); + + settings.setValue(kDisableGlobalHotkeyRegister, m_disableGlobalHotkeyRegister); } bool Hotkey::operator==(const Hotkey &hk) const { - return m_keySequence == hk.keySequence() && m_actions == hk.actions(); + return m_keySequence == hk.keySequence() && m_actions == hk.actions() && + m_disableGlobalHotkeyRegister == hk.disableGlobalHotkeyRegister(); } QTextStream &operator<<(QTextStream &outStream, const Hotkey &hotkey) diff --git a/src/lib/gui/Hotkey.h b/src/lib/gui/Hotkey.h index f06c892cd651..58965cf4324d 100644 --- a/src/lib/gui/Hotkey.h +++ b/src/lib/gui/Hotkey.h @@ -37,6 +37,10 @@ class Hotkey { return m_actions; } + bool disableGlobalHotkeyRegister() const + { + return m_disableGlobalHotkeyRegister; + } void loadSettings(QSettings &settings); void saveSettings(QSettings &settings) const; @@ -56,11 +60,17 @@ class Hotkey { return m_actions; } + void setDisableGlobalHotkeyRegister(bool b) + { + m_disableGlobalHotkeyRegister = b; + } private: KeySequence m_keySequence = {}; ActionList m_actions = {}; + bool m_disableGlobalHotkeyRegister = false; inline static const QString kSectionActions = QStringLiteral("actions"); + inline static const QString kDisableGlobalHotkeyRegister = QStringLiteral("disableGlobalHotkeyRegister"); inline static const QString kMousebutton = QStringLiteral("mousebutton(%1)"); inline static const QString kKeystroke = QStringLiteral("keystroke(%1)"); }; diff --git a/src/lib/gui/dialogs/ActionDialog.cpp b/src/lib/gui/dialogs/ActionDialog.cpp index ccbfc46d0ea6..c75e0d702b65 100644 --- a/src/lib/gui/dialogs/ActionDialog.cpp +++ b/src/lib/gui/dialogs/ActionDialog.cpp @@ -62,6 +62,8 @@ ActionDialog::ActionDialog(QWidget *parent, const ServerConfig &config, Hotkey & ui->comboSwitchToScreen->setVisible(false); ui->comboSwitchInDirection->setVisible(false); ui->comboLockCursorToScreen->setVisible(false); + ui->m_pCheckBoxActiveScreenOnly->setVisible(false); + ui->m_pCheckBoxActiveScreenOnly->setChecked(m_action.activeScreenOnly()); actionTypeChanged(ui->comboActionType->currentIndex()); } @@ -97,6 +99,7 @@ void ActionDialog::accept() m_action.setLockCursorMode(ui->comboLockCursorToScreen->currentIndex()); m_action.setActiveOnRelease(ui->comboTriggerOn->currentIndex()); m_action.setRestartServer(ui->comboActionType->currentIndex() == ActionTypes::RestartServer); + m_action.setActiveScreenOnly(ui->m_pCheckBoxActiveScreenOnly->isChecked()); QDialog::accept(); } @@ -112,6 +115,7 @@ void ActionDialog::updateSize() void ActionDialog::keySequenceChanged() { ui->listScreens->setEnabled(!ui->keySequenceWidget->keySequence().isMouseButton()); + ui->m_pCheckBoxActiveScreenOnly->setVisible(!ui->keySequenceWidget->keySequence().isMouseButton()); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(canSave()); } @@ -129,6 +133,7 @@ void ActionDialog::actionTypeChanged(int index) ui->comboSwitchToScreen->setVisible(index == ActionTypes::SwitchTo); ui->comboSwitchInDirection->setVisible(index == ActionTypes::SwitchInDirection); ui->comboLockCursorToScreen->setVisible(index == ActionTypes::ModifyCursorLock); + ui->m_pCheckBoxActiveScreenOnly->setVisible(isKeyAction(index) && !ui->keySequenceWidget->keySequence().isMouseButton()); QTimer::singleShot(1, this, &ActionDialog::updateSize); } diff --git a/src/lib/gui/dialogs/ActionDialog.ui b/src/lib/gui/dialogs/ActionDialog.ui index 59ebbd5292c7..37fb9cbe79c0 100644 --- a/src/lib/gui/dialogs/ActionDialog.ui +++ b/src/lib/gui/dialogs/ActionDialog.ui @@ -7,7 +7,7 @@ 0 0 706 - 256 + 290 @@ -167,6 +167,16 @@ + + + + Only fire when target screen is active (activeScreenOnly) + + + When enabled, this action will only fire when the specified screen(s) are currently active + + + diff --git a/src/lib/gui/dialogs/HotkeyDialog.cpp b/src/lib/gui/dialogs/HotkeyDialog.cpp index 1fb11b7c9544..4bd657d78c2d 100644 --- a/src/lib/gui/dialogs/HotkeyDialog.cpp +++ b/src/lib/gui/dialogs/HotkeyDialog.cpp @@ -16,6 +16,7 @@ HotkeyDialog::HotkeyDialog(QWidget *parent, Hotkey &hotkey) ui->setupUi(this); ui->m_pKeySequenceWidgetHotkey->setText(m_Hotkey.text()); + ui->m_pCheckBoxDisableGlobalHotkeyRegister->setChecked(m_Hotkey.disableGlobalHotkeyRegister()); } HotkeyDialog::~HotkeyDialog() = default; @@ -26,6 +27,7 @@ void HotkeyDialog::accept() return; hotkey().setKeySequence(sequenceWidget()->keySequence()); + hotkey().setDisableGlobalHotkeyRegister(ui->m_pCheckBoxDisableGlobalHotkeyRegister->isChecked()); QDialog::accept(); } diff --git a/src/lib/gui/dialogs/HotkeyDialog.ui b/src/lib/gui/dialogs/HotkeyDialog.ui index ed8e8927c9d2..3550c7688e38 100644 --- a/src/lib/gui/dialogs/HotkeyDialog.ui +++ b/src/lib/gui/dialogs/HotkeyDialog.ui @@ -7,7 +7,7 @@ 0 0 344 - 86 + 120 @@ -24,6 +24,16 @@ + + + + Disable global hotkey registration (allow server to use this key) + + + When enabled, the hotkey will not be registered as a global hotkey with the OS, allowing the server to handle the key normally when focused + + + From b43b968a3d37cd87da0bed35b1c1717a333d94a0 Mon Sep 17 00:00:00 2001 From: axm Date: Sun, 18 Jan 2026 18:08:52 -0500 Subject: [PATCH 11/15] lint --- src/lib/gui/Action.cpp | 2 +- src/lib/gui/Hotkey.cpp | 8 ++++---- src/lib/gui/dialogs/ActionDialog.cpp | 4 +++- src/unittests/server/ServerConfigTests.cpp | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/lib/gui/Action.cpp b/src/lib/gui/Action.cpp index 7c2b127dd978..f160367ea049 100644 --- a/src/lib/gui/Action.cpp +++ b/src/lib/gui/Action.cpp @@ -34,7 +34,7 @@ QString Action::text() const commandArgs.append(QStringLiteral(",%1").arg(screenList)); } else commandArgs.append(QStringLiteral(",*")); - + // Add activeScreenOnly option if set if (activeScreenOnly()) { commandArgs.append(QStringLiteral(",activeScreenOnly")); diff --git a/src/lib/gui/Hotkey.cpp b/src/lib/gui/Hotkey.cpp index 2172576baf3a..4cd1d8e73c69 100644 --- a/src/lib/gui/Hotkey.cpp +++ b/src/lib/gui/Hotkey.cpp @@ -14,13 +14,13 @@ QString Hotkey::text() const { QString hotkeyText = m_keySequence.isMouseButton() ? kMousebutton.arg(m_keySequence.toString()) : kKeystroke.arg(m_keySequence.toString()); - + // Add disableGlobalHotkeyRegister option if set (only for keystroke, not mousebutton) if (!m_keySequence.isMouseButton() && m_disableGlobalHotkeyRegister) { // Insert the option before the closing parenthesis hotkeyText.insert(hotkeyText.length() - 1, QStringLiteral(",disableGlobalHotkeyRegister")); } - + return hotkeyText; } @@ -38,7 +38,7 @@ void Hotkey::loadSettings(QSettings &settings) } settings.endArray(); - + m_disableGlobalHotkeyRegister = settings.value(kDisableGlobalHotkeyRegister, false).toBool(); } @@ -52,7 +52,7 @@ void Hotkey::saveSettings(QSettings &settings) const m_actions.at(i).saveSettings(settings); } settings.endArray(); - + settings.setValue(kDisableGlobalHotkeyRegister, m_disableGlobalHotkeyRegister); } diff --git a/src/lib/gui/dialogs/ActionDialog.cpp b/src/lib/gui/dialogs/ActionDialog.cpp index c75e0d702b65..38ffc3550bec 100644 --- a/src/lib/gui/dialogs/ActionDialog.cpp +++ b/src/lib/gui/dialogs/ActionDialog.cpp @@ -133,7 +133,9 @@ void ActionDialog::actionTypeChanged(int index) ui->comboSwitchToScreen->setVisible(index == ActionTypes::SwitchTo); ui->comboSwitchInDirection->setVisible(index == ActionTypes::SwitchInDirection); ui->comboLockCursorToScreen->setVisible(index == ActionTypes::ModifyCursorLock); - ui->m_pCheckBoxActiveScreenOnly->setVisible(isKeyAction(index) && !ui->keySequenceWidget->keySequence().isMouseButton()); + ui->m_pCheckBoxActiveScreenOnly->setVisible( + isKeyAction(index) && !ui->keySequenceWidget->keySequence().isMouseButton() + ); QTimer::singleShot(1, this, &ActionDialog::updateSize); } diff --git a/src/unittests/server/ServerConfigTests.cpp b/src/unittests/server/ServerConfigTests.cpp index 7e50c4045fb6..a33768d9a108 100644 --- a/src/unittests/server/ServerConfigTests.cpp +++ b/src/unittests/server/ServerConfigTests.cpp @@ -192,7 +192,7 @@ end std::istringstream configStream(configStr); Config config(nullptr); - + // Parse the config - this should succeed without throwing try { config.read(configStream); From b80157486a54989b8a67432306b3a3a4cf74883b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:18:14 +0000 Subject: [PATCH 12/15] Fix code review issues: remove disableGlobalHotkeyRegister from Action class and fix memory management - Removed m_disableGlobalHotkeyRegister field, getter, and setter from Action class (should only be on Hotkey) - Removed DisableGlobalHotkeyRegister from Action.h SettingsKeys namespace - Fixed critical memory management issue in IKeyState::KeyInfo::alloc() - changed from new to malloc to match free() in KeystrokeCondition - Simplified Server.cpp ScrollLock initialization - removed awkward copy/free workaround now that alloc uses malloc consistently Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- src/lib/deskflow/IKeyState.cpp | 2 +- src/lib/gui/Action.cpp | 2 -- src/lib/gui/Action.h | 10 ---------- src/lib/server/Server.cpp | 6 +----- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/lib/deskflow/IKeyState.cpp b/src/lib/deskflow/IKeyState.cpp index 3b8d73db92e1..b83d007dda86 100644 --- a/src/lib/deskflow/IKeyState.cpp +++ b/src/lib/deskflow/IKeyState.cpp @@ -25,7 +25,7 @@ IKeyState::IKeyState(const IEventQueue *) IKeyState::KeyInfo *IKeyState::KeyInfo::alloc(KeyID id, KeyModifierMask mask, KeyButton button, int32_t count) { - auto *info = new KeyInfo(); + auto *info = (KeyInfo *)malloc(sizeof(KeyInfo)); info->m_key = id; info->m_mask = mask; info->m_button = button; diff --git a/src/lib/gui/Action.cpp b/src/lib/gui/Action.cpp index f160367ea049..b53435f76e7d 100644 --- a/src/lib/gui/Action.cpp +++ b/src/lib/gui/Action.cpp @@ -82,7 +82,6 @@ void Action::loadSettings(QSettings &settings) setHaveScreens(settings.value(SettingsKeys::HasScreens, false).toBool()); setRestartServer(settings.value(SettingsKeys::RestartServer, false).toBool()); setActiveScreenOnly(settings.value(SettingsKeys::ActiveScreenOnly, false).toBool()); - setDisableGlobalHotkeyRegister(settings.value(SettingsKeys::DisableGlobalHotkeyRegister, false).toBool()); } void Action::saveSettings(QSettings &settings) const @@ -104,7 +103,6 @@ void Action::saveSettings(QSettings &settings) const settings.setValue(SettingsKeys::HasScreens, haveScreens()); settings.setValue(SettingsKeys::RestartServer, restartServer()); settings.setValue(SettingsKeys::ActiveScreenOnly, activeScreenOnly()); - settings.setValue(SettingsKeys::DisableGlobalHotkeyRegister, disableGlobalHotkeyRegister()); } QTextStream &operator<<(QTextStream &outStream, const Action &action) diff --git a/src/lib/gui/Action.h b/src/lib/gui/Action.h index 69d2d4c1c1bb..b5cba2d45c0e 100644 --- a/src/lib/gui/Action.h +++ b/src/lib/gui/Action.h @@ -29,7 +29,6 @@ inline static const QString ActiveOnRelease = QStringLiteral("activeOnRelease"); inline static const QString HasScreens = QStringLiteral("hasScreens"); inline static const QString RestartServer = QStringLiteral("restartServer"); inline static const QString ActiveScreenOnly = QStringLiteral("activeScreenOnly"); -inline static const QString DisableGlobalHotkeyRegister = QStringLiteral("disableGlobalHotkeyRegister"); } // namespace SettingsKeys class Action @@ -112,10 +111,6 @@ class Action { return m_activeScreenOnly; } - bool disableGlobalHotkeyRegister() const - { - return m_disableGlobalHotkeyRegister; - } bool operator==(const Action &a) const = default; @@ -164,10 +159,6 @@ class Action { m_activeScreenOnly = b; } - void setDisableGlobalHotkeyRegister(bool b) - { - m_disableGlobalHotkeyRegister = b; - } private: KeySequence m_keySequence; @@ -180,7 +171,6 @@ class Action bool m_hasScreens = false; bool m_restartServer = false; bool m_activeScreenOnly = false; - bool m_disableGlobalHotkeyRegister = false; inline static const QString m_commandTemplate = QStringLiteral("(%1)"); inline static const QStringList m_actionTypeNames{ diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index 221113126953..a325c121d2bb 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -205,11 +205,7 @@ bool Server::setConfig(const ServerConfig &config) // ScrollLock as a hotkey. if (!m_disableLockToScreen && !m_config->hasLockToScreenAction()) { IPlatformScreen::KeyInfo *key = IPlatformScreen::KeyInfo::alloc(kKeyScrollLock, 0, 0, 0); - // Create a copy using new so that KeystrokeCondition can safely delete it - IPlatformScreen::KeyInfo *keyCopy = new IPlatformScreen::KeyInfo(*key); - // Free the original KeyInfo using the appropriate deallocator - std::free(key); - InputFilter::Rule rule(new InputFilter::KeystrokeCondition(m_events, keyCopy, true)); + InputFilter::Rule rule(new InputFilter::KeystrokeCondition(m_events, key, true)); rule.adoptAction(new InputFilter::LockCursorToScreenAction(m_events), true); m_inputFilter->addFilterRule(rule); } From 86a73cc4942ee06aedcac481e3dad91db66e7925 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:27:08 +0000 Subject: [PATCH 13/15] Add validation for hotkey options and document server screen limitation - Added error checking in parseKeystrokeConditionOptions() and parseKeystrokeActionOptions() to throw exceptions for invalid/unknown option strings instead of silently ignoring them - This helps users catch typos like "activeScreenonly" or "disableGlobalHotKeyRegister" - Added documentation warning about activeScreenOnly limitation when targeting the primary/server screen with the same keystroke as the condition - Improved code clarity with explicit default value initialization and early returns Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- doc/user/configuration.md | 2 ++ src/lib/server/Config.cpp | 30 ++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/doc/user/configuration.md b/doc/user/configuration.md index 46b4a7259e10..5ed5fe81c39f 100644 --- a/doc/user/configuration.md +++ b/doc/user/configuration.md @@ -687,6 +687,8 @@ end The following example shows how to use the `activeScreenOnly` option to create screen-specific hotkeys. This is useful when you want different hotkey behaviors on different screens. +**Important limitation**: When using `activeScreenOnly` with the primary/server screen as a target, the action may not work if it uses the same keystroke as the condition. This is because the primary client registers the original keystroke with the OS as a hotkey, which blocks Deskflow from creating fake events for them. To work around this, ensure that actions targeting the server screen use different keystrokes than the condition, or only target client screens. + ``` # Physical monitor arrangement # +----------+----------+ diff --git a/src/lib/server/Config.cpp b/src/lib/server/Config.cpp index 19df363cf413..35bf416dc89c 100644 --- a/src/lib/server/Config.cpp +++ b/src/lib/server/Config.cpp @@ -1199,12 +1199,38 @@ void Config::parseKeystrokeConditionOptions( const ConfigReadContext &c, const std::string &s, bool &disableGlobalHotkeyRegister ) const { - disableGlobalHotkeyRegister = (s == "disableGlobalHotkeyRegister"); + // Default to false when no option is provided + disableGlobalHotkeyRegister = false; + + if (s.empty()) { + return; + } + + if (s == "disableGlobalHotkeyRegister") { + disableGlobalHotkeyRegister = true; + return; + } + + // Unknown option string: fail fast to surface configuration errors + throw ServerConfigReadException(c, "unknown keystroke condition option \"%{1}\"", s); } void Config::parseKeystrokeActionOptions(const ConfigReadContext &c, const std::string &s, bool &activeScreenOnly) const { - activeScreenOnly = (s == "activeScreenOnly"); + // Default to false when no option is provided + activeScreenOnly = false; + + if (s.empty()) { + return; + } + + if (s == "activeScreenOnly") { + activeScreenOnly = true; + return; + } + + // Unknown option string: fail fast to surface configuration errors + throw ServerConfigReadException(c, "unknown keystroke action option \"%{1}\"", s); } const char *Config::getOptionName(OptionID id) From ef435af74009d1bbd31ee28ac656941206ca45e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:39:48 +0000 Subject: [PATCH 14/15] Fix ServerConfigTests compilation error: use operator>> instead of read() Changed config.read(configStream) to configStream >> config to match the correct API. The read() method takes a ConfigReadContext&, while operator>> handles stream input properly. Co-authored-by: axm2 <24306299+axm2@users.noreply.github.com> --- src/unittests/server/ServerConfigTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unittests/server/ServerConfigTests.cpp b/src/unittests/server/ServerConfigTests.cpp index a33768d9a108..b1adbfb512fa 100644 --- a/src/unittests/server/ServerConfigTests.cpp +++ b/src/unittests/server/ServerConfigTests.cpp @@ -195,7 +195,7 @@ end // Parse the config - this should succeed without throwing try { - config.read(configStream); + configStream >> config; // Verify screens were added QVERIFY(config.isScreen("Server")); QVERIFY(config.isScreen("Mac-Mini")); From e72ce1abecea20bbe95cae3ede815bda847ffe86 Mon Sep 17 00:00:00 2001 From: axm Date: Sun, 18 Jan 2026 21:12:29 -0500 Subject: [PATCH 15/15] Remove useless test --- src/unittests/server/CMakeLists.txt | 2 +- src/unittests/server/ServerConfigTests.cpp | 44 ---------------------- src/unittests/server/ServerConfigTests.h | 1 - 3 files changed, 1 insertion(+), 46 deletions(-) diff --git a/src/unittests/server/CMakeLists.txt b/src/unittests/server/CMakeLists.txt index 864d1462f816..5f0eeb70a392 100644 --- a/src/unittests/server/CMakeLists.txt +++ b/src/unittests/server/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: (C) 2025 Deskflow Developers +# SPDX-FileCopyrightText: 2025 Deskflow Developers # SPDX-License-Identifier: MIT if(WIN32) diff --git a/src/unittests/server/ServerConfigTests.cpp b/src/unittests/server/ServerConfigTests.cpp index b1adbfb512fa..aa870a9524c3 100644 --- a/src/unittests/server/ServerConfigTests.cpp +++ b/src/unittests/server/ServerConfigTests.cpp @@ -161,48 +161,4 @@ void ServerConfigTests::equalityCheck_diff_neighbours3() QVERIFY(a != b); } -void ServerConfigTests::activeScreenOnly_multipleScreens() -{ - // Test that activeScreenOnly works with multiple screens using colon-separated list - const char *configStr = R"( -section: screens - Server: - Mac-Mini: - Macbook: -end - -section: links - Server: - right = Mac-Mini - Mac-Mini: - left = Server - right = Macbook - Macbook: - left = Mac-Mini -end - -section: options - # Test colon-separated screen list - keystroke(Control+c,disableGlobalHotkeyRegister) = keystroke(Super+c,Mac-Mini:Macbook,activeScreenOnly) - - # Test chained actions - keystroke(Control+v,disableGlobalHotkeyRegister) = keystroke(Super+v,Mac-Mini,activeScreenOnly),keystroke(Super+v,Macbook,activeScreenOnly) -end -)"; - - std::istringstream configStream(configStr); - Config config(nullptr); - - // Parse the config - this should succeed without throwing - try { - configStream >> config; - // Verify screens were added - QVERIFY(config.isScreen("Server")); - QVERIFY(config.isScreen("Mac-Mini")); - QVERIFY(config.isScreen("Macbook")); - } catch (const std::exception &e) { - QFAIL(e.what()); - } -} - QTEST_MAIN(ServerConfigTests) diff --git a/src/unittests/server/ServerConfigTests.h b/src/unittests/server/ServerConfigTests.h index 7a0cee833f8c..4bb2cf3a2546 100644 --- a/src/unittests/server/ServerConfigTests.h +++ b/src/unittests/server/ServerConfigTests.h @@ -18,5 +18,4 @@ private Q_SLOTS: void equalityCheck_diff_neighbours1(); void equalityCheck_diff_neighbours2(); void equalityCheck_diff_neighbours3(); - void activeScreenOnly_multipleScreens(); };