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

This commit is contained in:
agrynco committed 2026-11-04 19:40:54 +02:00
1 parent fa2f6d2ce4
commit 00c9cb0ee4
22 files changed
+747 -49

No files matched your search

+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;