Files
hrynco-kswitcher/src/app.cpp
T
agrynco 14f33012a3 feat: release kswitcher 0.6.64
Add mouse button actions and application profiles, with volume controls and per-monitor settings layout fixes.
2026-11-12 00:39:12 +02:00

2826 lines
122 KiB
C++

#include "app.h"
#include <utility>
#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 <algorithm>
#include <commctrl.h>
#include <set>
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<ULONG_PTR>(&marker);
}
std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
{
std::vector<std::pair<int, std::wstring>> 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<PasteProbe> 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<Bytes> 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<PasteProbe>(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<std::wstring> 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<bool(const std::wstring &)> &matches,
const std::function<bool(const std::wstring &)> &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<History>(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<void()> job)
{
{
std::lock_guard lock(jobsMutex);
if (stop)
return;
jobs.push_back(std::move(job));
}
jobsReady.notify_one();
}
void App::enqueueHistorySearch(std::function<void()> 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<void()> 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<std::pair<int, Hotkey>> 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<LanService>(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<std::wstring> 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<std::wstring> 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<std::wstring> 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<bool> &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<WORD>(key.vkCode);
input.ki.wScan = static_cast<WORD>(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<INPUT> 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<UINT>(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<int>(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<HWND>(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<int>(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<UINT>(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<MousePress> 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<WORD>(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<WPARAM>(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<const MSLLHOOKSTRUCT *>(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<uintptr_t>(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<LastTransform> previous;
Settings config;
std::vector<Layout> 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<bool> &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<Layout> &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<bool()> 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<int> 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<bool> &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<LPARAM>(packet)))delete packet;
}
void App::receiveClipboard()
{
if(!lanService)return;
auto packets=lanService->takeClipboard();auto config=settings;
for(auto &packet:packets)enqueue([this,packet=std::move(packet),config]{
try {
auto clip=sharedClipboard(decodeClip(packet.data));if(clip.formats.empty())return;
auto sequence=GetClipboardSequenceNumber();
if(!suspended&&(sequence==packet.sequence||sequence==lanSequence.load())){
const std::wstring marker=L"KSwitcher.LAN.Clipboard";
auto active=lan::clipboardImageForPaste(clip);
active.formats.push_back({RegisterClipboardFormatW(marker.c_str()),marker,Bytes{1}});
ownSequence=lanSequence=writeClipboardGuarded(window,active,sequence);
log(L"LAN clipboard activated: "+std::to_wstring(packet.data.size())+L" bytes");
}
if(config.history&&history){history->add(clip,wide(packet.app),config,packet.peer,wide(packet.name));
scheduleHistoryOcr(config);PostMessageW(window,HistoryMessage,0,0);}
}catch(const std::exception &e){log(L"LAN clipboard receive: "+wide(e.what()));}
});
}
void App::captureHistory()
{
DWORD sequence = GetClipboardSequenceNumber();
if (suspended || sequence == ownSequence || !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<bool> &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<UpdateResult>();
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<LPARAM>(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<std::wstring> text(reinterpret_cast<std::wstring *>(lp));
MessageBoxW(app->historyWindow, text->c_str(), L"Clipboard history", MB_OK | MB_ICONWARNING);
return 0;
}
}
if (!app->historyService && msg == app->taskbarCreated && app->taskbarCreated)
{
app->addTray();
return 0;
}
switch (msg)
{
case UpdateMessage:
{
std::unique_ptr<UpdateResult> result(reinterpret_cast<UpdateResult *>(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<HWND>(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<std::wstring> 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<LanClipboard> packet(reinterpret_cast<LanClipboard*>(lp));
try{if(app->lanService)app->lanService->publishClipboard(std::move(packet->data),packet->app);}
catch(const std::exception &e){log(L"LAN clipboard send: "+wide(e.what()));}
return 0;
}
case LanClipboardReadyMessage: app->receiveClipboard();return 0;
case LanInputReadyMessage:
if(app->lanService&&app->lanService->tickInput(!app->busy&&!app->suspended&&!app->hotkeyCapture))app->resetLanInput();
return 0;
case WM_TIMER:
if (wp == MouseClickTimer) { app->flushMouseClicks(); return 0; }
if (wp >= FirstMouseVolumeTimer && wp < FirstMouseVolumeTimer + std::size(MouseButtonNames))
{
app->repeatMouseVolume(static_cast<int>(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<void()> 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<std::string>{});
} catch (...) {}
showHistory(*this);
remoteHistory("ready", std::to_wstring(reinterpret_cast<uintptr_t>(historyWindow)));
SetTimer(window, 1, 1000, nullptr);
MSG message{};
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE && historyWindow &&
(message.hwnd == historyWindow || IsChild(historyWindow, message.hwnd))) {
SendMessageW(historyWindow, WM_CLOSE, 0, 0);
continue;
}
if (historyWindow && message.message == WM_SYSCHAR && IsDialogMessageW(historyWindow, &message)) continue;
TranslateMessage(&message); DispatchMessageW(&message);
}
return int(message.wParam);
}
keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, keyProc, instance, 0);
mouseHook = SetWindowsHookExW(WH_MOUSE_LL, mouseProc, instance, 0);
registerHotkeys(settings);
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<LanService>(data,window); }
catch (const std::exception &e) { report(L"LAN module: " + wide(e.what())); }
if (lanService && lanService->inputEnabled()) SetTimer(window, 7, 10, nullptr);
SetTimer(window, 1, 60000, nullptr);
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