Release v0.6.19: retry dropped copies and stabilize snippet integration

This commit is contained in:
2026-10-31 02:03:07 +02:00
parent 0d3b2acae9
commit 6603d28f68
9 changed files with 127 additions and 17 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.18 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.19 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+5
View File
@@ -226,11 +226,16 @@ thumbnails and source labels aligned with the scaled fonts and spacing.
If the editor's word boundary differs from the recorded typing, it falls back to paced character
selection: Teams can coalesce a burst of Shift+Left events into just one character.
- Clipboard snapshot/restoration with concurrent-copy, foreground and input-version checks.
A silently dropped copy is retried up to three total attempts while the clipboard and input
context remain unchanged. Temporary transaction contents are excluded from Windows clipboard
history and cloud synchronization. Paste is not blindly retried.
- Case cycling, Russian/Ukrainian transliteration, verified undo and repeated conversion.
- Text snippets across layouts, multiline insertion and a verified `{cursor}` position;
`{enter}` presses Enter after insertion and cursor placement (once per marker), before buffered typing resumes.
For example, `rider .{enter}` inserts `rider .` and presses Enter.
Automatic correction is suppressed during snippet expansion and for typing buffered by that expansion; the global setting is preserved.
If the trigger cannot be verified after the copy attempts, it is preserved, queued typing resumes,
and no Enter action is sent.
`{{cursor}}` and `{{enter}}` insert literal markers. Snippets are stored as plaintext settings, as in [Typlo](https://github.com/bitcodepro/Typlo).
- Clipboard history for text, HTML/RTF, file lists, DIB and PNG images; plain-text paste;
full-text search, pinning, deletion, full text viewing and image previews.
Binary file not shown.
+9 -9
View File
@@ -1,18 +1,18 @@
# Latest Windows release
## v0.6.18 — 2026-10-31
## v0.6.19 — 2026-10-31
## Changes
- Verify the actual editor text after manual conversion using read-only Windows Accessibility, without moving the caret, changing selection or copying again. Record unchanged calculations, original text remaining, unexpected readback and unavailable verification separately.
- Replace the event-only diagnostic list with sortable Time, Program, Command, Source layout, Target layout and Result columns; add program/command/result filters and a separate panel with original, expected and read-back text and operation context.
- Keep successful operations only in a bounded in-memory context (40 events, 30 seconds). Only problem operations create file rows; verified successes do not fill the log. Diagnostics work in normal Release builds and the additional readback check is skipped when logging is disabled.
- Refresh English and Ukrainian Wiki documentation through v0.6.18, including previously undocumented snippet, updater and constrained-pencil changes since v0.6.14.
## Limitations
Some editors do not expose their text through Windows Accessibility. Changed focus, protected or unsupported controls, provider-specific character units, delayed text updates and the 4096-character readback limit can prevent verification; these cases are recorded as unverified rather than successful. Historical unstructured entries remain readable without retroactive filter fields.
- Retry a silently dropped Ctrl+C up to three total attempts, only while the transaction clipboard remains unchanged and the original input context is still valid. Failed snippet verification preserves the trigger and following input without sending Enter.
- Exclude temporary transaction clipboard contents from Windows clipboard history and cloud synchronization. The original clipboard snapshot is still restored after the operation when ownership permits.
- Fix snippet integration test isolation: restore a multiline editor after earlier single-line scenarios, reset layout/correction state, coordinate test-editor clipboard access and disable unrelated background update checks.
- Add deterministic coverage for one dropped copy and exhausted retries. Keep exact assertions for Enter ordering, buffered typing, clipboard restoration and subsequent auto-correction.
## Validation
Passed 230 native regression checks, 472 input/UI checks, screenshot editor UI and notification registration checks, 48 updater checks, updater progress checks, 45 isolated configurator process checks, conversion-specific editor integration and diagnostic viewer checks. Verified that 1000 successful operations create no log file and that a subsequent failure creates one row with bounded context. The broader editor integration suite still intermittently fails in snippet Enter/concurrent-input scenarios; similar failures reproduce with the pre-change code.
The fixes passed 230 native regression checks, 472 input/UI checks, screenshot editor UI and notification registration checks, 48 updater checks and updater progress checks. The full editor integration suite passed all 63 assertions; conversion-specific diagnostics also passed. The final snippet suite passed 20 consecutive runs, including injected copy failures and the three-attempt limit. The v0.6.19 staged Release build also passed the native, input/UI, screenshot and updater checks; the installed application was not replaced or restarted.
## Limitations
The fix does not retry paste blindly: an editor may already have changed its selection or text. Clipboard contention, editor capabilities and input privileges can still prevent an operation. Existing accessibility readback limitations and problem-only diagnostic logging remain unchanged.
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
+1 -1
View File
@@ -1 +1 @@
3e76d8a566c623a3084c00cc909f4cc7c8a6edb38f9c06adbb32edfd4cfcb5b7 KSwitcher-win-x64.zip
8b2008e97ca625b7eebdf509c038d6d1f6c5c853dce6da1b939abaa18223e26d KSwitcher-win-x64.zip
+3 -3
View File
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
1 RT_MANIFEST "app.manifest"
201 RCDATA "update.ps1"
1 VERSIONINFO
FILEVERSION 0,6,18,0
PRODUCTVERSION 0,6,18,0
FILEVERSION 0,6,19,0
PRODUCTVERSION 0,6,19,0
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
BEGIN
@@ -20,7 +20,7 @@ BEGIN
BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.6.18"
VALUE "FileVersion", "0.6.19"
END
END
BLOCK "VarFileInfo"
+24 -1
View File
@@ -124,7 +124,13 @@ class Transaction
{
check();
adopted.clear();
app.ownSequence = expected = writeClipboard(app.window, c);
// 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)
@@ -146,6 +152,8 @@ class Transaction
return {};
}
ULONGLONG deadline = GetTickCount64() + 2000;
ULONGLONG retryAt = GetTickCount64() + 250;
unsigned attempts = 1;
DWORD changed = before;
ULONGLONG stable = 0;
bool changedAgain = false;
@@ -162,6 +170,21 @@ class Transaction
}
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(L'C', MOD_CONTROL))
return {};
diagnosticEvent(L"Copy retry: clipboard unchanged after Ctrl+C");
++attempts;
retryAt = GetTickCount64() + 250;
}
Sleep(10);
}
if (changed == before || !valid())
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.18";
inline constexpr wchar_t AppVersion[] = L"0.6.19";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+83 -1
View File
@@ -25,8 +25,51 @@ bool captured = false, failed = false;
bool conversionOnly = false;
int pasteBehavior = 0;
int enterPresses = 0;
int copyFailuresRemaining = 0, copyRequests = 0;
bool temporaryClipboardExcluded = true;
bool clipboardAccessTimedOut = false;
LRESULT CALLBACK observeEnter(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
{
// This bare EDIT control is our test fixture, not a full editor with
// clipboard retries. Keep clipboard access on its owning UI thread while
// it handles Ctrl+C/Ctrl+V, so unrelated Windows clipboard listeners cannot
// make the fixture silently discard the operation under test.
struct ClipboardAccess
{
bool opened = false;
~ClipboardAccess() { if (opened) CloseClipboard(); }
} access;
if (message == WM_CHAR && (wp == 3 || wp == 22))
{
auto deadline = GetTickCount64() + 1000;
do
{
if (OpenClipboard(w)) { access.opened = true; break; }
Sleep(10);
} while (GetTickCount64() < deadline);
if (!access.opened)
{
clipboardAccessTimedOut = true;
return 0;
}
}
if ((message == WM_CHAR && wp == 3) || message == WM_PASTE)
{
temporaryClipboardExcluded &= IsClipboardFormatAvailable(
RegisterClipboardFormatW(L"ExcludeClipboardContentFromMonitorProcessing")) != FALSE;
if (message == WM_CHAR)
{
++copyRequests;
// Native EDIT silently ignores Ctrl+C when OpenClipboard fails.
// Reproduce that outcome deterministically, without depending on
// the timing of the Windows clipboard-history service.
if (copyFailuresRemaining > 0)
{
--copyFailuresRemaining;
return 0;
}
}
}
if (message == WM_PASTE && pasteBehavior)
{
if (pasteBehavior == 2) SendMessageW(w, EM_REPLACESEL, TRUE, (LPARAM)L"other!");
@@ -51,13 +94,15 @@ std::string diagnosticLog()
size_t diagnosticStart{};
void require(bool ok, const char *name)
{
if (!ok)
if (!ok || clipboardAccessTimedOut)
{
wchar_t observed[2048]{};
GetWindowTextW(field, observed, 2048);
std::cerr << "Editor at failure: " << ks::utf8(observed)
<< "; foregroundSame=" << (GetForegroundWindow() == host)
<< "; inputVersion=" << app->inputVersion.load() << '\n';
if (clipboardAccessTimedOut)
throw std::runtime_error("Test editor could not acquire clipboard within 1000 ms");
throw std::runtime_error(name);
}
std::cout << "PASS " << name << '\n';
@@ -109,6 +154,9 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
return 0;
if (msg == WM_TIMER)
{
// Input tests must not start a network update check. Its close veto can
// otherwise leave a completed test running after the editor is gone.
if (app->window) KillTimer(app->window, 5);
try
{
if (GetTickCount64() > deadline)
@@ -257,6 +305,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
DestroyWindow(field);
field = CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_PASSWORD, 20, 20,
590, 30, host, nullptr, GetModuleHandleW(nullptr), nullptr);
SetWindowSubclass(field, observeEnter, 1, 0);
prepare(L"ghbdtn ", false);
record(L"ghbdtn ");
app->transform(ks::AutoCorrect);
@@ -268,6 +317,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
DestroyWindow(field);
field = CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER, 20, 20, 590, 30, host,
nullptr, GetModuleHandleW(nullptr), nullptr);
SetWindowSubclass(field, observeEnter, 1, 0);
prepare(L"(ljvf ", false);
record(L"(ljvf ");
app->transform(ks::AutoCorrect);
@@ -347,6 +397,18 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
case 30:
case 32:
case 34:
case 37:
// Earlier password/word tests replace the original multiline
// editor with a single-line EDIT. Restore the snippet fixture:
// a single-line control cannot insert the tested Enter action.
if (!(GetWindowLongPtrW(field, GWL_STYLE) & ES_MULTILINE))
{
DestroyWindow(field);
field = CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER |
ES_MULTILINE | ES_WANTRETURN, 20, 20, 590, 230, host, nullptr,
GetModuleHandleW(nullptr), nullptr);
SetWindowSubclass(field, observeEnter, 1, 0);
}
if (!captured)
{
saved = ks::readClipboard(app->window, 64 * 1024 * 1024);
@@ -355,9 +417,16 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
app->ownSequence = ks::writeClipboard(app->window, ks::textClip(sentinel));
app->settings.autoCorrect = stage == 35;
app->settings.autoLanguages = {L"en", L"ru"};
app->settings.cycle = {app->layouts[en].id, app->layouts[ru].id};
app->settings.switchLayout = false;
app->autoCorrector = ks::AutoCorrector{};
app->autoPredictor = ks::AutoCorrector{};
app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages);
app->settings.snippets = {{L";r", stage == 31 ? L"rider .{cursor}" : L"rider .{cursor}{enter}"}};
enterPresses = 0;
copyRequests = 0;
copyFailuresRemaining = stage == 38 ? 10 : stage == 33 ? 1 : 0;
temporaryClipboardExcluded = true;
ActivateKeyboardLayout(app->layouts[en].handle, 0);
prepare(L"prefix ", false);
{
@@ -384,6 +453,10 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
break;
case 33:
require(text() == L"prefix rider .\r\n x", "Enter action precedes buffered typing");
require(copyRequests == 2 && copyFailuresRemaining == 0,
"snippet retries a silently dropped copy without duplicating input");
require(temporaryClipboardExcluded,
"temporary snippet clipboard contents are excluded from Windows history");
require(enterPresses == 1, "Enter marker sends an actual Enter key press");
require(ks::readClipboard(app->window).text() == sentinel, "Enter snippet restores clipboard");
stage = 34;
@@ -407,6 +480,15 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
break;
case 36:
require(text() == L"prefix rider .\r\nghbdtn привет ", "automatic correction resumes for subsequent normal typing");
stage = 37;
break;
case 38:
require(copyRequests == 3, "copy retries are bounded to three attempts");
require(text() == L"prefix ;r x" && enterPresses == 0,
"exhausted copy retries preserve trigger and following input without Enter");
require(ks::readClipboard(app->window).text() == sentinel,
"exhausted copy retries restore the clipboard");
copyFailuresRemaining = 0;
finish();
break;
case 20: