diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a7402a..732951f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.24) -project(KSwitcher VERSION 0.6.12 LANGUAGES CXX RC) +project(KSwitcher VERSION 0.6.13 LANGUAGES CXX RC) if(NOT WIN32) message(FATAL_ERROR "KSwitcher uses native Windows APIs") endif() diff --git a/Release/KSwitcher-win-x64.zip b/Release/KSwitcher-win-x64.zip index 95ec5e4..b2a7efb 100644 Binary files a/Release/KSwitcher-win-x64.zip and b/Release/KSwitcher-win-x64.zip differ diff --git a/Release/README.md b/Release/README.md index bdeb6b3..e95c0e2 100644 --- a/Release/README.md +++ b/Release/README.md @@ -1,16 +1,14 @@ # Latest Windows release -## v0.6.12 — 2026-10-30 +## v0.6.13 — 2026-10-30 ## Changes -- Start automatic layout correction immediately after Space and preserve subsequent typing while the correction runs. -- Accept identical corrected words shared by multiple selected languages; use the selected language order to choose the target layout. Different valid spellings remain ambiguous. -- Run the log viewer in the configurator process, separate from the resident input hooks. Clearing logs remains coordinated with the resident process. -- Keep the native four-column ListView. Resize the table with its window, anchor the footer and Close button, and make Details fill the remaining width. -- Exclude copied diagnostic rows from clipboard history. +- Correct words containing punctuation keys that become letters in the target layout, such as `hj,bnm` → `робить`. Validate letters and capitalization after conversion so personal dictionary entries can match. +- Extend automatic word selection across internal punctuation only while the selected text matches a suffix of the tracked input. Verify the complete tracked word before replacing it. +- Give the fixed-layout automatic-correction settings dialog a fixed window size, retaining scrolling on small displays instead of allowing empty space when resized. ## Validation -Native and input regression suites, screenshot UI and updater checks, log-viewer integration, continuous-typing integration, and isolated configurator/log process checks. Manual development-build checks covered window dragging and automatic Details column width. +220 native regression checks, 466 input/UI checks, screenshot editor and notification registration checks, 45 updater checks and updater progress checks passed. Native editor integration verified continuous typing during correction and complete selection/conversion of `hj,bnm` to `робить`. The correction was also confirmed in the user's editor. **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. diff --git a/Release/SHA256SUMS.txt b/Release/SHA256SUMS.txt index 7fda9b5..c1f3a70 100644 --- a/Release/SHA256SUMS.txt +++ b/Release/SHA256SUMS.txt @@ -1 +1 @@ -866de2d1d6fe121a7845760519f934bf0bd7820bbd11734a14c454b69a6bb8ad KSwitcher-win-x64.zip +eae70031520882cc6a798e11a640fa101df01db12aeba4e6c96d8aa90037ae1e KSwitcher-win-x64.zip diff --git a/resources/app.rc b/resources/app.rc index 28d24b0..f675d8c 100644 --- a/resources/app.rc +++ b/resources/app.rc @@ -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,12,0 - PRODUCTVERSION 0,6,12,0 + FILEVERSION 0,6,13,0 + PRODUCTVERSION 0,6,13,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.12" + VALUE "FileVersion", "0.6.13" END END BLOCK "VarFileInfo" diff --git a/src/app.cpp b/src/app.cpp index 6ba7b11..b6ed949 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -940,6 +940,7 @@ void App::transform(int command, int snippet) std::wstring predicted; for (wchar_t key : recent.keys) predicted += key == L' ' ? L' ' : available[hint].at(physicalLayout().code(key)); + expected = predicted; if (!autoCorrector.suggest(predicted, hint, available, config) || !autoCorrectionAllowed(target, config) || !valid()) return; @@ -979,9 +980,12 @@ void App::transform(int command, int snippet) autoSelected = true; Sleep(25); text = tx.copy(); - // Some editors first select only ") ". Extend by whole-word - // boundaries, then still verify against the complete tracked run. - for (int i = 0; i < 3 && autoClosingSelection(text, config) && tx.valid(); ++i) + // Editors may split hj,bnm at the comma. Extend only a verified + // suffix of the tracked input, and still require a complete match. + for (int i = 0; i < 3 && tx.valid() && + matchAutoInput(recent, text, available, physicalLayout(), config) < 0 && + (autoClosingSelection(text, config) || + (!text.empty() && text.size() < expected.size() && expected.ends_with(text))); ++i) { if (!chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT)) break; diff --git a/src/autocorrect.cpp b/src/autocorrect.cpp index 5d8b35c..880061e 100644 --- a/src/autocorrect.cpp +++ b/src/autocorrect.cpp @@ -152,7 +152,11 @@ std::optional AutoCorrector::suggest(const std::wstring &typed, suffix.insert(suffix.begin(), word.back()); word.pop_back(); } - if (!safeAutoWord(word, settings.autoMinLength) || suffix.size() > 6 || rejected.contains(lower(word))) + // Punctuation keys can be letters in the target layout (for example ',' -> 'б'). + // Validate letters and capitalization after conversion, not before it. + if (settings.autoMinLength < 1 || settings.autoMinLength > 32 || + word.size() < (size_t)settings.autoMinLength || word.size() > 32 || + suffix.size() > 6 || rejected.contains(lower(word))) return {}; for (auto &never : settings.autoNever) if (lower(never) == lower(word)) diff --git a/src/ui.cpp b/src/ui.cpp index 20e3473..dbf7240 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -1072,7 +1072,7 @@ LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) void showAutoOptions(HWND parent, SettingsView *v) { HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic layout correction", autoOptionsProc, - 560, 680); + 560, 680, 0, false); SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent); SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v}); label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525); diff --git a/src/update.h b/src/update.h index 81640c0..5f4f0cc 100644 --- a/src/update.h +++ b/src/update.h @@ -2,7 +2,7 @@ #include "platform.h" namespace ks { -inline constexpr wchar_t AppVersion[] = L"0.6.12"; +inline constexpr wchar_t AppVersion[] = L"0.6.13"; inline constexpr UINT UpdateMessage = WM_APP + 7; inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000; inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000; diff --git a/tests/integration.cpp b/tests/integration.cpp index 835dcb1..51e4bfb 100644 --- a/tests/integration.cpp +++ b/tests/integration.cpp @@ -334,7 +334,25 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp) stage = 21; break; case 21: + { require(text() == L"привет а", "automatic correction preserves immediate next-word typing without a pause"); + auto uk = std::find_if(app->layouts.begin(), app->layouts.end(), [](const auto &layout) { + return ks::layoutLanguage(layout) == L"uk"; + }); + require(uk != app->layouts.end(), "Ukrainian layout available for punctuation-key regression"); + app->settings.autoLanguages = {L"en", L"uk"}; + app->settings.cycle = {app->layouts[en].id, uk->id}; + app->settings.autoWords[L"uk"] = {L"робить"}; + app->autoCorrector = ks::AutoCorrector{}; + ActivateKeyboardLayout(app->layouts[en].handle, 0); + prepare(L"hj,bnm ", false); + record(L"hj,bnm "); + app->transform(ks::AutoCorrect); + stage = 22; + break; + } + case 22: + require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma"); finish(); break; } diff --git a/tests/tests.cpp b/tests/tests.cpp index 00c4bee..a6befb9 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -276,6 +276,15 @@ int main(int argc, char **argv) require(!supplemental.suggest(L"релызик ", 1, layouts, options), "never-correct list takes precedence over personal word"); options.autoNever.clear(); + options.autoWords[L"uk"].push_back(L"робить"); + auto commaWord = supplemental.suggest(L"hj,bnm ", 0, layouts, options); + require(commaWord && commaWord->text == L"робить " && commaWord->target == 2, + "personal Ukrainian word accepts a punctuation key that maps to a letter"); + require(!supplemental.suggest(L"hj,bnm123 ", 0, layouts, options), + "punctuation-key correction still rejects digits"); + options.autoMinLength = 1; + require(!supplemental.suggest(L"ye ", 0, layouts, options), + "known English ye remains protected at minimum length one"); options.cycle = {L"en", L"ru"}; require(!supplemental.suggest(L"релызик ", 1, layouts, options), "personal dictionary respects disabled target layouts");