Compare commits

...

2 Commits

9 changed files with 84 additions and 15 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24) cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.29 LANGUAGES CXX RC) project(KSwitcher VERSION 0.6.30 LANGUAGES CXX RC)
if(NOT WIN32) if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs") message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif() endif()
+5 -1
View File
@@ -271,7 +271,11 @@ thumbnails and source labels aligned with the scaled fonts and spacing.
- Clipboard snapshot/restoration with concurrent-copy, foreground and input-version checks. - 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 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 context remain unchanged. Temporary transaction contents are excluded from Windows clipboard
history and cloud synchronization. Paste is not blindly retried. history and cloud synchronization. Paste is not blindly retried: with manual paste readback
enabled by Write log, a native Windows EDIT gets at most one retry when readback reports the
original and fresh native queries confirm that exactly the original range is still selected.
Other controls and unknown/changed selections are not retried. A verified failed paste clears
repeat-conversion state and does not switch the Windows layout.
- Case cycling, Russian/Ukrainian transliteration, verified undo and repeated conversion. - Case cycling, Russian/Ukrainian transliteration, verified undo and repeated conversion.
- Text snippets across layouts, multiline insertion and a verified `{cursor}` position; - 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. `{enter}` presses Enter after insertion and cursor placement (once per marker), before buffered typing resumes.
Binary file not shown.
+8 -8
View File
@@ -1,18 +1,18 @@
# Latest Windows release # Latest Windows release
## v0.6.29 — 2026-11-02 ## v0.6.30 — 2026-11-02
## Changes ## Changes
- Added **Clear history… / Alt+H** directly to the Clipboard history window beside Edit image / Open item. - A manual paste confirmed unsuccessful by readback no longer records the expected replacement as a previous result or switches the Windows layout. This prevents repeated commands from searching for text that was never inserted.
- Clearing requires confirmation, with **No** selected by default. It removes all saved entries, including pinned entries and entries hidden by the current search. Cancelling leaves history unchanged. - A previous-result verification mismatch clears stale repeat-conversion state.
- The button follows the existing bottom action row when resizing. Settings retains its existing Clear history action. - A writable native Windows EDIT field may receive one guarded paste retry: readback must equal the original text, and fresh native checks must confirm the exact original selection, unchanged focus and input context. Other controls, protected/read-only fields and unknown/changed selections are not retried.
- Clearing saved history does not clear the current Windows clipboard or disable future recording. This release does not change memory management. - The guarded retry and failed-paste readback handling require **Write log**, which enables manual-conversion readback. Clipboard restoration remains conditional on ownership.
## Validation ## Validation
The final build passed isolated checks for the new button, cancellation, confirmed deletion of pinned/search-hidden entries and persistence after reloading the history store. 48 updater regression checks passed; all executable versions and release-manifest hashes were verified. The full desktop-interaction suite was not rerun for this focused patch. The final isolated conversion integration run passed: permanently blocked paste, unexpected editor content, clearing fictional previous results, ignored-first-paste recovery, clipboard restoration, repeat conversion and partial-selection recovery. Typo integration also passed, including Undo and uninterrupted typing. 541 input/UI checks (240 DPI), 105 sync/protocol checks, 483 native regressions and 48 updater checks passed. All release executable versions and manifest hashes were verified. Earlier interactive attempts were interrupted by concurrent input or a transient clipboard-restoration assertion; the final isolated rerun passed.
## Known limitations ## Limitations
Existing asynchronous-editor selection/clipboard limitations remain; this release does not fix the reported ChatGPT selection mismatch. Dictionary typo correction and OpenBao synchronization remain experimental as documented in v0.6.28. The cause of every ignored paste is not established. This release does not claim to solve all asynchronous selection failures in ChatGPT/browser editors. Keep Write log enabled and send complete operation details if the issue recurs. Native retry is bounded to one attempt; it is not a general retry policy for other applications. All existing dictionary-correction and synchronization limitations remain.
**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. **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, close screenshot editors and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries. For manual installation, exit KSwitcher, close screenshot editors and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
+1 -1
View File
@@ -1 +1 @@
c9c3e3579575a41d434201d4f37bf3829e53c1ef23c42cb1c077930dfe8fdfa6 KSwitcher-win-x64.zip 7e557851ab68a37a8211ea39de41e79314f460ef8ac6471be723d0cfe03cfba9 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" 1 RT_MANIFEST "app.manifest"
201 RCDATA "update.ps1" 201 RCDATA "update.ps1"
1 VERSIONINFO 1 VERSIONINFO
FILEVERSION 0,6,29,0 FILEVERSION 0,6,30,0
PRODUCTVERSION 0,6,29,0 PRODUCTVERSION 0,6,30,0
FILEOS VOS_NT_WINDOWS32 FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP FILETYPE VFT_APP
BEGIN BEGIN
@@ -20,7 +20,7 @@ BEGIN
BEGIN BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility" VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher" VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.6.29" VALUE "FileVersion", "0.6.30"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+50
View File
@@ -338,6 +338,32 @@ class Transaction
Sleep(300); Sleep(300);
return true; 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 } // namespace
App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instance(h), smoke(test) App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instance(h), smoke(test)
@@ -1263,6 +1289,11 @@ void App::transform(int command, int snippet)
actual = text; actual = text;
verification = L"selection-mismatch"; verification = L"selection-mismatch";
reason = L"previous-result-verification-mismatch"; reason = L"previous-result-verification-mismatch";
{
std::lock_guard lock(stateMutex);
last.reset();
run.reset();
}
if (tx.valid()) if (tx.valid())
chord(VK_RIGHT); chord(VK_RIGHT);
return; return;
@@ -1332,11 +1363,30 @@ void App::transform(int command, int snippet)
if (tx.valid()) if (tx.valid())
{ {
auto observed = probe.read((int)caretSteps(result)); auto observed = 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 (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 = probe.status; verification = probe.status;
if (observed && tx.valid()) if (observed && tx.valid())
{ {
actual = *observed; actual = *observed;
reason = conversionReadbackOutcome(newlines(actual), newlines(text), newlines(result)); 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"; else reason = L"Paste not verified";
} }
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h" #include "platform.h"
namespace ks namespace ks
{ {
inline constexpr wchar_t AppVersion[] = L"0.6.29"; inline constexpr wchar_t AppVersion[] = L"0.6.30";
inline constexpr UINT UpdateMessage = WM_APP + 7; inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000; inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000; inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+15
View File
@@ -9,6 +9,7 @@ namespace ks
{ {
struct InputRegressionAccess struct InputRegressionAccess
{ {
static bool hasLast(App &app) { return app.last.has_value(); }
static bool deferring(App &app) static bool deferring(App &app)
{ {
std::lock_guard lock(app.stateMutex); std::lock_guard lock(app.stateMutex);
@@ -80,6 +81,7 @@ LRESULT CALLBACK observeEnter(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_P
} }
if (message == WM_PASTE && pasteBehavior) if (message == WM_PASTE && pasteBehavior)
{ {
if (pasteBehavior == 3) { pasteBehavior = 0; return 0; }
if (pasteBehavior == 2) SendMessageW(w, EM_REPLACESEL, TRUE, (LPARAM)L"other!"); if (pasteBehavior == 2) SendMessageW(w, EM_REPLACESEL, TRUE, (LPARAM)L"other!");
return 0; return 0;
} }
@@ -371,6 +373,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
require(text() == L"ghbdtn" && rows.back().outcome == L"Original text remained", require(text() == L"ghbdtn" && rows.back().outcome == L"Original text remained",
"blocked paste records original text remaining in the actual editor"); "blocked paste records original text remaining in the actual editor");
require(ks::readClipboard(app->window).text() == sentinel, "readback preserves clipboard after blocked paste"); require(ks::readClipboard(app->window).text() == sentinel, "readback preserves clipboard after blocked paste");
require(!ks::InputRegressionAccess::hasLast(*app), "failed paste does not retain a fictional previous result");
pasteBehavior = 2; pasteBehavior = 2;
prepare(L"ghbdtn", true); prepare(L"ghbdtn", true);
app->transform(ks::Direct + ru); app->transform(ks::Direct + ru);
@@ -381,6 +384,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
auto rows = ks::parseOperationLog(ks::wide(diagnosticLog())); auto rows = ks::parseOperationLog(ks::wide(diagnosticLog()));
require(text() == L"other!" && rows.back().outcome == L"Readback differs", require(text() == L"other!" && rows.back().outcome == L"Readback differs",
"unexpected editor content records readback mismatch"); "unexpected editor content records readback mismatch");
require(!ks::InputRegressionAccess::hasLast(*app), "changed readback cannot seed repeat conversion");
pasteBehavior = 0; pasteBehavior = 0;
prepare(L"привет", true); prepare(L"привет", true);
app->transform(ks::Direct + ru); app->transform(ks::Direct + ru);
@@ -449,6 +453,17 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
details.find(L"Selection initial match=0") != details.npos && details.find(L"Selection initial match=0") != details.npos &&
details.find(L"SelectPrevious") != details.npos, details.find(L"SelectPrevious") != details.npos,
"incident includes clipboard and selection stages"); "incident includes clipboard and selection stages");
pasteBehavior = 3;
prepare(L"=nhtdjuf", true);
app->transform(ks::Direct + ru);
stage = 49;
break;
}
case 49:
{
require(text() == L"=тревога" && pasteBehavior == 0,
"native Edit retries one ignored paste only while original remains selected");
require(ks::readClipboard(app->window).text() == sentinel, "native paste retry restores clipboard");
finish(); finish();
break; break;
} }