#include "app.h" #include #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" #include "utilities.h" #include "screenshot_notifications.h" #include #include #include namespace ks { #include "app_sync.inl" App *App::current = nullptr; #include "app_history.inl" namespace { const char *transformCommandName(int command) { return command == Toggle ? "Convert / cycle" : command >= Direct ? "Convert / direct" : command == Case ? "Change case" : command == Translit ? "Transliterate" : command == Undo ? "Undo" : "Automatic / snippet"; } ULONG_PTR snippetReplayMarker() { static const char marker{}; return reinterpret_cast(&marker); } std::vector> shortcuts(const Settings &s) { std::vector> out{{Toggle, s.toggle}, {Case, s.casing}, {Translit, s.translit}, {Undo, s.undo}, {ShowHistory, s.clipboard}, {Screenshot, s.screenshot}}; for (size_t i = 0; i < s.direct.size(); ++i) out.emplace_back(Direct + (int)i, s.direct[i]); return out; } size_t caretSteps(std::wstring text) { size_t n = 0; for (size_t i = 0; i < text.size(); ++i) { if (text[i] == L'\r' && i + 1 < text.size() && text[i + 1] == L'\n') ++i; else if (text[i] >= 0xd800 && text[i] <= 0xdbff && i + 1 < text.size() && text[i + 1] >= 0xdc00 && text[i + 1] <= 0xdfff) ++i; ++n; } return n; } std::wstring newlines(std::wstring text) { std::wstring out; for (size_t i = 0; i < text.size(); ++i) { if (text[i] == L'\r' && i + 1 < text.size() && text[i + 1] == L'\n') continue; out += text[i]; } return out; } // Every transaction checks input, foreground and clipboard ownership before replacing text. class Transaction { App &app; HWND target; uint64_t version; ClipboardSnapshot snapshot; DWORD expected{}; bool captured{}; mutable bool invalidLogged{}; unsigned copyNumber{}; std::wstring adopted; Bytes adoptedFormats; bool trustedContentsObserved{}; std::unique_ptr selectionProof; std::wstring provenSelection; bool farSnippet{}; size_t farSelectedSteps{}; public: Transaction(App &a, HWND t, uint64_t v, bool farEditor = false) : app(a), target(t), version(v), farSnippet(farEditor) { app.suspended = true; try { for (unsigned attempt = 0; ; ++attempt) { expected = GetClipboardSequenceNumber(); try { snapshot.capture(app.window, expected); if (expected != GetClipboardSequenceNumber()) throw std::runtime_error("Clipboard changed while taking snapshot"); break; } catch (...) { // No selection or clipboard write has happened yet. Recapture // the latest clipboard after a concurrent publisher settles. if (attempt >= 2 || !valid() || expected == GetClipboardSequenceNumber()) throw; diagnosticEvent(L"Snapshot retry: clipboard changed during capture"); Sleep(20); } } captured = true; } catch (...) { app.suspended = false; throw; } } ~Transaction() { if (farSnippet) finishSelection(false); if (captured && GetClipboardSequenceNumber() == expected) try { app.ownSequence = snapshot.restore(app.window, expected); } catch (...) { app.report(L"Could not restore clipboard; the copied text remains available."); } app.suspended = false; } bool valid() const { auto foreground = GetForegroundWindow(); auto currentVersion = app.inputVersion.load(); bool ok = target && IsWindow(target) && foreground == target && currentVersion == version; if (!ok && !invalidLogged) { invalidLogged = true; diagnosticEvent(L"Context invalid: target=" + std::to_wstring((uintptr_t)target) + L" alive=" + std::to_wstring(IsWindow(target)) + L" foreground=" + std::to_wstring((uintptr_t)foreground) + L" expectedVersion=" + std::to_wstring(version) + L" currentVersion=" + std::to_wstring(currentVersion)); } return ok; } void check() { if (GetClipboardSequenceNumber() != expected) { auto now = GetClipboardSequenceNumber(); auto current = readClipboard(app.window, 32 * 1024 * 1024, ClipboardRead::Selection); const bool sameContents = selectionClipboardIdentity(current) == adoptedFormats; if (adopted.empty() || (!trustedContentsObserved && !ownsClipboard(target)) || !sameContents || GetClipboardSequenceNumber() != now) { diagnosticEvent(L"Clipboard check rejected: expectedSequence=" + std::to_wstring(expected) + L" readSequence=" + std::to_wstring(now) + L" currentSequence=" + std::to_wstring(GetClipboardSequenceNumber()) + L" owner=" + std::to_wstring((uintptr_t)GetClipboardOwner()) + L" sameContents=" + std::to_wstring(sameContents) + L" expectedText=" + diagnosticText(adopted) + L" currentText=" + diagnosticText(current.text())); throw std::runtime_error("Operation cancelled: another application changed the clipboard"); } expected = now; } } void write(const Clip &c) { check(); adopted.clear(); adoptedFormats.clear(); trustedContentsObserved = false; // These are temporary transaction contents, not a user's clipboard entry. // In particular, do not invite the Windows history service to read our // sentinel while the target editor is trying to open the clipboard. auto temporary = c; const std::wstring exclusion = L"ExcludeClipboardContentFromMonitorProcessing"; temporary.formats.push_back({RegisterClipboardFormatW(exclusion.c_str()), exclusion, Bytes{0}}); app.ownSequence = expected = writeClipboard(app.window, temporary); // A concurrent writer can win immediately after CloseClipboard, before we read its sequence. // Never adopt that writer's sequence as our own and paste its content. if (GetClipboardOwner() != app.window) throw std::runtime_error("Clipboard changed immediately after writing"); auto text = c.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"); } std::wstring copy() { const auto started = GetTickCount64(); const auto number = ++copyNumber; if (!valid()) return {}; const auto sentinel = L"\x2063KSwitcher-" + std::to_wstring(GetTickCount64()); write(textClip(sentinel)); DWORD before = expected; diagnosticEvent(L"Copy begin number=" + std::to_wstring(number) + L" sentinelSequence=" + std::to_wstring(before) + L" target=" + std::to_wstring((uintptr_t)target) + L" sentinel=" + diagnosticText(sentinel)); if (!valid() || !chord(farSnippet ? VK_INSERT : L'C', MOD_CONTROL)) { diagnosticEvent(L"Copy failed: context changed or simulated Ctrl+C blocked"); return {}; } ULONGLONG deadline = GetTickCount64() + 2000; ULONGLONG retryAt = GetTickCount64() + 250; unsigned attempts = 1; DWORD changed = before; ULONGLONG stable = 0; bool changedAgain = false; std::optional observedTargetCopy; while (GetTickCount64() < deadline) { if (!valid()) return {}; DWORD now = GetClipboardSequenceNumber(); if (now != changed) { diagnosticEvent(L"Copy sequence number=" + std::to_wstring(number) + L" from=" + std::to_wstring(changed) + L" to=" + std::to_wstring(now) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started) + L" owner=" + std::to_wstring((uintptr_t)GetClipboardOwner())); changedAgain |= changed != before; changed = now; stable = GetTickCount64(); // Clipboard synchronizers can republish a legitimate copy under // their own HWND. Record provenance before that handoff; an // unobserved or altered foreign copy must still be rejected. if (ownsClipboard(target)) { auto candidate = readClipboard(app.window, 32 * 1024 * 1024, ClipboardRead::Selection); if (GetClipboardSequenceNumber() == now && ownsClipboard(target) && !candidate.text().empty()) { observedTargetCopy = selectionClipboardIdentity(candidate); diagnosticEvent(L"Copy: target-owned selection observed number=" + std::to_wstring(number)); } } } if (changed != before && GetTickCount64() - stable >= 60) break; // Native editors can silently drop Ctrl+C if another process has // the clipboard open. Retry only while our sentinel is untouched; // never overwrite a copy result or another application's content. if (changed == before && attempts < 3 && GetTickCount64() >= retryAt && !GetOpenClipboardWindow()) { check(); if (!valid() || GetClipboardSequenceNumber() != before) return {}; if (!chord(farSnippet ? VK_INSERT : L'C', MOD_CONTROL)) return {}; diagnosticEvent(L"Copy retry: clipboard unchanged after Ctrl+C number=" + std::to_wstring(number) + L" attempt=" + std::to_wstring(attempts + 1) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started)); ++attempts; retryAt = GetTickCount64() + 250; } Sleep(10); } if (changed == before || !valid()) { diagnosticEvent(L"Copy end number=" + std::to_wstring(number) + L" attempts=" + std::to_wstring(attempts) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started) + L" sequence=" + std::to_wstring(GetClipboardSequenceNumber()) + L" clipboardOpenBy=" + std::to_wstring((uintptr_t)GetOpenClipboardWindow())); diagnosticEvent(changed == before ? L"Copy failed: clipboard timeout (2000 ms)" : L"Copy failed: context changed"); return {}; } const bool targetOwned = ownsClipboard(target); auto clip = readClipboard(app.window, 32 * 1024 * 1024, ClipboardRead::Selection); const auto formats = selectionClipboardIdentity(clip); const bool verifiedRelay = observedTargetCopy && *observedTargetCopy == formats; if (((GetClipboardOwner() ? !targetOwned : changedAgain) && !verifiedRelay) || changed != GetClipboardSequenceNumber()) { diagnosticEvent(L"Copy rejected: observedTarget=" + std::to_wstring(observedTargetCopy.has_value()) + L" verifiedRelay=" + std::to_wstring(verifiedRelay) + L" expectedSequence=" + std::to_wstring(changed) + L" currentSequence=" + std::to_wstring(GetClipboardSequenceNumber()) + L" owner=" + std::to_wstring((uintptr_t)GetClipboardOwner()) + L" targetText=" + diagnosticText(observedTargetCopy ? decodeClip(*observedTargetCopy).text() : L"") + L" currentText=" + diagnosticText(clip.text())); throw std::runtime_error("Operation cancelled: copy did not belong to the target application"); } if (!targetOwned && verifiedRelay) diagnosticEvent(L"Copy: identical target selection republished by clipboard synchronizer"); expected = changed; 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) + L" text=" + diagnosticText(text)); adopted = text; adoptedFormats = formats; trustedContentsObserved = targetOwned || verifiedRelay; check(); for (auto &f : clip.formats) { if (f.name == L"VisualStudioEditorOperationsLineCutCopyClipboardTag" || f.name == L"MSDEVLineSelect") { diagnosticEvent(L"Copy rejected: editor copied line without selection"); return {}; } if (f.name == L"vscode-editor-data") { std::string json(f.data.begin(), f.data.end()); if (json.find("\"isFromEmptySelection\":true") != json.npos || json.find("\"isFromEmptySelection\": true") != json.npos) { diagnosticEvent(L"Copy rejected: editor reported empty selection"); return {}; } } } selectionProof.reset(); provenSelection.clear(); // Viber and some other editors omit trailing ASCII spaces from Ctrl+C. // Recover only from a real selection, never from the recorded keys alone. if (valid() && !text.empty() && text.size() < 8192 && text.back() != L' ') { auto proof = std::make_unique(true, target); auto selected = proof->read(1, false); if (selected && selected->size() > text.size() && selected->starts_with(text) && selected->find_first_not_of(L' ', text.size()) == std::wstring::npos && valid()) { diagnosticEvent(L"Copy: trailing spaces verified via accessibility copied=" + diagnosticText(text) + L" selected=" + diagnosticText(*selected)); text = *selected; provenSelection = text; selectionProof = std::move(proof); } } return text; } bool recoveredSelection() const { return selectionProof != nullptr; } bool select(size_t n) { if (farSnippet) { if (n > 4096 || farSelectedSteps) return false; for (size_t i = 0; i < n; ++i) { if (!valid() || !chord(VK_LEFT, MOD_ALT | MOD_SHIFT)) return false; ++farSelectedSteps; } return valid(); } return selectPrevious(n, [&] { return valid(); }); } bool finishSelection(bool atStart) { if (!farSnippet) return valid() && chord(atStart ? VK_LEFT : VK_RIGHT); if (!farSelectedSteps) return valid(); if (atStart) { // Far arrows move one character, rather than collapsing a selection // to its edge. Move into the nonempty selected range and back. if (!valid() || !chord(VK_RIGHT)) return false; farSelectedSteps = 0; return valid() && chord(VK_LEFT); } while (farSelectedSteps && valid()) { if (!chord(VK_RIGHT, MOD_ALT | MOD_SHIFT)) return false; --farSelectedSteps; } return !farSelectedSteps && valid(); } std::optional readFarSnippet(size_t count) { if (!farSnippet || !count || !select(count)) return {}; auto observed = copy(); if (!finishSelection(false) || observed.empty()) return {}; return observed; } std::wstring copyRange(size_t count, const std::wstring &wordCandidate, const std::function &matches, const std::function &suffixMatches = {}) { // WebView word boundaries are only candidates; exact verification is mandatory. diagnosticEvent(L"Selection begin steps=" + std::to_wstring(count) + L" expected=" + diagnosticText(wordCandidate)); GUITHREADINFO gui{sizeof(gui)}; if (GetGUIThreadInfo(GetWindowThreadProcessId(target, nullptr), &gui)) { wchar_t cls[128]{}; GetClassNameW(gui.hwndFocus, cls, 128); std::wstring offsets = L"unavailable"; if (_wcsicmp(cls, L"Edit") == 0) { DWORD_PTR range{}; if (SendMessageTimeoutW(gui.hwndFocus, EM_GETSEL, 0, 0, SMTO_ABORTIFHUNG | SMTO_BLOCK, 25, &range) && (DWORD)range != 0xffffffff) offsets = std::to_wstring(LOWORD(range)) + L"," + std::to_wstring(HIWORD(range)); } diagnosticEvent(L"Selection editor class=" + std::wstring(cls) + L" focus=" + std::to_wstring((uintptr_t)gui.hwndFocus) + L" caretWindow=" + std::to_wstring((uintptr_t)gui.hwndCaret) + L" caretRect=" + std::to_wstring(gui.rcCaret.left) + L"," + std::to_wstring(gui.rcCaret.top) + L"," + std::to_wstring(gui.rcCaret.right) + L"," + std::to_wstring(gui.rcCaret.bottom) + L" layout=" + std::to_wstring((uintptr_t)windowLayout(target)) + L" selectionOffsets=" + offsets); } else diagnosticEvent(L"Selection editor: focus/caret/selection unavailable"); if (needsSelectionDelay(target) && !wordCandidate.empty() && std::all_of(wordCandidate.begin(), wordCandidate.end(), [](wchar_t c) { return letter(c) || iswdigit(c); })) { if (!valid() || !chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT)) return {}; Sleep(25); auto text = copy(); diagnosticEvent(L"Selection word-boundary match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text)); if (valid() && matches(text)) { diagnosticEvent(L"Selection: verified word boundary"); return text; } if (!valid() || !chord(VK_RIGHT)) return {}; Sleep(25); } auto text = select(count) ? copy() : std::wstring{}; diagnosticEvent(L"Selection initial match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text)); // Chromium editors can apply only part of a burst of Shift+Left events. // Extend only a verified suffix; never replace a partial or unrelated range. if (valid() && !matches(text) && !text.empty() && (suffixMatches ? suffixMatches(text) : newlines(wordCandidate).ends_with(newlines(text))) && caretSteps(text) < count) { const auto missing = count - caretSteps(text); diagnosticEvent(L"Selection: extending verified suffix by " + std::to_wstring(missing)); for (size_t i = 0; i < missing; ++i) { if (!valid() || !chord(VK_LEFT, MOD_SHIFT)) return {}; diagnosticEvent(L"Selection recovery Shift+Left sent=" + std::to_wstring(i + 1) + L" total=" + std::to_wstring(missing)); Sleep(25); } text = copy(); diagnosticEvent(L"Selection: suffix recovery match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text)); } return text; } bool paste(const Clip &clip) { if (selectionProof) { auto selected = selectionProof->read(1, false); if (!selected || *selected != provenSelection || !valid()) { diagnosticEvent(L"Paste cancelled: recovered selection changed or unavailable"); return false; } } if (!valid()) return false; write(clip); // Our verified payload is also trusted across an identical relay. The // sentinel written by copy() deliberately does not get this allowance. if (clip.formats.size() == 1 && clip.formats.front().id == CF_UNICODETEXT) { adopted = clip.text(); adoptedFormats = selectionClipboardIdentity(clip); trustedContentsObserved = true; } // Viber's Qt editor observes clipboard changes asynchronously. Give it // a chance to discard the data object from Ctrl+C before requesting paste. // Recheck ownership and input below; never retry a possibly applied paste. if (lower(processName(target)) == L"viber") { Sleep(75); diagnosticEvent(L"Paste: Viber clipboard notification settling interval completed"); } for (int i = 0; i < 2; ++i) { check(); if (!valid()) return false; } if (!chord(farSnippet ? VK_INSERT : L'V', farSnippet ? MOD_SHIFT : MOD_CONTROL)) throw std::runtime_error("Windows blocked simulated input; check application privilege levels"); // The selected trigger was replaced. Do not restore its old caret range // if clipboard verification below subsequently rejects a concurrent copy. if (farSnippet) farSelectedSteps = 0; Sleep(300); // A clipboard synchronizer may replace our payload after Ctrl+V is // queued but before the editor consumes it. Do not run snippet actions // (especially Enter) after such a race. check(); return true; } bool nativeSelectionIs(const std::wstring &original) const { if (!valid()) return false; GUITHREADINFO gui{sizeof(gui)}; if (!GetGUIThreadInfo(GetWindowThreadProcessId(target, nullptr), &gui) || !gui.hwndFocus) return false; wchar_t cls[32]{}; GetClassNameW(gui.hwndFocus, cls, 32); if (_wcsicmp(cls, L"Edit") != 0 || (GetWindowLongPtrW(gui.hwndFocus, GWL_STYLE) & (ES_PASSWORD | ES_READONLY))) return false; auto query = [&](UINT message, WPARAM wp, LPARAM lp, DWORD_PTR &result) { return SendMessageTimeoutW(gui.hwndFocus, message, wp, lp, SMTO_ABORTIFHUNG | SMTO_BLOCK, 100, &result) != 0; }; DWORD_PTR range{}, length{}, copied{}; if (!query(EM_GETSEL, 0, 0, range) || (DWORD)range == 0xffffffff || size_t(HIWORD(range) - LOWORD(range)) != original.size() || !query(WM_GETTEXTLENGTH, 0, 0, length) || length > 65534 || HIWORD(range) > length) return false; std::wstring content(length + 1, L'\0'); if (!query(WM_GETTEXT, content.size(), (LPARAM)content.data(), copied) || copied != length) return false; DWORD_PTR after{}; GUITHREADINFO focused{sizeof(focused)}; return query(EM_GETSEL, 0, 0, after) && after == range && valid() && GetGUIThreadInfo(GetWindowThreadProcessId(target, nullptr), &focused) && focused.hwndFocus == gui.hwndFocus && content.substr(LOWORD(range), original.size()) == original; } }; } // namespace App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot, HWND service) : historyService(service), instance(h), smoke(test) { updateNotifier = notifyUpdateAvailable; current = this; data = dataRoot.empty() ? dataDirectory() : dataRoot; std::filesystem::create_directories(data); layouts = readLayouts(); try { settings = loadSettings(data / L"settings.json", layouts); } catch (const std::exception &e) { settings = loadSettings(data / L"missing-defaults.json", layouts); registrationWarning = L"Settings were not loaded: " + wide(e.what()); } setLoggingEnabled(settings.writeLog); if (settings.autoCorrect && !historyService) try { autoPredictor.configure(std::filesystem::path(executable()).parent_path() / L"dictionaries", settings.autoLanguages); } catch (const std::exception &e) { settings.autoCorrect = false; registrationWarning += L"\nAutomatic correction unavailable: " + wide(e.what()); } try { history = std::make_unique(data / L"history"); } catch (const std::exception &e) { registrationWarning += L"\nHistory is unavailable: " + wide(e.what()); settings.history = false; } } App::~App() { lanService.reset(); stopBrowserChatMonitor(); 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()) updateWorker.join(); if (keyboardHook) UnhookWindowsHookEx(keyboardHook); if (mouseHook) UnhookWindowsHookEx(mouseHook); { std::lock_guard lock(jobsMutex); stop = true; jobs.clear(); pendingHistorySearch = {}; } jobsReady.notify_all(); historySearchReady.notify_all(); historyOcrReady.notify_all(); if (historyOcrWorker.joinable()) historyOcrWorker.join(); if (historySearchWorker.joinable()) historySearchWorker.join(); if (worker.joinable()) worker.join(); RemoveClipboardFormatListener(window); NOTIFYICONDATAW icon{sizeof(icon)}; icon.hWnd = window; icon.uID = 1; Shell_NotifyIconW(NIM_DELETE, &icon); SetThreadExecutionState(ES_CONTINUOUS); current = nullptr; } void App::enqueue(std::function job) { { std::lock_guard lock(jobsMutex); if (stop) return; jobs.push_back(std::move(job)); } jobsReady.notify_one(); } void App::enqueueHistorySearch(std::function job) { { std::lock_guard lock(jobsMutex); if (stop) return; // Only the latest query matters. Clipboard capture and OCR must not delay it. pendingHistorySearch = std::move(job); if (!historySearchWorker.joinable()) historySearchWorker = std::thread([this] { for (;;) { std::function searchJob; { std::unique_lock lock(jobsMutex); historySearchReady.wait(lock, [&] { return stop || bool(pendingHistorySearch); }); if (stop) return; searchJob = std::move(pendingHistorySearch); pendingHistorySearch = {}; } try { searchJob(); } catch (const std::exception &e) { report(wide(e.what())); } } }); } historySearchReady.notify_one(); } void App::scheduleHistoryOcr(const Settings &config) { { std::lock_guard lock(jobsMutex); if (stop || !history || !config.history) return; historyOcrSettings = config; historyOcrRequested = true; if (!historyOcrWorker.joinable()) historyOcrWorker = std::thread([this] { for (;;) { Settings config; { std::unique_lock lock(jobsMutex); historyOcrReady.wait(lock, [&] { return stop || historyOcrRequested; }); if (stop) return; historyOcrRequested = false; config = historyOcrSettings; } for (const auto &entry : history->entries()) { { std::lock_guard lock(jobsMutex); if (stop) return; } auto profile = ocrProfile(config); if (!entry.image || (entry.ocrState != 1 && entry.ocrProfile == profile)) continue; try { auto text = history->recognizeImage(entry.id, config, [this, &profile] { std::lock_guard lock(jobsMutex); return stop || ocrProfile(historyOcrSettings) != profile; }); std::lock_guard lock(jobsMutex); if (stop || ocrProfile(historyOcrSettings) != profile) continue; if (history->saveOcr(entry.id, text, config)) PostMessageW(window, HistoryMessage, 0, 0); } catch (const std::exception &e) { // Automatic OCR never changes the clipboard or opens error dialogs. log(L"History OCR: " + wide(e.what())); try { std::lock_guard lock(jobsMutex); if (!stop && ocrProfile(historyOcrSettings) == profile) history->saveOcr(entry.id, std::nullopt, config); } catch (const std::exception &saveError) { log(L"History OCR save: " + wide(saveError.what())); } } } } }); } historyOcrReady.notify_one(); } void App::report(const std::wstring &text) { log(text); PostMessageW(window, ErrorMessage, 0, (LPARAM) new std::wstring(text)); } void App::addTray() { NOTIFYICONDATAW icon{sizeof(icon)}; icon.hWnd = window; icon.uID = 1; icon.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; icon.uCallbackMessage = TrayMessage; icon.hIcon = (HICON)LoadImageW(instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_SHARED); wcscpy_s(icon.szTip, localBuildTitle(L"KSwitcher — keyboard layouts & clipboard").c_str()); Shell_NotifyIconW(NIM_ADD, &icon); icon.uVersion = NOTIFYICON_VERSION_4; Shell_NotifyIconW(NIM_SETVERSION, &icon); } bool App::registerHotkeys(const Settings &s, bool rollback) { std::vector> bindings; for (auto &[id, text] : shortcuts(s)) { auto key = Hotkey::parse(text); if (key && key->key) bindings.emplace_back(id, *key); } if (!bindings.empty() && !keyboardHook) { if (!rollback) registrationWarning = L"Keyboard interception could not start."; return false; } hookBindings = std::move(bindings); return true; } Bytes App::lanCommand(const Bytes &command) { if (!lanService) lanService = std::make_unique(data,window); auto reply = lanService->command(command); if (lanService->inputEnabled()) SetTimer(window, 7, 10, nullptr); else KillTimer(window, 7); if (lanService->takeInputChange()) resetLanInput(); return reply; } void App::applySettings(const Settings &requested) { Settings s = requested; std::set present, removed(s.snippetTombstones.begin(), s.snippetTombstones.end()); removed.insert(settings.snippetTombstones.begin(), settings.snippetTombstones.end()); for (auto &snippet : s.snippets) { snippet.id = snippetId(snippet); present.insert(snippet.id); } for (const auto &snippet : settings.snippets) if (!present.contains(snippetId(snippet))) removed.insert(snippetId(snippet)); for (const auto &id : present) removed.erase(id); s.snippetTombstones.assign(removed.begin(), removed.end()); bool autostartChanged = false; AutoCorrector predictor; validateSettings(s); validateTesseract(s); if (s.autoCorrect) { predictor.configure(std::filesystem::path(executable()).parent_path() / L"dictionaries", s.autoLanguages); } if (!registerHotkeys(s)) { registerHotkeys(settings, true); throw std::runtime_error(utf8(registrationWarning)); } try { if (s.autostart != settings.autostart || s.elevatedAutostart != settings.elevatedAutostart) { setAutostart(s.autostart, s.elevatedAutostart); autostartChanged = true; } saveSettings(data / L"settings.json", s); } catch (...) { try { if (autostartChanged) setAutostart(settings.autostart, settings.elevatedAutostart); } catch (...) { } registerHotkeys(settings, true); throw; } { std::lock_guard lock(stateMutex); settings = s; autoPredictor = std::move(predictor); setLoggingEnabled(s.writeLog); run.reset(); last.reset(); ++inputVersion; if (busy) diagnosticEvent(L"Input context changed: settings applied version=" + std::to_wstring(inputVersion.load())); } // Release the worker's old snapshot; the next check shares the freshly loaded predictor. enqueue([this] { autoCorrector.configure({}, {}); }); scheduleHistoryOcr(s); nextSync = std::max(GetTickCount64() + 2000, syncRetryAfter); SetThreadExecutionState(ES_CONTINUOUS | (settings.awake ? (ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED) : 0)); } void App::reloadLayouts() { auto latest = readLayouts(); Settings s = settings; std::vector direct(latest.size()); for (size_t i = 0; i < latest.size(); ++i) for (size_t j = 0; j < layouts.size(); ++j) if (latest[i].id == layouts[j].id && j < s.direct.size()) direct[i] = s.direct[j]; s.direct = direct; s.directIds.clear(); for (auto &layout : latest) s.directIds.push_back(layout.id); { std::lock_guard lock(stateMutex); layouts = std::move(latest); settings = s; run.reset(); last.reset(); ++inputVersion; if (busy) diagnosticEvent(L"Input context changed: layouts reloaded version=" + std::to_wstring(inputVersion.load())); } registerHotkeys(settings); } 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); } void App::setHotkeyCapture(HWND field) { hotkeyCapture = field; hotkeyCaptureProcess = 0; if (field) GetWindowThreadProcessId(field, &hotkeyCaptureProcess); } bool App::isHotkey(DWORD key, UINT mods) { for (auto &[id, text] : shortcuts(settings)) { auto h = Hotkey::parse(text); if (h && h->key == key && h->modifiers == mods) return true; } 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 = {}; pendingMouseClicks = {}; mouseActions.clear(); resetInput(); } void App::resetInput() { std::lock_guard lock(stateMutex); ++inputVersion; if (busy) diagnosticEvent(L"Input context changed: reset version=" + std::to_wstring(inputVersion.load())); run.reset(); last.reset(); } void App::releaseIdleMemory(ULONGLONG now) { expireDiagnostics(now); std::lock_guard lock(stateMutex); // Match the Undo eligibility deadline; never discard an available operation. if (!busy && last && now - last->at > 120000) last.reset(); } void App::clearLog(bool previous) { clearDiagnosticLog(previous); } void App::keyboard(const KBDLLHOOKSTRUCT &k, UINT mods) { if (ownKeyboardInput(k)) return; DWORD vk = k.vkCode; if (vk == VK_SHIFT || vk == VK_CONTROL || vk == VK_MENU || vk == VK_LSHIFT || vk == VK_RSHIFT || vk == VK_LCONTROL || vk == VK_RCONTROL || vk == VK_LMENU || vk == VK_RMENU || vk == VK_LWIN || vk == VK_RWIN) return; std::lock_guard lock(stateMutex); if (isHotkey(vk, mods)) return; ++inputVersion; if (busy) diagnosticEvent(L"Input context changed: keyboard vk=" + std::to_wstring(vk) + L" modifiers=" + std::to_wstring(mods) + L" injected=" + std::to_wstring((k.flags & LLKHF_INJECTED) != 0) + L" version=" + std::to_wstring(inputVersion.load())); last.reset(); HWND fg = GetForegroundWindow(); HKL layout = windowLayout(fg); if (mods & (MOD_CONTROL | MOD_ALT | MOD_WIN)) { run.reset(); return; } if (vk == VK_BACK) { run.backspace(fg, GetTickCount64()); return; } bool shift = (mods & MOD_SHIFT) != 0; // VK_PACKET carries UTF-16 in scanCode, not a physical key position. DWORD scan = vk == VK_PACKET ? 0 : k.scanCode; if (!scan && vk != VK_PACKET) scan = MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, layout); wchar_t ch = vk == VK_SPACE ? L' ' : physicalKey(scan, shift); if (ch && letter(ch) && (GetKeyState(VK_CAPITAL) & 1)) ch = shift ? lower(std::wstring(1, ch))[0] : upper(std::wstring(1, ch))[0]; if (!ch) { run.reset(); return; } run.record(ch, fg, layout, GetTickCount64()); if (settings.autoCorrect && !snippetTyping && k.dwExtraInfo != snippetReplayMarker() && mods == 0 && ch == L' ' && run.keys.size() >= (size_t)settings.autoMinLength + 1 && run.keys.size() <= 41 && !busy && !run.overflow) { for (int source = 0; source < (int)layouts.size(); ++source) if (layouts[source].handle == layout) { std::wstring predicted; for (wchar_t key : run.keys) predicted += key == L' ' ? L' ' : layouts[source].at(physicalLayout().code(key)); if (autoPredictor.suggest(predicted, source, layouts, settings)) { // Reserve the transaction before the next physical key arrives. // The delimiter itself passes through; subsequent input is queued. typingTarget = fg; autoPending = true; busy = true; if (!PostMessageW(window, AutoCorrectMessage, (WPARAM)inputVersion.load(), 0)) { typingTarget = nullptr; autoPending = false; busy = false; } } break; } } if (busy || settings.snippets.empty()) return; auto keys = lower(run.keys); auto snippetProcess = processName(fg); for (int priority : {2, 1}) for (size_t i = 0; i < settings.snippets.size(); ++i) { auto &sn = settings.snippets[i]; if (snippetAppPriority(sn, snippetProcess) != priority) continue; std::vector variants{lower(sn.trigger)}; for (auto &l : layouts) { bool valid = true; std::wstring normalized; for (wchar_t c : sn.trigger) { auto p = physicalLayout().at(l.code(c)); if (!p) { valid = false; break; } normalized += p; } if (valid) variants.push_back(lower(normalized)); } for (auto &trigger : variants) if (keys.size() >= trigger.size() && keys.ends_with(trigger)) { // Keep subsequent typing out of the selection/paste transaction. typingTarget = fg; snippetPending = (int)i; snippetTyping = true; busy = true; if (!PostMessageW(window, SnippetMessage, (WPARAM)i, (LPARAM)inputVersion.load())) { typingTarget = nullptr; snippetPending = -1; snippetTyping = false; busy = false; } return; } } } void App::refreshChatContext() { HWND target = GetForegroundWindow(); chatWindow = target; activeChatTitle = chatTitle(target); activeChat = windowLanguageProfileKey(settings, target); } void App::switchChatLayout(bool reverse) { HWND target = chatWindow; auto key = activeChat; if (!target || target != GetForegroundWindow() || key.empty() || windowLanguageProfileKey(settings, target) != key || busy.exchange(true)) return; auto cycle = chatCycle(settings, key, layouts); auto available = layouts; resetInput(); enqueue([this, target, key, cycle, available, reverse, profileSettings = settings] { struct Reset { std::atomic &busy; ~Reset() { busy = false; } } reset{busy}; auto valid = [&] { return GetForegroundWindow() == target && windowLanguageProfileKey(profileSettings, target) == key; }; if (key.starts_with(L"telegram-web\n") && readChromeChat(target) != key) return; if (!valid()) return; auto next = nextChatLayout(cycle, available, windowLayout(target), reverse); if (next) switchWindowLayout(target, next, valid); }); } bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods) { // Filter before capture, hotkey latching and typing history. External injected // down AND up events use exactly the same path as physical keyboard input. if (ownKeyboardInput(key) || installingUpdate) return false; { std::lock_guard lock(stateMutex); if (typingTarget && GetForegroundWindow() == typingTarget) { INPUT input{}; input.type = INPUT_KEYBOARD; input.ki.wVk = static_cast(key.vkCode); input.ki.wScan = static_cast(key.scanCode); input.ki.dwFlags = (down ? 0 : KEYEVENTF_KEYUP) | ((key.flags & LLKHF_EXTENDED) ? KEYEVENTF_EXTENDEDKEY : 0); if (key.vkCode == VK_PACKET) { input.ki.wVk = 0; input.ki.dwFlags |= KEYEVENTF_UNICODE; } // Replay as ordinary input so tracking and shortcuts see it once, // after the replacement and layout switch have finished. deferredTyping.push_back(input); return true; } } DWORD captureProcess{}; if (hotkeyCapture) GetWindowThreadProcessId(hotkeyCapture, &captureProcess); if (hotkeyCapture && (!captureProcess || captureProcess != hotkeyCaptureProcess)) setHotkeyCapture(nullptr); if (hotkeyCapture && GetForegroundWindow() == GetAncestor(hotkeyCapture, GA_ROOT)) { bool modifier = key.vkCode == VK_CONTROL || key.vkCode == VK_SHIFT || key.vkCode == VK_MENU || key.vkCode == VK_LCONTROL || key.vkCode == VK_RCONTROL || key.vkCode == VK_LSHIFT || key.vkCode == VK_RSHIFT || key.vkCode == VK_LMENU || key.vkCode == VK_RMENU || key.vkCode == VK_LWIN || key.vkCode == VK_RWIN; if (!modifier) { if (!down) hotkeyHook.update(key.vkCode, false, 0, {}); if (down) { if (mods & MOD_WIN) chord(0xE8); PostMessageW(hotkeyCapture, CaptureHotkeyMessage, key.vkCode, mods); } return 1; // Record the chord without executing a global shortcut or typing into an editor. } } bool chatEnabled = false; if (down && ChatSwitchHook::candidate(key.vkCode, mods) && (!settings.chatCycles.empty() || !settings.appLanguageSets.empty()) && !isHotkey(key.vkCode, mods)) { refreshChatContext(); chatEnabled = !activeChat.empty(); } const int chatAction = chatSwitchHook.update(key.vkCode, down, mods, chatEnabled); if (chatAction) { if (chatAction > 0) { chord(0xE8); // Suppress the shell's bare Alt/Win action. switchChatLayout(chatAction == 2); // Captures this chat before another keyboard event. } return true; } if (down) { auto action = hotkeyHook.update(key.vkCode, true, mods, hookBindings); if (action > 0) { // Tell the shell that Win participated in a chord, preventing Start // from opening on Win-up after we consumed V. 0xE8 is unassigned. if (mods & (MOD_WIN | MOD_ALT)) chord(0xE8); PostMessageW(window, WM_HOTKEY, action, 0); } if (action != 0) return 1; keyboard(key, mods); } else if (hotkeyHook.update(key.vkCode, false, 0, hookBindings) != 0) return true; return false; } bool App::beginTypingDeferral(HWND target, uint64_t version, bool snippet) { std::lock_guard lock(stateMutex); if (inputVersion != version || GetForegroundWindow() != target || !modifiersReleased()) return false; typingTarget = target; snippetTyping = snippet; return true; } void App::replayTyping() { std::vector inputs; HWND target; { std::lock_guard lock(stateMutex); target = typingTarget; typingTarget = nullptr; autoPending = false; snippetPending = -1; inputs.swap(deferredTyping); if (snippetTyping) for (auto &input : inputs) input.ki.dwExtraInfo = snippetReplayMarker(); snippetTyping = false; } busy = false; if (inputs.empty()) return; if (GetForegroundWindow() != target) { resetInput(); report(L"Typing during correction was not replayed because the active window changed."); return; } // Runs on the hook thread: no physical key can pass between releasing the // gate and submitting the entire ordered batch to the Windows input queue. if (SendInput(static_cast(inputs.size()), inputs.data(), sizeof(INPUT)) != inputs.size()) { resetInput(); report(L"Windows could not replay all typing buffered during correction."); } } LRESULT CALLBACK App::keyProc(int code, WPARAM wp, LPARAM lp) { if (code == HC_ACTION && current && current->lanService && (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->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 // downstream hooks route the event before tracking it or starting a snippet. // Call the chain exactly once; a local event can still be consumed below. const auto routed = CallNextHookEx(nullptr, code, wp, lp); return keyboardAfterHooks(code, wp, lp, routed); } LRESULT App::keyboardAfterHooks(int code, WPARAM wp, LPARAM lp, LRESULT routed) { if (routed) return routed; if (code == HC_ACTION && current && (wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN || wp == WM_KEYUP || wp == WM_SYSKEYUP)) { // The current event has not updated asynchronous state yet; the preceding // modifier events (including MWB input) have. Own events must still reach Windows. UINT mods = 0; if (GetAsyncKeyState(VK_CONTROL) & 0x8000) mods |= MOD_CONTROL; if (GetAsyncKeyState(VK_MENU) & 0x8000) mods |= MOD_ALT; if (GetAsyncKeyState(VK_SHIFT) & 0x8000) mods |= MOD_SHIFT; if ((GetAsyncKeyState(VK_LWIN) | GetAsyncKeyState(VK_RWIN)) & 0x8000) mods |= MOD_WIN; if (current->handleKeyboard(*(KBDLLHOOKSTRUCT *)lp, wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN, mods)) return 1; } return routed; } constexpr UINT MouseActionMessage = WM_APP + 79; constexpr UINT MouseVolumeReleasedMessage = WM_APP + 80; constexpr UINT_PTR MouseClickTimer = 9; constexpr UINT_PTR FirstMouseVolumeTimer = 10; namespace { constexpr wchar_t VolumeIndicatorClass[] = L"KSwitcher.VolumeIndicator"; constexpr UINT_PTR VolumeIndicatorTimer = 1; constexpr WORD VolumeIndicatorMuted = 1, VolumeIndicatorHeld = 2; void paintVolumeIndicator(HWND w) { PAINTSTRUCT paint{}; const auto dc = BeginPaint(w, &paint); RECT bounds{}; GetClientRect(w, &bounds); FillRect(dc, &bounds, GetSysColorBrush(COLOR_WINDOW)); FrameRect(dc, &bounds, GetSysColorBrush(COLOR_WINDOWFRAME)); const int dpi = static_cast(GetDpiForWindow(w)); const int padding = MulDiv(8, dpi, 96); const int captionHeight = MulDiv(26, dpi, 96); const auto state = GetWindowLongPtrW(w, GWLP_USERDATA); const int percent = LOWORD(state); const bool muted = (HIWORD(state) & VolumeIndicatorMuted) != 0; RECT track{padding, padding, bounds.right - padding, bounds.bottom - captionHeight}; FillRect(dc, &track, GetSysColorBrush(COLOR_3DFACE)); RECT fill = track; fill.top = fill.bottom - MulDiv(fill.bottom - fill.top, percent, 100); FillRect(dc, &fill, GetSysColorBrush(muted ? COLOR_GRAYTEXT : COLOR_HIGHLIGHT)); RECT caption{0, track.bottom, bounds.right, bounds.bottom}; const auto text = muted ? std::wstring(L"Mute") : std::to_wstring(percent) + L"%"; const auto font = CreateFontW(-MulDiv(12, dpi, 96), 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, 0, 0, 0, 0, L"Segoe UI"); const auto previous = font ? SelectObject(dc, font) : nullptr; SetBkMode(dc, TRANSPARENT); SetTextColor(dc, GetSysColor(COLOR_WINDOWTEXT)); DrawTextW(dc, text.c_str(), -1, &caption, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); if (previous) SelectObject(dc, previous); if (font) DeleteObject(font); EndPaint(w, &paint); } LRESULT CALLBACK volumeIndicatorProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_PAINT: paintVolumeIndicator(w); return 0; case WM_NCHITTEST: return HTTRANSPARENT; case WM_MOUSEACTIVATE: return MA_NOACTIVATE; case WM_TIMER: if (wp == VolumeIndicatorTimer) { KillTimer(w, VolumeIndicatorTimer); if (!(HIWORD(GetWindowLongPtrW(w, GWLP_USERDATA)) & VolumeIndicatorHeld)) ShowWindow(w, SW_HIDE); return 0; } break; } return DefWindowProcW(w, msg, wp, lp); } void setVolumeIndicatorHold(HWND indicator, bool held) { const auto state = GetWindowLongPtrW(indicator, GWLP_USERDATA); const WORD flags = (HIWORD(state) & VolumeIndicatorMuted) | (held ? VolumeIndicatorHeld : 0); SetWindowLongPtrW(indicator, GWLP_USERDATA, MAKELPARAM(LOWORD(state), flags)); if (held) KillTimer(indicator, VolumeIndicatorTimer); else SetTimer(indicator, VolumeIndicatorTimer, 1000, nullptr); } void showVolumeIndicator(HWND owner, HINSTANCE instance, POINT point, SystemVolume volume, bool held) { MONITORINFO monitor{sizeof(monitor)}; const auto targetMonitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST); if (!GetMonitorInfoW(targetMonitor, &monitor)) return; const auto &area = monitor.rcWork; auto indicator = reinterpret_cast(GetPropW(owner, VolumeIndicatorClass)); if (!indicator) { WNDCLASSW type{}; type.lpfnWndProc = volumeIndicatorProc; type.hInstance = instance; type.lpszClassName = VolumeIndicatorClass; RegisterClassW(&type); indicator = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_LAYERED | WS_EX_TRANSPARENT, VolumeIndicatorClass, L"Volume", WS_POPUP, area.left, area.top, 1, 1, owner, nullptr, instance, nullptr); if (!indicator) return; if (!SetPropW(owner, VolumeIndicatorClass, indicator) || !SetLayeredWindowAttributes(indicator, 0, 255, LWA_ALPHA)) { RemovePropW(owner, VolumeIndicatorClass); DestroyWindow(indicator); return; } } // Place the window on the destination monitor before querying its DPI. if (MonitorFromWindow(indicator, MONITOR_DEFAULTTONEAREST) != targetMonitor) SetWindowPos(indicator, nullptr, area.left, area.top, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW); const int dpi = static_cast(GetDpiForWindow(indicator)); const int width = MulDiv(48, dpi, 96); const int height = MulDiv(132, dpi, 96); const int margin = MulDiv(16, dpi, 96); SetWindowLongPtrW(indicator, GWLP_USERDATA, MAKELPARAM(volume.percent, volume.muted ? VolumeIndicatorMuted : 0)); setVolumeIndicatorHold(indicator, held); SetWindowPos(indicator, HWND_TOPMOST, area.right - width - margin, area.bottom - height - margin, width, height, SWP_NOACTIVATE | SWP_SHOWWINDOW); InvalidateRect(indicator, nullptr, FALSE); } } void App::queueMouseAction(MousePress press) { if (press.action.type == MouseActionType::Standard) return; mouseActions.push_back(std::move(press)); PostMessageW(window, MouseActionMessage, 0, 0); } void App::flushMouseClicks() { ULONGLONG next = 0; const auto now = GetTickCount64(); for (auto &press : pendingMouseClicks) { if (!press.target) continue; const auto deadline = press.started + GetDoubleClickTime(); if (now >= deadline) { queueMouseAction(std::exchange(press, {})); } else if (!next || deadline < next) next = deadline; } if (next) SetTimer(window, MouseClickTimer, static_cast(next - now), nullptr); else KillTimer(window, MouseClickTimer); } void App::beginMouseVolume(int button) { const auto &press = mousePresses[button]; if (!press.waitForSecond) queueMouseAction(press); const UINT delay = press.waitForSecond ? std::max(MouseVolumeHoldDelay, GetDoubleClickTime()) : MouseVolumeHoldDelay; SetTimer(window, FirstMouseVolumeTimer + button, delay, nullptr); } void App::repeatMouseVolume(int button) { const auto timer = FirstMouseVolumeTimer + button; auto &press = mousePresses[button]; if (!press.held || !isMouseVolumeStep(press.action.type) || busy || suspended || installingUpdate || hotkeyCapture || !modifiersReleased() || !IsWindow(press.target) || (!press.action.applications.empty() && press.target != GetForegroundWindow())) { KillTimer(window, timer); return; } const auto elapsed = GetTickCount64() - press.started; const UINT delay = press.waitForSecond ? std::max(MouseVolumeHoldDelay, GetDoubleClickTime()) : MouseVolumeHoldDelay; if (elapsed < delay) return; press.waitForSecond = false; queueMouseAction(press); SetTimer(window, timer, mouseVolumeRepeatInterval(elapsed), nullptr); } bool App::volumeButtonHeld() const { return std::any_of(mousePresses.begin(), mousePresses.end(), [](const MousePress &press) { return press.held && !press.waitForSecond && isMouseVolumeStep(press.action.type); }); } void App::dispatchMouseActions() { std::deque actions; actions.swap(mouseActions); if (busy || suspended || installingUpdate || hotkeyCapture || !modifiersReleased()) { if (!actions.empty()) log(L"Mouse action skipped while input was unavailable."); return; } for (const auto &press : actions) { if (!IsWindow(press.target)) continue; const auto &action = press.action; switch (action.type) { case MouseActionType::Standard: break; case MouseActionType::Shortcut: if (press.target != GetForegroundWindow()) log(L"Mouse shortcut skipped because the active window changed."); else if (!chord(static_cast(action.shortcut.key), action.shortcut.modifiers)) log(L"Windows did not accept all input for the mouse shortcut."); else log(L"Windows accepted input for the mouse shortcut."); break; case MouseActionType::Back: case MouseActionType::Forward: PostMessageW(press.target, WM_APPCOMMAND, reinterpret_cast(press.target), MAKELPARAM(0, FAPPCOMMAND_MOUSE | (action.type == MouseActionType::Back ? APPCOMMAND_BROWSER_BACKWARD : APPCOMMAND_BROWSER_FORWARD))); break; case MouseActionType::VolumeUp: case MouseActionType::VolumeDown: case MouseActionType::Mute: if (const auto volume = changeSystemVolume(action.type, action.volumeStep)) showVolumeIndicator(window, instance, press.point, *volume, volumeButtonHeld()); else log(L"Mouse action could not change or read system volume."); break; case MouseActionType::Launch: if (!launchThroughExplorer(action.executable)) log(L"Mouse action could not launch " + action.executable); else log(L"Mouse action launched " + std::filesystem::path(action.executable).filename().wstring()); break; } } } LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp) { if (code == HC_ACTION && current) { auto &app = *current; const auto &mouse = *reinterpret_cast(lp); const int button = mouse.flags & LLMHF_INJECTED ? -1 : mouseButton(UINT(wp), HIWORD(mouse.mouseData)); const bool down = mouseButtonDown(UINT(wp)); if (button >= 0 && app.mousePresses[button].held) { auto &active = app.mousePresses[button]; const bool consumed = active.action.type != MouseActionType::Standard; if (!down) { auto press = std::exchange(active, {}); if (isMouseVolumeStep(press.action.type)) { KillTimer(app.window, FirstMouseVolumeTimer + button); PostMessageW(app.window, MouseVolumeReleasedMessage, 0, 0); } if (press.waitForSecond) { app.pendingMouseClicks[button] = std::move(press); app.flushMouseClicks(); } else if (!isMouseVolumeStep(press.action.type)) app.queueMouseAction(std::move(press)); } if (consumed) return 1; } if (app.lanService) { const bool mouseActionHeld = std::any_of(app.mousePresses.begin(), app.mousePresses.end(), [](const MousePress &press) { return press.held; }); bool consumed = app.lanService->mouse(UINT(wp), mouse, !app.busy && !app.suspended && !app.hotkeyCapture && !mouseActionHeld); if (app.lanService->takeInputChange()) app.resetLanInput(); if (consumed) return 1; } if (button >= 0 && down && !app.settings.mouseBindings.empty() && !app.busy && !app.suspended && !app.installingUpdate && !app.hotkeyCapture && modifiersReleased()) { const auto hit = WindowFromPoint(mouse.pt); const auto target = GetAncestor(hit, GA_ROOT); const auto name = target ? mouseApplicationName(target) : L""; wchar_t windowClass[64]{}; if (target && name == L"explorer") GetClassNameW(target, windowClass, std::size(windowClass)); const bool ownWindow = name == L"kswitcher" || name == L"kswitcher.config"; const bool shellSurface = target == GetShellWindow() || wcscmp(windowClass, L"Progman") == 0 || wcscmp(windowClass, L"WorkerW") == 0 || wcscmp(windowClass, L"Shell_TrayWnd") == 0 || wcscmp(windowClass, L"Shell_SecondaryTrayWnd") == 0; const auto *global = mouseBinding(app.settings, GlobalMouseProfile, button); const bool globalLaunch = button >= 2 && global && global->type == MouseActionType::Launch && mouseActionApplies(*global, name); const bool restricted = ownWindow || shellSurface || name.empty(); const bool testButton = name == L"kswitcher.config" && hit == GetDlgItem(target, 737); const auto *fallback = restricted ? effectiveMouseAction(app.settings, L"", button, false) : nullptr; const auto *restrictedAction = isMouseVolumeAction(fallback) ? fallback : globalLaunch ? global : nullptr; const bool eligible = target && !testButton && (!restricted || (button >= 2 && restrictedAction)) && (button >= 2 || target == GetForegroundWindow()); if (eligible) { const bool foreground = target == GetForegroundWindow(); bool singleScoped{}, doubleScoped{}; auto *single = restricted ? restrictedAction : effectiveMouseAction(app.settings, name, button, foreground, &singleScoped); auto *doubleClick = !restricted && button < 3 ? effectiveMouseAction(app.settings, name, 5 + button, foreground, &doubleScoped) : nullptr; if (!foreground) { if (single && single->type == MouseActionType::Shortcut) single = nullptr; if (doubleClick && doubleClick->type == MouseActionType::Shortcut) doubleClick = nullptr; } const auto now = GetTickCount64(); auto &pending = app.pendingMouseClicks[button]; const bool second = doubleClick && pending.target == target && doubleMouseClick(pending.point, mouse.pt, now - pending.started); if (second) pending = {}; else if (pending.target) app.queueMouseAction(std::exchange(pending, {})); const auto *action = second ? doubleClick : single; if (action || doubleClick) app.mousePresses[button] = {target, mouse.pt, now, action ? *action : MouseAction{}, true, doubleClick && !second}; if (action) { if (second ? doubleScoped : singleScoped) app.mousePresses[button].action.applications.insert(name); if (isMouseVolumeStep(action->type)) app.beginMouseVolume(button); app.resetInput(); return 1; } } } // Remote clicks invalidate a typing range just like local clicks. if (down || wp == WM_MOUSEWHEEL || wp == WM_MOUSEHWHEEL) { if (app.busy) diagnosticEvent(L"Input context changed: mouse message=" + std::to_wstring(wp)); app.resetInput(); } } return CallNextHookEx(nullptr, code, wp, lp); } void App::dispatch(int command) { if (installingUpdate) return; try { if (command == 23) { checkUpdate(true); return; } if (command == Screenshot) { if (!busy) { resetInput(); launchScreenshot(*this); } return; } if (command == ShowHistory) { showHistory(*this); return; } if (command == 20) { showSettings(*this); return; } if (command == 22) { showLogs(*this); return; } if (command == 21) { showSnippets(*this); return; } if (command == 99) { PostMessageW(window, WM_CLOSE, 0, 0); return; } transform(command); } catch (const std::exception &e) { report(wide(e.what())); } } void App::transform(int command, int snippet) { if (installingUpdate) return; const auto attempt = ++transformAttempt; const auto requestedAt = GetTickCount64(); HWND target = GetForegroundWindow(); const bool manual = command != AutoCorrect && snippet < 0; if (manual) { const auto currentVersion = inputVersion.load(); repeatAttempts = target == previousAttemptWindow && command == previousAttemptCommand && currentVersion == previousAttemptVersion && requestedAt - previousAttemptAt < 10000 ? repeatAttempts + 1 : 1; previousAttemptWindow = target; previousAttemptCommand = command; previousAttemptVersion = currentVersion; previousAttemptAt = requestedAt; diagnosticEvent(L"Transform request id=" + std::to_wstring(attempt) + L" command=" + std::to_wstring(command) + L" process=" + processName(target) + L" window=" + std::to_wstring(reinterpret_cast(target)) + L" version=" + std::to_wstring(currentVersion) + L" repeat=" + std::to_wstring(repeatAttempts)); } const bool preDeferred = (command == AutoCorrect && autoPending) || (snippet >= 0 && snippetPending == snippet); if (busy.exchange(true) && !preDeferred) { if (manual) { diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=busy"); try { DiagnosticJson record{{"attempt", std::to_string(attempt)}, {"process", utf8(processName(target))}, {"command", transformCommandName(command)}, {"source", "Unknown"}, {"target", "Unknown"}, {"outcome", "busy"}, {"verification", "not-attempted"}, {"problem", true}, {"elapsedMs", GetTickCount64() - requestedAt}}; diagnosticOperation(record.dump()); } catch (...) {} } return; } if (preDeferred) { autoPending = false; snippetPending = -1; } Run recent; std::optional previous; Settings config; std::vector available; uint64_t version; { std::lock_guard lock(stateMutex); recent = run; previous = last; config = settings; available = layouts; version = inputVersion; } const auto requestedChat = config.chatCycles.empty() ? L"" : windowChatKey(target); enqueue( [this, target, recent, previous, config, available, version, command, snippet, attempt, manual, requestedAt, preDeferred, requestedChat]() mutable { struct BusyReset { std::atomic &b; HWND window; bool deferred{}; ~BusyReset() { if (deferred) PostMessageW(window, ReplayTypingMessage, 0, 0); else b = false; } } reset{busy, window, preDeferred}; auto effectiveChat = requestedChat; if (lower(processName(target)) == L"chrome") effectiveChat = readChromeChat(target); config.cycle = chatCycle(config, languageProfileKey(config, target, effectiveChat), available); auto start = GetTickCount64(); std::wstring reason = L"cancelled-before-selection"; std::wstring text, result, expected, actual, verification = L"not-attempted"; int hint = -1, to = command >= Direct ? command - Direct : -1; if (manual) { std::wstring layoutIds; for (const auto &layout : available) { layoutIds += layout.id + L","; if (layout.handle == recent.layout) for (wchar_t key : recent.keys) expected += key == L' ' ? L' ' : layout.at(physicalLayout().code(key)); } diagnosticEvent(L"Transform context id=" + std::to_wstring(attempt) + L" layouts=" + layoutIds + L" physicalKeys=" + diagnosticText(recent.keys) + L" trackedExpected=" + diagnosticText(expected)); } struct Audit { App &app; HWND target; uint64_t version, attempt; ULONGLONG start; bool enabled; std::wstring &reason; std::wstring &text, &result, &expected; std::wstring &actual, &verification; int &hint, &to; int command; const std::vector &layouts; ~Audit() { if (enabled) try { auto layout = [&](int index) { return index >= 0 && index < (int)layouts.size() ? utf8(layouts[index].name + L" (" + layouts[index].id + L")") : std::string("Unknown"); }; auto bounded = [](const std::wstring &value) { return utf8(value.substr(0, 4096)); }; DiagnosticJson record{{"attempt", std::to_string(attempt)}, {"process", utf8(processName(target))}, {"command", transformCommandName(command)}, {"source", layout(text.empty() ? hint : detect(text, layouts, hint))}, {"target", layout(to)}, {"outcome", utf8(reason)}, {"verification", utf8(verification)}, {"problem", reason != L"Paste verified" && reason != L"layout-only" && !(command == 0 && reason == L"paste-sent-unverified")}, {"elapsedMs", GetTickCount64() - start}, {"foregroundSame", GetForegroundWindow() == target}, {"inputSame", app.inputVersion.load() == version}, {"original", bounded(text)}, {"expected", bounded(verification == L"selection-mismatch" ? expected : result)}, {"actual", bounded(actual)}, {"selectionExpected", bounded(expected)}, {"originalUnits", text.size()}, {"expectedUnits", (verification == L"selection-mismatch" ? expected : result).size()}, {"actualUnits", actual.size()}, {"textTruncated", text.size() > 4096 || result.size() > 4096 || actual.size() > 4096 || expected.size() > 4096}}; diagnosticOperation(record.dump(-1, ' ', true, DiagnosticJson::error_handler_t::replace)); } catch (...) {} // Logging must never abort a completed input transaction. } } audit{*this, target, version, attempt, requestedAt, manual || snippet >= 0, reason, text, result, expected, actual, verification, hint, to, command, available}; try { auto valid = [&] { return GetForegroundWindow() == target && inputVersion.load() == version; }; for (int i = 0; i < 8 && !modifiersReleased(); ++i) Sleep(10); if (!modifiersReleased() && command == AutoCorrect) return; if (!modifiersReleased()) releaseModifiers(); if (!valid()) return; for (int i = 0; i < (int)available.size(); ++i) if (available[i].handle == windowLayout(target)) hint = i; reason = L"invalid-target-layout"; if (to >= (int)available.size()) return; if (to >= 0 && !available[to].convertible) { reason = switchWindowLayout(target, available[to].handle, valid) ? L"layout-only" : L"layout-switch-failed"; return; } if (command == AutoCorrect) { if (!config.autoCorrect || recent.window != target || recent.overflow || recent.keys.empty() || recent.keys.back() != L' ' || hint < 0) return; { std::lock_guard lock(stateMutex); if (inputVersion.load() != version) return; autoCorrector.shareDictionaries(autoPredictor); } std::wstring predicted; for (wchar_t key : recent.keys) predicted += key == L' ' ? L' ' : available[hint].at(physicalLayout().code(key)); expected = predicted; if (!autoCorrector.suggest(predicted, hint, available, config) || !autoCorrectionAllowed(target, config) || !valid()) return; } reason = L"clipboard-snapshot"; if (command == AutoCorrect || snippet >= 0) { if (!beginTypingDeferral(target, version, snippet >= 0)) return; reset.deferred = true; } // Collapse a temporary selection on every failed auto-correction, // including clipboard exceptions, before queued typing is replayed. const bool farSnippet = snippet >= 0 && lower(processName(target)) == L"far"; bool autoSelected = false; struct SelectionReset { bool &selected; std::function valid; bool farEditor; ~SelectionReset() { if (selected && !farEditor && valid()) chord(VK_RIGHT); } } selectionReset{autoSelected, valid, farSnippet}; Transaction tx(*this, target, version, farSnippet); reason = L"selection-unavailable"; diagnosticEvent(L"Transform target=" + processName(target) + L" command=" + std::to_wstring(command) + L" recent=" + std::to_wstring(config.recent) + L" keys=" + std::to_wstring(recent.keys.size()) + L" sameWindow=" + std::to_wstring(recent.window == target) + L" overflow=" + std::to_wstring(recent.overflow) + L" ageMs=" + std::to_wstring(recent.at ? start - recent.at : 0) + L" id=" + std::to_wstring(attempt)); bool automatic = false; if (command == AutoCorrect) { // The editor must select the complete previous word. Unlike manual // conversion, automatic correction must never fall back to a suffix. if (!tx.valid() || !chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT)) return; autoSelected = true; Sleep(25); text = tx.copy(); // Editors may split hj,bnm at the comma. Extend only a verified // suffix of the tracked input, and still require a complete match. for (int i = 0; i < 3 && tx.valid() && matchAutoInput(recent, text, available, physicalLayout(), config) < 0 && (autoClosingSelection(text, config) || (!text.empty() && text.size() < expected.size() && expected.ends_with(text))); ++i) { if (!chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT)) break; Sleep(25); text = tx.copy(); } hint = matchAutoInput(recent, text, available, physicalLayout(), config); auto suggestion = autoCorrector.suggest(text, hint, available, config); if (!suggestion || !tx.valid() || !autoCorrectionAllowed(target, config)) { if (tx.valid()) { chord(VK_RIGHT); autoSelected = false; } return; } to = suggestion->target; result = suggestion->text; automatic = true; } else if (command == Undo) { if (previous) expected = previous->result; reason = L"undo-unavailable"; if (!previous || previous->window != target || previous->version != version || GetTickCount64() - previous->at > 120000) return; text = tx.copyRange(caretSteps(previous->result), previous->result, [&](const std::wstring &candidate) { return newlines(candidate) == newlines(previous->result); }); if (newlines(text) != newlines(previous->result)) { reason = L"undo-verification-mismatch"; actual = text; verification = L"selection-mismatch"; if (tx.valid()) chord(VK_RIGHT); return; } result = previous->original; if (previous->autoCorrected) { to = previous->sourceLayout; } automatic = true; } else if (snippet >= 0) { if (snippet >= (int)config.snippets.size() || recent.window != target) return; const auto &sn = config.snippets[snippet]; if (!snippetAppPriority(sn, processName(target))) return; if (recent.keys.size() < sn.trigger.size()) return; result = parseTemplate(sn.text).text; // A multiline clipboard paste can execute a console command // before readback. Only explicit, verified {enter} may do so. if (farSnippet && (result.empty() || result.find_first_of(L"\r\n") != result.npos || caretSteps(result) > 4096)) { reason = L"far-snippet-template-unsupported"; return; } Run tail = recent; tail.keys = tail.keys.substr(tail.keys.size() - sn.trigger.size()); autoSelected = true; reason = L"snippet-verification-mismatch"; if (farSnippet) { if (!tx.select(sn.trigger.size())) return; text = tx.copy(); } else text = tx.copyRange(sn.trigger.size(), tail.keys, [&](const std::wstring &candidate) { return tail.match(candidate, available, physicalLayout()) >= 0; }, [&](const std::wstring &candidate) { return tail.matchSuffix(candidate, available, physicalLayout()) >= 0; }); if (tail.match(text, available, physicalLayout()) < 0) return; automatic = true; } else if (config.recent && recent.window == target && !recent.keys.empty() && !recent.overflow) { text = tx.copyRange(recent.keys.size(), recent.keys, [&](const std::wstring &candidate) { return recent.match(candidate, available, physicalLayout()) >= 0; }, [&](const std::wstring &candidate) { return recent.matchSuffix(candidate, available, physicalLayout()) >= 0; }); hint = recent.match(text, available, physicalLayout()); diagnosticEvent(L"Recent verification copied=" + std::to_wstring(text.size()) + L" keys=" + std::to_wstring(recent.keys.size()) + L" match=" + std::to_wstring(hint)); if (hint < 0) { actual = text; verification = L"selection-mismatch"; reason = text.empty() ? L"recent-copy-empty" : L"recent-verification-mismatch"; if (tx.valid()) chord(VK_RIGHT); return; } automatic = true; } else if (config.recent && previous && previous->window == target && previous->version == version) { expected = previous->result; diagnosticEvent(L"Previous result expected=" + diagnosticText(expected)); text = tx.copyRange(caretSteps(previous->result), previous->result, [&](const std::wstring &candidate) { return newlines(candidate) == newlines(previous->result); }); if (newlines(text) != newlines(previous->result)) { actual = text; verification = L"selection-mismatch"; reason = L"previous-result-verification-mismatch"; { std::lock_guard lock(stateMutex); last.reset(); run.reset(); } if (tx.valid()) chord(VK_RIGHT); return; } hint = previous->layout; automatic = true; } else text = tx.copy(); reason = L"context-changed-after-copy"; if (!tx.valid()) return; if (text.empty()) { reason = L"copy-empty"; if (to >= 0 && config.switchLayout) switchWindowLayout(target, available[to].handle, valid); return; } if (command != Undo && command != AutoCorrect && snippet < 0) { if (command == Case) result = cycleCase(text); else if (command == Translit) result = transliterate(text); else { if (command == Toggle) { std::vector cycle; for (auto &id : config.cycle) for (int i = 0; i < (int)available.size(); ++i) if (available[i].id == id && available[i].convertible) cycle.push_back(i); if (cycle.empty()) { reason = L"cycle-has-no-layouts"; if (automatic && tx.valid()) chord(VK_RIGHT); return; } to = nextConversionLayout(text, available, cycle, hint); } result = convertTo(text, available, to, hint); } } bool sent = false; const bool requireReadback = tx.recoveredSelection() || snippet >= 0; PasteProbe probe(!farSnippet && ((config.writeLog && manual) || requireReadback) && result != text, text, target); if (requireReadback && !farSnippet && result != text && probe.status != L"ready") { verification = probe.status; reason = snippet >= 0 ? L"snippet-verification-unavailable" : L"recovered-selection-unavailable"; std::lock_guard lock(stateMutex); last.reset(); run.reset(); return; } diagnosticEvent(L"Conversion source=" + std::to_wstring(hint) + L" target=" + std::to_wstring(to) + L" changed=" + std::to_wstring(result != text)); if (result != text) sent = tx.paste(textClip(result)); else if (automatic && tx.valid()) tx.finishSelection(false); if (requireReadback && result != text && !sent) { reason = snippet >= 0 ? L"snippet-paste-cancelled" : L"recovered-selection-paste-cancelled"; std::lock_guard lock(stateMutex); last.reset(); run.reset(); return; } if (sent || result == text) autoSelected = false; reason = result == text ? L"conversion-unchanged" : sent ? L"paste-sent-unverified" : L"paste-cancelled"; if (manual && result == text) { reason = L"Conversion unchanged"; verification = L"calculated-result-equals-original"; } if (sent && ((manual && config.writeLog) || requireReadback)) { verification = probe.status; if (tx.valid()) { auto observed = farSnippet ? tx.readFarSnippet(caretSteps(result)) : probe.read((int)caretSteps(result)); // A native Edit may silently ignore Ctrl+V while the clipboard is busy. // Retry once only with fresh native proof that the exact original range // is still selected. Never retry an unknown or changed editor state. if (!farSnippet && observed && newlines(*observed) == newlines(text) && tx.nativeSelectionIs(text)) { diagnosticEvent(L"Paste retry: native Edit still selects the unchanged original"); if (tx.paste(textClip(result)) && tx.valid()) observed = probe.read((int)caretSteps(result)); else observed.reset(); } verification = farSnippet ? (observed ? L"read-back-via-far-clipboard" : L"far-readback-unavailable") : probe.status; if (observed && tx.valid()) { actual = *observed; reason = conversionReadbackOutcome(newlines(actual), newlines(text), newlines(result)); if (newlines(actual) != newlines(result)) { std::lock_guard lock(stateMutex); last.reset(); run.reset(); diagnosticEvent(L"Paste rejected by readback: previous result cleared; layout not switched"); return; } } else { reason = L"Paste not verified"; if (requireReadback) { std::lock_guard lock(stateMutex); last.reset(); run.reset(); return; } } } else verification = L"context-changed-after-paste"; } if (sent && !tx.valid()) reason = L"context-changed-after-paste"; if (sent && command == Undo && previous && previous->autoCorrected) autoCorrector.reject(previous->original, config); if (tx.valid() && (sent || automatic)) { std::lock_guard lock(stateMutex); run.reset(); if (command == Undo || snippet >= 0) last.reset(); else last = LastTransform{target, text, result, to >= 0 ? to : hint, GetTickCount64(), version, hint, command == AutoCorrect}; } if (snippet >= 0 && sent && tx.valid()) { tx.check(); auto t = parseTemplate(config.snippets[snippet].text); if (!t.suffix.empty()) { if (!tx.select(caretSteps(t.suffix))) return; auto suffix = tx.copy(); if (!tx.valid()) return; if (newlines(suffix) != newlines(t.suffix)) { tx.finishSelection(false); return; } if (!tx.finishSelection(true)) return; } // Action markers run after insertion and cursor placement, // before any physical typing buffered during expansion. for (int i = 0; i < t.enters && tx.valid(); ++i) { tx.check(); if (!chord(VK_RETURN)) break; } } if (to >= 0 && config.switchLayout && tx.valid() && (sent || result == text)) { bool switched = switchWindowLayout(target, available[to].handle, valid); diagnosticEvent(L"Layout switch confirmed=" + std::to_wstring(switched)); if (!switched) reason += L";layout-switch-failed"; } diagnosticEvent(L"Text operation completed in " + std::to_wstring(GetTickCount64() - start) + L" ms"); } catch (const std::exception &e) { reason = L"exception:" + wide(e.what()); report(wide(e.what())); } }); } void App::pasteEntry(std::wstring id, bool plain) { if (historyService) { remoteHistory("paste", id, plain); return; } if (installingUpdate) return; if (!history || busy.exchange(true)) return; HWND target = pasteTarget; if (historyWindow) SendMessageW(historyWindow, WM_CLOSE, 0, 0); activateWindow(target); auto version = inputVersion.load(); enqueue( [this, id, plain, target, version] { struct Reset { std::atomic &b; ~Reset() { b = false; } } reset{busy}; try { // 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) { auto text = clip.text(); if (text.empty()) return; clip = textClip(text); } Transaction tx(*this, target, version); log(tx.paste(clip) ? L"History paste: simulated paste completed" : L"History paste cancelled before simulated paste"); } catch (const std::exception &e) { report(wide(e.what())); } }); } 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(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 || !history || GetTickCount64() < pauseUntil) return; const bool share=lanService&&lanService->clipboardEnabled(); if(!settings.history&&!share)return; Settings config = settings; enqueue( [this, sequence, config, share] { try { if (suspended || sequence != GetClipboardSequenceNumber() || sequence == ownSequence) return; UINT exclude = RegisterClipboardFormatW(L"ExcludeClipboardContentFromMonitorProcessing"), ignore = RegisterClipboardFormatW(L"Clipboard Viewer 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); } return; } auto clip = readClipboard(window, (size_t)config.maxImageMiB * 1024 * 1024, ClipboardRead::WithoutImages, sequence); if (sequence != GetClipboardSequenceNumber()) return; for (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; history->add(clip, app, config); if(share)publishClipboard(clip,app); scheduleHistoryOcr(config); PostMessageW(window, HistoryMessage, 0, 0); } catch (const std::exception &e) { log(L"History capture: " + wide(e.what())); } }); } void App::recognizeEntry(std::wstring id) { if (historyService) { remoteHistory("ocr", id); return; } if (!history || busy.exchange(true)) return; auto sequence = GetClipboardSequenceNumber(); Settings config = settings; enqueue( [this, id, sequence, config] { struct Reset { std::atomic &b; ~Reset() { b = false; } } reset{busy}; try { auto cached = history->ocrText(id, ocrProfile(config)); auto text = cached ? *cached : history->recognizeImage(id, config, [this] { std::lock_guard lock(jobsMutex); return stop; }); if (!cached) history->saveOcr(id, text, config); if (text.empty()) { report(L"No text was found in the image."); return; } auto clip = textClip(text); if (config.history) history->add(clip, L"Windows OCR", config); if (GetClipboardSequenceNumber() == sequence) ownSequence = writeClipboard(window, clip); else report(L"The clipboard changed during OCR. Recognized text is available in history."); PostMessageW(window, HistoryMessage, 0, 0); } catch (const std::exception &e) { report(wide(e.what())); } }); } void App::trayMenu() { constexpr int utilityCommandBase = 1000; std::error_code utilitiesError; auto utilities = discoverUtilities(std::filesystem::path(executable()).parent_path() / L"tools", utilitiesError); if (utilities.size() > 50000) utilities.resize(50000); HMENU menu = CreatePopupMenu(); AppendMenuW(menu, MF_STRING, 10, L"Clipboard &history"); AppendMenuW(menu, MF_STRING, 24, L"&Capture screen region…"); AppendMenuW(menu, MF_STRING, 11, L"&Settings…"); SetMenuDefaultItem(menu, 11, FALSE); AppendMenuW(menu, MF_STRING, 12, L"S&nippets…"); AppendMenuW(menu, MF_STRING, 13, L"&Keyboard shortcuts…"); HMENU utilityMenu = CreatePopupMenu(); for (size_t i = 0; i < utilities.size(); ++i) AppendMenuW(utilityMenu, MF_STRING, utilityCommandBase + i, utilityMenuLabel(utilities[i]).c_str()); if (utilities.empty()) AppendMenuW(utilityMenu, MF_STRING | MF_GRAYED, 0, utilitiesError ? L"Cannot read tools folder" : L"No utilities in tools folder"); AppendMenuW(menu, MF_POPUP, (UINT_PTR)utilityMenu, L"&Tools"); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); AppendMenuW(menu, MF_STRING | (settings.awake ? MF_CHECKED : 0), 14, L"Keep screen a&wake"); HMENU pause = CreatePopupMenu(); AppendMenuW(pause, MF_STRING, 20, L"&Resume"); AppendMenuW(pause, MF_STRING, 21, L"&10 minutes"); AppendMenuW(pause, MF_STRING, 22, L"1 &hour"); AppendMenuW(pause, MF_STRING, 23, L"&Until restart"); AppendMenuW(menu, MF_POPUP, (UINT_PTR)pause, L"&Pause clipboard history"); AppendMenuW(menu, MF_STRING, 15, L"&Open data folder"); AppendMenuW(menu, MF_STRING, 18, L"View &log…"); AppendMenuW(menu, MF_STRING | (updateRunning ? MF_GRAYED : 0), 19, updateRunning ? L"&Update (checking / preparing…)" : availableUpdate.empty() ? L"&Update" : L"&Update (new version available)"); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); AppendMenuW(menu, MF_STRING, 16, L"&About KSwitcher"); AppendMenuW(menu, MF_STRING, 17, L"E&xit"); POINT pt; GetCursorPos(&pt); pasteTarget = GetForegroundWindow(); SetForegroundWindow(window); showMenuAccessKeys(menu); int cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON, pt.x, pt.y, 0, window, nullptr); DestroyMenu(menu); PostMessageW(window, WM_NULL, 0, 0); try { if (cmd >= utilityCommandBase && size_t(cmd - utilityCommandBase) < utilities.size()) { const auto &path = utilities[cmd - utilityCommandBase]; auto command = L"\"" + path.wstring() + L"\""; STARTUPINFOW startup{sizeof(startup)}; PROCESS_INFORMATION process{}; if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr, path.parent_path().c_str(), &startup, &process)) throw std::runtime_error(utf8(lastError(L"Cannot start utility " + path.filename().wstring()))); AllowSetForegroundWindow(process.dwProcessId); CloseHandle(process.hThread); CloseHandle(process.hProcess); return; } switch (cmd) { case 24: dispatch(Screenshot); break; case 10: showHistory(*this); break; case 11: showSettings(*this); break; case 12: showSnippets(*this); break; case 13: showShortcuts(*this); break; case 14: { auto s = settings; s.awake = !s.awake; applySettings(s); break; } case 15: ShellExecuteW(window, L"open", data.c_str(), nullptr, nullptr, SW_SHOWNORMAL); break; case 16: MessageBoxW(window, (std::wstring(L"KSwitcher ") + AppVersion + L"\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on " L"Typlo by bitcodepro. MIT license.\nHistory is encrypted for your Windows " L"account.\nNo .NET, WebView or graphics framework.").c_str(), L"About KSwitcher", MB_OK); break; case 17: PostMessageW(window, WM_CLOSE, 0, 0); break; case 18: showLogs(*this); break; case 19: checkUpdate(true); break; case 20: pauseUntil = 0; break; case 21: pauseUntil = GetTickCount64() + 600000; break; case 22: pauseUntil = GetTickCount64() + 3600000; break; case 23: pauseUntil = ~ULONGLONG{}; break; } } catch (const std::exception &e) { report(wide(e.what())); } } void App::checkUpdate(bool interactive, bool install, const std::wstring &version) { if (updateRunning) return; if (install && FindWindowW(L"KSwitcher.Config", nullptr)) { report(L"Close the configurator before installing an update."); return; } if (install && busy) { report(L"Wait for the current text operation to finish, then select Update again."); return; } if (updateWorker.joinable()) updateWorker.join(); updateRunning = true; installingUpdate = install; updateWorker = std::thread([this, interactive, install, version] { auto result = std::make_unique(); try { *result = runUpdate(install, version, interactive); } catch (const std::exception &e) { result->state = L"error"; result->error = wide(e.what()); } result->interactive = interactive; result->install = install; if (PostMessageW(window, UpdateMessage, 0, reinterpret_cast(result.get()))) result.release(); }); } LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { auto *app = current; if (!app) return DefWindowProcW(hwnd, msg, wp, lp); 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 text(reinterpret_cast(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; } switch (msg) { case UpdateMessage: { std::unique_ptr result(reinterpret_cast(lp)); if (app->updateWorker.joinable()) app->updateWorker.join(); app->updateRunning = false; app->installingUpdate = false; log(L"Update check: " + result->state + (result->version.empty() ? L"" : L" " + result->version) + (result->error.empty() ? L"" : L"; " + result->error)); if (result->state == L"prepared") { // The helper has verified and staged the package; it waits for process exit. DestroyWindow(hwnd); } else if (result->state == L"available") { app->availableUpdate = result->version; if (result->interactive) { auto prompt = L"Install KSwitcher " + result->version + L"?\n\nThe program will close and restart. Settings, history and existing dictionaries are preserved."; if (MessageBoxW(hwnd, prompt.c_str(), L"KSwitcher Update", MB_YESNO | MB_ICONQUESTION) == IDYES) app->checkUpdate(true, true, result->version); } else if (app->notifiedUpdate != result->version) { if (app->updateNotifier(result->version)) { app->notifiedUpdate = result->version; log(L"Update notification submitted to Windows: " + result->version); } else { log(L"Update notification failed; retrying in 5 minutes: " + result->version); SetTimer(hwnd, 5, UpdateNotificationRetryMs, nullptr); } } } else if (result->state == L"current") { app->availableUpdate.clear(); if (result->interactive) MessageBoxW(hwnd, (std::wstring(L"No newer stable release is available.\nInstalled version: ") + AppVersion).c_str(), L"KSwitcher Update", MB_OK | MB_ICONINFORMATION); } else if (result->interactive) MessageBoxW(hwnd, (L"Update failed.\n\n" + result->error).c_str(), L"KSwitcher Update", MB_OK | MB_ICONERROR); else diagnosticEvent(L"Background update check failed: " + result->error); return 0; } case WM_HOTKEY: app->dispatch((int)wp); return 0; case MouseActionMessage: app->dispatchMouseActions(); return 0; case MouseVolumeReleasedMessage: if (const auto indicator = reinterpret_cast(GetPropW(hwnd, VolumeIndicatorClass))) setVolumeIndicatorHold(indicator, app->volumeButtonHeld()); return 0; case WM_POWERBROADCAST: if (wp == PBT_APMSUSPEND && app->lanService) { app->lanService->cancelInput(); app->resetInput(); } return TRUE; case ReplayTypingMessage: app->replayTyping(); return 0; case TrayMessage: if (LOWORD(lp) == NIN_BALLOONUSERCLICK) showLogs(*app); else if (LOWORD(lp) == WM_CONTEXTMENU) app->trayMenu(); else if (LOWORD(lp) == WM_LBUTTONDBLCLK) // The shell still owns the mouse gesture on the second button-down. // Activate after release, so the tray cannot take focus back on button-up. SetTimer(hwnd, 4, 50, nullptr); else if (LOWORD(lp) == NIN_KEYSELECT) app->dispatch(20); // Settings is the tray's default action. return 0; case WM_CLIPBOARDUPDATE: app->captureHistory(); return 0; case AutoCorrectMessage: if (app->autoPending) { if (!app->installingUpdate && !app->snippetTyping && app->settings.autoCorrect && app->inputVersion.load() == (uint64_t)wp) app->transform(AutoCorrect); else app->replayTyping(); } return 0; case SnippetMessage: if (app->snippetPending == (int)wp) { if (!app->installingUpdate && app->inputVersion.load() == (uint64_t)lp) app->transform(0, (int)wp); else app->replayTyping(); } return 0; case HistoryMessage: if (app->historyWindow && IsWindowVisible(app->historyWindow)) refreshHistory(*app); return 0; case ErrorMessage: { std::unique_ptr text((std::wstring *)lp); NOTIFYICONDATAW icon{sizeof(icon)}; icon.hWnd = hwnd; icon.uID = 1; icon.uFlags = NIF_INFO; wcsncpy_s(icon.szInfo, text->c_str(), _TRUNCATE); wcscpy_s(icon.szInfoTitle, L"KSwitcher"); icon.dwInfoFlags = NIIF_WARNING; Shell_NotifyIconW(NIM_MODIFY, &icon); return 0; } 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; return configurationRequest(*app, (HWND)wp, *data); } if (data && data->dwData == 1 && data->cbData == sizeof(int)) { int cmd; memcpy(&cmd, data->lpData, sizeof(cmd)); app->dispatch(cmd); } return TRUE; } case LanClipboardPublishMessage: { std::unique_ptr packet(reinterpret_cast(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 == MouseClickTimer) { app->flushMouseClicks(); return 0; } if (wp >= FirstMouseVolumeTimer && wp < FirstMouseVolumeTimer + std::size(MouseButtonNames)) { app->repeatMouseVolume(static_cast(wp - FirstMouseVolumeTimer)); return 0; } if (wp == 8) { if (app->settings.appLanguageSets.empty() || app->busy || app->installingUpdate) return 0; app->refreshChatContext(); if (!app->activeChat.starts_with(L"app\n")) return 0; auto cycle = chatCycle(app->settings, app->activeChat, app->layouts); auto current = windowLayout(app->chatWindow); bool allowed = std::any_of(app->layouts.begin(), app->layouts.end(), [&](const Layout &layout) { return layout.handle == current && std::find(cycle.begin(), cycle.end(), layout.id) != cycle.end(); }); if (!allowed) app->switchChatLayout(false); return 0; } if (wp == 7) { if (app->lanService) { // 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; } if (wp == 6) { if (app->notificationRegistration && WaitForSingleObject(app->notificationRegistration, 0) == WAIT_OBJECT_0) { DWORD code = ERROR_GEN_FAILURE; GetExitCodeProcess(app->notificationRegistration, &code); if (code) log(L"Screenshot notification registration helper failed: " + std::to_wstring(code)); CloseHandle(app->notificationRegistration); app->notificationRegistration = nullptr; } app->releaseIdleMemory(GetTickCount64()); app->pollSync(); return 0; } if (wp == 5) { // After the initial 5-second check, repeat while the resident stays open. // Rearm even when a check is already running or the previous one failed. SetTimer(hwnd, 5, UpdateCheckIntervalMs, nullptr); app->checkUpdate(false); return 0; } if (wp == 4) { if (GetAsyncKeyState(VK_LBUTTON) & 0x8000) return 0; KillTimer(hwnd, 4); app->dispatch(20); return 0; } if (wp == 2) { PostMessageW(hwnd, WM_CLOSE, 0, 0); return 0; } if (app->settings.awake) { LASTINPUTINFO idle{sizeof(idle)}; if (GetLastInputInfo(&idle) && DWORD(GetTickCount() - idle.dwTime) >= 180000) chord(VK_F15); } return 0; case WM_CLOSE: if (app->updateRunning) { app->report(L"Wait for the update check or preparation to finish before exiting."); return 0; } if (app->busy) { app->report(L"Wait for the current text operation to finish before exiting."); return 0; } DestroyWindow(hwnd); return 0; case WM_DESTROY: if (auto config = FindWindowW(L"KSwitcher.Config", nullptr); config && configurationPeer(config, L"KSwitcher.Config.exe")) PostMessageW(config, WM_CLOSE, 0, 0); PostQuitMessage(0); return 0; default: return DefWindowProcW(hwnd, msg, wp, lp); } } int App::execute() { INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_STANDARD_CLASSES | ICC_LISTVIEW_CLASSES}; InitCommonControlsEx(&controls); WNDCLASSW cls{}; cls.hInstance = instance; cls.lpfnWndProc = windowProc; 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, nullptr, instance, nullptr); if (!window) throw std::runtime_error("Cannot create message window"); taskbarCreated = RegisterWindowMessageW(L"TaskbarCreated"); worker = std::thread( [this] { CoInitializeEx(nullptr, COINIT_MULTITHREADED); for (;;) { std::function job; { std::unique_lock lock(jobsMutex); jobsReady.wait(lock, [&] { return stop || !jobs.empty(); }); if (stop) break; job = std::move(jobs.front()); jobs.pop_front(); } try { job(); } catch (const std::exception &e) { report(wide(e.what())); } } 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{})); historyPinnedOnly = state.value("pinnedOnly", false); historyComputers = state.value("computers",std::vector{}); } catch (...) {} showHistory(*this); remoteHistory("ready", std::to_wstring(reinterpret_cast(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); if (!keyboardHook || !mouseHook) report(L"Input tracking could not start. Automatic recent-text conversion is unavailable."); AddClipboardFormatListener(window); // Resume pending OCR, including older images without a saved OCR state. scheduleHistoryOcr(settings); addTray(); if (!smoke) startBrowserChatMonitor(); if (!smoke && std::filesystem::exists(data / L"lan-pairing.bin")) try { lanService = std::make_unique(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); if (!smoke) SetTimer(window, 6, 1000, nullptr); if (!smoke) SetTimer(window, 8, 250, nullptr); SetThreadExecutionState(ES_CONTINUOUS | (settings.awake ? (ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED) : 0)); if (!registrationWarning.empty()) report(registrationWarning); if (smoke) SetTimer(window, 2, 10000, nullptr); else SetTimer(window, 5, 5000, nullptr); if (keyboardHook && mouseHook && confirmUpdatedStartup(updateStartupEvent)) log(std::wstring(L"Update completed; running version ") + AppVersion); if (startCommand) PostMessageW(window, WM_HOTKEY, startCommand, 0); try { notificationRegistration = startScreenshotNotificationRegistration(); } catch(const std::exception &e) { log(L"Screenshot notification registration: "+wide(e.what())); } auto screenshotActivation=startScreenshotActivationListener(); MSG msg; while (GetMessageW(&msg, nullptr, 0, 0) > 0) { if (settingsWindow && !IsWindowEnabled(settingsWindow)) { HWND popup = GetLastActivePopup(settingsWindow); if (popup != settingsWindow && IsDialogMessageW(popup, &msg)) continue; } // Preserve history-specific Enter/Shift+Enter and Ctrl+digit handling. if (!isolateHistory && historyWindow && msg.message == WM_SYSCHAR && IsDialogMessageW(historyWindow, &msg)) continue; if (settingsWindow && IsDialogMessageW(settingsWindow, &msg)) continue; if (snippetsWindow && IsDialogMessageW(snippetsWindow, &msg)) continue; if (logWindow && IsDialogMessageW(logWindow, &msg)) continue; TranslateMessage(&msg); DispatchMessageW(&msg); } if(screenshotActivation) CoRevokeClassObject(screenshotActivation); return (int)msg.wParam; } } // namespace ks