diff --git a/macos/ClipboardHandler.h b/macos/ClipboardHandler.h new file mode 100644 index 0000000..cd9482b --- /dev/null +++ b/macos/ClipboardHandler.h @@ -0,0 +1,186 @@ +#pragma once +#import +#include +#include + +// macOS 剪贴板操作封装 +// 使用 NSPasteboard API 实现 + +class ClipboardHandler +{ +public: + // 检查剪贴板功能是否可用 (macOS 总是可用) + static bool IsAvailable() + { + return true; + } + + // 获取剪贴板中的文件列表 + // 返回文件的完整路径列表(UTF-8),失败返回空列表 + static std::vector GetFiles() + { + std::vector files; + + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + + // 方法1: 尝试获取文件 URL 列表 (macOS 10.13+) + NSArray* urls = [pasteboard readObjectsForClasses:@[[NSURL class]] + options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}]; + if (urls && urls.count > 0) { + for (NSURL* url in urls) { + if (url.isFileURL) { + NSString* path = url.path; + if (path) { + files.push_back([path UTF8String]); + } + } + } + return files; + } + + // 方法2: 兼容旧版 API (NSFilenamesPboardType) + NSArray* filenames = [pasteboard propertyListForType:NSFilenamesPboardType]; + if (filenames && [filenames isKindOfClass:[NSArray class]]) { + for (NSString* path in filenames) { + if ([path isKindOfClass:[NSString class]]) { + files.push_back([path UTF8String]); + } + } + } + } + + return files; + } + + // 获取剪贴板文本 + // 返回 UTF-8 编码的文本,失败返回空字符串 + static std::string GetText() + { + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + NSString* text = [pasteboard stringForType:NSPasteboardTypeString]; + if (text) { + return [text UTF8String]; + } + } + return ""; + } + + // 设置剪贴板文本 + // text: UTF-8 编码的文本 + // 返回是否成功 + static bool SetText(const std::string& text) + { + if (text.empty()) return true; + + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + + NSString* nsText = [NSString stringWithUTF8String:text.c_str()]; + if (nsText) { + return [pasteboard setString:nsText forType:NSPasteboardTypeString]; + } + } + return false; + } + + // 设置剪贴板文本(从原始字节) + // data: 文本数据(可能是 GBK 或 UTF-8) + // len: 数据长度 + static bool SetTextRaw(const char* data, size_t len) + { + if (!data || len == 0) return true; + + // 服务端发来的文本可能是 GBK 编码,尝试转换为 UTF-8 + std::string text = ConvertToUtf8(data, len); + return SetText(text); + } + + // 设置剪贴板文件列表 + // files: UTF-8 编码的文件路径列表 + // 返回是否成功 + static bool SetFiles(const std::vector& files) + { + if (files.empty()) return true; + + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + + NSMutableArray* urls = [NSMutableArray arrayWithCapacity:files.size()]; + for (const auto& path : files) { + NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + if (nsPath) { + NSURL* url = [NSURL fileURLWithPath:nsPath]; + if (url) { + [urls addObject:url]; + } + } + } + + if (urls.count > 0) { + return [pasteboard writeObjects:urls]; + } + } + return false; + } + +private: + // 检查是否是有效的 UTF-8 序列 + static bool IsValidUtf8(const char* data, size_t len) + { + const unsigned char* bytes = (const unsigned char*)data; + size_t i = 0; + + while (i < len) { + if (bytes[i] <= 0x7F) { + // ASCII + i++; + } else if ((bytes[i] & 0xE0) == 0xC0) { + // 2-byte sequence + if (i + 1 >= len || (bytes[i + 1] & 0xC0) != 0x80) return false; + i += 2; + } else if ((bytes[i] & 0xF0) == 0xE0) { + // 3-byte sequence + if (i + 2 >= len || (bytes[i + 1] & 0xC0) != 0x80 || (bytes[i + 2] & 0xC0) != 0x80) return false; + i += 3; + } else if ((bytes[i] & 0xF8) == 0xF0) { + // 4-byte sequence + if (i + 3 >= len || (bytes[i + 1] & 0xC0) != 0x80 || + (bytes[i + 2] & 0xC0) != 0x80 || (bytes[i + 3] & 0xC0) != 0x80) return false; + i += 4; + } else { + return false; + } + } + return true; + } + + // 尝试将 GBK 转换为 UTF-8 + // 如果已经是 UTF-8,直接返回 + static std::string ConvertToUtf8(const char* data, size_t len) + { + // 检查是否已经是有效的 UTF-8 + if (IsValidUtf8(data, len)) { + return std::string(data, len); + } + + // 使用 NSString 进行编码转换 (GBK = CFStringEncodingGB_18030_2000) + @autoreleasepool { + // 尝试 GBK (GB18030) 编码 + NSStringEncoding gbkEncoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000); + NSString* str = [[NSString alloc] initWithBytes:data length:len encoding:gbkEncoding]; + if (str) { + const char* utf8 = [str UTF8String]; + if (utf8) { + return std::string(utf8); + } + } + } + + // 转换失败,返回原始数据 + return std::string(data, len); + } +}; diff --git a/macos/ScreenHandler.mm b/macos/ScreenHandler.mm index 6ba67a5..f54d31b 100644 --- a/macos/ScreenHandler.mm +++ b/macos/ScreenHandler.mm @@ -1,6 +1,7 @@ #import "ScreenHandler.h" #import "H264Encoder.h" #import "InputHandler.h" +#import "ClipboardHandler.h" #import "../client/IOCPClient.h" #import "../common/commands.h" #import "../common/FileTransferV2.h" @@ -282,6 +283,56 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size) } break; + case COMMAND_SCREEN_SET_CLIPBOARD: + // 服务端设置剪贴板: [cmd:1][text:N] + if (size > 1) { + if (ClipboardHandler::SetTextRaw((const char*)(data + 1), size - 1)) { + NSLog(@">>> Clipboard SET: %zu bytes", size - 1); + } else { + NSLog(@"*** Clipboard SET failed"); + } + } + break; + + case COMMAND_SCREEN_GET_CLIPBOARD: + // 服务端请求剪贴板: [cmd:1][hash:64][hmac:16] + // 返回: [TOKEN_CLIPBOARD_TEXT:1][text:N] 或 [COMMAND_GET_FOLDER:1][files] + { + // 优先检查剪贴板中的文件 + auto files = ClipboardHandler::GetFiles(); + if (!files.empty()) { + // 返回 COMMAND_GET_FOLDER + 文件列表(多字符串格式:file1\0file2\0\0) + std::vector buf; + buf.push_back(COMMAND_GET_FOLDER); + for (const auto& f : files) { + // 文件路径需要转换为 GBK 编码(服务端预期) + std::string gbkPath = FileTransferV2::utf8ToGbk(f); + buf.insert(buf.end(), gbkPath.begin(), gbkPath.end()); + buf.push_back(0); // 每个路径后的 null 终止符 + } + buf.push_back(0); // 结束标记 + m_client->Send2Server((char*)buf.data(), buf.size()); + NSLog(@">>> Clipboard GET: %zu files", files.size()); + break; + } + + // 没有文件,返回文本 + std::string text = ClipboardHandler::GetText(); + if (!text.empty()) { + std::vector buf(1 + text.size()); + buf[0] = TOKEN_CLIPBOARD_TEXT; + memcpy(&buf[1], text.data(), text.size()); + m_client->Send2Server((char*)buf.data(), buf.size()); + NSLog(@">>> Clipboard GET: %zu bytes text", text.size()); + } else { + // 返回空剪贴板 + uint8_t empty = TOKEN_CLIPBOARD_TEXT; + m_client->Send2Server((char*)&empty, 1); + NSLog(@">>> Clipboard GET: empty"); + } + } + break; + case COMMAND_GET_FILE: // Server requests file download: [cmd:1][targetDir\0][file1\0file2\0...\0] // Use V2 protocol to upload files @@ -303,7 +354,10 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size) ptr += fileGbk.length() + 1; } - // TODO: If no file list, get from clipboard (ClipboardHandler not implemented yet) + // 如果没有文件列表,从剪贴板获取 + if (files.empty()) { + files = ClipboardHandler::GetFiles(); + } if (!files.empty() && !targetDir.empty()) { NSLog(@">>> COMMAND_GET_FILE: %zu files -> %s", files.size(), targetDir.c_str()); diff --git a/macos/main.mm b/macos/main.mm index 6c00d5a..a81c003 100644 --- a/macos/main.mm +++ b/macos/main.mm @@ -27,6 +27,7 @@ #import "../common/FileManager.h" #import "../common/FileTransferV2.h" #import "../common/logger.h" +#import "ClipboardHandler.h" // Global state static std::atomic g_running(true); @@ -740,6 +741,22 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength) // C2C 准备接收通知 FileTransferV2::HandleC2CPrepare(szBuffer, ulLength, nullptr); Mprintf("** [%p] C2C Prepare received ***\n", user); + } else if (szBuffer[0] == COMMAND_C2C_TEXT) { + // C2C 文本剪贴板: [cmd:1][dstClientID:8][textLen:4][text:N] + if (ulLength >= 13) { + uint32_t textLen; + memcpy(&textLen, szBuffer + 9, 4); + if (ulLength >= 13 + textLen && textLen > 0) { + if (!ClipboardHandler::IsAvailable()) { + Mprintf("** [%p] C2C Text: clipboard unavailable ***\n", user); + } else { + std::string utf8Text((const char*)szBuffer + 13, textLen); + if (ClipboardHandler::SetText(utf8Text)) { + Mprintf("** [%p] C2C Text received: %u bytes ***\n", user, textLen); + } + } + } + } } else if (szBuffer[0] == COMMAND_SEND_FILE_V2 || szBuffer[0] == COMMAND_FILE_COMPLETE_V2) { // V2 文件接收 int result = FileTransferV2::RecvFileChunkV2(szBuffer, ulLength, g_myClientID);