Compare commits
2 Commits
ca553a5e7d
...
c78b51d341
| Author | SHA1 | Date | |
|---|---|---|---|
| c78b51d341 | |||
| 0184b837f4 |
+1
-1
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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 @@
|
|||||||
c9c3e3579575a41d434201d4f37bf3829e53c1ef23c42cb1c077930dfe8fdfa6 KSwitcher-win-x64.zip
|
7e557851ab68a37a8211ea39de41e79314f460ef8ac6471be723d0cfe03cfba9 KSwitcher-win-x64.zip
|
||||||
|
|||||||
+3
-3
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user