Release v0.6.57: multi-PC input, shared clipboard and resilient Settings Save

This commit is contained in:
agrynco committed 2026-11-06 21:33:37 +02:00
1 parent 82292c8a7e
commit 5de010ee44
39 files changed
+1707 -312

No files matched your search

+84 -10
View File
@@ -3,6 +3,7 @@
#include "../vendor/json.hpp"
#include "../resources/resource.h"
#include "ocr.h"
#include "lan_clipboard_image.h"
#include "configuration.h"
#include "diagnostics.h"
#include "paste_probe.h"
@@ -724,11 +725,11 @@ bool App::registerHotkeys(const Settings &s, bool rollback)
Bytes App::lanCommand(const Bytes &command)
{
if (!lanService) lanService = std::make_unique<LanService>(data);
if (!lanService) lanService = std::make_unique<LanService>(data,window);
auto reply = lanService->command(command);
if (lanService->inputEnabled()) SetTimer(window, 7, 10, nullptr);
else KillTimer(window, 7);
if (lanService->takeInputChange()) resetInput();
if (lanService->takeInputChange()) resetLanInput();
return reply;
}
void App::applySettings(const Settings &requested)
@@ -840,6 +841,14 @@ bool App::isHotkey(DWORD key, UINT mods)
}
return false;
}
void App::resetLanInput()
{
// Releases may have been consumed on another desktop. Do not retain local
// shortcut/chat latches across an ownership handoff.
hotkeyHook = {};
chatSwitchHook = {};
resetInput();
}
void App::resetInput()
{
std::lock_guard lock(stateMutex);
@@ -1132,7 +1141,7 @@ LRESULT CALLBACK App::keyProc(int code, WPARAM wp, LPARAM lp)
(wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN || wp == WM_KEYUP || wp == WM_SYSKEYUP))
{
bool consumed = current->lanService->keyboard(*(KBDLLHOOKSTRUCT *)lp, wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN);
if (current->lanService->takeInputChange()) current->resetInput();
if (current->lanService->takeInputChange()) current->resetLanInput();
if (consumed) return 1; // Forwarded keys must not reach local hotkeys/snippets or other routers.
}
// MWB suppresses source-machine events that it forwards elsewhere. Let
@@ -1168,7 +1177,7 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
{
bool consumed = current->lanService->mouse(UINT(wp), *(MSLLHOOKSTRUCT *)lp,
!current->busy && !current->suspended && !current->hotkeyCapture);
if (current->lanService->takeInputChange()) current->resetInput();
if (current->lanService->takeInputChange()) current->resetLanInput();
if (consumed) return 1;
}
// KSwitcher generates no mouse input. Remote clicks invalidate a typing
@@ -1805,15 +1814,56 @@ void App::pasteEntry(std::wstring id, bool plain)
}
});
}
namespace {
Clip sharedClipboard(Clip clip)
{
// File handles/paths and application-private formats are local to their PC.
std::erase_if(clip.formats,[](const Format &f){return !(
(f.name.empty()&&(f.id==CF_UNICODETEXT||f.id==CF_TEXT||f.id==CF_LOCALE||f.id==CF_DIB||f.id==CF_DIBV5))||
f.name==L"PNG"||f.name==L"HTML Format"||f.name==L"Rich Text Format");});
return clip;
}
}
void App::publishClipboard(Clip clip,const std::wstring &app)
{
clip=sharedClipboard(std::move(clip));if(clip.formats.empty())return;
auto originalSize=clip.size();
clip=lan::compactClipboardImage(std::move(clip));
auto packet=new LanClipboard{{},{},utf8(app),encodeClip(clip)};
log(L"LAN clipboard queued: original="+std::to_wstring(originalSize)+L" bytes, wire="+std::to_wstring(packet->data.size())+L" bytes");
if(!PostMessageW(window,LanClipboardPublishMessage,0,reinterpret_cast<LPARAM>(packet)))delete packet;
}
void App::receiveClipboard()
{
if(!lanService)return;
auto packets=lanService->takeClipboard();auto config=settings;
for(auto &packet:packets)enqueue([this,packet=std::move(packet),config]{
try {
auto clip=sharedClipboard(decodeClip(packet.data));if(clip.formats.empty())return;
auto sequence=GetClipboardSequenceNumber();
if(!suspended&&(sequence==packet.sequence||sequence==lanSequence.load())){
const std::wstring marker=L"KSwitcher.LAN.Clipboard";
auto active=lan::clipboardImageForPaste(clip);
active.formats.push_back({RegisterClipboardFormatW(marker.c_str()),marker,Bytes{1}});
ownSequence=lanSequence=writeClipboardGuarded(window,active,sequence);
log(L"LAN clipboard activated: "+std::to_wstring(packet.data.size())+L" bytes");
}
if(config.history&&history){history->add(clip,wide(packet.app),config,packet.peer,wide(packet.name));
scheduleHistoryOcr(config);PostMessageW(window,HistoryMessage,0,0);}
}catch(const std::exception &e){log(L"LAN clipboard receive: "+wide(e.what()));}
});
}
void App::captureHistory()
{
DWORD sequence = GetClipboardSequenceNumber();
if (suspended || sequence == ownSequence || !settings.history || !history ||
if (suspended || sequence == ownSequence || !history ||
GetTickCount64() < pauseUntil)
return;
const bool share=lanService&&lanService->clipboardEnabled();
if(!settings.history&&!share)return;
Settings config = settings;
enqueue(
[this, sequence, config]
[this, sequence, config, share]
{
try
{
@@ -1821,19 +1871,28 @@ void App::captureHistory()
return;
UINT exclude = RegisterClipboardFormatW(L"ExcludeClipboardContentFromMonitorProcessing"),
ignore = RegisterClipboardFormatW(L"Clipboard Viewer Ignore");
if (IsClipboardFormatAvailable(exclude) || IsClipboardFormatAvailable(ignore))
if (IsClipboardFormatAvailable(exclude) || IsClipboardFormatAvailable(ignore) ||
IsClipboardFormatAvailable(RegisterClipboardFormatW(L"KSwitcher.LAN.Clipboard")))
return;
auto owner = GetClipboardOwner();
auto app = processName(owner ? owner : GetForegroundWindow());
for (auto &name : config.excluded)
if (lower(name) == lower(app))
return;
if(!config.history){
auto clip=readClipboard(window,(size_t)config.maxImageMiB*1024*1024,ClipboardRead::All,sequence);
for(const auto &f:clip.formats)if(f.name==L"CanIncludeInClipboardHistory"&&f.data.size()>=4&&!f.data[0]&&!f.data[1]&&!f.data[2]&&!f.data[3])return;
if(sequence==GetClipboardSequenceNumber())publishClipboard(std::move(clip),app);
return;
}
if (IsClipboardFormatAvailable(CF_DIB) || IsClipboardFormatAvailable(CF_DIBV5) ||
IsClipboardFormatAvailable(CF_BITMAP) ||
IsClipboardFormatAvailable(RegisterClipboardFormatW(L"PNG")))
{
if (history->captureImage(sequence, app, config))
{
auto entries=history->entries();
if(share&&!entries.empty())publishClipboard(history->load(entries.front().id),app);
scheduleHistoryOcr(config);
PostMessageW(window, HistoryMessage, 0, 0);
}
@@ -1848,6 +1907,7 @@ void App::captureHistory()
!f.data[1] && !f.data[2] && !f.data[3])
return;
history->add(clip, app, config);
if(share)publishClipboard(clip,app);
scheduleHistoryOcr(config);
PostMessageW(window, HistoryMessage, 0, 0);
}
@@ -2222,13 +2282,26 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
}
return TRUE;
}
case LanClipboardPublishMessage:
{
std::unique_ptr<LanClipboard> packet(reinterpret_cast<LanClipboard*>(lp));
try{if(app->lanService)app->lanService->publishClipboard(std::move(packet->data),packet->app);}
catch(const std::exception &e){log(L"LAN clipboard send: "+wide(e.what()));}
return 0;
}
case LanClipboardReadyMessage: app->receiveClipboard();return 0;
case LanInputReadyMessage:
if(app->lanService&&app->lanService->tickInput(!app->busy&&!app->suspended&&!app->hotkeyCapture))app->resetLanInput();
return 0;
case WM_TIMER:
if (wp == 7)
{
if (app->lanService)
{
if (app->suspended) app->lanService->cancelInput();
if (app->lanService->tickInput(!app->busy && !app->hotkeyCapture)) app->resetInput();
// suspended also guards clipboard/text transactions (correction,
// conversion, snippets). Those must not disconnect remote input.
// Actual system suspension is handled by WM_POWERBROADCAST.
if (app->lanService->tickInput(!app->busy && !app->suspended && !app->hotkeyCapture)) app->resetLanInput();
}
return 0;
}
@@ -2346,6 +2419,7 @@ int App::execute()
historyTop = wide(state.value("top", std::string{}));
historyQuery = wide(state.value("query", std::string{}));
historyPinnedOnly = state.value("pinnedOnly", false);
historyComputers = state.value("computers",std::vector<std::string>{});
} catch (...) {}
showHistory(*this);
remoteHistory("ready", std::to_wstring(reinterpret_cast<uintptr_t>(historyWindow)));
@@ -2373,7 +2447,7 @@ int App::execute()
addTray();
if (!smoke) startBrowserChatMonitor();
if (!smoke && std::filesystem::exists(data / L"lan-pairing.bin"))
try { lanService = std::make_unique<LanService>(data); }
try { lanService = std::make_unique<LanService>(data,window); }
catch (const std::exception &e) { report(L"LAN module: " + wide(e.what())); }
if (lanService && lanService->inputEnabled()) SetTimer(window, 7, 10, nullptr);
SetTimer(window, 1, 60000, nullptr);