Compare commits
3 Commits
release-v0.6.28
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c78b51d341 | |||
| 0184b837f4 | |||
| ca553a5e7d |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.28 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.
|
||||||
@@ -321,6 +325,10 @@ existing saved shortcuts are preserved. While KSwitcher runs, this binding opens
|
|||||||
|
|
||||||
History controls: Enter to paste, Shift+Enter for plain text, Ctrl+1…9 for quick paste,
|
History controls: Enter to paste, Shift+Enter for plain text, Ctrl+1…9 for quick paste,
|
||||||
Ctrl+P to pin/unpin, Delete to delete, Ctrl+O for the full item, Escape to hide.
|
Ctrl+P to pin/unpin, Delete to delete, Ctrl+O for the full item, Escape to hide.
|
||||||
|
|
||||||
|
**Clear history… / Alt+H** in the history window clears all saved entries after confirmation,
|
||||||
|
including pinned entries and entries hidden by search. No is the default choice. The current
|
||||||
|
Windows clipboard and future recording are unchanged. Settings also retains its Clear history action.
|
||||||
For an image, **Edit image**, **Ctrl+O**, or **Edit image in screenshot editor**
|
For an image, **Edit image**, **Ctrl+O**, or **Edit image in screenshot editor**
|
||||||
in the context menu opens the original image in the built-in screenshot editor.
|
in the context menu opens the original image in the built-in screenshot editor.
|
||||||
Annotate it there, then copy the result. Opening the editor leaves the
|
Annotate it there, then copy the result. Opening the editor leaves the
|
||||||
|
|||||||
Binary file not shown.
+7
-8
@@ -1,19 +1,18 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.28 — 2026-11-02
|
## v0.6.30 — 2026-11-02
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Custom automatic replacement pairs and optional dictionary typo correction. Both are off by default; enable Automatic correction and the desired modes in Auto-correction options. Custom pairs take priority; dictionary correction accepts one unambiguous single edit in words of at least four letters. Case, punctuation, exclusions and Undo are preserved.
|
- 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.
|
||||||
- First synchronization now merges independent entries immediately. Different values for one setting remain pending as individual conflicts instead of requiring Download/Upload of a whole profile.
|
- A previous-result verification mismatch clears stale repeat-conversion state.
|
||||||
- Optional partial sync keeps this PC's layout cycle and direct shortcuts while syncing other settings. Existing server layouts and bindings are preserved, including during explicit Upload. Missing-layout errors identify the language where available and exact layout IDs.
|
- 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.
|
||||||
- Disabled synchronization disables dependent controls. Save can persist disabling without connecting; forgetting the saved password remains available. Status messages can be selected, copied and scrolled.
|
- The guarded retry and failed-paste readback handling require **Write log**, which enables manual-conversion readback. Clipboard restoration remains conditional on ownership.
|
||||||
- Selection diagnostics record expected/copied text before and after recovery, clipboard sequences and retries, timing, focus/caret information and context interruptions. Failed verification now includes expected and actual text. Context retains up to 128 events for 30 seconds.
|
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
105 sync/protocol/conflict checks and 483 native regression checks and 48 updater checks passed. All 12 manifest files and all three executable versions were verified in the release ZIP. Focused partial-sync UI checks and screenshot PNG-save regressions passed. During development, conversion/diagnostic and typo integration scenarios passed, including Undo and uninterrupted typing. Final interactive input testing is limited by this execution session reporting no foreground window; the full screenshot UI suite also failed its pan assertion at 240 DPI. These interactive checks are not claimed as passing.
|
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.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
Dictionary typo correction is experimental and has no sentence context. Ambiguous words are unchanged. The reported asynchronous selection mismatch in ChatGPT is not fixed by the new diagnostics; full-range verification still cancels unsafe replacements. OpenBao synchronization remains experimental and requires HTTPS, KV v2 and a provisioned userpass account/policy. Ordered layout/language lists are resolved as whole settings; partial mode keeps the entire local layout configuration. Update participating PCs together for the new profile fields. No account registration, MFA, non-root namespaces or reverse-proxy URL prefixes.
|
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 @@
|
|||||||
c15937f52c981e7c6b15203aae51ae47dd2a261b495bd458533b01fd4cfe5498 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,28,0
|
FILEVERSION 0,6,30,0
|
||||||
PRODUCTVERSION 0,6,28,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.28"
|
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";
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -83,7 +83,7 @@ void arrangeWindow(HWND w)
|
|||||||
{
|
{
|
||||||
int id = GetDlgCtrlID(child);
|
int id = GetDlgCtrlID(child);
|
||||||
RECT next{scale(w, r.left) - layout->x,
|
RECT next{scale(w, r.left) - layout->x,
|
||||||
scale(w, r.top) - layout->y + (id >= 110 && id <= 114 ? extraHeight : 0)};
|
scale(w, r.top) - layout->y + ((id >= 110 && id <= 114) || id == 116 ? extraHeight : 0)};
|
||||||
next.right = next.left + scale(w, r.right - r.left) + (id == 101 || id == 102 ? extraWidth : 0);
|
next.right = next.left + scale(w, r.right - r.left) + (id == 101 || id == 102 ? extraWidth : 0);
|
||||||
next.bottom = next.top + scale(w, r.bottom - r.top) + (id == 102 ? extraHeight : 0);
|
next.bottom = next.top + scale(w, r.bottom - r.top) + (id == 102 ? extraHeight : 0);
|
||||||
if (child == layout->stretch)
|
if (child == layout->stretch)
|
||||||
@@ -730,6 +730,13 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
if (id == 102 && HIWORD(wp) == LBN_DBLCLK)
|
if (id == 102 && HIWORD(wp) == LBN_DBLCLK)
|
||||||
id = 110;
|
id = 110;
|
||||||
|
if (id == 116)
|
||||||
|
{
|
||||||
|
if (MessageBoxW(w, L"Delete all clipboard history, including pinned items? This also deletes items hidden by the current search.",
|
||||||
|
L"Clear history", MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2) == IDYES)
|
||||||
|
v->app->clearHistory();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
auto entry = selected(w, v);
|
auto entry = selected(w, v);
|
||||||
if (id == 110 || id == 111)
|
if (id == 110 || id == 111)
|
||||||
{
|
{
|
||||||
@@ -1650,6 +1657,7 @@ void showHistory(App &app, bool visible)
|
|||||||
button(w, L"P&in / unpin", 112, 222, 452, 110);
|
button(w, L"P&in / unpin", 112, 222, 452, 110);
|
||||||
button(w, L"&Delete", 113, 340, 452, 90);
|
button(w, L"&Delete", 113, 340, 452, 90);
|
||||||
button(w, L"&Open item", 114, 438, 452, 100);
|
button(w, L"&Open item", 114, 438, 452, 100);
|
||||||
|
button(w, L"Clear &history…", 116, 546, 452, 150);
|
||||||
SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w);
|
SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w);
|
||||||
SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w);
|
SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.28";
|
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