Compare commits
2 Commits
release-v0.6.29
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c78b51d341 | |||
| 0184b837f4 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
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)
|
||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
- 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.
|
||||
|
||||
Binary file not shown.
+8
-8
@@ -1,18 +1,18 @@
|
||||
# Latest Windows release
|
||||
|
||||
## v0.6.29 — 2026-11-02
|
||||
## v0.6.30 — 2026-11-02
|
||||
|
||||
## Changes
|
||||
- Added **Clear history… / Alt+H** directly to the Clipboard history window beside Edit image / Open item.
|
||||
- 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.
|
||||
- The button follows the existing bottom action row when resizing. Settings retains its existing Clear history action.
|
||||
- Clearing saved history does not clear the current Windows clipboard or disable future recording. This release does not change memory management.
|
||||
- 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.
|
||||
- A previous-result verification mismatch clears stale repeat-conversion state.
|
||||
- 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.
|
||||
- The guarded retry and failed-paste readback handling require **Write log**, which enables manual-conversion readback. Clipboard restoration remains conditional on ownership.
|
||||
|
||||
## 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
|
||||
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.
|
||||
## Limitations
|
||||
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.
|
||||
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"
|
||||
201 RCDATA "update.ps1"
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,6,29,0
|
||||
PRODUCTVERSION 0,6,29,0
|
||||
FILEVERSION 0,6,30,0
|
||||
PRODUCTVERSION 0,6,30,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.29"
|
||||
VALUE "FileVersion", "0.6.30"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
+50
@@ -338,6 +338,32 @@ class Transaction
|
||||
Sleep(300);
|
||||
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) : instance(h), smoke(test)
|
||||
@@ -1263,6 +1289,11 @@ void App::transform(int command, int snippet)
|
||||
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;
|
||||
@@ -1332,11 +1363,30 @@ void App::transform(int command, int snippet)
|
||||
if (tx.valid())
|
||||
{
|
||||
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;
|
||||
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";
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
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 UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ks
|
||||
{
|
||||
struct InputRegressionAccess
|
||||
{
|
||||
static bool hasLast(App &app) { return app.last.has_value(); }
|
||||
static bool deferring(App &app)
|
||||
{
|
||||
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 (pasteBehavior == 3) { pasteBehavior = 0; return 0; }
|
||||
if (pasteBehavior == 2) SendMessageW(w, EM_REPLACESEL, TRUE, (LPARAM)L"other!");
|
||||
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",
|
||||
"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::InputRegressionAccess::hasLast(*app), "failed paste does not retain a fictional previous result");
|
||||
pasteBehavior = 2;
|
||||
prepare(L"ghbdtn", true);
|
||||
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()));
|
||||
require(text() == L"other!" && rows.back().outcome == L"Readback differs",
|
||||
"unexpected editor content records readback mismatch");
|
||||
require(!ks::InputRegressionAccess::hasLast(*app), "changed readback cannot seed repeat conversion");
|
||||
pasteBehavior = 0;
|
||||
prepare(L"привет", true);
|
||||
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"SelectPrevious") != details.npos,
|
||||
"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();
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user