Compare commits

..

No commits in common. "0b7a72ae0ea25532e002dd25245e39ca4343a5a3" and "adb4f89453c890033288bf2ed6f36fa76f5caec5" have entirely different histories.

37 changed files with 215 additions and 1698 deletions

View file

@ -155,11 +155,10 @@ This table lists all protocol messages in alphabetical order. For a typical sequ
| [**DDRG**](@ref kMsgDDragInfo) | @ref kMsgDDragInfo | Data | Server→Client | Drag file info | [MsgSize](#constraint-protocol-max-message-length), [ListSize](#constraint-max-list) | 1.5+ |
| [**DFTR**](@ref kMsgDFileTransfer) | @ref kMsgDFileTransfer | Data | Both | File transfer data | [MsgSize](#constraint-protocol-max-message-length) | 1.5+ |
| [**DINF**](@ref kMsgDInfo) | @ref kMsgDInfo | Data | Client→Server | Screen information | [MsgSize](#constraint-protocol-max-message-length) | 1.0+ |
| [**DKDL**](@ref kMsgDKeyDown) | @ref kMsgDKeyDown | Data | Server→Client | Key down with language | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.8+ |
| [**DKDN**](@ref kMsgDKeyDown1_1) | @ref kMsgDKeyDown1_1 | Data | Server→Client | Key down | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.1+ |
| [**DKDL**](@ref kMsgDKeyDownLang) | @ref kMsgDKeyDownLang | Data | Server→Client | Key down with language | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.8+ |
| [**DKDN**](@ref kMsgDKeyDown) | @ref kMsgDKeyDown | Data | Server→Client | Key down | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.1+ |
| [**DKDN**](@ref kMsgDKeyDown1_0) | @ref kMsgDKeyDown1_0 | Data | Server→Client | Key down (legacy) | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.0 |
| [**DKRP**](@ref kMsgDKeyRepeat) | @ref kMsgDKeyRepeat | Data | Server→Client | Key repeat with language | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.8+ |
| [**DKRP**](@ref kMsgDKeyRepeat1_1) | @ref kMsgDKeyRepeat1_1 | Data | Server→Client | Key repeat | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.1+ |
| [**DKRP**](@ref kMsgDKeyRepeat) | @ref kMsgDKeyRepeat | Data | Server→Client | Key repeat | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.1+ |
| [**DKRP**](@ref kMsgDKeyRepeat1_0) | @ref kMsgDKeyRepeat1_0 | Data | Server→Client | Key repeat (legacy) | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.0 |
| [**DKUP**](@ref kMsgDKeyUp) | @ref kMsgDKeyUp | Data | Server→Client | Key up | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.1+ |
| [**DKUP**](@ref kMsgDKeyUp1_0) | @ref kMsgDKeyUp1_0 | Data | Server→Client | Key up (legacy) | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.0 |
@ -289,8 +288,8 @@ A modifier (modifier mask) represents the state of modifier keys (like Shift, Co
| **1.4** | Nov 2012 | Synergy | Encryption support (@ref SecureSocket) | 1.4+ |
| **1.5** | Sep 2013 | Synergy | File transfer | 1.5+ |
| **1.6** | Jan 2014 | Synergy | Clipboard streaming | 1.6+ |
| **1.7** | Sep 2021 | Synergy | Secure input notifications | 1.7+ |
| **1.8** | Nov 2021 | Synergy | Language synchronization | 1.8+ |
| **1.7** | Nov 2021 | Synergy | Secure input notifications | 1.7+ |
| **1.8** | Jun 2025 | Synergy | Language synchronization | 1.8+ |
### Version Migration Guide

View file

@ -1,17 +1,9 @@
# SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
# SPDX-FileCopyrightText: (C) 2024 Chris Rizzitello <sithlord48@gmail.com>
# SPDX-FileCopyrightText: (C) 2012 - 2024 Symless Ltd
# SPDX-FileCopyrightText: (C) 2009 - 2012 Nick Bolton
# SPDX-License-Identifier: MIT
if(APPLE)
set(PLATFORM_CODE
OSXAutoReleasePool.h
OSXAutoReleasePool.mm
)
endif()
add_library(base STATIC ${PLATFORM_CODE}
add_library(base STATIC
BaseException.cpp
BaseException.h
DirectionTypes.h

View file

@ -18,10 +18,6 @@
#include <stdexcept>
#if defined(__APPLE__)
#include "base/OSXAutoReleasePool.h"
#endif
// interrupt handler. this just adds a quit event to the queue.
static void interrupt(Arch::ThreadSignal, void *data)
{
@ -170,9 +166,6 @@ bool EventQueue::getEvent(Event &event, double timeout)
bool EventQueue::dispatchEvent(const Event &event)
{
#if defined(__APPLE__)
auto dispatch = [&] {
#endif
void *target = event.getTarget();
if (auto typeHandler = getHandler(event.getType(), target); typeHandler.has_value()) {
(*typeHandler)(event);
@ -183,10 +176,6 @@ bool EventQueue::dispatchEvent(const Event &event)
return true;
}
return false;
#if defined(__APPLE__)
};
return deskflow::runInAutoReleasePool(dispatch);
#endif
}
void EventQueue::addEvent(Event &&event)

View file

@ -138,7 +138,7 @@ bool FileLogOutputter::write(LogLevel::Level, const QString &message)
if (file.size() > s_logFileSizeLimit) {
const auto oldFile = QStringLiteral("%1.1").arg(m_fileName);
QFile::remove(oldFile);
QFile::remove(m_fileName);
QFile::rename(m_fileName, oldFile);
}

View file

@ -1,16 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include <functional>
namespace deskflow {
//! Run \p fn inside an Objective-C \c \@autoreleasepool block.
bool runInAutoReleasePool(const std::function<bool()> &fn);
} // namespace deskflow

View file

@ -1,20 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#import "base/OSXAutoReleasePool.h"
#import <Foundation/Foundation.h>
namespace deskflow {
bool runInAutoReleasePool(const std::function<bool()> &fn)
{
@autoreleasepool {
return fn();
}
}
} // namespace deskflow

View file

@ -8,10 +8,6 @@ add_library(client STATIC
Client.h
ServerProxy.cpp
ServerProxy.h
ServerProxy1_7.cpp
ServerProxy1_7.h
ServerProxy1_8.cpp
ServerProxy1_8.h
)
target_link_libraries(client PUBLIC common)

View file

@ -12,8 +12,6 @@
#include "base/IEventQueue.h"
#include "base/Log.h"
#include "client/ServerProxy.h"
#include "client/ServerProxy1_7.h"
#include "client/ServerProxy1_8.h"
#include "common/NetworkProtocol.h"
#include "common/Settings.h"
#include "deskflow/Clipboard.h"
@ -467,38 +465,18 @@ void Client::setupConnection()
});
}
bool Client::setupScreen(int16_t protocolMinor)
void Client::setupScreen()
{
assert(m_server == nullptr);
m_ready = false;
// only 1.6 and later have a proxy: the clipboard, mouse wheel and key message formats
// differ below that, and nothing older (synergy 1.4 and earlier) still needs supporting.
// a version with no case is refused by the hello handler.
switch (protocolMinor) {
case 6:
m_server = new ServerProxy(this, m_stream, m_events);
break;
case 7:
m_server = new ServerProxy1_7(this, m_stream, m_events);
break;
case 8:
m_server = new ServerProxy1_8(this, m_stream, m_events);
break;
default:
break;
}
if (m_server != nullptr) {
m_events->addHandler(EventTypes::ScreenShapeChanged, getEventTarget(), [this](const auto &) {
handleShapeChanged();
});
m_events->addHandler(EventTypes::ClipboardGrabbed, getEventTarget(), [this](const auto &e) {
handleClipboardGrabbed(e);
});
}
return m_server != nullptr;
}
void Client::setupTimer()
@ -705,17 +683,6 @@ void Client::handleHello()
);
}
// no proxy speaks the negotiated version, so hang up as incompatible rather than
// talk a version the client does not implement, the same rule the server applies
// when it picks a client proxy
if (!setupScreen(helloBackMinor)) {
LOG_WARN("server protocol version not supported: %d.%d", serverMajor, serverMinor);
sendConnectionFailedEvent(IncompatibleClientException(serverMajor, serverMinor).what());
cleanupTimer();
cleanupConnection();
return;
}
LOG_DEBUG("saying hello back with version %s %d.%d", protocolName.c_str(), kProtocolMajorVersion, helloBackMinor);
// dynamically build write format for hello back since `ProtocolUtil::writef`
@ -724,6 +691,7 @@ void Client::handleHello()
ProtocolUtil::writef(m_stream, helloBackMessage.c_str(), kProtocolMajorVersion, helloBackMinor, &m_name);
// now connected but waiting to complete handshake
setupScreen();
cleanupTimer();
// make sure we process any remaining messages later. we won't

View file

@ -197,7 +197,7 @@ private:
void sendConnectionFailedEvent(const char *msg);
void setupConnecting();
void setupConnection();
bool setupScreen(int16_t protocolMinor);
void setupScreen();
void setupTimer();
void cleanup();
void cleanupConnecting();

View file

@ -1,7 +1,7 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2012 - 2016, 2026 Synergy App Ltd
* SPDX-FileCopyrightText: (C) 2012 - 2016 Synergy App Ltd
* SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
@ -18,6 +18,7 @@
#include "deskflow/ProtocolTypes.h"
#include "deskflow/ProtocolUtil.h"
#include "deskflow/StreamChunker.h"
#include "deskflow/ipc/CoreIpc.h"
#include "io/IStream.h"
#include <cstring>
@ -139,7 +140,12 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
setOptions();
// handshake is complete
m_parser = &ServerProxy::handleMessage;
m_parser = &ServerProxy::parseMessage;
if (const auto missedKeyboardLayouts = m_layoutManager.getMissedLayouts(); !missedKeyboardLayouts.empty()) {
LOG_WARN("server layouts missing on this computer: %s", missedKeyboardLayouts.c_str());
ipcSendToClient("missingKeyboardLayouts", QString::fromStdString(missedKeyboardLayouts));
}
m_client->handshakeComplete();
}
@ -190,6 +196,8 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
LOG_ERR("server disconnected due to a protocol error");
requestRefuseConnection(ProtocolError, "server reported a protocol error");
return Disconnect;
} else if (memcmp(code, kMsgDLanguageSynchronisation, 4) == 0) {
setServerLanguages();
} else {
return Unknown;
}
@ -197,22 +205,6 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
return Okay;
}
ServerProxy::ConnectionResult ServerProxy::handleMessage(const uint8_t *code)
{
const auto result = parseMessage(code);
if (result == ConnectionResult::Okay) {
// send a reply. this is intended to work around a delay when
// running a linux server and an OS X (any BSD?) client. the
// client waits to send an ACK (if the system control flag
// net.inet.tcp.delayed_ack is 1) in hopes of piggybacking it
// on a data packet. we provide that packet here. i don't
// know why a delayed ACK should cause the server to wait since
// TCP_NODELAY is enabled.
ProtocolUtil::writef(m_stream, kMsgCNoop);
}
return result;
}
ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
{
using enum ConnectionResult;
@ -229,16 +221,28 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
mouseWheel();
}
else if (memcmp(code, kMsgDKeyDown1_1, 4) == 0) {
else if (memcmp(code, kMsgDKeyDown, 4) == 0) {
uint16_t id = 0;
uint16_t mask = 0;
uint16_t button = 0;
ProtocolUtil::readf(m_stream, kMsgDKeyDown1_1 + 4, &id, &mask, &button);
ProtocolUtil::readf(m_stream, kMsgDKeyDown + 4, &id, &mask, &button);
LOG_VERBOSE("recv key down id=0x%08x, mask=0x%04x, button=0x%04x", id, mask, button);
keyDown(id, mask, button, "");
}
else if (memcmp(code, kMsgDKeyDownLang, 4) == 0) {
std::string lang;
uint16_t id = 0;
uint16_t mask = 0;
uint16_t button = 0;
ProtocolUtil::readf(m_stream, kMsgDKeyDownLang + 4, &id, &mask, &button, &lang);
LOG_VERBOSE("recv key down id=0x%08x, mask=0x%04x, button=0x%04x, lang=\"%s\"", id, mask, button, lang.c_str());
keyDown(id, mask, button, lang);
}
else if (memcmp(code, kMsgDKeyUp, 4) == 0) {
keyUp();
}
@ -251,15 +255,8 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
mouseUp();
}
else if (memcmp(code, kMsgDKeyRepeat1_1, 4) == 0) {
uint16_t id = 0;
uint16_t mask = 0;
uint16_t count = 0;
uint16_t button = 0;
ProtocolUtil::readf(m_stream, kMsgDKeyRepeat1_1 + 4, &id, &mask, &count, &button);
LOG_VERBOSE("recv key repeat id=0x%08x, mask=0x%04x, count=%d, button=0x%04x", id, mask, count, button);
keyRepeat(id, mask, count, button, "");
else if (memcmp(code, kMsgDKeyRepeat, 4) == 0) {
keyRepeat();
}
else if (memcmp(code, kMsgCKeepAlive, 4) == 0) {
@ -308,6 +305,10 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
setOptions();
}
else if (memcmp(code, kMsgDSecureInputNotification, 4) == 0) {
secureInputNotification();
}
else if (memcmp(code, kMsgCClose, 4) == 0) {
// server wants us to hangup
LOG_VERBOSE("recv close");
@ -321,6 +322,15 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
return Unknown;
}
// send a reply. this is intended to work around a delay when
// running a linux server and an OS X (any BSD?) client. the
// client waits to send an ACK (if the system control flag
// net.inet.tcp.delayed_ack is 1) in hopes of piggybacking it
// on a data packet. we provide that packet here. i don't
// know why a delayed ACK should cause the server to wait since
// TCP_NODELAY is enabled.
ProtocolUtil::writef(m_stream, kMsgCNoop);
return Okay;
}
@ -511,6 +521,8 @@ void ServerProxy::enter()
m_dxMouse = 0;
m_dyMouse = 0;
m_seqNum = seqNum;
m_serverLayout = "";
m_isUserNotifiedAboutLayoutSyncError = false;
// forward
m_client->enter(x, y, seqNum, static_cast<KeyModifierMask>(mask), false);
@ -576,25 +588,45 @@ void ServerProxy::grabClipboard()
void ServerProxy::keyDown(uint16_t id, uint16_t mask, uint16_t button, const std::string &lang)
{
// get mouse up to date
flushCompressedMouse();
setActiveServerLanguage(lang);
// translate
KeyID id2 = translateKey(static_cast<KeyID>(id));
KeyModifierMask mask2 = translateModifierMask(static_cast<KeyModifierMask>(mask));
if (id2 != static_cast<KeyID>(id) || mask2 != static_cast<KeyModifierMask>(mask))
LOG_VERBOSE("key down translated to id=0x%08x, mask=0x%04x", id2, mask2);
// forward
m_client->keyDown(id2, mask2, button, lang);
}
void ServerProxy::keyRepeat(uint16_t id, uint16_t mask, uint16_t count, uint16_t button, const std::string &lang)
void ServerProxy::keyRepeat()
{
// get mouse up to date
flushCompressedMouse();
// parse
uint16_t id;
uint16_t mask;
uint16_t count;
uint16_t button;
std::string lang;
ProtocolUtil::readf(m_stream, kMsgDKeyRepeat + 4, &id, &mask, &count, &button, &lang);
LOG(
(CLOG_VERBOSE "recv key repeat id=0x%08x, mask=0x%04x, count=%d, "
"button=0x%04x, lang=\"%s\"",
id, mask, count, button, lang.c_str())
);
// translate
KeyID id2 = translateKey(static_cast<KeyID>(id));
KeyModifierMask mask2 = translateModifierMask(static_cast<KeyModifierMask>(mask));
if (id2 != static_cast<KeyID>(id) || mask2 != static_cast<KeyModifierMask>(mask))
LOG_VERBOSE("key repeat translated to id=0x%08x, mask=0x%04x", id2, mask2);
// forward
m_client->keyRepeat(id2, mask2, count, button, lang);
}
@ -809,3 +841,38 @@ void ServerProxy::infoAcknowledgment()
LOG_VERBOSE("recv info acknowledgment");
m_ignoreMouse = false;
}
void ServerProxy::secureInputNotification()
{
std::string app;
ProtocolUtil::readf(m_stream, kMsgDSecureInputNotification + 4, &app);
LOG_INFO("application \"%s\" is blocking the keyboard", app.c_str());
}
void ServerProxy::setServerLanguages()
{
std::string serverLayout;
ProtocolUtil::readf(m_stream, kMsgDLanguageSynchronisation + 4, &serverLayout);
m_layoutManager.setRemoteLayouts(serverLayout);
}
void ServerProxy::setActiveServerLanguage(const std::string_view &language)
{
if (!language.empty() && (language.size() > 0)) {
if (m_serverLayout != language) {
m_isUserNotifiedAboutLayoutSyncError = false;
m_serverLayout = language;
}
if (!m_layoutManager.isLayoutInstalled(m_serverLayout)) {
if (!m_isUserNotifiedAboutLayoutSyncError) {
LOG_WARN("current server layout is not installed on client");
m_isUserNotifiedAboutLayoutSyncError = true;
}
} else {
m_isUserNotifiedAboutLayoutSyncError = false;
}
} else {
LOG_VERBOSE("active server layout is empty");
}
}

View file

@ -1,7 +1,7 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2012 - 2016, 2026 Synergy App Ltd
* SPDX-FileCopyrightText: (C) 2012 - 2016 Synergy App Ltd
* SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
@ -12,6 +12,7 @@
#include "deskflow/ClipboardChunk.h"
#include "deskflow/ClipboardTypes.h"
#include "deskflow/KeyTypes.h"
#include "deskflow/KeyboardLayoutManager.h"
class Client;
class ClientInfo;
@ -22,12 +23,10 @@ class IStream;
}
class IEventQueue;
//! Proxy for server implementing protocol version 1.6
//! Proxy for server
/*!
This class acts a proxy for the server, converting calls into messages
to the server and messages from the server to calls on the client.
It implements 1.6, the oldest version the client supports; later
versions subclass it and add their messages.
*/
class ServerProxy
{
@ -39,7 +38,7 @@ public:
ServerProxy(Client *client, deskflow::IStream *stream, IEventQueue *events);
ServerProxy(ServerProxy const &) = delete;
ServerProxy(ServerProxy &&) = delete;
virtual ~ServerProxy();
~ServerProxy();
ServerProxy &operator=(ServerProxy const &) = delete;
ServerProxy &operator=(ServerProxy &&) = delete;
@ -60,16 +59,8 @@ protected:
Unknown,
Disconnect
};
virtual ConnectionResult parseHandshakeMessage(const uint8_t *code);
virtual ConnectionResult parseMessage(const uint8_t *code);
void keyDown(uint16_t id, uint16_t mask, uint16_t button, const std::string &lang);
void keyRepeat(uint16_t id, uint16_t mask, uint16_t count, uint16_t button, const std::string &lang);
deskflow::IStream *getStream() const
{
return m_stream;
}
ConnectionResult parseHandshakeMessage(const uint8_t *code);
ConnectionResult parseMessage(const uint8_t *code);
private:
// if compressing mouse motion then send the last motion now
@ -86,7 +77,6 @@ private:
// event handlers
void handleData();
ConnectionResult handleMessage(const uint8_t *code);
void handleKeepAliveAlarm();
void requestDisconnect(const char *message);
void requestRefuseConnection(deskflow::core::ConnectionRefusal reason, const char *message);
@ -96,6 +86,8 @@ private:
void leave();
void setClipboard();
void grabClipboard();
void keyDown(uint16_t id, uint16_t mask, uint16_t button, const std::string &lang);
void keyRepeat();
void keyUp();
void mouseDown();
void mouseUp();
@ -107,6 +99,9 @@ private:
void setOptions();
void queryInfo();
void infoAcknowledgment();
void secureInputNotification();
void setServerLanguages();
void setActiveServerLanguage(const std::string_view &language);
private:
using MessageParser = ConnectionResult (ServerProxy::*)(const uint8_t *);
@ -132,6 +127,9 @@ private:
MessageParser m_parser = &ServerProxy::parseHandshakeMessage;
IEventQueue *m_events = nullptr;
std::string m_serverLayout = "";
std::string m_clipboardDataCached;
ClipboardChunkAssemblyState m_clipboardChunkState;
bool m_isUserNotifiedAboutLayoutSyncError = false;
deskflow::KeyboardLayoutManager m_layoutManager;
};

View file

@ -1,37 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "client/ServerProxy1_7.h"
#include "base/Log.h"
#include "deskflow/ProtocolTypes.h"
#include "deskflow/ProtocolUtil.h"
#include <cstring>
#include <string>
ServerProxy1_7::ServerProxy1_7(Client *client, deskflow::IStream *stream, IEventQueue *events)
: ServerProxy(client, stream, events)
{
}
ServerProxy::ConnectionResult ServerProxy1_7::parseMessage(const uint8_t *code)
{
auto result = ConnectionResult::Okay;
if (memcmp(code, kMsgDSecureInputNotification, 4) == 0) {
secureInputNotification();
} else {
result = ServerProxy::parseMessage(code);
}
return result;
}
void ServerProxy1_7::secureInputNotification() const
{
std::string app;
ProtocolUtil::readf(getStream(), kMsgDSecureInputNotification + 4, &app);
LOG_INFO("application \"%s\" is blocking the keyboard", app.c_str());
}

View file

@ -1,23 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "client/ServerProxy.h"
//! Proxy for server implementing protocol version 1.7
class ServerProxy1_7 : public ServerProxy
{
public:
ServerProxy1_7(Client *client, deskflow::IStream *stream, IEventQueue *events);
~ServerProxy1_7() override = default;
protected:
ConnectionResult parseMessage(const uint8_t *code) override;
private:
void secureInputNotification() const;
};

View file

@ -1,103 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "client/ServerProxy1_8.h"
#include "base/Log.h"
#include "deskflow/ProtocolTypes.h"
#include "deskflow/ProtocolUtil.h"
#include "deskflow/ipc/CoreIpc.h"
#include <cstring>
#include <string>
#include <QString>
ServerProxy1_8::ServerProxy1_8(Client *client, deskflow::IStream *stream, IEventQueue *events)
: ServerProxy1_7(client, stream, events)
{
}
ServerProxy::ConnectionResult ServerProxy1_8::parseHandshakeMessage(const uint8_t *code)
{
auto result = ConnectionResult::Okay;
if (memcmp(code, kMsgDLanguageSynchronisation, 4) == 0) {
setServerLayouts();
} else {
result = ServerProxy1_7::parseHandshakeMessage(code);
}
return result;
}
ServerProxy::ConnectionResult ServerProxy1_8::parseMessage(const uint8_t *code)
{
auto result = ConnectionResult::Okay;
if (memcmp(code, kMsgDKeyDown, 4) == 0) {
uint16_t id = 0;
uint16_t mask = 0;
uint16_t button = 0;
std::string lang;
ProtocolUtil::readf(getStream(), kMsgDKeyDown + 4, &id, &mask, &button, &lang);
LOG_VERBOSE("recv key down id=0x%08x, mask=0x%04x, button=0x%04x, lang=\"%s\"", id, mask, button, lang.c_str());
setActiveServerLayout(lang);
keyDown(id, mask, button, lang);
} else if (memcmp(code, kMsgDKeyRepeat, 4) == 0) {
uint16_t id = 0;
uint16_t mask = 0;
uint16_t count = 0;
uint16_t button = 0;
std::string lang;
ProtocolUtil::readf(getStream(), kMsgDKeyRepeat + 4, &id, &mask, &count, &button, &lang);
LOG_VERBOSE(
"recv key repeat id=0x%08x, mask=0x%04x, count=%d, button=0x%04x, lang=\"%s\"", id, mask, count, button,
lang.c_str()
);
keyRepeat(id, mask, count, button, lang);
} else if (memcmp(code, kMsgCEnter, 4) == 0) {
m_serverLayout.clear();
m_isUserNotifiedAboutLayoutSyncError = false;
result = ServerProxy1_7::parseMessage(code);
} else {
result = ServerProxy1_7::parseMessage(code);
}
return result;
}
void ServerProxy1_8::setServerLayouts()
{
std::string serverLayouts;
ProtocolUtil::readf(getStream(), kMsgDLanguageSynchronisation + 4, &serverLayouts);
m_layoutManager.setRemoteLayouts(serverLayouts);
if (const auto missingLayouts = m_layoutManager.getMissedLayouts(); !missingLayouts.empty()) {
LOG_WARN("server layouts missing on this computer: %s", missingLayouts.c_str());
ipcSendToClient("missingKeyboardLayouts", QString::fromStdString(missingLayouts));
}
}
void ServerProxy1_8::setActiveServerLayout(const std::string_view &layout)
{
if (layout.empty()) {
LOG_VERBOSE("active server layout is empty");
return;
}
if (m_serverLayout != layout) {
m_isUserNotifiedAboutLayoutSyncError = false;
m_serverLayout = layout;
}
if (!m_layoutManager.isLayoutInstalled(m_serverLayout)) {
if (!m_isUserNotifiedAboutLayoutSyncError) {
LOG_WARN("current server layout is not installed on client");
m_isUserNotifiedAboutLayoutSyncError = true;
}
} else {
m_isUserNotifiedAboutLayoutSyncError = false;
}
}

View file

@ -1,33 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "client/ServerProxy1_7.h"
#include "deskflow/KeyboardLayoutManager.h"
#include <string>
#include <string_view>
//! Proxy for server implementing protocol version 1.8
class ServerProxy1_8 : public ServerProxy1_7
{
public:
ServerProxy1_8(Client *client, deskflow::IStream *stream, IEventQueue *events);
~ServerProxy1_8() override = default;
protected:
ConnectionResult parseHandshakeMessage(const uint8_t *code) override;
ConnectionResult parseMessage(const uint8_t *code) override;
private:
void setServerLayouts();
void setActiveServerLayout(const std::string_view &layout);
std::string m_serverLayout;
bool m_isUserNotifiedAboutLayoutSyncError = false;
deskflow::KeyboardLayoutManager m_layoutManager;
};

View file

@ -13,7 +13,6 @@ const char *const kMsgHello = "%7s%2i%2i";
const char *const kMsgHelloArgs = "%2i%2i";
const char *const kMsgHelloBack = "%7s%2i%2i%s";
const char *const kMsgHelloBackArgs = "%2i%2i%s";
const char *const kMsgCNoop = "CNOP";
const char *const kMsgCClose = "CBYE";
const char *const kMsgCEnter = "CINN%2i%2i%4i%2i";
@ -23,15 +22,10 @@ const char *const kMsgCScreenSaver = "CSEC%1i";
const char *const kMsgCResetOptions = "CROP";
const char *const kMsgCInfoAck = "CIAK";
const char *const kMsgCKeepAlive = "CALV";
// This is weird, it goes from DKDN to DKDL. Someone misunderstood the pattern (keep the
// code, pick the form by negotiated version). Do not copy this.
const char *const kMsgDKeyDown = "DKDL%2i%2i%2i%s";
const char *const kMsgDKeyDown1_1 = "DKDN%2i%2i%2i";
const char *const kMsgDKeyDownLang = "DKDL%2i%2i%2i%s";
const char *const kMsgDKeyDown = "DKDN%2i%2i%2i";
const char *const kMsgDKeyDown1_0 = "DKDN%2i%2i";
const char *const kMsgDKeyRepeat = "DKRP%2i%2i%2i%2i%s";
const char *const kMsgDKeyRepeat1_1 = "DKRP%2i%2i%2i%2i";
const char *const kMsgDKeyRepeat1_0 = "DKRP%2i%2i%2i";
const char *const kMsgDKeyUp = "DKUP%2i%2i%2i";
const char *const kMsgDKeyUp1_0 = "DKUP%2i%2i";

View file

@ -504,7 +504,7 @@ extern const char *const kMsgCKeepAlive;
*/
/**
* @brief Key press event
* @brief Key press with language code (v1.8+)
*
* **Message Code**: `"DKDL"`
* **Direction**: Primary → Secondary
@ -524,6 +524,34 @@ extern const char *const kMsgCKeepAlive;
* "DKDL\x00\x61\x00\x00\x00\x1E\x00\x00\x00\x02en"
* ```
*
* Enhanced version of kMsgDKeyDown that includes language information
* to help clients handle unknown language characters correctly.
*
* @see kMsgDKeyDown
* @since Protocol version 1.8
*/
extern const char *const kMsgDKeyDownLang;
/**
* @brief Key press event
*
* **Message Code**: `"DKDN"`
* **Direction**: Primary → Secondary
* **Format**: `"DKDN%2i%2i%2i"`
* **Parameters**:
* - `$1`: KeyID (2 bytes) - Virtual key identifier, often called a "keysym" on Linux/X11. This is platform-dependent
* and corresponds to values like `XK_a` on X11/Linux, `'a'` on macOS, and `'A'` on Windows.
* - `$2`: KeyModifierMask (2 bytes) - Active modifier keys
* - `$3`: KeyButton (2 bytes) - Physical key code, often called a "keycode" or "scancode". This is the raw,
* platform-dependent scan code of the key pressed.
*
* **Example**:
*
* 'a' key (KeyID 0x61), no modifiers, physical key (KeyButton 0x1E)
* ```
* "DKDN\x00\x61\x00\x00\x00\x1E"
* ```
*
* **Key Mapping Strategy**:
* The KeyButton parameter is crucial for proper key release handling.
* The secondary screen should:
@ -536,41 +564,10 @@ extern const char *const kMsgCKeepAlive;
* - Different keyboard layouts may produce different KeyIDs
* - Modifier keys released before the main key can alter KeyID
*
* The language code helps clients handle characters from a keyboard layout
* they do not have active. Only sent to clients that replied 1.8 in their hello.
*
* @see kMsgDKeyUp, kMsgDKeyDown1_1
* @since Protocol version 1.8
*/
extern const char *const kMsgDKeyDown;
/**
* @brief Key press event (v1.1 to v1.7)
*
* **Message Code**: `"DKDN"`
* **Direction**: Primary → Secondary
* **Format**: `"DKDN%2i%2i%2i"`
* **Parameters**:
* - `$1`: KeyID (2 bytes) - Virtual key identifier
* - `$2`: KeyModifierMask (2 bytes) - Active modifier keys
* - `$3`: KeyButton (2 bytes) - Physical key code
*
* **Example**:
*
* 'a' key (KeyID 0x61), no modifiers, physical key (KeyButton 0x1E)
* ```
* "DKDN\x00\x61\x00\x00\x00\x1E"
* ```
*
* Version without the language code. Used when communicating with
* protocol version 1.1 through 1.7 clients (Synergy 1.4 to 1.14.1,
* Barrier, Input Leap). The key mapping strategy is the same as for
* kMsgDKeyDown.
*
* @see kMsgDKeyDown
* @see kMsgDKeyUp, kMsgDKeyDownLang
* @since Protocol version 1.1
*/
extern const char *const kMsgDKeyDown1_1;
extern const char *const kMsgDKeyDown;
/**
* @brief Key press event (legacy v1.0)
@ -585,8 +582,8 @@ extern const char *const kMsgDKeyDown1_1;
* Legacy version without KeyButton parameter. Used only when
* communicating with protocol version 1.0 clients.
*
* @deprecated Use kMsgDKeyDown1_1 for protocol version 1.1+
* @see kMsgDKeyDown1_1
* @deprecated Use kMsgDKeyDown for protocol version 1.1+
* @see kMsgDKeyDown
* @since Protocol version 1.0
*/
extern const char *const kMsgDKeyDown1_0;
@ -616,42 +613,10 @@ extern const char *const kMsgDKeyDown1_0;
* Sent when a key is held down and auto-repeating. The repeat count
* indicates how many repeat events occurred since the last message.
*
* Shares the `DKRP` message code with kMsgDKeyRepeat1_1, so the receiver can
* only tell the two apart by the negotiated protocol version. Only sent to
* clients that replied 1.8 in their hello.
*
* @see kMsgDKeyDown, kMsgDKeyRepeat1_1
* @since Protocol version 1.8
*/
extern const char *const kMsgDKeyRepeat;
/**
* @brief Key auto-repeat event (v1.1 to v1.7)
*
* **Message Code**: `"DKRP"`
* **Direction**: Primary → Secondary
* **Format**: `"DKRP%2i%2i%2i%2i"`
* **Parameters**:
* - `$1`: KeyID (2 bytes) - Virtual key identifier
* - `$2`: KeyModifierMask (2 bytes) - Active modifier keys
* - `$3`: Repeat count (2 bytes) - Number of repeats
* - `$4`: KeyButton (2 bytes) - Physical key code
*
* **Example**:
*
* 'a' key repeating 3 times
* ```
* "DKRP\x00\x61\x00\x00\x00\x03\x00\x1E"
* ```
*
* Version without the language code. Used when communicating with
* protocol version 1.1 through 1.7 clients (Synergy 1.4 to 1.14.1,
* Barrier, Input Leap).
*
* @see kMsgDKeyRepeat
* @see kMsgDKeyDown
* @since Protocol version 1.1
*/
extern const char *const kMsgDKeyRepeat1_1;
extern const char *const kMsgDKeyRepeat;
/**
* @brief Key auto-repeat event (legacy v1.0)
@ -664,10 +629,10 @@ extern const char *const kMsgDKeyRepeat1_1;
* - `$2`: KeyModifierMask (2 bytes) - Active modifier keys
* - `$3`: Repeat count (2 bytes) - Number of repeats
*
* Legacy version without the KeyButton parameter.
* Legacy version without KeyButton and language parameters.
*
* @deprecated Use kMsgDKeyRepeat1_1 for protocol version 1.1+
* @see kMsgDKeyRepeat1_1
* @deprecated Use kMsgDKeyRepeat for protocol version 1.1+
* @see kMsgDKeyRepeat
* @since Protocol version 1.0
*/
extern const char *const kMsgDKeyRepeat1_0;

View file

@ -380,22 +380,13 @@ void SecureSocket::createSSL()
void SecureSocket::freeSSL()
{
isFatal(true);
// take socket from multiplexer ASAP otherwise the race condition
// could cause events to get called on a dead object. TCPSocket
// will do this, too, but the double-call is harmless.
//
// this must happen *before* ssl_mutex_ is taken. the multiplexer holds its
// job list across job->run(), and secureAccept()/secureConnect() take
// ssl_mutex_ from inside a job, so taking ssl_mutex_ here first and then
// waiting on the job list inverts the lock order against the multiplexer
// thread and hangs both. removeSocket() also returns only once no job is
// running, so the teardown below still cannot race a live job.
setJob(nullptr);
std::scoped_lock ssl_lock{ssl_mutex_};
isFatal(true);
// take socket from multiplexer ASAP otherwise the race condition
// could cause events to get called on a dead object. TCPSocket
// will do this, too, but the double-call is harmless
setJob(nullptr);
if (m_ssl) {
if (m_ssl->m_ssl != nullptr) {
SSL_set_quiet_shutdown(m_ssl->m_ssl, 1);

View file

@ -18,7 +18,6 @@ if(UNIX AND NOT APPLE)
set(CMAKE_REQUIRED_LIBRARIES "${CMAKE_REQUIRED_LIBRARIES};${LIBPORTAL_LINK_LIBRARIES};")
check_symbol_exists(xdp_input_capture_session_get_restore_token "libportal/inputcapture.h" HAVE_IC_RESTORE)
check_symbol_exists(xdp_session_request_clipboard "libportal/portal-helpers.h;libportal/clipboard.h" HAVE_LP_CLIPBOARD)
check_symbol_exists(xdp_global_shortcut_new "libportal/globalshortcuts.h" HAVE_LP_SHORTCUTS)
cmake_pop_check_state()
endif()
@ -167,12 +166,6 @@ elseif(UNIX)
PortalClipboard.h
)
endif()
if(HAVE_LP_SHORTCUTS)
list(APPEND PLATFORM_SOURCES
PortalGlobalShortcuts.cpp
PortalGlobalShortcuts.h
)
endif()
endif()
endif()
endif()
@ -199,9 +192,6 @@ if(UNIX)
if (HAVE_LP_CLIPBOARD)
target_compile_definitions(platform PUBLIC HAVE_LIBPORTAL_CLIPBOARD)
endif()
if (HAVE_LP_SHORTCUTS)
target_compile_definitions(platform PUBLIC HAVE_LIBPORTAL_SHORTCUTS)
endif()
target_include_directories(platform PUBLIC ${LIBEI_INCLUDE_DIRS} ${LIBPORTAL_INCLUDE_DIRS})
target_link_libraries(platform
PUBLIC

View file

@ -345,20 +345,6 @@ void EiKeyState::updateXkbState(uint32_t keyval, bool isPressed)
xkb_state_update_key(m_xkbState, keyval, isPressed ? XKB_KEY_DOWN : XKB_KEY_UP);
}
void EiKeyState::updateLockedModifiers(xkb_mod_mask_t lockedMods)
{
// The compositor's lock state (Caps/Num/Scroll Lock) is authoritative; it
// also covers locks toggled while we weren't seeing the key events.
const auto depressedMods = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_DEPRESSED);
const auto latchedMods = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_LATCHED);
const auto depressedLayout = xkb_state_serialize_layout(m_xkbState, XKB_STATE_LAYOUT_DEPRESSED);
const auto latchedLayout = xkb_state_serialize_layout(m_xkbState, XKB_STATE_LAYOUT_LATCHED);
const auto lockedLayout = xkb_state_serialize_layout(m_xkbState, XKB_STATE_LAYOUT_LOCKED);
xkb_state_update_mask(
m_xkbState, depressedMods, latchedMods, lockedMods, depressedLayout, latchedLayout, lockedLayout
);
}
void EiKeyState::clearStaleModifiers()
{
const auto lockedMods = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_LOCKED);

View file

@ -35,7 +35,6 @@ public:
void pollPressedKeys(KeyButtonSet &pressedKeys) const override;
KeyID mapKeyFromKeyval(std::uint32_t keyval) const;
void updateXkbState(std::uint32_t keyval, bool isPressed);
void updateLockedModifiers(xkb_mod_mask_t lockedMods);
void clearStaleModifiers() override;
protected:

View file

@ -22,7 +22,6 @@
#include "platform/PortalRemoteDesktop.h"
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstdlib>
#include <cstring>
@ -59,11 +58,8 @@ EiScreen::EiScreen(bool isPrimary, IEventQueue *events, bool usePortal)
handleConnectedToEisEvent(e);
});
if (isPrimary) {
// Portal input capture manages its own clipboard
m_portalInputCapture = new PortalInputCapture(this, m_events);
#ifdef HAVE_LIBPORTAL_SHORTCUTS
m_portalGlobalShortcuts = new PortalGlobalShortcuts(this, m_events);
#endif
// Portal input capture manages its own clipboard
} else {
m_events->addHandler(EventTypes::EISessionClosed, getEventTarget(), [this](const auto &) {
handlePortalSessionClosed();
@ -100,11 +96,8 @@ EiScreen::~EiScreen()
delete m_keyState;
delete m_clipboard;
#ifdef HAVE_LIBPORTAL_SHORTCUTS
delete m_portalGlobalShortcuts;
#endif
delete m_portalInputCapture;
delete m_portalRemoteDesktop;
delete m_portalInputCapture;
}
void EiScreen::eiLogEvent(ei_log_priority priority, const char *message) const
@ -240,47 +233,19 @@ std::uint32_t EiScreen::registerHotKey(KeyID key, KeyModifierMask mask)
}
set->second.addItem(HotKeyItem(mask, id));
updatePortalGlobalShortcuts();
return id;
}
void EiScreen::unregisterHotKey(uint32_t id)
{
for (auto it = m_hotkeys.begin(); it != m_hotkeys.end(); ++it) {
if (it->second.removeById(id)) {
if (it->second.empty())
m_hotkeys.erase(it);
updatePortalGlobalShortcuts();
for (auto &[key, set] : m_hotkeys) {
(void)key;
if (set.removeById(id)) {
break;
}
}
}
void EiScreen::updatePortalGlobalShortcuts()
{
#ifdef HAVE_LIBPORTAL_SHORTCUTS
if (!m_portalGlobalShortcuts) {
return;
}
if (!m_activated) {
return;
}
std::vector<PortalGlobalShortcuts::HotKey> hotkeys;
for (const auto &[key, set] : m_hotkeys) {
(void)key;
const auto setHotKeys = set.getPortalHotKeys();
hotkeys.insert(hotkeys.end(), setHotKeys.begin(), setHotKeys.end());
}
m_portalGlobalShortcuts->setHotKeys(std::move(hotkeys));
#endif
}
void EiScreen::fakeInputBegin()
{
// FIXME -- not implemented
@ -296,13 +261,9 @@ std::int32_t EiScreen::getJumpZoneSize() const
return 1;
}
bool EiScreen::isAnyMouseButtonDown(uint32_t &buttonID) const
bool EiScreen::isAnyMouseButtonDown(uint32_t &) const
{
if (m_buttons.none())
return false;
buttonID = std::countr_zero(m_buttons.to_ulong());
return true;
}
void EiScreen::getCursorCenter(int32_t &x, int32_t &y) const
@ -393,13 +354,16 @@ void EiScreen::fakeKey(uint32_t keycode, bool isDown) const
void EiScreen::enable()
{
m_activated = true;
updatePortalGlobalShortcuts();
// Nothing really to be done here
// Portal-based clipboard gets notifications via events
// Socket-based clipboard is passive (no monitoring needed)
}
void EiScreen::disable()
{
m_activated = false;
// Nothing to do here
// Portal-based clipboard gets notifications via events
// Socket-based clipboard is passive (no monitoring needed)
}
void EiScreen::cancelIdleEmulationTimer() const
@ -460,8 +424,6 @@ void EiScreen::enter()
} else if (m_isPrimary) {
LOG_DEBUG("releasing input capture at x=%i y=%i", m_cursorX, m_cursorY);
m_portalInputCapture->release(m_cursorX, m_cursorY);
// no more button events once capture is released, so drop any held state
updateButtons();
}
}
@ -669,8 +631,6 @@ void EiScreen::removeDevice(struct ei_device *device)
if (wasTracked) {
m_isEmulating = false;
cancelIdleEmulationTimer();
// same as for a paused device: no release events follow a removal
updateButtons();
}
delete static_cast<ScrollRemainder *>(ei_device_get_user_data(device));
@ -734,10 +694,6 @@ ButtonID EiScreen::mapButtonFromEvdev(ei_event *event) const
bool EiScreen::onHotkey(KeyID keyid, bool isPressed, KeyModifierMask mask)
{
// Check if the keyid is registered as a hotkey
// This is the implementation if InputCapture is active. If InputCapture is not active,
// the hotkey is handled by the portal and we don't get here.
auto it = m_hotkeys.find(keyid);
if (it == m_hotkeys.end()) {
@ -752,6 +708,7 @@ bool EiScreen::onHotkey(KeyID keyid, bool isPressed, KeyModifierMask mask)
sendEvent(type, HotKeyInfo::alloc(id));
return true;
}
return false;
}
@ -800,8 +757,6 @@ void EiScreen::onButtonEvent(ei_event *event)
return;
}
m_buttons.set(buttonID, pressed);
auto eventType = pressed ? EventTypes::PrimaryScreenButtonDown : EventTypes::PrimaryScreenButtonUp;
sendEvent(eventType, ButtonInfo::alloc(buttonID, mask));
@ -1013,9 +968,6 @@ void EiScreen::handleSystemEvent(const Event &)
case EI_EVENT_DEVICE_PAUSED:
LOG_DEBUG("device %s is paused", ei_device_get_name(device));
m_isEmulating = false;
// a paused device is reset to neutral by the EIS side and sends no
// further events, so the releases for held buttons never arrive
updateButtons();
cancelIdleEmulationTimer();
break;
case EI_EVENT_DEVICE_RESUMED:
@ -1025,8 +977,7 @@ void EiScreen::handleSystemEvent(const Event &)
}
break;
case EI_EVENT_KEYBOARD_MODIFIERS:
LOG_VERBOSE("event: keyboard modifiers locked=0x%x", ei_event_keyboard_get_xkb_mods_locked(event));
m_keyState->updateLockedModifiers(ei_event_keyboard_get_xkb_mods_locked(event));
// FIXME
break;
// events below are for a receiver context (barriers)
@ -1075,9 +1026,7 @@ void EiScreen::handleSystemEvent(const Event &)
void EiScreen::updateButtons()
{
// libei relies on the EIS implementation to keep our button count correct,
// and the held buttons cannot be polled, so resyncing means assuming that
// everything is released.
m_buttons.reset();
// so there's not much we need to/can do here.
}
IKeyState *EiScreen::getKeyState() const
@ -1126,21 +1075,4 @@ std::uint32_t EiScreen::HotKeySet::findByMask(std::uint32_t mask) const
return 0;
}
#ifdef HAVE_LIBPORTAL_SHORTCUTS
const std::vector<PortalGlobalShortcuts::HotKey> EiScreen::HotKeySet::getPortalHotKeys() const
{
std::vector<PortalGlobalShortcuts::HotKey> portalHotKeys;
portalHotKeys.reserve(m_set.size());
for (const auto &item : m_set) {
portalHotKeys.push_back({
.id = item.id,
.key = m_id,
.mask = item.mask,
});
}
return portalHotKeys;
}
#endif
} // namespace deskflow

View file

@ -9,12 +9,8 @@
#include "deskflow/IScreen.h"
#include "deskflow/PlatformScreen.h"
#ifdef HAVE_LIBPORTAL_SHORTCUTS
#include "platform/PortalGlobalShortcuts.h"
#endif
#include "platform/XDGPowerManager.h"
#include <bitset>
#include <climits>
#include <libei.h>
#include <map>
@ -33,7 +29,6 @@ namespace deskflow {
class EiKeyState;
class PortalRemoteDesktop;
class PortalInputCapture;
class PortalGlobalShortcuts;
class EiClipboard;
using ClipboardInfo = IScreen::ClipboardInfo;
@ -132,7 +127,6 @@ private:
void ensureEmulating() const;
void stopEmulating() const;
void cancelIdleEmulationTimer() const;
void updatePortalGlobalShortcuts();
static void handleEiLogEvent(ei *ei, const ei_log_priority priority, const char *message, ei_log_context *)
{
@ -150,9 +144,6 @@ private:
KeyID m_lastPressed = kKeyNone;
// mouse buttons currently held, indexed by ButtonID
std::bitset<NumButtonIDs> m_buttons;
// clipboard stuff
EiClipboard *m_clipboard = nullptr;
size_t m_maximumClipboardSize = INT_MAX;
@ -197,9 +188,6 @@ private:
PortalRemoteDesktop *m_portalRemoteDesktop = nullptr;
PortalInputCapture *m_portalInputCapture = nullptr;
PortalGlobalShortcuts *m_portalGlobalShortcuts = nullptr;
bool m_activated = false;
struct HotKeyItem
{
@ -226,13 +214,6 @@ private:
bool removeById(std::uint32_t id);
void addItem(HotKeyItem item);
std::uint32_t findByMask(std::uint32_t mask) const;
bool empty() const
{
return m_set.empty();
}
#ifdef HAVE_LIBPORTAL_SHORTCUTS
const std::vector<PortalGlobalShortcuts::HotKey> getPortalHotKeys() const;
#endif
private:
KeyID m_id = 0;

View file

@ -1,520 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "platform/PortalGlobalShortcuts.h"
#include "base/Log.h"
#include "base/TMethodJob.h"
#include "platform/EiScreen.h"
#include <inttypes.h>
#include <algorithm> // std::find_if, std::ranges::all_of
#include <cctype> // std::isalnum
#include <cstdio> // snprintf
namespace deskflow {
PortalGlobalShortcuts::PortalGlobalShortcuts(EiScreen *screen, IEventQueue *events)
: m_screen{screen},
m_events{events},
m_portal{xdp_portal_new()}
{
m_glibMainLoop = g_main_loop_new(nullptr, true);
auto tMethodJob = new TMethodJob<PortalGlobalShortcuts>(this, &PortalGlobalShortcuts::glibThread);
m_glibThread = new Thread(tMethodJob);
}
PortalGlobalShortcuts::~PortalGlobalShortcuts()
{
{
std::scoped_lock lock{m_bindingsMutex};
if (m_pendingIdleSource != 0) {
g_source_remove(m_pendingIdleSource);
m_pendingIdleSource = 0;
}
}
if (m_cancellable) {
g_cancellable_cancel(m_cancellable);
}
if (m_glibMainLoop && g_main_loop_is_running(m_glibMainLoop)) {
g_main_loop_quit(m_glibMainLoop);
}
if (m_glibThread) {
m_glibThread->cancel();
m_glibThread->wait();
delete m_glibThread;
m_glibThread = nullptr;
}
if (m_glibMainLoop) {
g_main_loop_unref(m_glibMainLoop);
m_glibMainLoop = nullptr;
}
closeSession();
if (m_cancellable) {
g_object_unref(m_cancellable);
m_cancellable = nullptr;
}
if (m_portal) {
g_object_unref(m_portal);
m_portal = nullptr;
}
}
void PortalGlobalShortcuts::setHotKeys(std::vector<HotKey> hotkeys)
{
std::vector<Binding> bindings;
bindings.reserve(hotkeys.size());
for (const auto &hotkey : hotkeys) {
bindings.push_back(makeBinding(hotkey));
}
std::scoped_lock lock{m_bindingsMutex};
m_bindings = std::move(bindings);
if (m_pendingIdleSource != 0) {
// Already scheduled — it will pick up the bindings we just stored via
// bindingsSnapshot() once it runs, so nothing further to do here.
return;
}
m_pendingIdleSource = g_idle_add(
[](gpointer data) -> gboolean {
auto *self = static_cast<PortalGlobalShortcuts *>(data);
{
std::scoped_lock lock{self->m_bindingsMutex};
self->m_pendingIdleSource = 0;
}
if (self->m_session) {
self->closeSession();
}
return self->createSession();
},
this
);
}
PortalGlobalShortcuts::Binding PortalGlobalShortcuts::makeBinding(const HotKey &hotkey)
{
Binding binding;
binding.id = hotkey.id;
binding.key = hotkey.key;
binding.mask = hotkey.mask;
binding.shortcutId = makeShortcutId(hotkey.key, hotkey.mask);
const std::string trigger = makePreferredTrigger(hotkey.key, hotkey.mask);
// This is not the prettiest way, but we currently have no direct access to the Hotkey object
// of lib/gui/Hotkey.cpp, so we cannot use Hotkey::text()
binding.description = trigger.empty() ? "Deskflow hotkey #" + std::to_string(hotkey.id) : "Hotkey " + trigger;
binding.preferredTrigger = trigger;
return binding;
}
std::vector<PortalGlobalShortcuts::Binding> PortalGlobalShortcuts::bindingsSnapshot() const
{
std::scoped_lock lock{m_bindingsMutex};
return m_bindings;
}
std::uint32_t PortalGlobalShortcuts::hotKeyIdForShortcutId(const char *shortcutId) const
{
std::scoped_lock lock{m_bindingsMutex};
const auto it = std::find_if(m_bindings.begin(), m_bindings.end(), [shortcutId](const Binding &binding) {
return binding.shortcutId == shortcutId;
});
if (it == m_bindings.end()) {
return 0;
}
return it->id;
}
gboolean PortalGlobalShortcuts::createSession()
{
// Supersede any still-pending CreateSession/BindShortcuts from an earlier attempt.
if (m_cancellable) {
g_cancellable_cancel(m_cancellable);
g_object_unref(m_cancellable);
m_cancellable = nullptr;
}
m_cancellable = g_cancellable_new();
LOG_DEBUG("creating global shortcuts session");
xdp_portal_create_global_shortcuts_session(
m_portal, m_cancellable,
[](GObject *obj, GAsyncResult *res, gpointer data) {
static_cast<PortalGlobalShortcuts *>(data)->createSessionDone(obj, res);
},
this
);
return G_SOURCE_REMOVE;
}
void PortalGlobalShortcuts::closeSession()
{
if (!m_session) {
LOG_DEBUG("no session to close");
return;
}
if (m_cancellable) {
g_cancellable_cancel(m_cancellable);
g_object_unref(m_cancellable);
m_cancellable = nullptr;
}
using enum Signal;
if (XdpSession *parentSession = xdp_global_shortcuts_session_get_session(m_session)) {
if (m_signals.at(SessionClosed)) {
g_signal_handler_disconnect(parentSession, m_signals.at(SessionClosed));
m_signals.at(SessionClosed) = 0;
}
}
if (m_signals.at(Activated)) {
g_signal_handler_disconnect(m_session, m_signals.at(Activated));
m_signals.at(Activated) = 0;
}
if (m_signals.at(Deactivated)) {
g_signal_handler_disconnect(m_session, m_signals.at(Deactivated));
m_signals.at(Deactivated) = 0;
}
if (m_signals.at(ShortcutsChanged)) {
g_signal_handler_disconnect(m_session, m_signals.at(ShortcutsChanged));
m_signals.at(ShortcutsChanged) = 0;
}
xdp_global_shortcuts_session_close(m_session);
g_object_unref(m_session);
m_session = nullptr;
}
void PortalGlobalShortcuts::setupSession(XdpGlobalShortcutsSession *session)
{
auto parentSession = xdp_global_shortcuts_session_get_session(session);
using enum Signal;
m_signals.at(SessionClosed) = g_signal_connect(G_OBJECT(parentSession), "closed", G_CALLBACK(sessionClosed), this);
m_signals.at(Activated) = g_signal_connect(G_OBJECT(session), "activated", G_CALLBACK(activated), this);
m_signals.at(Deactivated) = g_signal_connect(G_OBJECT(session), "deactivated", G_CALLBACK(deactivated), this);
m_signals.at(ShortcutsChanged) =
g_signal_connect(G_OBJECT(session), "shortcuts-changed", G_CALLBACK(shortcutsChanged), this);
}
void PortalGlobalShortcuts::bindShortcuts()
{
const auto bindings = bindingsSnapshot();
LOG_DEBUG("binding %zu global shortcuts", bindings.size());
g_autoptr(GPtrArray) shortcuts = nullptr;
shortcuts = g_ptr_array_new_with_free_func((GDestroyNotify)xdp_global_shortcut_free);
for (const auto &binding : bindings) {
g_ptr_array_add(
shortcuts, xdp_global_shortcut_new(
binding.shortcutId.c_str(), binding.description.c_str(),
binding.preferredTrigger.empty() ? nullptr : binding.preferredTrigger.c_str()
)
);
}
xdp_global_shortcuts_session_bind_shortcuts(
m_session, shortcuts,
nullptr, // parent window
m_cancellable,
[](GObject *object, GAsyncResult *res, gpointer data) {
static_cast<PortalGlobalShortcuts *>(data)->bindShortcutsDone(object, res);
},
this
);
}
void PortalGlobalShortcuts::createSessionDone(GObject *object, GAsyncResult *res)
{
g_autoptr(GError) error = nullptr;
auto session = xdp_portal_create_global_shortcuts_session_finish(XDP_PORTAL(object), res, &error);
if (!session) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
LOG_DEBUG("global shortcuts session creation was superseded");
return;
}
LOG_WARN("failed to create global shortcuts session, hotkeys will not be forwarded: %s", error->message);
return;
}
LOG_DEBUG("portal global shortcuts session created");
m_session = session;
setupSession(session);
bindShortcuts();
}
void PortalGlobalShortcuts::bindShortcutsDone(GObject *object, GAsyncResult *res)
{
g_autoptr(GError) error = nullptr;
g_autoptr(GPtrArray) shortcuts = nullptr;
if (XDP_GLOBAL_SHORTCUTS_SESSION(object) != m_session) {
return;
}
shortcuts = xdp_global_shortcuts_session_bind_shortcuts_finish(m_session, res, &error);
if (!shortcuts) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
LOG_DEBUG("global shortcuts binding was superseded");
return;
}
LOG_ERR("failed to bind global shortcuts: %s", error->message);
return;
}
LOG_DEBUG("bound %u global shortcuts", shortcuts->len);
for (guint i = 0; i < shortcuts->len; i++) {
XdpGlobalShortcutAssigned *shortcut;
shortcut = static_cast<XdpGlobalShortcutAssigned *>(g_ptr_array_index(shortcuts, i));
LOG_DEBUG(
"global shortcut %s assigned to %s", xdp_global_shortcut_assigned_get_shortcut_id(shortcut),
xdp_global_shortcut_assigned_get_trigger_description(shortcut)
);
}
// EiScreen can't be notified about the change, because triggerDescription is only a
// human-readable string and not a preferred trigger string
}
void PortalGlobalShortcuts::handleSessionClosed(XdpSession *)
{
LOG_WARN("portal global shortcuts session was closed by the system, will attempt to recreate it");
closeSession();
createSession();
}
void PortalGlobalShortcuts::handleActivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *
)
{
if (session != m_session) {
return;
}
const auto hotKeyId = hotKeyIdForShortcutId(shortcutId);
if (hotKeyId == 0) {
LOG_WARN("unknown global shortcut activated: %s", shortcutId);
return;
}
LOG_DEBUG("global shortcut activated: %s timestamp=%" PRIu64, shortcutId, timestamp);
m_events->addEvent(Event(
EventTypes::PrimaryScreenHotkeyDown, m_screen->getEventTarget(), IPrimaryScreen::HotKeyInfo::alloc(hotKeyId)
));
}
void PortalGlobalShortcuts::handleDeactivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *
)
{
if (session != m_session) {
return;
}
const auto hotKeyId = hotKeyIdForShortcutId(shortcutId);
if (hotKeyId == 0) {
LOG_WARN("unknown global shortcut deactivated: %s", shortcutId);
return;
}
LOG_DEBUG("global shortcut deactivated: %s timestamp=%" PRIu64, shortcutId, timestamp);
m_events->addEvent(
Event(EventTypes::PrimaryScreenHotkeyUp, m_screen->getEventTarget(), IPrimaryScreen::HotKeyInfo::alloc(hotKeyId))
);
}
void PortalGlobalShortcuts::handleShortcutsChanged(XdpGlobalShortcutsSession *session, GPtrArray *shortcuts)
{
if (session != m_session) {
return;
}
if (shortcuts == nullptr) {
LOG_WARN("global shortcuts changed without shortcuts data");
return;
}
LOG_DEBUG("global shortcuts changed");
for (guint i = 0; i < shortcuts->len; i++) {
XdpGlobalShortcutAssigned *shortcut;
const char *shortcutId;
const char *triggerDescription;
shortcut = static_cast<XdpGlobalShortcutAssigned *>(g_ptr_array_index(shortcuts, i));
shortcutId = xdp_global_shortcut_assigned_get_shortcut_id(shortcut);
triggerDescription = xdp_global_shortcut_assigned_get_trigger_description(shortcut);
LOG_DEBUG("global shortcut %s changed to %s", shortcutId, triggerDescription);
// EiScreen can't be notified about the change, because triggerDescription is only a
// human-readable string and not a preferred trigger string
}
}
void PortalGlobalShortcuts::glibThread(const void *)
{
auto context = g_main_loop_get_context(m_glibMainLoop);
LOG_DEBUG("global shortcuts glib thread running");
while (g_main_loop_is_running(m_glibMainLoop)) {
Thread::testCancel();
g_main_context_iteration(context, true);
}
LOG_DEBUG("global shortcuts glib thread shutting down");
}
// see https://specifications.freedesktop.org/shortcuts/latest/ on how to construct
// a preferred trigger string for a key combination
bool PortalGlobalShortcuts::isValidPreferredTriggerIdentifier(const std::string &name)
{
if (name.empty()) {
return false;
}
return std::ranges::all_of(name, [](unsigned char c) { return std::isalnum(c) || c == '_'; });
}
std::optional<xkb_keysym_t> PortalGlobalShortcuts::deskflowKeyToXkbKeysym(KeyID key)
{
if (key == kKeyNone) {
return std::nullopt;
}
// KeyTypes.h explicitly excludes
// 0xE000..0xE0FF from the "X11 keysym - 0x1000" rule.
if (key >= 0xE000 && key <= 0xE0FF) {
return std::nullopt;
}
// Deskflow control/function keys. KeyTypes.h says these
// are equal to the corresponding X11 keysym - 0x1000.
if (key >= 0xE100 && key <= 0xEFFF) {
return static_cast<xkb_keysym_t>(key + 0x1000);
}
// Remaining values are normal UTF-32/Unicode codepoints.
if (key < 0xE000) {
const auto keysym = xkb_utf32_to_keysym(key);
if (keysym == XKB_KEY_NoSymbol) {
return std::nullopt;
}
return keysym;
}
return std::nullopt;
}
std::string PortalGlobalShortcuts::keyNameForPreferredTrigger(KeyID key)
{
const auto keysym = deskflowKeyToXkbKeysym(key);
if (!keysym) {
return {};
}
char name[128] = {};
const int len = xkb_keysym_get_name(*keysym, name, sizeof(name));
if (len <= 0 || static_cast<std::size_t>(len) >= sizeof(name)) {
return {};
}
std::string result{name};
if (!isValidPreferredTriggerIdentifier(result)) {
return {};
}
return result;
}
std::string PortalGlobalShortcuts::makePreferredTrigger(KeyID key, KeyModifierMask mask)
{
constexpr KeyModifierMask supportedMask = KeyModifierShift | KeyModifierControl | KeyModifierAlt | KeyModifierMeta |
KeyModifierSuper | KeyModifierAltGr | KeyModifierLevel5Lock |
KeyModifierCapsLock | KeyModifierNumLock | KeyModifierScrollLock;
if ((mask & ~supportedMask) != 0) {
return {};
}
const auto keyName = keyNameForPreferredTrigger(key);
if (keyName.empty()) {
return {};
}
std::vector<std::string> parts;
if (mask & KeyModifierShift) {
parts.emplace_back("SHIFT");
}
if (mask & KeyModifierControl) {
parts.emplace_back("CTRL");
}
if (mask & KeyModifierAlt) {
parts.emplace_back("ALT");
}
if (mask & (KeyModifierMeta | KeyModifierSuper)) {
parts.emplace_back("LOGO");
}
if (mask & KeyModifierAltGr) {
parts.emplace_back("LEVEL3");
}
if (mask & KeyModifierLevel5Lock) {
parts.emplace_back("LEVEL5");
}
if (mask & KeyModifierCapsLock) {
parts.emplace_back("CAPS");
}
if (mask & KeyModifierNumLock) {
parts.emplace_back("NUM");
}
if (mask & KeyModifierScrollLock) {
parts.emplace_back("SCROLL");
}
parts.emplace_back(keyName);
std::string trigger;
for (std::size_t i = 0; i < parts.size(); ++i) {
if (i != 0) {
trigger += "+";
}
trigger += parts[i];
}
return trigger;
}
std::string PortalGlobalShortcuts::makeShortcutId(KeyID key, KeyModifierMask mask)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "deskflow-hotkey-%08x-%08x", mask, key);
return buffer;
}
} // namespace deskflow

View file

@ -1,140 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "base/IEventQueue.h"
#include "mt/Thread.h"
#include <glib.h>
#include <libportal/globalshortcuts.h>
#include <optional>
#include <xkbcommon/xkbcommon.h>
#include <map>
#include <mutex>
#include <string>
#include <vector>
#include "deskflow/KeyTypes.h"
namespace deskflow {
class EiScreen;
class PortalGlobalShortcuts
{
public:
PortalGlobalShortcuts(EiScreen *screen, IEventQueue *events);
~PortalGlobalShortcuts();
struct HotKey
{
std::uint32_t id = 0;
KeyID key = kKeyNone;
KeyModifierMask mask = 0;
};
void setHotKeys(std::vector<HotKey> hotkeys);
private:
void glibThread(const void *);
void setupSession(XdpGlobalShortcutsSession *session);
void createSessionDone(GObject *object, GAsyncResult *res);
void bindShortcutsDone(GObject *object, GAsyncResult *res);
void handleSessionClosed(XdpSession *session);
void handleActivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *options
);
void handleDeactivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *options
);
void handleShortcutsChanged(XdpGlobalShortcutsSession *session, GPtrArray *shortcuts);
/// g_signal_connect callback wrapper
static void sessionClosed(XdpSession *session, const gpointer data)
{
static_cast<PortalGlobalShortcuts *>(data)->handleSessionClosed(session);
}
static void activated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *options,
const gpointer data
)
{
static_cast<PortalGlobalShortcuts *>(data)->handleActivated(session, shortcutId, timestamp, options);
}
static void deactivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *options,
const gpointer data
)
{
static_cast<PortalGlobalShortcuts *>(data)->handleDeactivated(session, shortcutId, timestamp, options);
}
static void shortcutsChanged(XdpGlobalShortcutsSession *session, GPtrArray *shortcuts, const gpointer data)
{
static_cast<PortalGlobalShortcuts *>(data)->handleShortcutsChanged(session, shortcuts);
}
enum class Signal : uint8_t
{
SessionClosed,
Activated,
Deactivated,
ShortcutsChanged,
};
struct Binding
{
std::uint32_t id = 0;
KeyID key = kKeyNone;
KeyModifierMask mask = 0;
std::string shortcutId;
std::string description;
std::string preferredTrigger;
};
static std::optional<xkb_keysym_t> deskflowKeyToXkbKeysym(KeyID key);
static std::string keyNameForPreferredTrigger(KeyID key);
static std::string makePreferredTrigger(KeyID key, KeyModifierMask mask);
static bool isValidPreferredTriggerIdentifier(const std::string &name);
gboolean createSession();
void closeSession();
void bindShortcuts();
std::uint32_t hotKeyIdForShortcutId(const char *shortcutId) const;
static Binding makeBinding(const HotKey &hotkey);
std::vector<Binding> bindingsSnapshot() const;
static std::string makeShortcutId(KeyID key, KeyModifierMask mask);
EiScreen *m_screen = nullptr;
IEventQueue *m_events = nullptr;
Thread *m_glibThread = nullptr;
GMainLoop *m_glibMainLoop = nullptr;
XdpPortal *m_portal = nullptr;
XdpGlobalShortcutsSession *m_session = nullptr;
GCancellable *m_cancellable = nullptr;
std::map<Signal, gulong> m_signals = {
{Signal::SessionClosed, 0},
{Signal::Activated, 0},
{Signal::Deactivated, 0},
{Signal::ShortcutsChanged, 0},
};
mutable std::mutex m_bindingsMutex;
guint m_pendingIdleSource = 0; // guarded by m_bindingsMutex
// Snapshot derived from EiScreen::m_hotkeys. This is portal-facing state
std::vector<Binding> m_bindings;
};
} // namespace deskflow

View file

@ -23,16 +23,19 @@ ClientProxy1_1::ClientProxy1_1(const std::string &name, deskflow::IStream *strea
void ClientProxy1_1::keyDown(KeyID key, KeyModifierMask mask, KeyButton button, const std::string &)
{
LOG_VERBOSE("send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x", getName().c_str(), key, mask, button);
ProtocolUtil::writef(getStream(), kMsgDKeyDown1_1, key, mask, button);
ProtocolUtil::writef(getStream(), kMsgDKeyDown, key, mask, button);
}
void ClientProxy1_1::keyRepeat(KeyID key, KeyModifierMask mask, int32_t count, KeyButton button, const std::string &)
void ClientProxy1_1::keyRepeat(
KeyID key, KeyModifierMask mask, int32_t count, KeyButton button, const std::string &lang
)
{
LOG_VERBOSE(
"send key repeat to \"%s\" id=%d, mask=0x%04x, count=%d, button=0x%04x", getName().c_str(), key, mask, count,
button
LOG(
(CLOG_VERBOSE "send key repeat to \"%s\" id=%d, mask=0x%04x, count=%d, "
"button=0x%04x, lang=\"%s\"",
getName().c_str(), key, mask, count, button, lang.c_str())
);
ProtocolUtil::writef(getStream(), kMsgDKeyRepeat1_1, key, mask, count, button);
ProtocolUtil::writef(getStream(), kMsgDKeyRepeat, key, mask, count, button, &lang);
}
void ClientProxy1_1::keyUp(KeyID key, KeyModifierMask mask, KeyButton button)

View file

@ -32,20 +32,9 @@ void ClientProxy1_8::synchronizeLanguages() const
void ClientProxy1_8::keyDown(KeyID key, KeyModifierMask mask, KeyButton button, const std::string &language)
{
LOG_VERBOSE(
"send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x, layout=%s", getName().c_str(), key, mask, button,
language.c_str()
LOG(
(CLOG_VERBOSE "send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x, layout=%s", getName().c_str(), key,
mask, button, language.c_str())
);
ProtocolUtil::writef(getStream(), kMsgDKeyDown, key, mask, button, &language);
}
void ClientProxy1_8::keyRepeat(
KeyID key, KeyModifierMask mask, int32_t count, KeyButton button, const std::string &language
)
{
LOG_VERBOSE(
"send key repeat to \"%s\" id=%d, mask=0x%04x, count=%d, button=0x%04x, layout=%s", getName().c_str(), key, mask,
count, button, language.c_str()
);
ProtocolUtil::writef(getStream(), kMsgDKeyRepeat, key, mask, count, button, &language);
ProtocolUtil::writef(getStream(), kMsgDKeyDownLang, key, mask, button, &language);
}

View file

@ -15,7 +15,6 @@ public:
~ClientProxy1_8() override = default;
void keyDown(KeyID, KeyModifierMask, KeyButton, const std::string &) override;
void keyRepeat(KeyID, KeyModifierMask, int32_t count, KeyButton, const std::string &) override;
private:
void synchronizeLanguages() const;

View file

@ -126,19 +126,10 @@ Server::Server(ServerConfig &config, PrimaryClient *primaryClient, deskflow::Scr
// set initial configuration
setConfig(config);
// If we are using libportal, we need to set the primary client first.
// This enables the rules of the primary client. The hotkeys are then enabled in EiScreen.cpp after the rules
// registered them.
#if WINAPI_LIBPORTAL
// enable primary client
m_inputFilter->setPrimaryClient(m_primaryClient);
m_primaryClient->enable();
// If we are on Mac, X11 or Windows, we need a key map, which is created in screen->enable().
// So we need to enable the screen first, then enable the primary client.
#else
m_primaryClient->enable();
m_inputFilter->setPrimaryClient(m_primaryClient);
#endif
// Determine if scroll lock is already set. If so, lock the cursor to the
// primary screen (unless the user has disabled lock to screen in config)
if (!m_disableLockToScreen && (m_primaryClient->getToggleMask() & KeyModifierScrollLock)) {
@ -184,16 +175,11 @@ Server::~Server()
delete client;
}
// See at Server::Server for why we do this in this order.
#if WINAPI_LIBPORTAL
// disable and disconnect primary client
m_primaryClient->disable();
// remove input filter
m_inputFilter->setPrimaryClient(nullptr);
#else
m_inputFilter->setPrimaryClient(nullptr);
// disable and disconnect primary client
m_primaryClient->disable();
#endif
removeClient(m_primaryClient);
}

View file

@ -29,14 +29,6 @@ create_test(
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/base"
)
create_test(
NAME FileLogOutputterTests
DEPENDS base
LIBS arch ${extra_libs}
SOURCE FileLogOutputterTests.cpp
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/base"
)
create_test(
NAME BaseExceptionTests
DEPENDS base

View file

@ -1,79 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "FileLogOutputterTests.h"
#include <QFile>
#include <QFileInfo>
#include <QTest>
void FileLogOutputterTests::init()
{
QVERIFY(m_dir.isValid());
m_fileName = m_dir.filePath(QStringLiteral("deskflow.log"));
QFile::remove(m_fileName);
QFile::remove(rotatedFileName());
}
QString FileLogOutputterTests::rotatedFileName() const
{
return QStringLiteral("%1.1").arg(m_fileName);
}
QString FileLogOutputterTests::readAll(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly))
return {};
return QString::fromUtf8(file.readAll());
}
void FileLogOutputterTests::growLogPastSizeLimit(FileLogOutputter &outputter, const QString &marker)
{
const QString padding(s_paddingSize, QLatin1Char('x'));
QVERIFY(outputter.write(LogLevel::Level::Info, marker));
// rotating renames the live log away, so its absence is what ends the loop
for (int i = 0; i < s_maxPaddingWrites && QFileInfo::exists(m_fileName); ++i)
QVERIFY(outputter.write(LogLevel::Level::Info, padding));
}
void FileLogOutputterTests::write_multipleMessages_appendsEachLine()
{
FileLogOutputter outputter(m_fileName);
QVERIFY(outputter.write(LogLevel::Level::Info, QStringLiteral("first")));
QVERIFY(outputter.write(LogLevel::Level::Info, QStringLiteral("second")));
QCOMPARE(readAll(m_fileName), QStringLiteral("first\nsecond\n"));
}
void FileLogOutputterTests::write_sizeLimitExceeded_preservesPreviousLog()
{
FileLogOutputter outputter(m_fileName);
growLogPastSizeLimit(outputter, QStringLiteral("oldest entry"));
QVERIFY2(QFileInfo::exists(rotatedFileName()), "log was discarded instead of rotated");
QVERIFY(readAll(rotatedFileName()).startsWith(QStringLiteral("oldest entry\n")));
QVERIFY(outputter.write(LogLevel::Level::Info, QStringLiteral("after rotating")));
QCOMPARE(readAll(m_fileName), QStringLiteral("after rotating\n"));
}
void FileLogOutputterTests::write_rotatedLogExists_replacesIt()
{
FileLogOutputter outputter(m_fileName);
growLogPastSizeLimit(outputter, QStringLiteral("first batch"));
QVERIFY(readAll(rotatedFileName()).startsWith(QStringLiteral("first batch\n")));
growLogPastSizeLimit(outputter, QStringLiteral("second batch"));
QVERIFY(readAll(rotatedFileName()).startsWith(QStringLiteral("second batch\n")));
}
QTEST_MAIN(FileLogOutputterTests)

View file

@ -1,35 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "base/LogOutputters.h"
#include <QObject>
#include <QString>
#include <QTemporaryDir>
class FileLogOutputterTests : public QObject
{
Q_OBJECT
private Q_SLOTS:
void init();
void write_multipleMessages_appendsEachLine();
void write_sizeLimitExceeded_preservesPreviousLog();
void write_rotatedLogExists_replacesIt();
private:
QString rotatedFileName() const;
void growLogPastSizeLimit(FileLogOutputter &outputter, const QString &marker);
static QString readAll(const QString &fileName);
static constexpr int s_paddingSize = 64 * 1024;
static constexpr int s_maxPaddingWrites = 64;
QTemporaryDir m_dir;
QString m_fileName;
};

View file

@ -102,36 +102,4 @@ void EiKeyStateTests::clearStaleModifiers_shiftDownAndNumLockOn_shiftClearedAndN
QVERIFY((keyState.pollActiveModifiers() & KeyModifierNumLock) != 0);
}
void EiKeyStateTests::updateLockedModifiers_compositorLockState_numLockFollowsCompositor()
{
// Real modifier Mod2 is index 4 (Shift, Lock, Control, Mod1, Mod2, ...).
constexpr std::uint32_t xkbMod2Mask = 1 << 4;
TestAppUtil appUtil;
EventQueue eventQueue;
deskflow::EiKeyState keyState(nullptr, &eventQueue);
QTemporaryFile keymapFile;
QVERIFY(keymapFile.open());
const QByteArray keymapData = QByteArray::fromRawData(TestKeymap, sizeof(TestKeymap) - 1);
QCOMPARE(keymapFile.write(keymapData), keymapData.size());
QVERIFY(keymapFile.flush());
keyState.init(keymapFile.handle(), keymapFile.size());
keyState.updateXkbState(LeftShiftKeycode, true);
QVERIFY((keyState.pollActiveModifiers() & KeyModifierNumLock) == 0);
// NumLock toggled on locally, reported by the compositor
keyState.updateLockedModifiers(xkbMod2Mask);
QVERIFY((keyState.pollActiveModifiers() & KeyModifierNumLock) != 0);
QVERIFY((keyState.pollActiveModifiers() & KeyModifierShift) != 0);
keyState.clearStaleModifiers();
QVERIFY((keyState.pollActiveModifiers() & KeyModifierNumLock) != 0);
// NumLock toggled off locally
keyState.updateLockedModifiers(0);
QVERIFY((keyState.pollActiveModifiers() & KeyModifierNumLock) == 0);
}
QTEST_MAIN(EiKeyStateTests)

View file

@ -18,7 +18,6 @@ class EiKeyStateTests : public QObject
private Q_SLOTS:
void initTestCase();
void clearStaleModifiers_shiftDownAndNumLockOn_shiftClearedAndNumLockPreserved();
void updateLockedModifiers_compositorLockState_numLockFollowsCompositor();
private:
Arch m_arch;

View file

@ -21,10 +21,3 @@ create_test(
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/server"
)
create_test(
NAME ClientProxyTests
DEPENDS server
LIBS base arch io mt net platform app ${extra_libs}
SOURCE ClientProxyTests.cpp
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/server"
)

View file

@ -1,225 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "ClientProxyTests.h"
#include "../deskflow/MockEventQueue.h"
#include "deskflow/AppUtil.h"
#include "io/IStream.h"
#include "server/ClientProxy1_0.h"
#include "server/ClientProxy1_1.h"
#include "server/ClientProxy1_6.h"
#include "server/ClientProxy1_7.h"
#include "server/ClientProxy1_8.h"
#include <memory>
#include <string>
#include <vector>
#include <QByteArray>
#include <QTest>
namespace {
class TestAppUtil : public AppUtil
{
public:
int run() override
{
return 0;
}
std::vector<std::string> getKeyboardLayoutList() override
{
return {"en"};
}
std::string getCurrentLanguageCode() override
{
return "en";
}
};
class CapturingStream : public deskflow::IStream
{
public:
QByteArray take()
{
auto bytes = m_buffer;
m_buffer.clear();
return bytes;
}
void write(const void *buffer, uint32_t n) override
{
m_buffer.append(static_cast<const char *>(buffer), n);
}
void close() override
{
}
uint32_t read(void *, uint32_t) override
{
return 0;
}
void flush() override
{
}
void shutdownInput() override
{
}
void shutdownOutput() override
{
}
void *getEventTarget() const override
{
return const_cast<CapturingStream *>(this);
}
bool isReady() const override
{
return false;
}
uint32_t getSize() const override
{
return 0;
}
private:
QByteArray m_buffer;
};
std::unique_ptr<ClientProxy> makeProxy(int minor, deskflow::IStream *stream, IEventQueue *events)
{
// the 1.4 and later constructors assert the server pointer is non-null but only store it
auto *server = reinterpret_cast<Server *>(0x1);
std::unique_ptr<ClientProxy> proxy;
switch (minor) {
case 0:
proxy = std::make_unique<ClientProxy1_0>("client", stream, events);
break;
case 1:
proxy = std::make_unique<ClientProxy1_1>("client", stream, events);
break;
case 6:
proxy = std::make_unique<ClientProxy1_6>("client", stream, server, events);
break;
case 7:
proxy = std::make_unique<ClientProxy1_7>("client", stream, server, events);
break;
case 8:
proxy = std::make_unique<ClientProxy1_8>("client", stream, server, events);
break;
default:
break;
}
return proxy;
}
struct ProxyUnderTest
{
MockEventQueue events;
CapturingStream *stream = new CapturingStream;
std::unique_ptr<ClientProxy> proxy;
explicit ProxyUnderTest(int minor) : proxy(makeProxy(minor, stream, &events))
{
// drop the query-info and layout-sync messages the constructors send
stream->take();
}
};
const KeyID kKey = 0x61;
const KeyModifierMask kMask = 0;
const KeyButton kButton = 0x1e;
const int32_t kCount = 3;
const std::string kLang = "en";
} // namespace
void ClientProxyTests::initTestCase()
{
// the 1.8 constructor reads the keyboard layouts through AppUtil::instance()
static TestAppUtil appUtil;
}
// These formats are frozen because shipped third-party clients parse them byte
// for byte: Synergy 1.4 through 1.14.1 negotiate 1.4 through 1.7, Barrier and
// Input Leap negotiate 1.6, and Synergy 1.14.2 onwards and Deskflow negotiate 1.8.
void ClientProxyTests::keyDown_data()
{
QTest::addColumn<int>("minor");
QTest::addColumn<QByteArray>("expected");
QTest::newRow("1.0") << 0 << "DKDN" + QByteArray::fromHex("0061 0000");
QTest::newRow("1.1") << 1 << "DKDN" + QByteArray::fromHex("0061 0000 001e");
QTest::newRow("1.6") << 6 << "DKDN" + QByteArray::fromHex("0061 0000 001e");
QTest::newRow("1.7") << 7 << "DKDN" + QByteArray::fromHex("0061 0000 001e");
QTest::newRow("1.8") << 8 << "DKDL" + QByteArray::fromHex("0061 0000 001e 00000002") + "en";
}
void ClientProxyTests::keyDown()
{
QFETCH(int, minor);
QFETCH(QByteArray, expected);
ProxyUnderTest test(minor);
test.proxy->keyDown(kKey, kMask, kButton, kLang);
QCOMPARE(test.stream->take(), expected);
}
void ClientProxyTests::keyRepeat_data()
{
QTest::addColumn<int>("minor");
QTest::addColumn<QByteArray>("expected");
QTest::newRow("1.0") << 0 << "DKRP" + QByteArray::fromHex("0061 0000 0003");
QTest::newRow("1.1") << 1 << "DKRP" + QByteArray::fromHex("0061 0000 0003 001e");
QTest::newRow("1.6") << 6 << "DKRP" + QByteArray::fromHex("0061 0000 0003 001e");
QTest::newRow("1.7") << 7 << "DKRP" + QByteArray::fromHex("0061 0000 0003 001e");
QTest::newRow("1.8") << 8 << "DKRP" + QByteArray::fromHex("0061 0000 0003 001e 00000002") + "en";
}
void ClientProxyTests::keyRepeat()
{
QFETCH(int, minor);
QFETCH(QByteArray, expected);
ProxyUnderTest test(minor);
test.proxy->keyRepeat(kKey, kMask, kCount, kButton, kLang);
QCOMPARE(test.stream->take(), expected);
}
void ClientProxyTests::keyUp_data()
{
QTest::addColumn<int>("minor");
QTest::addColumn<QByteArray>("expected");
QTest::newRow("1.0") << 0 << "DKUP" + QByteArray::fromHex("0061 0000");
QTest::newRow("1.1") << 1 << "DKUP" + QByteArray::fromHex("0061 0000 001e");
QTest::newRow("1.6") << 6 << "DKUP" + QByteArray::fromHex("0061 0000 001e");
QTest::newRow("1.7") << 7 << "DKUP" + QByteArray::fromHex("0061 0000 001e");
QTest::newRow("1.8") << 8 << "DKUP" + QByteArray::fromHex("0061 0000 001e");
}
void ClientProxyTests::keyUp()
{
QFETCH(int, minor);
QFETCH(QByteArray, expected);
ProxyUnderTest test(minor);
test.proxy->keyUp(kKey, kMask, kButton);
QCOMPARE(test.stream->take(), expected);
}
QTEST_MAIN(ClientProxyTests)

View file

@ -1,28 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "base/Log.h"
#include <QObject>
class ClientProxyTests : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void keyDown_data();
void keyDown();
void keyRepeat_data();
void keyRepeat();
void keyUp_data();
void keyUp();
private:
Log m_log;
};