[feat] add acknowledged remote clipboard read and write

This commit is contained in:
dijunkun
2026-11-06 20:20:25 +08:00
parent 0eab020c38
commit 0aed57ed4d
13 changed files with 217 additions and 5 deletions
@@ -15,6 +15,7 @@
#include <nlohmann/json.hpp>
#include <optional>
#include <remote_action.h>
#include <web_clipboard_protocol.h>
#include <set>
#include <string>
#include <vector>
@@ -77,6 +78,8 @@ public:
bool Pointer(float x, float y, int flag, int wheel = 0);
uint64_t RequestRemote(const std::string &method, nlohmann::json arguments = {});
std::optional<nlohmann::json> TakeRemoteResult(uint64_t request);
void ForgetRemoteRequest(uint64_t request) { remote_results_.erase(request); }
uint64_t Clipboard(bool write, const std::string &text = {});
void BeginInput();
std::string InputResult();
bool InputAcknowledged() const { return remote_capabilities_.value("input_ack", nlohmann::json{}) == true; }
@@ -121,6 +124,8 @@ private:
std::vector<uint64_t> input_requests_;
std::string input_result_ = "applied";
bool input_ack_active_ = false;
bool clipboard_ready_ = false;
web_clipboard::Assembler clipboard_assembler_;
uint64_t generation_ = 0;
uint32_t preview_request_ = 0, preview_ack_ = 0;
bool control_ = false, preview_supported_ = false, resolution_ = false;
@@ -54,3 +54,19 @@ std::string ControllerSession::InputResult() {
return input_requests_.empty() ? input_result_ : "pending";
}
} // namespace crossdesk
namespace crossdesk {
uint64_t ControllerSession::Clipboard(bool write, const std::string &text) {
namespace clip = web_clipboard;
if (!Connected() || !control_ || !clipboard_ready_ || remote_results_.size() >= 512 ||
remote_sequence_ >= UINT32_MAX || (write && !text.empty() && !clip::ValidText(text)))
return 0;
const auto id = uint32_t(++remote_sequence_);
remote_results_[id] = nullptr;
const auto send = [this](const std::string &frame) { return Send(clip::kStream, frame); };
const bool sent = write && !text.empty() ? clip::SendText(text, id, 16 * 1024, send)
: send(clip::Frame(write ? clip::kClear : clip::kRead, id));
if (!sent) { remote_results_.erase(id); return 0; }
return id;
}
} // namespace crossdesk
@@ -3,6 +3,7 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <display_stream_id.h>
#include <log.h>
#include <preview_subscription.h>
@@ -42,6 +43,12 @@ void ControllerSession::Data(const char *data, size_t n, const char *, size_t,
const char *stream, size_t sn, void *p) {
auto &session = *static_cast<ControllerSession *>(p);
try {
if (data && n && n <= web_clipboard::kHeaderBytes + web_clipboard::kChunkBytes &&
stream && sn == std::strlen(web_clipboard::kStream) &&
std::string_view(stream, sn) == web_clipboard::kStream) {
session.Enqueue({3, 0, std::string(data, n)});
return;
}
if (data && n && n <= 65536 && stream && sn < 64 &&
(std::string(stream, sn) == kControlStream ||
std::string(stream, sn) == kWebControlStream))
@@ -122,6 +129,8 @@ void ControllerSession::Connect(SessionOptions options, std::string device,
remote_results_.clear();
input_requests_.clear();
capability_request_ = 0;
clipboard_ready_ = false;
clipboard_assembler_ = {};
displays_.clear();
host_platform_ = HostPlatform::Unknown;
preview_supported_ = resolution_ = fps15_ = bandwidth_ = text_supported_ =
@@ -155,6 +164,7 @@ void ControllerSession::Connect(SessionOptions options, std::string device,
AddDataStream(peer_, kDataStream, false);
for (auto stream : {kMouseStream, kKeyboardStream, kControlStream})
AddDataStream(peer_, stream, true);
AddDataStream(peer_, web_clipboard::kStream, true);
if (Init(peer_) != 0)
Fail("media_initialization_failed");
}
@@ -240,6 +250,24 @@ void ControllerSession::Tick() {
Fail(Connected() ? "connection_lost" : "connection_failed");
else if (e.code == Closed || e.code == Disconnected)
Fail("remote_disconnected");
} else if (e.kind == 3 && Connected()) {
namespace clip = web_clipboard;
if (e.data.size() < clip::kHeaderBytes) continue;
const auto kind = clip::Read32(e.data.data());
const auto id = clip::Read32(e.data.data() + 4);
if (kind == clip::kReady && !id) { clipboard_ready_ = true; continue; }
const auto found = remote_results_.find(id);
if (!id || found == remote_results_.end()) continue;
if (kind == clip::kApplied || kind == clip::kRejected) {
found->second = {{"ok", kind == clip::kApplied},
{"result", {{"applied", kind == clip::kApplied}}}, {"error", "CLIPBOARD_REJECTED"}};
} else if (kind == clip::kEmpty) {
found->second = {{"ok", true}, {"result", {{"text", ""}}}};
} else if (kind == clip::kText) {
const auto text = clipboard_assembler_.Receive(e.data);
if (!text.empty()) found->second = {{"ok", true},
{"result", {{"text", text.substr(clip::kHeaderBytes)}}}};
}
} else if (e.kind == 2 && Connected()) {
auto j = ParseBoundedJson(e.data);
if (!j.is_object())
@@ -254,6 +282,9 @@ void ControllerSession::Tick() {
if (id == capability_request_) {
if (j.value("ok", json{}) == true && j.value("result", json{}).is_object())
remote_capabilities_ = j["result"];
if (remote_capabilities_.value("clipboard", json{}) == true)
Send(web_clipboard::kStream, web_clipboard::Frame(
web_clipboard::kHello, 0, web_clipboard::kVersion));
remote_results_.erase(found);
}
}
@@ -475,6 +506,7 @@ json ControllerSession::Status() const {
{"display", options_.display},
{"text_input", text_supported_},
{"capabilities", remote_capabilities_},
{"clipboard_ready", clipboard_ready_},
{"preview_supported", preview_supported_},
{"preview_request", preview_request_},
{"preview_ack", preview_ack_},