Release v0.6.28: typo correction, partial sync and selection diagnostics

This commit is contained in:
2026-11-02 11:19:13 +02:00
parent 738c81dce2
commit 55993c0f90
23 changed files with 676 additions and 76 deletions
+75 -7
View File
@@ -73,6 +73,8 @@ class Transaction
Clip snapshot;
DWORD expected{};
bool captured{};
mutable bool invalidLogged{};
unsigned copyNumber{};
std::wstring adopted;
public:
@@ -108,8 +110,18 @@ class Transaction
}
bool valid() const
{
return target && IsWindow(target) && GetForegroundWindow() == target &&
app.inputVersion.load() == version;
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()
{
@@ -144,10 +156,14 @@ class Transaction
}
std::wstring copy()
{
const auto started = GetTickCount64();
const auto number = ++copyNumber;
if (!valid())
return {};
write(textClip(L"\x2063KSwitcher-" + std::to_wstring(GetTickCount64())));
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));
if (!valid() || !chord(L'C', MOD_CONTROL))
{
diagnosticEvent(L"Copy failed: context changed or simulated Ctrl+C blocked");
@@ -166,6 +182,10 @@ class Transaction
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();
@@ -183,7 +203,8 @@ class Transaction
return {};
if (!chord(L'C', MOD_CONTROL))
return {};
diagnosticEvent(L"Copy retry: clipboard unchanged after Ctrl+C");
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;
}
@@ -191,6 +212,10 @@ class Transaction
}
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 {};
@@ -201,6 +226,9 @@ class Transaction
expected = changed;
auto clip = readClipboard(app.window);
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;
check();
for (auto &f : clip.formats)
@@ -233,6 +261,27 @@ class Transaction
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); }))
@@ -241,6 +290,7 @@ class Transaction
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");
@@ -251,6 +301,7 @@ class Transaction
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() &&
@@ -263,10 +314,11 @@ class Transaction
{
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)));
diagnosticEvent(L"Selection: suffix recovery match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text));
}
return text;
}
@@ -532,6 +584,7 @@ void App::applySettings(const Settings &requested)
run.reset();
last.reset();
++inputVersion;
if (busy) diagnosticEvent(L"Input context changed: settings applied version=" + std::to_wstring(inputVersion.load()));
}
// Clear on the worker so the next check reloads edits to dictionary files.
enqueue([this] { autoCorrector.configure({}, {}); });
@@ -560,6 +613,7 @@ void App::reloadLayouts()
run.reset();
last.reset();
++inputVersion;
if (busy) diagnosticEvent(L"Input context changed: layouts reloaded version=" + std::to_wstring(inputVersion.load()));
}
registerHotkeys(settings);
}
@@ -589,6 +643,7 @@ 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();
}
@@ -606,6 +661,9 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k, UINT mods)
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);
@@ -839,7 +897,10 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
// KSwitcher generates no mouse input. Remote clicks invalidate a typing
// range just like local clicks, including while a transaction is pending.
if (wp == WM_LBUTTONDOWN || wp == WM_RBUTTONDOWN || wp == WM_MBUTTONDOWN || wp == WM_MOUSEWHEEL)
{
if (current->busy) diagnosticEvent(L"Input context changed: mouse message=" + std::to_wstring(wp));
current->resetInput();
}
}
return CallNextHookEx(nullptr, code, wp, lp);
}
@@ -1010,9 +1071,10 @@ void App::transform(int command, int snippet)
!(command == 0 && reason == L"paste-sent-unverified")},
{"elapsedMs", GetTickCount64() - start}, {"foregroundSame", GetForegroundWindow() == target},
{"inputSame", app.inputVersion.load() == version}, {"original", bounded(text)},
{"expected", bounded(result)}, {"actual", bounded(actual)},
{"originalUnits", text.size()}, {"expectedUnits", result.size()}, {"actualUnits", actual.size()},
{"textTruncated", text.size() > 4096 || result.size() > 4096 || actual.size() > 4096}};
{"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.
@@ -1135,6 +1197,8 @@ void App::transform(int command, int snippet)
if (newlines(text) != newlines(previous->result))
{
reason = L"undo-verification-mismatch";
actual = text;
verification = L"selection-mismatch";
if (tx.valid())
chord(VK_RIGHT);
return;
@@ -1177,6 +1241,8 @@ void App::transform(int command, int snippet)
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);
@@ -1194,6 +1260,8 @@ void App::transform(int command, int snippet)
{ return newlines(candidate) == newlines(previous->result); });
if (newlines(text) != newlines(previous->result))
{
actual = text;
verification = L"selection-mismatch";
reason = L"previous-result-verification-mismatch";
if (tx.valid())
chord(VK_RIGHT);