Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions doc/user/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.|


Expand All @@ -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)`
Expand Down Expand Up @@ -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
```
Comment on lines +686 to +720

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation should mention the limitation described in the code comments: when using activeScreenOnly with the primary/server screen, the action won't work if it uses the same keystroke as the condition, because the primary client registers the original keystroke with the OS as a hotkey, which blocks Deskflow from creating fake events for them. Users should be aware of this limitation when configuring hotkeys for the server screen.

Copilot uses AI. Check for mistakes.

### 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].
Expand Down
26 changes: 14 additions & 12 deletions src/lib/deskflow/IKeyState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,22 +39,26 @@ 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<std::string> &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<std::string> &destinations,
bool activeScreenOnly
)
{
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;
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;
Expand All @@ -63,17 +68,14 @@ 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;
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;
Expand Down
5 changes: 5 additions & 0 deletions src/lib/deskflow/IKeyState.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> &destinations);
static KeyInfo *alloc(
KeyID, KeyModifierMask, KeyButton, int32_t count, const std::set<std::string> &destinations,
bool activeScreenOnly
);
static KeyInfo *alloc(const KeyInfo &);

static bool isDefault(const char *screens);
Expand All @@ -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];
};
Expand Down
4 changes: 2 additions & 2 deletions src/lib/deskflow/IPrimaryScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
/*!
Expand Down
4 changes: 2 additions & 2 deletions src/lib/deskflow/PlatformScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions src/lib/deskflow/Screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions src/lib/deskflow/Screen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
/*!
Expand Down
6 changes: 4 additions & 2 deletions src/lib/platform/EiScreen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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)) {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/platform/EiScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 6 additions & 8 deletions src/lib/platform/MSWindowsScreen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/platform/MSWindowsScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/platform/OSXScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 12 additions & 8 deletions src/lib/platform/OSXScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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;
}

Expand All @@ -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);
Expand All @@ -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
Expand Down
Loading
Loading