// History UI has no clipboard listener or hooks. The resident remains the only writer. namespace { constexpr ULONG_PTR HistoryRequest = 0x4b530200; constexpr UINT HistoryHostShow = WM_APP + 71, HistoryHostRefresh = WM_APP + 72; constexpr wchar_t HistoryHostClass[] = L"KSwitcher.HistoryHost"; using HistoryJson = nlohmann::json; HWND historyController(HANDLE process) { if (!process || WaitForSingleObject(process, 0) == WAIT_OBJECT_0) return nullptr; auto window = FindWindowW(HistoryHostClass, nullptr); DWORD pid{}; GetWindowThreadProcessId(window, &pid); return pid == GetProcessId(process) ? window : nullptr; } } void App::openHistoryProcess() { auto foreground = GetForegroundWindow(); if (foreground != window && foreground != historyWindow) pasteTarget = foreground; if (historyProcess && WaitForSingleObject(historyProcess, 0) == WAIT_OBJECT_0) { CloseHandle(historyProcess); historyProcess = nullptr; historyWindow = nullptr; } if (historyProcess) { if (auto host = historyController(historyProcess)) { AllowSetForegroundWindow(GetProcessId(historyProcess)); PostMessageW(host, HistoryHostShow, 0, 0); } return; } auto path = executable(); auto command = L"\"" + path + L"\" --history-host=" + std::to_wstring(reinterpret_cast(window)); STARTUPINFOW startup{sizeof(startup)}; startup.dwFlags = STARTF_USESHOWWINDOW; startup.wShowWindow = SW_HIDE; PROCESS_INFORMATION child{}; if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, std::filesystem::path(path).parent_path().c_str(), &startup, &child)) throw std::runtime_error("Cannot start clipboard history window"); CloseHandle(child.hThread); historyProcess = child.hProcess; AllowSetForegroundWindow(child.dwProcessId); } void App::refreshHistoryProcess() { if (auto host = historyController(historyProcess)) PostMessageW(host, HistoryHostRefresh, 0, 0); } void App::remoteHistory(const char *action, const std::wstring &id, bool plain) { if (!historyService || !configurationPeer(historyService, L"KSwitcher.exe")) throw std::runtime_error("KSwitcher has stopped. Reopen clipboard history."); auto text = HistoryJson{{"action", action}, {"id", utf8(id)}, {"plain", plain}}.dump(); if (std::string_view(action) == "paste") { DWORD residentPid{}; GetWindowThreadProcessId(historyService, &residentPid); AllowSetForegroundWindow(residentPid); } COPYDATASTRUCT message{HistoryRequest, DWORD(text.size()), text.data()}; DWORD_PTR result{}; if (!SendMessageTimeoutW(historyService, WM_COPYDATA, reinterpret_cast(window), reinterpret_cast(&message), SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 15000, &result) || result != 1) throw std::runtime_error("KSwitcher could not complete the history request. Reopen history and retry."); } LRESULT App::historyRequest(HWND sender, const COPYDATASTRUCT &message) { DWORD pid{}; GetWindowThreadProcessId(sender, &pid); if (!historyProcess || pid != GetProcessId(historyProcess) || !configurationPeer(sender, L"KSwitcher.exe") || message.cbData > 4096 || !message.cbData || !message.lpData || installingUpdate) return FALSE; try { const auto *first = static_cast(message.lpData); auto job = HistoryJson::parse(first, first + message.cbData); auto action = job.at("action").get(); auto id = wide(job.at("id").get()); if (id.size() > 128) return FALSE; if (action == "ready") { auto ui = reinterpret_cast(uintptr_t(std::stoull(id))); DWORD uiPid{}; GetWindowThreadProcessId(ui, &uiPid); wchar_t name[64]{}; GetClassNameW(ui, name, 64); if (uiPid != pid || std::wstring(name) != L"KSwitcher.History") return FALSE; historyWindow = ui; } else if (action == "paste") { if (busy || !history) return FALSE; pasteEntry(id, job.at("plain").get()); } else if (action == "ocr") { if (busy || !history) return FALSE; recognizeEntry(id); } else if (action == "clear") clearHistory(); else if (action == "pin" || action == "remove") { if (busy || !history) return FALSE; if (action == "pin") history->pin(id); else history->remove(id); } else return FALSE; return TRUE; } catch (...) { return FALSE; } } void App::saveHistoryView(const std::wstring &selection, const std::wstring &top, const std::wstring &query, bool pinnedOnly) { if (!historyService) return; try { auto text = HistoryJson{{"selection", utf8(selection)}, {"top", utf8(top)}, {"query", utf8(query)}, {"pinnedOnly", pinnedOnly}}.dump(); atomicWrite(data / L"history-view.bin", protect(Bytes(text.begin(), text.end()))); } catch (...) {} // Closing the UI must still release its process if state cannot be saved. }