Release v0.6.39: isolate history UI and image clipboard memory

This commit is contained in:
2026-11-04 19:40:54 +02:00
parent fa2f6d2ce4
commit 00c9cb0ee4
22 changed files with 747 additions and 49 deletions
+86 -14
View File
@@ -15,6 +15,7 @@ namespace ks
{
#include "app_sync.inl"
App *App::current = nullptr;
#include "app_history.inl"
namespace
{
const char *transformCommandName(int command)
@@ -71,7 +72,7 @@ class Transaction
App &app;
HWND target;
uint64_t version;
Clip snapshot;
ClipboardSnapshot snapshot;
DWORD expected{};
bool captured{};
mutable bool invalidLogged{};
@@ -87,7 +88,7 @@ class Transaction
try
{
expected = GetClipboardSequenceNumber();
snapshot = readClipboard(app.window, 64 * 1024 * 1024);
snapshot.capture(app.window, expected);
if (expected != GetClipboardSequenceNumber())
throw std::runtime_error("Clipboard changed while taking snapshot");
captured = true;
@@ -103,7 +104,7 @@ class Transaction
if (captured && GetClipboardSequenceNumber() == expected)
try
{
app.ownSequence = writeClipboard(app.window, snapshot);
app.ownSequence = snapshot.restore(app.window, expected);
}
catch (...)
{
@@ -131,7 +132,7 @@ class Transaction
if (GetClipboardSequenceNumber() != expected)
{
auto now = GetClipboardSequenceNumber();
if (adopted.empty() || !ownsClipboard(target) || readClipboard(app.window).text() != adopted ||
if (adopted.empty() || !ownsClipboard(target) || readClipboard(app.window, 32 * 1024 * 1024, ClipboardRead::Selection).text() != adopted ||
GetClipboardSequenceNumber() != now)
throw std::runtime_error("Operation cancelled: another application changed the clipboard");
expected = now;
@@ -153,7 +154,7 @@ class Transaction
if (GetClipboardOwner() != app.window)
throw std::runtime_error("Clipboard changed immediately after writing");
auto text = c.text();
if ((!text.empty() && readClipboard(app.window).text() != text) ||
if ((!text.empty() && readClipboard(app.window, 32 * 1024 * 1024, ClipboardRead::Selection).text() != text) ||
GetClipboardSequenceNumber() != expected)
throw std::runtime_error("Clipboard changed while verifying our write");
}
@@ -227,7 +228,7 @@ class Transaction
changed != GetClipboardSequenceNumber())
throw std::runtime_error("Operation cancelled: copy did not belong to the target application");
expected = changed;
auto clip = readClipboard(app.window);
auto clip = readClipboard(app.window, 32 * 1024 * 1024, ClipboardRead::Selection);
auto text = clip.text();
diagnosticEvent(L"Copy result number=" + std::to_wstring(number) + L" attempts=" +
std::to_wstring(attempts) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started) +
@@ -405,7 +406,8 @@ class Transaction
}
};
} // namespace
App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instance(h), smoke(test)
App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot, HWND service)
: historyService(service), instance(h), smoke(test)
{
updateNotifier = notifyUpdateAvailable;
current = this;
@@ -422,7 +424,7 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instan
registrationWarning = L"Settings were not loaded: " + wide(e.what());
}
setLoggingEnabled(settings.writeLog);
if (settings.autoCorrect)
if (settings.autoCorrect && !historyService)
try { autoPredictor.configure(std::filesystem::path(executable()).parent_path() / L"dictionaries", settings.autoLanguages); }
catch (const std::exception &e)
{
@@ -441,6 +443,8 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instan
}
App::~App()
{
if (auto host = historyController(historyProcess)) PostMessageW(host, WM_CLOSE, 0, 0);
if (historyProcess) CloseHandle(historyProcess);
if (notificationRegistration) CloseHandle(notificationRegistration);
if (syncWorker.joinable()) syncWorker.join();
if (updateWorker.joinable())
@@ -688,6 +692,7 @@ void App::reloadLayouts()
}
void App::clearHistory()
{
if (historyService) { remoteHistory("clear"); history->reload(); return; }
if (busy) throw std::runtime_error("Wait for the current operation to finish.");
if (history) history->clear();
if (historyWindow) refreshHistory(*this);
@@ -1523,6 +1528,7 @@ void App::transform(int command, int snippet)
}
void App::pasteEntry(std::wstring id, bool plain)
{
if (historyService) { remoteHistory("paste", id, plain); return; }
if (installingUpdate) return;
if (!history || busy.exchange(true))
return;
@@ -1544,10 +1550,20 @@ void App::pasteEntry(std::wstring id, bool plain)
} reset{busy};
try
{
for (int i = 0; i < 20 && !modifiersReleased(); ++i)
// Foreground activation crosses a process boundary when the
// history UI closes. Let the destination finish activation,
// while retaining the same input-version cancellation guard.
for (int i = 0; i < 20 && inputVersion == version &&
(!modifiersReleased() || GetForegroundWindow() != target); ++i)
Sleep(10);
if (!modifiersReleased() || GetForegroundWindow() != target || inputVersion != version)
{
log(L"History paste cancelled before transaction: foreground=" +
std::to_wstring(GetForegroundWindow() == target) + L" modifiers=" +
std::to_wstring(modifiersReleased()) + L" expectedVersion=" + std::to_wstring(version) +
L" actualVersion=" + std::to_wstring(inputVersion.load()));
return;
}
auto clip = history->load(id);
if (plain)
{
@@ -1557,7 +1573,7 @@ void App::pasteEntry(std::wstring id, bool plain)
clip = textClip(text);
}
Transaction tx(*this, target, version);
tx.paste(clip);
log(tx.paste(clip) ? L"History paste: simulated paste completed" : L"History paste cancelled before simulated paste");
}
catch (const std::exception &e)
{
@@ -1599,7 +1615,8 @@ void App::captureHistory()
}
return;
}
auto clip = readClipboard(window, (size_t)config.maxImageMiB * 1024 * 1024);
auto clip = readClipboard(window, (size_t)config.maxImageMiB * 1024 * 1024,
ClipboardRead::WithoutImages, sequence);
if (sequence != GetClipboardSequenceNumber())
return;
for (auto &f : clip.formats)
@@ -1618,6 +1635,7 @@ void App::captureHistory()
}
void App::recognizeEntry(std::wstring id)
{
if (historyService) { remoteHistory("ocr", id); return; }
if (!history || busy.exchange(true))
return;
auto sequence = GetClipboardSequenceNumber();
@@ -1788,7 +1806,37 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
auto *app = current;
if (!app)
return DefWindowProcW(hwnd, msg, wp, lp);
if (msg == app->taskbarCreated && app->taskbarCreated)
if (app->historyService) {
if (msg == HistoryHostShow || msg == HistoryHostRefresh) {
try {
app->history->reload();
if (msg == HistoryHostShow) showHistory(*app);
else if (IsWindowVisible(app->historyWindow)) refreshHistory(*app);
} catch (const std::exception &e) { app->report(wide(e.what())); }
return 0;
}
if (msg == WM_TIMER) {
if (!IsWindow(app->historyService)) PostMessageW(hwnd, WM_CLOSE, 0, 0);
return 0;
}
if (msg == WM_CLOSE) {
EnumThreadWindows(GetCurrentThreadId(), [](HWND child, LPARAM) -> BOOL {
wchar_t name[64]{}; GetClassNameW(child, name, 64);
if (std::wstring(name) == L"KSwitcher.Entry" || std::wstring(name) == L"KSwitcher.History")
DestroyWindow(child);
return TRUE;
}, 0);
DestroyWindow(hwnd);
return 0;
}
if (msg == WM_DESTROY) { PostQuitMessage(0); return 0; }
if (msg == ErrorMessage) {
std::unique_ptr<std::wstring> text(reinterpret_cast<std::wstring *>(lp));
MessageBoxW(app->historyWindow, text->c_str(), L"Clipboard history", MB_OK | MB_ICONWARNING);
return 0;
}
}
if (!app->historyService && msg == app->taskbarCreated && app->taskbarCreated)
{
app->addTray();
return 0;
@@ -1902,6 +1950,7 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
case WM_COPYDATA:
{
auto *data = (COPYDATASTRUCT *)lp;
if (data && data->dwData == HistoryRequest) return app->historyRequest((HWND)wp, *data);
if (data && data->dwData >= ConfigRead && data->dwData < ConfigEnd)
{
if (app->installingUpdate) return FALSE;
@@ -1988,7 +2037,7 @@ int App::execute()
WNDCLASSW cls{};
cls.hInstance = instance;
cls.lpfnWndProc = windowProc;
cls.lpszClassName = L"KSwitcher.Main";
cls.lpszClassName = historyService ? HistoryHostClass : L"KSwitcher.Main";
cls.hIcon = LoadIconW(instance, MAKEINTRESOURCEW(IDI_KSWITCHER));
RegisterClassW(&cls);
window = CreateWindowExW(WS_EX_TOOLWINDOW, cls.lpszClassName, L"KSwitcher", WS_POPUP, 0, 0, 0, 0, nullptr,
@@ -2022,6 +2071,29 @@ int App::execute()
}
CoUninitialize();
});
if (historyService) {
try {
auto bytes = unprotect(readFile(data / L"history-view.bin", 4 * 1024 * 1024));
auto state = HistoryJson::parse(bytes);
historySelection = wide(state.value("selection", std::string{}));
historyTop = wide(state.value("top", std::string{}));
historyQuery = wide(state.value("query", std::string{}));
} catch (...) {}
showHistory(*this);
remoteHistory("ready", std::to_wstring(reinterpret_cast<uintptr_t>(historyWindow)));
SetTimer(window, 1, 1000, nullptr);
MSG message{};
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE && historyWindow &&
(message.hwnd == historyWindow || IsChild(historyWindow, message.hwnd))) {
SendMessageW(historyWindow, WM_CLOSE, 0, 0);
continue;
}
if (historyWindow && message.message == WM_SYSCHAR && IsDialogMessageW(historyWindow, &message)) continue;
TranslateMessage(&message); DispatchMessageW(&message);
}
return int(message.wParam);
}
keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, keyProc, instance, 0);
mouseHook = SetWindowsHookExW(WH_MOUSE_LL, mouseProc, instance, 0);
registerHotkeys(settings);
@@ -2058,7 +2130,7 @@ int App::execute()
continue;
}
// Preserve history-specific Enter/Shift+Enter and Ctrl+digit handling.
if (historyWindow && msg.message == WM_SYSCHAR && IsDialogMessageW(historyWindow, &msg))
if (!isolateHistory && historyWindow && msg.message == WM_SYSCHAR && IsDialogMessageW(historyWindow, &msg))
continue;
if (settingsWindow && IsDialogMessageW(settingsWindow, &msg))
continue;
+11 -1
View File
@@ -33,6 +33,10 @@ class App
{
public:
HWND window{}, historyWindow{}, settingsWindow{}, snippetsWindow{};
HWND historyService{}; // Non-null only in the short-lived history UI process.
bool isolateHistory{};
unsigned historyEntryWindows{};
std::wstring historySelection, historyTop, historyQuery;
HINSTANCE instance{};
std::filesystem::path data;
std::vector<Layout> layouts;
@@ -55,7 +59,7 @@ class App
bool smoke{};
int startCommand{};
std::wstring updateStartupEvent;
App(HINSTANCE, bool = false, const std::filesystem::path &dataRoot = {});
App(HINSTANCE, bool = false, const std::filesystem::path &dataRoot = {}, HWND historyService = nullptr);
~App();
int execute();
void dispatch(int);
@@ -72,6 +76,11 @@ class App
void applySettings(const Settings &);
void reloadLayouts();
void clearHistory();
void openHistoryProcess();
void refreshHistoryProcess();
void remoteHistory(const char *action, const std::wstring &id = {}, bool plain = false);
LRESULT historyRequest(HWND, const COPYDATASTRUCT &);
void saveHistoryView(const std::wstring &, const std::wstring &, const std::wstring &);
void clearLog(bool previous);
void setHotkeyCapture(HWND);
bool isHotkey(DWORD, UINT);
@@ -88,6 +97,7 @@ class App
friend struct InputRegressionAccess;
void releaseIdleMemory(ULONGLONG now);
HHOOK keyboardHook{}, mouseHook{};
HANDLE historyProcess{};
std::mutex jobsMutex;
std::condition_variable jobsReady;
std::deque<std::function<void()>> jobs;
+100
View File
@@ -0,0 +1,100 @@
// 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<uintptr_t>(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<WPARAM>(window),
reinterpret_cast<LPARAM>(&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<const char *>(message.lpData);
auto job = HistoryJson::parse(first, first + message.cbData);
auto action = job.at("action").get<std::string>();
auto id = wide(job.at("id").get<std::string>());
if (id.size() > 128) return FALSE;
if (action == "ready") {
auto ui = reinterpret_cast<HWND>(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<bool>());
} 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)
{
if (!historyService) return;
try {
auto text = HistoryJson{{"selection", utf8(selection)}, {"top", utf8(top)}, {"query", utf8(query)}}.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.
}
+1 -1
View File
@@ -79,7 +79,7 @@ LRESULT CALLBACK controller(HWND window, UINT message, WPARAM wp, LPARAM lp)
}
// The configurator owns no history store, dictionaries, worker or input hooks.
// All mutations are validated and committed by the resident process.
App::App(HINSTANCE h, bool, const std::filesystem::path &) : instance(h)
App::App(HINSTANCE h, bool, const std::filesystem::path &, HWND) : instance(h)
{
current = this;
data = dataDirectory();
+16
View File
@@ -1,12 +1,27 @@
#include "app.h"
#include <shellapi.h>
#include <shlobj.h>
#include "configuration.h"
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
{
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
int argc{};
auto argv = CommandLineToArgvW(GetCommandLineW(), &argc);
std::wstring arg = argc > 1 ? argv[1] : L"";
if (argc == 2 && arg.starts_with(L"--history-host=")) {
LocalFree(argv);
int result = 1;
try {
auto service = reinterpret_cast<HWND>(uintptr_t(std::stoull(arg.substr(15))));
wchar_t name[64]{}; GetClassNameW(service, name, 64);
if (std::wstring(name) != L"KSwitcher.Main" || !ks::configurationPeer(service, L"KSwitcher.exe"))
throw std::runtime_error("Invalid history service");
ks::App app(instance, true, {}, service);
result = app.execute();
} catch (...) {}
CoUninitialize();
return result;
}
if (arg.starts_with(L"--configure-autostart="))
{
int result = 1;
@@ -72,6 +87,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
try
{
ks::App app(instance, arg == L"--smoke-test");
app.isolateHistory = !app.smoke;
if (arg.starts_with(L"--update-ready-event="))
app.updateStartupEvent = arg.substr(std::wstring(L"--update-ready-event=").size());
else if (!arg.empty() && arg != L"--smoke-test")
+17 -4
View File
@@ -265,15 +265,27 @@ std::wstring processName(HWND window)
CloseHandle(h);
return ok ? std::filesystem::path(std::wstring(path, n)).stem().wstring() : L"";
}
Clip readClipboard(HWND window, size_t limit)
Clip readClipboard(HWND window, size_t limit, ClipboardRead mode, std::optional<DWORD> expected)
{
ClipboardLock lock(window);
if (expected && *expected != GetClipboardSequenceNumber())
throw std::runtime_error("Clipboard changed while taking snapshot");
if (mode == ClipboardRead::WithoutImages &&
(IsClipboardFormatAvailable(CF_DIB) || IsClipboardFormatAvailable(CF_DIBV5) ||
IsClipboardFormatAvailable(CF_BITMAP) || IsClipboardFormatAvailable(RegisterClipboardFormatW(L"PNG"))))
throw std::runtime_error("Clipboard changed to an image");
Clip c;
size_t total = 0;
for (UINT f = EnumClipboardFormats(0); f; f = EnumClipboardFormats(f))
{
if (!globalFormat(f))
continue;
wchar_t name[256]{};
if (f >= 0xc000) GetClipboardFormatNameW(f, name, 256);
if (mode == ClipboardRead::Selection && f != CF_UNICODETEXT &&
std::wstring_view(name) != L"VisualStudioEditorOperationsLineCutCopyClipboardTag" &&
std::wstring_view(name) != L"MSDEVLineSelect" && std::wstring_view(name) != L"vscode-editor-data")
continue;
HANDLE handle = GetClipboardData(f);
if (!handle)
continue;
@@ -291,8 +303,6 @@ Clip readClipboard(HWND window, size_t limit)
GlobalUnlock(handle);
if (f >= 0xc000)
{
wchar_t name[256]{};
GetClipboardFormatNameW(f, name, 256);
format.name = name;
}
total += size;
@@ -300,7 +310,8 @@ Clip readClipboard(HWND window, size_t limit)
}
return c;
}
DWORD writeClipboard(HWND window, const Clip &c)
DWORD writeClipboard(HWND window, const Clip &c) { return writeClipboardGuarded(window, c, {}); }
DWORD writeClipboardGuarded(HWND window, const Clip &c, std::optional<DWORD> expected)
{
// Allocate all blocks before replacing any existing data.
std::vector<std::pair<UINT, HGLOBAL>> blocks;
@@ -327,6 +338,8 @@ DWORD writeClipboard(HWND window, const Clip &c)
blocks.emplace_back(id, h);
}
ClipboardLock lock(window);
if (expected && *expected != GetClipboardSequenceNumber())
throw std::runtime_error("Clipboard changed before restoring snapshot");
if (!EmptyClipboard())
throw std::runtime_error("Cannot replace clipboard");
for (auto &[id, h] : blocks)
+4 -1
View File
@@ -43,8 +43,11 @@ bool ownsClipboard(HWND);
bool webViewDescendant(DWORD owner, DWORD host,
const std::function<std::optional<std::pair<DWORD, std::wstring>>(DWORD)> &read);
std::wstring processName(HWND);
Clip readClipboard(HWND, size_t limit = 32 * 1024 * 1024);
enum class ClipboardRead { All, Selection, WithoutImages };
Clip readClipboard(HWND, size_t limit = 32 * 1024 * 1024,
ClipboardRead = ClipboardRead::All, std::optional<DWORD> expected = {});
DWORD writeClipboard(HWND, const Clip &);
DWORD writeClipboardGuarded(HWND, const Clip &, std::optional<DWORD> expected);
Clip textClip(const std::wstring &);
bool chord(WORD key, UINT modifiers = 0);
// Process-local marker, shared by every KSwitcher input producer (not an authentication token).
+4
View File
@@ -50,6 +50,10 @@ int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
std::wstring token;
bool autoOpenEditor=false, writeLog=false;
int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc);
if(argc==3 && std::wstring(argv[1])==L"--clipboard-snapshot") {
std::filesystem::path request=argv[2]; LocalFree(argv);
return ks::runClipboardSnapshot(request);
}
if(argc==3 && (std::wstring(argv[1])==L"--capture-history" || std::wstring(argv[1])==L"--history-ocr")) {
bool ocr=std::wstring(argv[1])==L"--history-ocr";
std::filesystem::path request=argv[2]; LocalFree(argv);
+112 -8
View File
@@ -410,6 +410,87 @@ void History::loadIndex()
}
entries_ = std::move(loaded);
}
void History::reload() { std::lock_guard lock(mutex_); loadIndex(); }
void ClipboardSnapshot::cleanup() noexcept
{
if (request_.empty()) return;
std::error_code ignored;
for (const auto *suffix : {L"", L".snapshot", L".result"})
std::filesystem::remove(std::filesystem::path(request_.wstring() + suffix), ignored);
request_.clear();
}
ClipboardSnapshot::~ClipboardSnapshot() { cleanup(); }
DWORD ClipboardSnapshot::run(bool restore, DWORD sequence)
{
atomicWrite(request_, protect(jsonBytes(Json{{"schema", 1}, {"restore", restore}, {"sequence", sequence}})));
auto helper = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Screenshot.exe";
auto command = L"\"" + helper.wstring() + L"\" --clipboard-snapshot \"" + request_.wstring() + L"\"";
STARTUPINFOW startup{sizeof(startup)};
startup.dwFlags = STARTF_USESHOWWINDOW; startup.wShowWindow = SW_HIDE;
PROCESS_INFORMATION process{};
if (!CreateProcessW(helper.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
nullptr, helper.parent_path().c_str(), &startup, &process))
throw std::runtime_error("Cannot start clipboard snapshot helper");
CloseHandle(process.hThread);
auto wait = WaitForSingleObject(process.hProcess, 10000);
DWORD result = ERROR_GEN_FAILURE;
if (wait != WAIT_OBJECT_0)
{
TerminateProcess(process.hProcess, ERROR_TIMEOUT);
WaitForSingleObject(process.hProcess, INFINITE);
}
GetExitCodeProcess(process.hProcess, &result);
CloseHandle(process.hProcess);
if (wait != WAIT_OBJECT_0 || result) throw std::runtime_error("Clipboard snapshot helper failed");
return parse(unprotect(readFile(request_.wstring() + L".result", 4096))).at("sequence").get<DWORD>();
}
void ClipboardSnapshot::capture(HWND window, DWORD sequence)
{
cleanup();
clip_ = {};
if (!IsClipboardFormatAvailable(CF_DIB) && !IsClipboardFormatAvailable(CF_DIBV5) &&
!IsClipboardFormatAvailable(CF_BITMAP) && !IsClipboardFormatAvailable(RegisterClipboardFormatW(L"PNG")))
{
clip_ = readClipboard(window, 64 * 1024 * 1024, ClipboardRead::WithoutImages, sequence);
return;
}
request_ = std::filesystem::temp_directory_path() / (L"ks-clipboard-" + newSnippetId() + L".request");
try { run(false, sequence); }
catch (...) { cleanup(); throw; }
}
DWORD ClipboardSnapshot::restore(HWND window, DWORD sequence)
{
return request_.empty() ? writeClipboardGuarded(window, clip_, sequence) : run(true, sequence);
}
int runClipboardSnapshot(const std::filesystem::path &request)
{
try
{
if (!request.is_absolute() || !request.filename().wstring().starts_with(L"ks-clipboard-") ||
request.extension() != L".request") return 1;
auto job = parse(unprotect(readFile(request, 4096)));
if (job.at("schema") != 1) return 1;
auto sequence = job.at("sequence").get<DWORD>();
const auto snapshot = request.wstring() + L".snapshot";
if (job.at("restore").get<bool>())
{
auto clip = decodeClip(unprotect(readFile(snapshot, 70 * 1024 * 1024)));
HWND window = CreateWindowExW(0, L"STATIC", L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, nullptr);
if (!window) return 1;
struct WindowCleanup { HWND window; ~WindowCleanup() { DestroyWindow(window); } } cleanup{window};
sequence = writeClipboardGuarded(window, clip, sequence);
}
else
{
auto clip = readClipboard(nullptr, 64 * 1024 * 1024, ClipboardRead::All, sequence);
if (sequence != GetClipboardSequenceNumber()) return 2;
atomicWrite(snapshot, protect(encodeClip(clip)));
}
atomicWrite(request.wstring() + L".result", protect(jsonBytes(Json{{"sequence", sequence}})));
return 0;
}
catch (...) { return 1; }
}
bool History::captureImage(DWORD sequence, const std::wstring &app, const Settings &settings)
{
// Serialize helper index changes with all resident history mutations. Only
@@ -706,12 +787,12 @@ void History::clear()
std::filesystem::remove(folder_ / (e.id + L".ocr"), ec);
}
}
std::wstring History::recognizeImage(const std::wstring &id, const Settings &settings,
const std::function<bool()> &cancelled) const
static Bytes historyImageJob(const std::filesystem::path &folder, const std::wstring &id,
const Json &job, const std::function<bool()> &cancelled)
{
if (!validId(id)) throw std::runtime_error("Invalid history ID");
if (cancelled && cancelled()) throw std::runtime_error("History OCR cancelled");
auto request = std::filesystem::absolute(folder_) / (L"ocr-" + newSnippetId() + L".request");
auto request = std::filesystem::absolute(folder) / (L"ocr-" + newSnippetId() + L".request");
auto response = request; response += L".result";
struct Cleanup {
std::filesystem::path request, response;
@@ -721,9 +802,6 @@ std::wstring History::recognizeImage(const std::wstring &id, const Settings &set
std::filesystem::remove(response, ignored);
}
} cleanup{request, response};
// Send only OCR options and the entry ID. The resident never loads image bytes.
Json job{{"schema", 1}, {"id", utf8(id)}, {"tesseract", settings.useTesseract},
{"path", utf8(settings.tesseractPath)}, {"languages", utf8(settings.tesseractLanguages)}};
atomicWrite(request, protect(jsonBytes(job)));
auto helper = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Screenshot.exe";
auto command = L"\"" + helper.wstring() + L"\" --history-ocr \"" + request.wstring() + L"\"";
@@ -755,11 +833,28 @@ std::wstring History::recognizeImage(const std::wstring &id, const Settings &set
DWORD result{};
if (!GetExitCodeProcess(child.handle, &result) || result)
throw std::runtime_error("History OCR helper failed: " + std::to_string(result));
auto reply = parse(unprotect(readFile(response, 8 * 1024 * 1024)));
return unprotect(readFile(response, 8 * 1024 * 1024));
}
std::wstring History::recognizeImage(const std::wstring &id, const Settings &settings,
const std::function<bool()> &cancelled) const
{
Json job{{"schema", 1}, {"id", utf8(id)}, {"tesseract", settings.useTesseract},
{"path", utf8(settings.tesseractPath)}, {"languages", utf8(settings.tesseractLanguages)}};
auto reply = parse(historyImageJob(folder_, id, job, cancelled));
if (reply.at("schema") != 1 || str(reply, "id") != id)
throw std::runtime_error("Invalid history OCR response");
return str(reply, "text");
}
Pixels History::thumbnail(const std::wstring &id, const std::function<bool()> &cancelled) const
{
auto bytes = historyImageJob(folder_, id, Json{{"schema", 1}, {"id", utf8(id)}, {"thumbnail", true}}, cancelled);
size_t at = 0;
auto width = get32(bytes, at), height = get32(bytes, at);
if (!width || !height || width > 208 || height > 128 || bytes.size() - at != width * height * 4)
throw std::runtime_error("Invalid history thumbnail response");
bytes.erase(bytes.begin(), bytes.begin() + at);
return Pixels{width, height, std::move(bytes)};
}
int runHistoryOcr(const std::filesystem::path &request)
{
try {
@@ -768,11 +863,20 @@ int runHistoryOcr(const std::filesystem::path &request)
auto job = parse(unprotect(readFile(request, 1024 * 1024)));
auto id = str(job, "id");
if (job.at("schema") != 1 || !validId(id)) return 1;
auto clip = decodeClip(unprotect(readFile(request.parent_path() / (id + L".dat"), 80 * 1024 * 1024)));
if (job.value("thumbnail", false)) {
auto pixels = decodeImage(clip, 208, 128);
if (!pixels.width || !pixels.height) return 1;
Bytes reply;
put32(reply, pixels.width); put32(reply, pixels.height);
reply.insert(reply.end(), pixels.bgra.begin(), pixels.bgra.end());
atomicWrite(request.wstring() + L".result", protect(reply));
return 0;
}
Settings settings;
settings.useTesseract = job.at("tesseract").get<bool>();
settings.tesseractPath = str(job, "path");
settings.tesseractLanguages = str(job, "languages");
auto clip = decodeClip(unprotect(readFile(request.parent_path() / (id + L".dat"), 80 * 1024 * 1024)));
auto text = utf8(recognizeText(clip, settings));
if (text.size() > 1024 * 1024) return 1;
auto response = request; response += L".result";
+19
View File
@@ -4,6 +4,7 @@
#include <functional>
namespace ks
{
struct Pixels;
Settings loadSettings(const std::filesystem::path &, const std::vector<Layout> &);
Settings decodeSettings(const Bytes &, const std::vector<Layout> &);
Bytes encodeSettings(const Settings &);
@@ -29,12 +30,14 @@ class History
public:
explicit History(const std::filesystem::path &);
void reload();
std::vector<Entry> entries() const;
Clip load(const std::wstring &id) const;
void add(const Clip &, const std::wstring &app, const Settings &);
bool captureImage(DWORD sequence, const std::wstring &app, const Settings &);
std::wstring recognizeImage(const std::wstring &id, const Settings &,
const std::function<bool()> &cancelled = {}) const;
Pixels thumbnail(const std::wstring &id, const std::function<bool()> &cancelled = {}) const;
bool saveOcr(const std::wstring &id, const std::optional<std::wstring> &, const Settings &);
std::optional<std::wstring> ocrText(const std::wstring &id, const std::wstring &profile = L"") const;
void remove(const std::wstring &);
@@ -47,4 +50,20 @@ Clip decodeClip(const Bytes &);
bool sameClipboardImage(const Clip &, const Clip &);
int runImageHistoryCapture(const std::filesystem::path &request);
int runHistoryOcr(const std::filesystem::path &request);
// Image backups stay encrypted on disk and are read/written only by a short-lived helper.
class ClipboardSnapshot
{
Clip clip_;
std::filesystem::path request_;
void cleanup() noexcept;
DWORD run(bool restore, DWORD sequence);
public:
ClipboardSnapshot() = default;
ClipboardSnapshot(const ClipboardSnapshot &) = delete;
ClipboardSnapshot &operator=(const ClipboardSnapshot &) = delete;
~ClipboardSnapshot();
void capture(HWND, DWORD sequence);
DWORD restore(HWND, DWORD sequence);
};
int runClipboardSnapshot(const std::filesystem::path &request);
} // namespace ks
+26 -5
View File
@@ -549,11 +549,11 @@ void requestPreview(HWND w, HistoryView *v, const Entry &entry)
auto result = new PreviewResult{generation, id, {}, {}};
try
{
auto clip = app->history->load(id);
if (!active->load()) { delete result; return; }
if (image)
result->pixels = decodeImage(clip, 208, 128);
result->pixels = app->history->thumbnail(id, [active] { return !active->load(); });
else {
auto clip = app->history->load(id);
if (!active->load()) { delete result; return; }
result->text.rtf = richPreview(clip, false);
if (result->text.rtf.empty()) {
result->text.plain = clip.text();
@@ -771,7 +771,10 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
if (!entry.empty())
{
if (id == 112)
if (v->app->historyService) {
v->app->remoteHistory(id == 112 ? "pin" : "remove", entry);
v->app->history->reload();
} else if (id == 112)
v->app->history->pin(entry);
else
v->app->history->remove(entry);
@@ -907,6 +910,7 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
v->rememberedSelection = selected(w, v);
int top = (int)SendDlgItemMessageW(w,102,LB_GETTOPINDEX,0,0);
v->rememberedTop = top >= 0 && top < (int)v->items.size() ? v->items[top].id : L"";
v->app->saveHistoryView(v->rememberedSelection, v->rememberedTop, value(w, 101));
KillTimer(w, 1);
v->closed = true;
v->previewActive->store(false);
@@ -918,6 +922,8 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
v->richTexts.clear();
if (v->rich) SetWindowTextW(v->rich, L"");
v->pending.clear();
if (v->app->historyService && !v->app->historyEntryWindows)
PostMessageW(v->app->window, WM_CLOSE, 0, 0);
return 0;
}
case WM_NCDESTROY:
@@ -1681,13 +1687,19 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
#ifndef KS_CONFIG_APP
LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
if (msg == WM_NCCREATE)
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)((CREATESTRUCTW *)lp)->lpCreateParams);
if (msg == WM_SIZE)
{
MoveWindow(GetDlgItem(w, 1), 10, 10, LOWORD(lp) - 20, HIWORD(lp) - 20, TRUE);
return 0;
}
if (msg == WM_NCDESTROY)
if (msg == WM_NCDESTROY) {
disposeFont(w);
auto *app = (App *)GetWindowLongPtrW(w, GWLP_USERDATA);
if (app && app->historyService && app->historyEntryWindows && !--app->historyEntryWindows &&
!IsWindowVisible(app->historyWindow)) PostMessageW(app->window, WM_CLOSE, 0, 0);
}
return DefWindowProcW(w, msg, wp, lp);
}
#endif
@@ -1695,6 +1707,7 @@ LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
#ifndef KS_CONFIG_APP
void refreshHistory(App &app)
{
if (app.isolateHistory) { app.refreshHistoryProcess(); return; }
if (app.historyWindow)
{
auto *v = (HistoryView *)GetWindowLongPtrW(app.historyWindow, GWLP_USERDATA);
@@ -1703,6 +1716,7 @@ void refreshHistory(App &app)
}
void showHistory(App &app, bool visible)
{
if (app.isolateHistory) { if (visible) app.openHistoryProcess(); return; }
if (!app.history)
{
app.report(L"Clipboard history is unavailable. See the log in the data folder.");
@@ -1738,6 +1752,12 @@ void showHistory(App &app, bool visible)
button(w, L"Clear &history…", 116, 546, 452, 150);
SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w);
SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w);
if (app.historyService) {
auto *v = (HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA);
v->rememberedSelection = app.historySelection;
v->rememberedTop = app.historyTop;
SetDlgItemTextW(w, 101, app.historyQuery.c_str());
}
}
if (!visible) return;
POINT cursor;
@@ -1894,6 +1914,7 @@ void showEntry(App &app, const std::wstring &id)
}
HWND w = makeWindow(app, L"KSwitcher.Entry", L"Clipboard item", entryProc, 720, 500,
WS_THICKFRAME | WS_MAXIMIZEBOX);
if (app.historyService) ++app.historyEntryWindows;
auto field = edit(w, 1, 10, 10, 700, 480, ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL);
SetWindowTextW(field, text.c_str());
showRequestedWindow(w);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.38";
inline constexpr wchar_t AppVersion[] = L"0.6.39";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;