Compare commits
10 commits
adb4f89453
...
0b7a72ae0e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b7a72ae0e | ||
|
|
49e6293a4d | ||
|
|
6638076b85 | ||
|
|
1e32335890 | ||
|
|
ac475f0b1b | ||
|
|
23b0d7792d | ||
|
|
217166c06b | ||
|
|
ce2f948c29 | ||
|
|
71929a3418 | ||
|
|
d6874dfe27 |
37 changed files with 1697 additions and 214 deletions
|
|
@ -155,10 +155,11 @@ 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+ |
|
| [**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+ |
|
| [**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+ |
|
| [**DINF**](@ref kMsgDInfo) | @ref kMsgDInfo | Data | Client→Server | Screen information | [MsgSize](#constraint-protocol-max-message-length) | 1.0+ |
|
||||||
| [**DKDL**](@ref kMsgDKeyDownLang) | @ref kMsgDKeyDownLang | Data | Server→Client | Key down with language | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.8+ |
|
| [**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 kMsgDKeyDown) | @ref kMsgDKeyDown | Data | Server→Client | Key down | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.1+ |
|
| [**DKDN**](@ref kMsgDKeyDown1_1) | @ref kMsgDKeyDown1_1 | 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 |
|
| [**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 | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.1+ |
|
| [**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 kMsgDKeyRepeat1_0) | @ref kMsgDKeyRepeat1_0 | Data | Server→Client | Key repeat (legacy) | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.0 |
|
| [**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 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 |
|
| [**DKUP**](@ref kMsgDKeyUp1_0) | @ref kMsgDKeyUp1_0 | Data | Server→Client | Key up (legacy) | [MsgSize](#constraint-protocol-max-message-length), [KeyMap](#constraint-keymap) | 1.0 |
|
||||||
|
|
@ -288,8 +289,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.4** | Nov 2012 | Synergy | Encryption support (@ref SecureSocket) | 1.4+ |
|
||||||
| **1.5** | Sep 2013 | Synergy | File transfer | 1.5+ |
|
| **1.5** | Sep 2013 | Synergy | File transfer | 1.5+ |
|
||||||
| **1.6** | Jan 2014 | Synergy | Clipboard streaming | 1.6+ |
|
| **1.6** | Jan 2014 | Synergy | Clipboard streaming | 1.6+ |
|
||||||
| **1.7** | Nov 2021 | Synergy | Secure input notifications | 1.7+ |
|
| **1.7** | Sep 2021 | Synergy | Secure input notifications | 1.7+ |
|
||||||
| **1.8** | Jun 2025 | Synergy | Language synchronization | 1.8+ |
|
| **1.8** | Nov 2021 | Synergy | Language synchronization | 1.8+ |
|
||||||
|
|
||||||
### Version Migration Guide
|
### Version Migration Guide
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
|
# SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
|
||||||
# SPDX-FileCopyrightText: (C) 2024 Chris Rizzitello <sithlord48@gmail.com>
|
# SPDX-FileCopyrightText: (C) 2024 Chris Rizzitello <sithlord48@gmail.com>
|
||||||
# SPDX-FileCopyrightText: (C) 2012 - 2024 Symless Ltd
|
# SPDX-FileCopyrightText: (C) 2012 - 2024 Symless Ltd
|
||||||
# SPDX-FileCopyrightText: (C) 2009 - 2012 Nick Bolton
|
# SPDX-FileCopyrightText: (C) 2009 - 2012 Nick Bolton
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
add_library(base STATIC
|
if(APPLE)
|
||||||
|
set(PLATFORM_CODE
|
||||||
|
OSXAutoReleasePool.h
|
||||||
|
OSXAutoReleasePool.mm
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(base STATIC ${PLATFORM_CODE}
|
||||||
BaseException.cpp
|
BaseException.cpp
|
||||||
BaseException.h
|
BaseException.h
|
||||||
DirectionTypes.h
|
DirectionTypes.h
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@
|
||||||
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
#include "base/OSXAutoReleasePool.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
// interrupt handler. this just adds a quit event to the queue.
|
// interrupt handler. this just adds a quit event to the queue.
|
||||||
static void interrupt(Arch::ThreadSignal, void *data)
|
static void interrupt(Arch::ThreadSignal, void *data)
|
||||||
{
|
{
|
||||||
|
|
@ -166,16 +170,23 @@ bool EventQueue::getEvent(Event &event, double timeout)
|
||||||
|
|
||||||
bool EventQueue::dispatchEvent(const Event &event)
|
bool EventQueue::dispatchEvent(const Event &event)
|
||||||
{
|
{
|
||||||
void *target = event.getTarget();
|
#if defined(__APPLE__)
|
||||||
if (auto typeHandler = getHandler(event.getType(), target); typeHandler.has_value()) {
|
auto dispatch = [&] {
|
||||||
(*typeHandler)(event);
|
#endif
|
||||||
return true;
|
void *target = event.getTarget();
|
||||||
}
|
if (auto typeHandler = getHandler(event.getType(), target); typeHandler.has_value()) {
|
||||||
if (auto anyHandler = getHandler(EventTypes::Unknown, target); anyHandler.has_value()) {
|
(*typeHandler)(event);
|
||||||
(*anyHandler)(event);
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
if (auto anyHandler = getHandler(EventTypes::Unknown, target); anyHandler.has_value()) {
|
||||||
return false;
|
(*anyHandler)(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
};
|
||||||
|
return deskflow::runInAutoReleasePool(dispatch);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventQueue::addEvent(Event &&event)
|
void EventQueue::addEvent(Event &&event)
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ bool FileLogOutputter::write(LogLevel::Level, const QString &message)
|
||||||
|
|
||||||
if (file.size() > s_logFileSizeLimit) {
|
if (file.size() > s_logFileSizeLimit) {
|
||||||
const auto oldFile = QStringLiteral("%1.1").arg(m_fileName);
|
const auto oldFile = QStringLiteral("%1.1").arg(m_fileName);
|
||||||
QFile::remove(m_fileName);
|
QFile::remove(oldFile);
|
||||||
QFile::rename(m_fileName, oldFile);
|
QFile::rename(m_fileName, oldFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
16
src/lib/base/OSXAutoReleasePool.h
Normal file
16
src/lib/base/OSXAutoReleasePool.h
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
20
src/lib/base/OSXAutoReleasePool.mm
Normal file
20
src/lib/base/OSXAutoReleasePool.mm
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
@ -8,6 +8,10 @@ add_library(client STATIC
|
||||||
Client.h
|
Client.h
|
||||||
ServerProxy.cpp
|
ServerProxy.cpp
|
||||||
ServerProxy.h
|
ServerProxy.h
|
||||||
|
ServerProxy1_7.cpp
|
||||||
|
ServerProxy1_7.h
|
||||||
|
ServerProxy1_8.cpp
|
||||||
|
ServerProxy1_8.h
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(client PUBLIC common)
|
target_link_libraries(client PUBLIC common)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
#include "base/IEventQueue.h"
|
#include "base/IEventQueue.h"
|
||||||
#include "base/Log.h"
|
#include "base/Log.h"
|
||||||
#include "client/ServerProxy.h"
|
#include "client/ServerProxy.h"
|
||||||
|
#include "client/ServerProxy1_7.h"
|
||||||
|
#include "client/ServerProxy1_8.h"
|
||||||
#include "common/NetworkProtocol.h"
|
#include "common/NetworkProtocol.h"
|
||||||
#include "common/Settings.h"
|
#include "common/Settings.h"
|
||||||
#include "deskflow/Clipboard.h"
|
#include "deskflow/Clipboard.h"
|
||||||
|
|
@ -465,18 +467,38 @@ void Client::setupConnection()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::setupScreen()
|
bool Client::setupScreen(int16_t protocolMinor)
|
||||||
{
|
{
|
||||||
assert(m_server == nullptr);
|
assert(m_server == nullptr);
|
||||||
|
|
||||||
m_ready = false;
|
m_ready = false;
|
||||||
m_server = new ServerProxy(this, m_stream, m_events);
|
|
||||||
m_events->addHandler(EventTypes::ScreenShapeChanged, getEventTarget(), [this](const auto &) {
|
// only 1.6 and later have a proxy: the clipboard, mouse wheel and key message formats
|
||||||
handleShapeChanged();
|
// 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.
|
||||||
m_events->addHandler(EventTypes::ClipboardGrabbed, getEventTarget(), [this](const auto &e) {
|
switch (protocolMinor) {
|
||||||
handleClipboardGrabbed(e);
|
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()
|
void Client::setupTimer()
|
||||||
|
|
@ -683,6 +705,17 @@ 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);
|
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`
|
// dynamically build write format for hello back since `ProtocolUtil::writef`
|
||||||
|
|
@ -691,7 +724,6 @@ void Client::handleHello()
|
||||||
ProtocolUtil::writef(m_stream, helloBackMessage.c_str(), kProtocolMajorVersion, helloBackMinor, &m_name);
|
ProtocolUtil::writef(m_stream, helloBackMessage.c_str(), kProtocolMajorVersion, helloBackMinor, &m_name);
|
||||||
|
|
||||||
// now connected but waiting to complete handshake
|
// now connected but waiting to complete handshake
|
||||||
setupScreen();
|
|
||||||
cleanupTimer();
|
cleanupTimer();
|
||||||
|
|
||||||
// make sure we process any remaining messages later. we won't
|
// make sure we process any remaining messages later. we won't
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ private:
|
||||||
void sendConnectionFailedEvent(const char *msg);
|
void sendConnectionFailedEvent(const char *msg);
|
||||||
void setupConnecting();
|
void setupConnecting();
|
||||||
void setupConnection();
|
void setupConnection();
|
||||||
void setupScreen();
|
bool setupScreen(int16_t protocolMinor);
|
||||||
void setupTimer();
|
void setupTimer();
|
||||||
void cleanup();
|
void cleanup();
|
||||||
void cleanupConnecting();
|
void cleanupConnecting();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Deskflow -- mouse and keyboard sharing utility
|
* Deskflow -- mouse and keyboard sharing utility
|
||||||
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
|
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
|
||||||
* SPDX-FileCopyrightText: (C) 2012 - 2016 Synergy App Ltd
|
* SPDX-FileCopyrightText: (C) 2012 - 2016, 2026 Synergy App Ltd
|
||||||
* SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman
|
* SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman
|
||||||
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
||||||
*/
|
*/
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
#include "deskflow/ProtocolTypes.h"
|
#include "deskflow/ProtocolTypes.h"
|
||||||
#include "deskflow/ProtocolUtil.h"
|
#include "deskflow/ProtocolUtil.h"
|
||||||
#include "deskflow/StreamChunker.h"
|
#include "deskflow/StreamChunker.h"
|
||||||
#include "deskflow/ipc/CoreIpc.h"
|
|
||||||
#include "io/IStream.h"
|
#include "io/IStream.h"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
@ -140,12 +139,7 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
|
||||||
setOptions();
|
setOptions();
|
||||||
|
|
||||||
// handshake is complete
|
// handshake is complete
|
||||||
m_parser = &ServerProxy::parseMessage;
|
m_parser = &ServerProxy::handleMessage;
|
||||||
|
|
||||||
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();
|
m_client->handshakeComplete();
|
||||||
}
|
}
|
||||||
|
|
@ -196,8 +190,6 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
|
||||||
LOG_ERR("server disconnected due to a protocol error");
|
LOG_ERR("server disconnected due to a protocol error");
|
||||||
requestRefuseConnection(ProtocolError, "server reported a protocol error");
|
requestRefuseConnection(ProtocolError, "server reported a protocol error");
|
||||||
return Disconnect;
|
return Disconnect;
|
||||||
} else if (memcmp(code, kMsgDLanguageSynchronisation, 4) == 0) {
|
|
||||||
setServerLanguages();
|
|
||||||
} else {
|
} else {
|
||||||
return Unknown;
|
return Unknown;
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +197,22 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
|
||||||
return Okay;
|
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)
|
ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
|
||||||
{
|
{
|
||||||
using enum ConnectionResult;
|
using enum ConnectionResult;
|
||||||
|
|
@ -221,28 +229,16 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
|
||||||
mouseWheel();
|
mouseWheel();
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (memcmp(code, kMsgDKeyDown, 4) == 0) {
|
else if (memcmp(code, kMsgDKeyDown1_1, 4) == 0) {
|
||||||
uint16_t id = 0;
|
uint16_t id = 0;
|
||||||
uint16_t mask = 0;
|
uint16_t mask = 0;
|
||||||
uint16_t button = 0;
|
uint16_t button = 0;
|
||||||
ProtocolUtil::readf(m_stream, kMsgDKeyDown + 4, &id, &mask, &button);
|
ProtocolUtil::readf(m_stream, kMsgDKeyDown1_1 + 4, &id, &mask, &button);
|
||||||
LOG_VERBOSE("recv key down id=0x%08x, mask=0x%04x, button=0x%04x", id, mask, button);
|
LOG_VERBOSE("recv key down id=0x%08x, mask=0x%04x, button=0x%04x", id, mask, button);
|
||||||
|
|
||||||
keyDown(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) {
|
else if (memcmp(code, kMsgDKeyUp, 4) == 0) {
|
||||||
keyUp();
|
keyUp();
|
||||||
}
|
}
|
||||||
|
|
@ -255,8 +251,15 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
|
||||||
mouseUp();
|
mouseUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (memcmp(code, kMsgDKeyRepeat, 4) == 0) {
|
else if (memcmp(code, kMsgDKeyRepeat1_1, 4) == 0) {
|
||||||
keyRepeat();
|
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, kMsgCKeepAlive, 4) == 0) {
|
else if (memcmp(code, kMsgCKeepAlive, 4) == 0) {
|
||||||
|
|
@ -305,10 +308,6 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
|
||||||
setOptions();
|
setOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (memcmp(code, kMsgDSecureInputNotification, 4) == 0) {
|
|
||||||
secureInputNotification();
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (memcmp(code, kMsgCClose, 4) == 0) {
|
else if (memcmp(code, kMsgCClose, 4) == 0) {
|
||||||
// server wants us to hangup
|
// server wants us to hangup
|
||||||
LOG_VERBOSE("recv close");
|
LOG_VERBOSE("recv close");
|
||||||
|
|
@ -322,15 +321,6 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
|
||||||
return Unknown;
|
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;
|
return Okay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -521,8 +511,6 @@ void ServerProxy::enter()
|
||||||
m_dxMouse = 0;
|
m_dxMouse = 0;
|
||||||
m_dyMouse = 0;
|
m_dyMouse = 0;
|
||||||
m_seqNum = seqNum;
|
m_seqNum = seqNum;
|
||||||
m_serverLayout = "";
|
|
||||||
m_isUserNotifiedAboutLayoutSyncError = false;
|
|
||||||
|
|
||||||
// forward
|
// forward
|
||||||
m_client->enter(x, y, seqNum, static_cast<KeyModifierMask>(mask), false);
|
m_client->enter(x, y, seqNum, static_cast<KeyModifierMask>(mask), false);
|
||||||
|
|
@ -588,45 +576,25 @@ void ServerProxy::grabClipboard()
|
||||||
|
|
||||||
void ServerProxy::keyDown(uint16_t id, uint16_t mask, uint16_t button, const std::string &lang)
|
void ServerProxy::keyDown(uint16_t id, uint16_t mask, uint16_t button, const std::string &lang)
|
||||||
{
|
{
|
||||||
// get mouse up to date
|
|
||||||
flushCompressedMouse();
|
flushCompressedMouse();
|
||||||
setActiveServerLanguage(lang);
|
|
||||||
|
|
||||||
// translate
|
|
||||||
KeyID id2 = translateKey(static_cast<KeyID>(id));
|
KeyID id2 = translateKey(static_cast<KeyID>(id));
|
||||||
KeyModifierMask mask2 = translateModifierMask(static_cast<KeyModifierMask>(mask));
|
KeyModifierMask mask2 = translateModifierMask(static_cast<KeyModifierMask>(mask));
|
||||||
if (id2 != static_cast<KeyID>(id) || mask2 != 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);
|
LOG_VERBOSE("key down translated to id=0x%08x, mask=0x%04x", id2, mask2);
|
||||||
|
|
||||||
// forward
|
|
||||||
m_client->keyDown(id2, mask2, button, lang);
|
m_client->keyDown(id2, mask2, button, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServerProxy::keyRepeat()
|
void ServerProxy::keyRepeat(uint16_t id, uint16_t mask, uint16_t count, uint16_t button, const std::string &lang)
|
||||||
{
|
{
|
||||||
// get mouse up to date
|
|
||||||
flushCompressedMouse();
|
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));
|
KeyID id2 = translateKey(static_cast<KeyID>(id));
|
||||||
KeyModifierMask mask2 = translateModifierMask(static_cast<KeyModifierMask>(mask));
|
KeyModifierMask mask2 = translateModifierMask(static_cast<KeyModifierMask>(mask));
|
||||||
if (id2 != static_cast<KeyID>(id) || mask2 != 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);
|
LOG_VERBOSE("key repeat translated to id=0x%08x, mask=0x%04x", id2, mask2);
|
||||||
|
|
||||||
// forward
|
|
||||||
m_client->keyRepeat(id2, mask2, count, button, lang);
|
m_client->keyRepeat(id2, mask2, count, button, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -841,38 +809,3 @@ void ServerProxy::infoAcknowledgment()
|
||||||
LOG_VERBOSE("recv info acknowledgment");
|
LOG_VERBOSE("recv info acknowledgment");
|
||||||
m_ignoreMouse = false;
|
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Deskflow -- mouse and keyboard sharing utility
|
* Deskflow -- mouse and keyboard sharing utility
|
||||||
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
|
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
|
||||||
* SPDX-FileCopyrightText: (C) 2012 - 2016 Synergy App Ltd
|
* SPDX-FileCopyrightText: (C) 2012 - 2016, 2026 Synergy App Ltd
|
||||||
* SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman
|
* SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman
|
||||||
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
||||||
*/
|
*/
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
#include "deskflow/ClipboardChunk.h"
|
#include "deskflow/ClipboardChunk.h"
|
||||||
#include "deskflow/ClipboardTypes.h"
|
#include "deskflow/ClipboardTypes.h"
|
||||||
#include "deskflow/KeyTypes.h"
|
#include "deskflow/KeyTypes.h"
|
||||||
#include "deskflow/KeyboardLayoutManager.h"
|
|
||||||
|
|
||||||
class Client;
|
class Client;
|
||||||
class ClientInfo;
|
class ClientInfo;
|
||||||
|
|
@ -23,10 +22,12 @@ class IStream;
|
||||||
}
|
}
|
||||||
class IEventQueue;
|
class IEventQueue;
|
||||||
|
|
||||||
//! Proxy for server
|
//! Proxy for server implementing protocol version 1.6
|
||||||
/*!
|
/*!
|
||||||
This class acts a proxy for the server, converting calls into messages
|
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.
|
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
|
class ServerProxy
|
||||||
{
|
{
|
||||||
|
|
@ -38,7 +39,7 @@ public:
|
||||||
ServerProxy(Client *client, deskflow::IStream *stream, IEventQueue *events);
|
ServerProxy(Client *client, deskflow::IStream *stream, IEventQueue *events);
|
||||||
ServerProxy(ServerProxy const &) = delete;
|
ServerProxy(ServerProxy const &) = delete;
|
||||||
ServerProxy(ServerProxy &&) = delete;
|
ServerProxy(ServerProxy &&) = delete;
|
||||||
~ServerProxy();
|
virtual ~ServerProxy();
|
||||||
|
|
||||||
ServerProxy &operator=(ServerProxy const &) = delete;
|
ServerProxy &operator=(ServerProxy const &) = delete;
|
||||||
ServerProxy &operator=(ServerProxy &&) = delete;
|
ServerProxy &operator=(ServerProxy &&) = delete;
|
||||||
|
|
@ -59,8 +60,16 @@ protected:
|
||||||
Unknown,
|
Unknown,
|
||||||
Disconnect
|
Disconnect
|
||||||
};
|
};
|
||||||
ConnectionResult parseHandshakeMessage(const uint8_t *code);
|
|
||||||
ConnectionResult parseMessage(const uint8_t *code);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// if compressing mouse motion then send the last motion now
|
// if compressing mouse motion then send the last motion now
|
||||||
|
|
@ -77,6 +86,7 @@ private:
|
||||||
|
|
||||||
// event handlers
|
// event handlers
|
||||||
void handleData();
|
void handleData();
|
||||||
|
ConnectionResult handleMessage(const uint8_t *code);
|
||||||
void handleKeepAliveAlarm();
|
void handleKeepAliveAlarm();
|
||||||
void requestDisconnect(const char *message);
|
void requestDisconnect(const char *message);
|
||||||
void requestRefuseConnection(deskflow::core::ConnectionRefusal reason, const char *message);
|
void requestRefuseConnection(deskflow::core::ConnectionRefusal reason, const char *message);
|
||||||
|
|
@ -86,8 +96,6 @@ private:
|
||||||
void leave();
|
void leave();
|
||||||
void setClipboard();
|
void setClipboard();
|
||||||
void grabClipboard();
|
void grabClipboard();
|
||||||
void keyDown(uint16_t id, uint16_t mask, uint16_t button, const std::string &lang);
|
|
||||||
void keyRepeat();
|
|
||||||
void keyUp();
|
void keyUp();
|
||||||
void mouseDown();
|
void mouseDown();
|
||||||
void mouseUp();
|
void mouseUp();
|
||||||
|
|
@ -99,9 +107,6 @@ private:
|
||||||
void setOptions();
|
void setOptions();
|
||||||
void queryInfo();
|
void queryInfo();
|
||||||
void infoAcknowledgment();
|
void infoAcknowledgment();
|
||||||
void secureInputNotification();
|
|
||||||
void setServerLanguages();
|
|
||||||
void setActiveServerLanguage(const std::string_view &language);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using MessageParser = ConnectionResult (ServerProxy::*)(const uint8_t *);
|
using MessageParser = ConnectionResult (ServerProxy::*)(const uint8_t *);
|
||||||
|
|
@ -127,9 +132,6 @@ private:
|
||||||
|
|
||||||
MessageParser m_parser = &ServerProxy::parseHandshakeMessage;
|
MessageParser m_parser = &ServerProxy::parseHandshakeMessage;
|
||||||
IEventQueue *m_events = nullptr;
|
IEventQueue *m_events = nullptr;
|
||||||
std::string m_serverLayout = "";
|
|
||||||
std::string m_clipboardDataCached;
|
std::string m_clipboardDataCached;
|
||||||
ClipboardChunkAssemblyState m_clipboardChunkState;
|
ClipboardChunkAssemblyState m_clipboardChunkState;
|
||||||
bool m_isUserNotifiedAboutLayoutSyncError = false;
|
|
||||||
deskflow::KeyboardLayoutManager m_layoutManager;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
37
src/lib/client/ServerProxy1_7.cpp
Normal file
37
src/lib/client/ServerProxy1_7.cpp
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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());
|
||||||
|
}
|
||||||
23
src/lib/client/ServerProxy1_7.h
Normal file
23
src/lib/client/ServerProxy1_7.h
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
103
src/lib/client/ServerProxy1_8.cpp
Normal file
103
src/lib/client/ServerProxy1_8.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/lib/client/ServerProxy1_8.h
Normal file
33
src/lib/client/ServerProxy1_8.h
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
|
|
@ -13,6 +13,7 @@ const char *const kMsgHello = "%7s%2i%2i";
|
||||||
const char *const kMsgHelloArgs = "%2i%2i";
|
const char *const kMsgHelloArgs = "%2i%2i";
|
||||||
const char *const kMsgHelloBack = "%7s%2i%2i%s";
|
const char *const kMsgHelloBack = "%7s%2i%2i%s";
|
||||||
const char *const kMsgHelloBackArgs = "%2i%2i%s";
|
const char *const kMsgHelloBackArgs = "%2i%2i%s";
|
||||||
|
|
||||||
const char *const kMsgCNoop = "CNOP";
|
const char *const kMsgCNoop = "CNOP";
|
||||||
const char *const kMsgCClose = "CBYE";
|
const char *const kMsgCClose = "CBYE";
|
||||||
const char *const kMsgCEnter = "CINN%2i%2i%4i%2i";
|
const char *const kMsgCEnter = "CINN%2i%2i%4i%2i";
|
||||||
|
|
@ -22,10 +23,15 @@ const char *const kMsgCScreenSaver = "CSEC%1i";
|
||||||
const char *const kMsgCResetOptions = "CROP";
|
const char *const kMsgCResetOptions = "CROP";
|
||||||
const char *const kMsgCInfoAck = "CIAK";
|
const char *const kMsgCInfoAck = "CIAK";
|
||||||
const char *const kMsgCKeepAlive = "CALV";
|
const char *const kMsgCKeepAlive = "CALV";
|
||||||
const char *const kMsgDKeyDownLang = "DKDL%2i%2i%2i%s";
|
|
||||||
const char *const kMsgDKeyDown = "DKDN%2i%2i%2i";
|
// 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 kMsgDKeyDown1_0 = "DKDN%2i%2i";
|
const char *const kMsgDKeyDown1_0 = "DKDN%2i%2i";
|
||||||
|
|
||||||
const char *const kMsgDKeyRepeat = "DKRP%2i%2i%2i%2i%s";
|
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 kMsgDKeyRepeat1_0 = "DKRP%2i%2i%2i";
|
||||||
const char *const kMsgDKeyUp = "DKUP%2i%2i%2i";
|
const char *const kMsgDKeyUp = "DKUP%2i%2i%2i";
|
||||||
const char *const kMsgDKeyUp1_0 = "DKUP%2i%2i";
|
const char *const kMsgDKeyUp1_0 = "DKUP%2i%2i";
|
||||||
|
|
|
||||||
|
|
@ -504,7 +504,7 @@ extern const char *const kMsgCKeepAlive;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Key press with language code (v1.8+)
|
* @brief Key press event
|
||||||
*
|
*
|
||||||
* **Message Code**: `"DKDL"`
|
* **Message Code**: `"DKDL"`
|
||||||
* **Direction**: Primary → Secondary
|
* **Direction**: Primary → Secondary
|
||||||
|
|
@ -524,34 +524,6 @@ extern const char *const kMsgCKeepAlive;
|
||||||
* "DKDL\x00\x61\x00\x00\x00\x1E\x00\x00\x00\x02en"
|
* "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**:
|
* **Key Mapping Strategy**:
|
||||||
* The KeyButton parameter is crucial for proper key release handling.
|
* The KeyButton parameter is crucial for proper key release handling.
|
||||||
* The secondary screen should:
|
* The secondary screen should:
|
||||||
|
|
@ -564,11 +536,42 @@ extern const char *const kMsgDKeyDownLang;
|
||||||
* - Different keyboard layouts may produce different KeyIDs
|
* - Different keyboard layouts may produce different KeyIDs
|
||||||
* - Modifier keys released before the main key can alter KeyID
|
* - Modifier keys released before the main key can alter KeyID
|
||||||
*
|
*
|
||||||
* @see kMsgDKeyUp, kMsgDKeyDownLang
|
* The language code helps clients handle characters from a keyboard layout
|
||||||
* @since Protocol version 1.1
|
* 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;
|
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
|
||||||
|
* @since Protocol version 1.1
|
||||||
|
*/
|
||||||
|
extern const char *const kMsgDKeyDown1_1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Key press event (legacy v1.0)
|
* @brief Key press event (legacy v1.0)
|
||||||
*
|
*
|
||||||
|
|
@ -582,8 +585,8 @@ extern const char *const kMsgDKeyDown;
|
||||||
* Legacy version without KeyButton parameter. Used only when
|
* Legacy version without KeyButton parameter. Used only when
|
||||||
* communicating with protocol version 1.0 clients.
|
* communicating with protocol version 1.0 clients.
|
||||||
*
|
*
|
||||||
* @deprecated Use kMsgDKeyDown for protocol version 1.1+
|
* @deprecated Use kMsgDKeyDown1_1 for protocol version 1.1+
|
||||||
* @see kMsgDKeyDown
|
* @see kMsgDKeyDown1_1
|
||||||
* @since Protocol version 1.0
|
* @since Protocol version 1.0
|
||||||
*/
|
*/
|
||||||
extern const char *const kMsgDKeyDown1_0;
|
extern const char *const kMsgDKeyDown1_0;
|
||||||
|
|
@ -613,11 +616,43 @@ extern const char *const kMsgDKeyDown1_0;
|
||||||
* Sent when a key is held down and auto-repeating. The repeat count
|
* Sent when a key is held down and auto-repeating. The repeat count
|
||||||
* indicates how many repeat events occurred since the last message.
|
* indicates how many repeat events occurred since the last message.
|
||||||
*
|
*
|
||||||
* @see kMsgDKeyDown
|
* Shares the `DKRP` message code with kMsgDKeyRepeat1_1, so the receiver can
|
||||||
* @since Protocol version 1.1
|
* 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;
|
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
|
||||||
|
* @since Protocol version 1.1
|
||||||
|
*/
|
||||||
|
extern const char *const kMsgDKeyRepeat1_1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Key auto-repeat event (legacy v1.0)
|
* @brief Key auto-repeat event (legacy v1.0)
|
||||||
*
|
*
|
||||||
|
|
@ -629,10 +664,10 @@ extern const char *const kMsgDKeyRepeat;
|
||||||
* - `$2`: KeyModifierMask (2 bytes) - Active modifier keys
|
* - `$2`: KeyModifierMask (2 bytes) - Active modifier keys
|
||||||
* - `$3`: Repeat count (2 bytes) - Number of repeats
|
* - `$3`: Repeat count (2 bytes) - Number of repeats
|
||||||
*
|
*
|
||||||
* Legacy version without KeyButton and language parameters.
|
* Legacy version without the KeyButton parameter.
|
||||||
*
|
*
|
||||||
* @deprecated Use kMsgDKeyRepeat for protocol version 1.1+
|
* @deprecated Use kMsgDKeyRepeat1_1 for protocol version 1.1+
|
||||||
* @see kMsgDKeyRepeat
|
* @see kMsgDKeyRepeat1_1
|
||||||
* @since Protocol version 1.0
|
* @since Protocol version 1.0
|
||||||
*/
|
*/
|
||||||
extern const char *const kMsgDKeyRepeat1_0;
|
extern const char *const kMsgDKeyRepeat1_0;
|
||||||
|
|
|
||||||
|
|
@ -380,13 +380,22 @@ void SecureSocket::createSSL()
|
||||||
|
|
||||||
void SecureSocket::freeSSL()
|
void SecureSocket::freeSSL()
|
||||||
{
|
{
|
||||||
std::scoped_lock ssl_lock{ssl_mutex_};
|
|
||||||
|
|
||||||
isFatal(true);
|
isFatal(true);
|
||||||
|
|
||||||
// take socket from multiplexer ASAP otherwise the race condition
|
// take socket from multiplexer ASAP otherwise the race condition
|
||||||
// could cause events to get called on a dead object. TCPSocket
|
// could cause events to get called on a dead object. TCPSocket
|
||||||
// will do this, too, but the double-call is harmless
|
// 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);
|
setJob(nullptr);
|
||||||
|
|
||||||
|
std::scoped_lock ssl_lock{ssl_mutex_};
|
||||||
|
|
||||||
if (m_ssl) {
|
if (m_ssl) {
|
||||||
if (m_ssl->m_ssl != nullptr) {
|
if (m_ssl->m_ssl != nullptr) {
|
||||||
SSL_set_quiet_shutdown(m_ssl->m_ssl, 1);
|
SSL_set_quiet_shutdown(m_ssl->m_ssl, 1);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ if(UNIX AND NOT APPLE)
|
||||||
set(CMAKE_REQUIRED_LIBRARIES "${CMAKE_REQUIRED_LIBRARIES};${LIBPORTAL_LINK_LIBRARIES};")
|
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_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_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()
|
cmake_pop_check_state()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
@ -166,6 +167,12 @@ elseif(UNIX)
|
||||||
PortalClipboard.h
|
PortalClipboard.h
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
if(HAVE_LP_SHORTCUTS)
|
||||||
|
list(APPEND PLATFORM_SOURCES
|
||||||
|
PortalGlobalShortcuts.cpp
|
||||||
|
PortalGlobalShortcuts.h
|
||||||
|
)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -192,6 +199,9 @@ if(UNIX)
|
||||||
if (HAVE_LP_CLIPBOARD)
|
if (HAVE_LP_CLIPBOARD)
|
||||||
target_compile_definitions(platform PUBLIC HAVE_LIBPORTAL_CLIPBOARD)
|
target_compile_definitions(platform PUBLIC HAVE_LIBPORTAL_CLIPBOARD)
|
||||||
endif()
|
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_include_directories(platform PUBLIC ${LIBEI_INCLUDE_DIRS} ${LIBPORTAL_INCLUDE_DIRS})
|
||||||
target_link_libraries(platform
|
target_link_libraries(platform
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,20 @@ void EiKeyState::updateXkbState(uint32_t keyval, bool isPressed)
|
||||||
xkb_state_update_key(m_xkbState, keyval, isPressed ? XKB_KEY_DOWN : XKB_KEY_UP);
|
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()
|
void EiKeyState::clearStaleModifiers()
|
||||||
{
|
{
|
||||||
const auto lockedMods = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_LOCKED);
|
const auto lockedMods = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_LOCKED);
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ public:
|
||||||
void pollPressedKeys(KeyButtonSet &pressedKeys) const override;
|
void pollPressedKeys(KeyButtonSet &pressedKeys) const override;
|
||||||
KeyID mapKeyFromKeyval(std::uint32_t keyval) const;
|
KeyID mapKeyFromKeyval(std::uint32_t keyval) const;
|
||||||
void updateXkbState(std::uint32_t keyval, bool isPressed);
|
void updateXkbState(std::uint32_t keyval, bool isPressed);
|
||||||
|
void updateLockedModifiers(xkb_mod_mask_t lockedMods);
|
||||||
void clearStaleModifiers() override;
|
void clearStaleModifiers() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
#include "platform/PortalRemoteDesktop.h"
|
#include "platform/PortalRemoteDesktop.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <bit>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
@ -58,8 +59,11 @@ EiScreen::EiScreen(bool isPrimary, IEventQueue *events, bool usePortal)
|
||||||
handleConnectedToEisEvent(e);
|
handleConnectedToEisEvent(e);
|
||||||
});
|
});
|
||||||
if (isPrimary) {
|
if (isPrimary) {
|
||||||
m_portalInputCapture = new PortalInputCapture(this, m_events);
|
|
||||||
// Portal input capture manages its own clipboard
|
// 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
|
||||||
} else {
|
} else {
|
||||||
m_events->addHandler(EventTypes::EISessionClosed, getEventTarget(), [this](const auto &) {
|
m_events->addHandler(EventTypes::EISessionClosed, getEventTarget(), [this](const auto &) {
|
||||||
handlePortalSessionClosed();
|
handlePortalSessionClosed();
|
||||||
|
|
@ -96,8 +100,11 @@ EiScreen::~EiScreen()
|
||||||
delete m_keyState;
|
delete m_keyState;
|
||||||
delete m_clipboard;
|
delete m_clipboard;
|
||||||
|
|
||||||
delete m_portalRemoteDesktop;
|
#ifdef HAVE_LIBPORTAL_SHORTCUTS
|
||||||
|
delete m_portalGlobalShortcuts;
|
||||||
|
#endif
|
||||||
delete m_portalInputCapture;
|
delete m_portalInputCapture;
|
||||||
|
delete m_portalRemoteDesktop;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EiScreen::eiLogEvent(ei_log_priority priority, const char *message) const
|
void EiScreen::eiLogEvent(ei_log_priority priority, const char *message) const
|
||||||
|
|
@ -233,19 +240,47 @@ std::uint32_t EiScreen::registerHotKey(KeyID key, KeyModifierMask mask)
|
||||||
}
|
}
|
||||||
set->second.addItem(HotKeyItem(mask, id));
|
set->second.addItem(HotKeyItem(mask, id));
|
||||||
|
|
||||||
|
updatePortalGlobalShortcuts();
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EiScreen::unregisterHotKey(uint32_t id)
|
void EiScreen::unregisterHotKey(uint32_t id)
|
||||||
{
|
{
|
||||||
for (auto &[key, set] : m_hotkeys) {
|
for (auto it = m_hotkeys.begin(); it != m_hotkeys.end(); ++it) {
|
||||||
(void)key;
|
if (it->second.removeById(id)) {
|
||||||
if (set.removeById(id)) {
|
if (it->second.empty())
|
||||||
|
m_hotkeys.erase(it);
|
||||||
|
updatePortalGlobalShortcuts();
|
||||||
break;
|
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()
|
void EiScreen::fakeInputBegin()
|
||||||
{
|
{
|
||||||
// FIXME -- not implemented
|
// FIXME -- not implemented
|
||||||
|
|
@ -261,9 +296,13 @@ std::int32_t EiScreen::getJumpZoneSize() const
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EiScreen::isAnyMouseButtonDown(uint32_t &) const
|
bool EiScreen::isAnyMouseButtonDown(uint32_t &buttonID) const
|
||||||
{
|
{
|
||||||
return false;
|
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
|
void EiScreen::getCursorCenter(int32_t &x, int32_t &y) const
|
||||||
|
|
@ -354,16 +393,13 @@ void EiScreen::fakeKey(uint32_t keycode, bool isDown) const
|
||||||
|
|
||||||
void EiScreen::enable()
|
void EiScreen::enable()
|
||||||
{
|
{
|
||||||
// Nothing really to be done here
|
m_activated = true;
|
||||||
// Portal-based clipboard gets notifications via events
|
updatePortalGlobalShortcuts();
|
||||||
// Socket-based clipboard is passive (no monitoring needed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EiScreen::disable()
|
void EiScreen::disable()
|
||||||
{
|
{
|
||||||
// Nothing to do here
|
m_activated = false;
|
||||||
// Portal-based clipboard gets notifications via events
|
|
||||||
// Socket-based clipboard is passive (no monitoring needed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EiScreen::cancelIdleEmulationTimer() const
|
void EiScreen::cancelIdleEmulationTimer() const
|
||||||
|
|
@ -424,6 +460,8 @@ void EiScreen::enter()
|
||||||
} else if (m_isPrimary) {
|
} else if (m_isPrimary) {
|
||||||
LOG_DEBUG("releasing input capture at x=%i y=%i", m_cursorX, m_cursorY);
|
LOG_DEBUG("releasing input capture at x=%i y=%i", m_cursorX, m_cursorY);
|
||||||
m_portalInputCapture->release(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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -631,6 +669,8 @@ void EiScreen::removeDevice(struct ei_device *device)
|
||||||
if (wasTracked) {
|
if (wasTracked) {
|
||||||
m_isEmulating = false;
|
m_isEmulating = false;
|
||||||
cancelIdleEmulationTimer();
|
cancelIdleEmulationTimer();
|
||||||
|
// same as for a paused device: no release events follow a removal
|
||||||
|
updateButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
delete static_cast<ScrollRemainder *>(ei_device_get_user_data(device));
|
delete static_cast<ScrollRemainder *>(ei_device_get_user_data(device));
|
||||||
|
|
@ -694,6 +734,10 @@ ButtonID EiScreen::mapButtonFromEvdev(ei_event *event) const
|
||||||
|
|
||||||
bool EiScreen::onHotkey(KeyID keyid, bool isPressed, KeyModifierMask mask)
|
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);
|
auto it = m_hotkeys.find(keyid);
|
||||||
|
|
||||||
if (it == m_hotkeys.end()) {
|
if (it == m_hotkeys.end()) {
|
||||||
|
|
@ -708,7 +752,6 @@ bool EiScreen::onHotkey(KeyID keyid, bool isPressed, KeyModifierMask mask)
|
||||||
sendEvent(type, HotKeyInfo::alloc(id));
|
sendEvent(type, HotKeyInfo::alloc(id));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -757,6 +800,8 @@ void EiScreen::onButtonEvent(ei_event *event)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_buttons.set(buttonID, pressed);
|
||||||
|
|
||||||
auto eventType = pressed ? EventTypes::PrimaryScreenButtonDown : EventTypes::PrimaryScreenButtonUp;
|
auto eventType = pressed ? EventTypes::PrimaryScreenButtonDown : EventTypes::PrimaryScreenButtonUp;
|
||||||
|
|
||||||
sendEvent(eventType, ButtonInfo::alloc(buttonID, mask));
|
sendEvent(eventType, ButtonInfo::alloc(buttonID, mask));
|
||||||
|
|
@ -968,6 +1013,9 @@ void EiScreen::handleSystemEvent(const Event &)
|
||||||
case EI_EVENT_DEVICE_PAUSED:
|
case EI_EVENT_DEVICE_PAUSED:
|
||||||
LOG_DEBUG("device %s is paused", ei_device_get_name(device));
|
LOG_DEBUG("device %s is paused", ei_device_get_name(device));
|
||||||
m_isEmulating = false;
|
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();
|
cancelIdleEmulationTimer();
|
||||||
break;
|
break;
|
||||||
case EI_EVENT_DEVICE_RESUMED:
|
case EI_EVENT_DEVICE_RESUMED:
|
||||||
|
|
@ -977,7 +1025,8 @@ void EiScreen::handleSystemEvent(const Event &)
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case EI_EVENT_KEYBOARD_MODIFIERS:
|
case EI_EVENT_KEYBOARD_MODIFIERS:
|
||||||
// FIXME
|
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));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// events below are for a receiver context (barriers)
|
// events below are for a receiver context (barriers)
|
||||||
|
|
@ -1026,7 +1075,9 @@ void EiScreen::handleSystemEvent(const Event &)
|
||||||
void EiScreen::updateButtons()
|
void EiScreen::updateButtons()
|
||||||
{
|
{
|
||||||
// libei relies on the EIS implementation to keep our button count correct,
|
// libei relies on the EIS implementation to keep our button count correct,
|
||||||
// so there's not much we need to/can do here.
|
// and the held buttons cannot be polled, so resyncing means assuming that
|
||||||
|
// everything is released.
|
||||||
|
m_buttons.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
IKeyState *EiScreen::getKeyState() const
|
IKeyState *EiScreen::getKeyState() const
|
||||||
|
|
@ -1075,4 +1126,21 @@ std::uint32_t EiScreen::HotKeySet::findByMask(std::uint32_t mask) const
|
||||||
return 0;
|
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
|
} // namespace deskflow
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,12 @@
|
||||||
|
|
||||||
#include "deskflow/IScreen.h"
|
#include "deskflow/IScreen.h"
|
||||||
#include "deskflow/PlatformScreen.h"
|
#include "deskflow/PlatformScreen.h"
|
||||||
|
#ifdef HAVE_LIBPORTAL_SHORTCUTS
|
||||||
|
#include "platform/PortalGlobalShortcuts.h"
|
||||||
|
#endif
|
||||||
#include "platform/XDGPowerManager.h"
|
#include "platform/XDGPowerManager.h"
|
||||||
|
|
||||||
|
#include <bitset>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
#include <libei.h>
|
#include <libei.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
@ -29,6 +33,7 @@ namespace deskflow {
|
||||||
class EiKeyState;
|
class EiKeyState;
|
||||||
class PortalRemoteDesktop;
|
class PortalRemoteDesktop;
|
||||||
class PortalInputCapture;
|
class PortalInputCapture;
|
||||||
|
class PortalGlobalShortcuts;
|
||||||
class EiClipboard;
|
class EiClipboard;
|
||||||
|
|
||||||
using ClipboardInfo = IScreen::ClipboardInfo;
|
using ClipboardInfo = IScreen::ClipboardInfo;
|
||||||
|
|
@ -127,6 +132,7 @@ private:
|
||||||
void ensureEmulating() const;
|
void ensureEmulating() const;
|
||||||
void stopEmulating() const;
|
void stopEmulating() const;
|
||||||
void cancelIdleEmulationTimer() const;
|
void cancelIdleEmulationTimer() const;
|
||||||
|
void updatePortalGlobalShortcuts();
|
||||||
|
|
||||||
static void handleEiLogEvent(ei *ei, const ei_log_priority priority, const char *message, ei_log_context *)
|
static void handleEiLogEvent(ei *ei, const ei_log_priority priority, const char *message, ei_log_context *)
|
||||||
{
|
{
|
||||||
|
|
@ -144,6 +150,9 @@ private:
|
||||||
|
|
||||||
KeyID m_lastPressed = kKeyNone;
|
KeyID m_lastPressed = kKeyNone;
|
||||||
|
|
||||||
|
// mouse buttons currently held, indexed by ButtonID
|
||||||
|
std::bitset<NumButtonIDs> m_buttons;
|
||||||
|
|
||||||
// clipboard stuff
|
// clipboard stuff
|
||||||
EiClipboard *m_clipboard = nullptr;
|
EiClipboard *m_clipboard = nullptr;
|
||||||
size_t m_maximumClipboardSize = INT_MAX;
|
size_t m_maximumClipboardSize = INT_MAX;
|
||||||
|
|
@ -188,6 +197,9 @@ private:
|
||||||
|
|
||||||
PortalRemoteDesktop *m_portalRemoteDesktop = nullptr;
|
PortalRemoteDesktop *m_portalRemoteDesktop = nullptr;
|
||||||
PortalInputCapture *m_portalInputCapture = nullptr;
|
PortalInputCapture *m_portalInputCapture = nullptr;
|
||||||
|
PortalGlobalShortcuts *m_portalGlobalShortcuts = nullptr;
|
||||||
|
|
||||||
|
bool m_activated = false;
|
||||||
|
|
||||||
struct HotKeyItem
|
struct HotKeyItem
|
||||||
{
|
{
|
||||||
|
|
@ -214,6 +226,13 @@ private:
|
||||||
bool removeById(std::uint32_t id);
|
bool removeById(std::uint32_t id);
|
||||||
void addItem(HotKeyItem item);
|
void addItem(HotKeyItem item);
|
||||||
std::uint32_t findByMask(std::uint32_t mask) const;
|
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:
|
private:
|
||||||
KeyID m_id = 0;
|
KeyID m_id = 0;
|
||||||
|
|
|
||||||
520
src/lib/platform/PortalGlobalShortcuts.cpp
Normal file
520
src/lib/platform/PortalGlobalShortcuts.cpp
Normal file
|
|
@ -0,0 +1,520 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
140
src/lib/platform/PortalGlobalShortcuts.h
Normal file
140
src/lib/platform/PortalGlobalShortcuts.h
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
@ -23,19 +23,16 @@ ClientProxy1_1::ClientProxy1_1(const std::string &name, deskflow::IStream *strea
|
||||||
void ClientProxy1_1::keyDown(KeyID key, KeyModifierMask mask, KeyButton button, const std::string &)
|
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);
|
LOG_VERBOSE("send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x", getName().c_str(), key, mask, button);
|
||||||
ProtocolUtil::writef(getStream(), kMsgDKeyDown, key, mask, button);
|
ProtocolUtil::writef(getStream(), kMsgDKeyDown1_1, key, mask, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientProxy1_1::keyRepeat(
|
void ClientProxy1_1::keyRepeat(KeyID key, KeyModifierMask mask, int32_t count, KeyButton button, const std::string &)
|
||||||
KeyID key, KeyModifierMask mask, int32_t count, KeyButton button, const std::string &lang
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
LOG(
|
LOG_VERBOSE(
|
||||||
(CLOG_VERBOSE "send key repeat to \"%s\" id=%d, mask=0x%04x, count=%d, "
|
"send key repeat to \"%s\" id=%d, mask=0x%04x, count=%d, button=0x%04x", getName().c_str(), key, mask, count,
|
||||||
"button=0x%04x, lang=\"%s\"",
|
button
|
||||||
getName().c_str(), key, mask, count, button, lang.c_str())
|
|
||||||
);
|
);
|
||||||
ProtocolUtil::writef(getStream(), kMsgDKeyRepeat, key, mask, count, button, &lang);
|
ProtocolUtil::writef(getStream(), kMsgDKeyRepeat1_1, key, mask, count, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientProxy1_1::keyUp(KeyID key, KeyModifierMask mask, KeyButton button)
|
void ClientProxy1_1::keyUp(KeyID key, KeyModifierMask mask, KeyButton button)
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,20 @@ void ClientProxy1_8::synchronizeLanguages() const
|
||||||
|
|
||||||
void ClientProxy1_8::keyDown(KeyID key, KeyModifierMask mask, KeyButton button, const std::string &language)
|
void ClientProxy1_8::keyDown(KeyID key, KeyModifierMask mask, KeyButton button, const std::string &language)
|
||||||
{
|
{
|
||||||
LOG(
|
LOG_VERBOSE(
|
||||||
(CLOG_VERBOSE "send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x, layout=%s", getName().c_str(), key,
|
"send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x, layout=%s", getName().c_str(), key, mask, button,
|
||||||
mask, button, language.c_str())
|
language.c_str()
|
||||||
);
|
);
|
||||||
ProtocolUtil::writef(getStream(), kMsgDKeyDownLang, key, mask, button, &language);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public:
|
||||||
~ClientProxy1_8() override = default;
|
~ClientProxy1_8() override = default;
|
||||||
|
|
||||||
void keyDown(KeyID, KeyModifierMask, KeyButton, const std::string &) override;
|
void keyDown(KeyID, KeyModifierMask, KeyButton, const std::string &) override;
|
||||||
|
void keyRepeat(KeyID, KeyModifierMask, int32_t count, KeyButton, const std::string &) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void synchronizeLanguages() const;
|
void synchronizeLanguages() const;
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,19 @@ Server::Server(ServerConfig &config, PrimaryClient *primaryClient, deskflow::Scr
|
||||||
// set initial configuration
|
// set initial configuration
|
||||||
setConfig(config);
|
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
|
// 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_primaryClient->enable();
|
||||||
m_inputFilter->setPrimaryClient(m_primaryClient);
|
m_inputFilter->setPrimaryClient(m_primaryClient);
|
||||||
|
#endif
|
||||||
// Determine if scroll lock is already set. If so, lock the cursor to the
|
// 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)
|
// primary screen (unless the user has disabled lock to screen in config)
|
||||||
if (!m_disableLockToScreen && (m_primaryClient->getToggleMask() & KeyModifierScrollLock)) {
|
if (!m_disableLockToScreen && (m_primaryClient->getToggleMask() & KeyModifierScrollLock)) {
|
||||||
|
|
@ -175,11 +184,16 @@ Server::~Server()
|
||||||
delete client;
|
delete client;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove input filter
|
// See at Server::Server for why we do this in this order.
|
||||||
m_inputFilter->setPrimaryClient(nullptr);
|
#if WINAPI_LIBPORTAL
|
||||||
|
|
||||||
// disable and disconnect primary client
|
// disable and disconnect primary client
|
||||||
m_primaryClient->disable();
|
m_primaryClient->disable();
|
||||||
|
// remove input filter
|
||||||
|
m_inputFilter->setPrimaryClient(nullptr);
|
||||||
|
#else
|
||||||
|
m_inputFilter->setPrimaryClient(nullptr);
|
||||||
|
m_primaryClient->disable();
|
||||||
|
#endif
|
||||||
removeClient(m_primaryClient);
|
removeClient(m_primaryClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,14 @@ create_test(
|
||||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/base"
|
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(
|
create_test(
|
||||||
NAME BaseExceptionTests
|
NAME BaseExceptionTests
|
||||||
DEPENDS base
|
DEPENDS base
|
||||||
|
|
|
||||||
79
src/unittests/base/FileLogOutputterTests.cpp
Normal file
79
src/unittests/base/FileLogOutputterTests.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* 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)
|
||||||
35
src/unittests/base/FileLogOutputterTests.h
Normal file
35
src/unittests/base/FileLogOutputterTests.h
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
|
|
@ -102,4 +102,36 @@ void EiKeyStateTests::clearStaleModifiers_shiftDownAndNumLockOn_shiftClearedAndN
|
||||||
QVERIFY((keyState.pollActiveModifiers() & KeyModifierNumLock) != 0);
|
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)
|
QTEST_MAIN(EiKeyStateTests)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class EiKeyStateTests : public QObject
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void initTestCase();
|
void initTestCase();
|
||||||
void clearStaleModifiers_shiftDownAndNumLockOn_shiftClearedAndNumLockPreserved();
|
void clearStaleModifiers_shiftDownAndNumLockOn_shiftClearedAndNumLockPreserved();
|
||||||
|
void updateLockedModifiers_compositorLockState_numLockFollowsCompositor();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Arch m_arch;
|
Arch m_arch;
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,10 @@ create_test(
|
||||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/server"
|
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"
|
||||||
|
)
|
||||||
|
|
|
||||||
225
src/unittests/server/ClientProxyTests.cpp
Normal file
225
src/unittests/server/ClientProxyTests.cpp
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
/*
|
||||||
|
* 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)
|
||||||
28
src/unittests/server/ClientProxyTests.h
Normal file
28
src/unittests/server/ClientProxyTests.h
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue