Release v0.6.13: correct words across punctuation keys

This commit is contained in:
2026-10-30 00:28:16 +02:00
parent 77f27ea087
commit 4cdcc92eb7
11 changed files with 51 additions and 18 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.12 LANGUAGES CXX RC) project(KSwitcher VERSION 0.6.13 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()
Binary file not shown.
+5 -7
View File
@@ -1,16 +1,14 @@
# Latest Windows release # Latest Windows release
## v0.6.12 — 2026-10-30 ## v0.6.13 — 2026-10-30
## Changes ## Changes
- Start automatic layout correction immediately after Space and preserve subsequent typing while the correction runs. - 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.
- Accept identical corrected words shared by multiple selected languages; use the selected language order to choose the target layout. Different valid spellings remain ambiguous. - 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.
- Run the log viewer in the configurator process, separate from the resident input hooks. Clearing logs remains coordinated with the resident process. - Give the fixed-layout automatic-correction settings dialog a fixed window size, retaining scrolling on small displays instead of allowing empty space when resized.
- 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.
## Validation ## 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. **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. For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
+1 -1
View File
@@ -1 +1 @@
866de2d1d6fe121a7845760519f934bf0bd7820bbd11734a14c454b69a6bb8ad KSwitcher-win-x64.zip eae70031520882cc6a798e11a640fa101df01db12aeba4e6c96d8aa90037ae1e 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,12,0 FILEVERSION 0,6,13,0
PRODUCTVERSION 0,6,12,0 PRODUCTVERSION 0,6,13,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.12" VALUE "FileVersion", "0.6.13"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+7 -3
View File
@@ -940,6 +940,7 @@ void App::transform(int command, int snippet)
std::wstring predicted; std::wstring predicted;
for (wchar_t key : recent.keys) for (wchar_t key : recent.keys)
predicted += key == L' ' ? L' ' : available[hint].at(physicalLayout().code(key)); predicted += key == L' ' ? L' ' : available[hint].at(physicalLayout().code(key));
expected = predicted;
if (!autoCorrector.suggest(predicted, hint, available, config) || if (!autoCorrector.suggest(predicted, hint, available, config) ||
!autoCorrectionAllowed(target, config) || !valid()) !autoCorrectionAllowed(target, config) || !valid())
return; return;
@@ -979,9 +980,12 @@ void App::transform(int command, int snippet)
autoSelected = true; autoSelected = true;
Sleep(25); Sleep(25);
text = tx.copy(); text = tx.copy();
// Some editors first select only ") ". Extend by whole-word // Editors may split hj,bnm at the comma. Extend only a verified
// boundaries, then still verify against the complete tracked run. // suffix of the tracked input, and still require a complete match.
for (int i = 0; i < 3 && autoClosingSelection(text, config) && tx.valid(); ++i) 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)) if (!chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT))
break; break;
+5 -1
View File
@@ -152,7 +152,11 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
suffix.insert(suffix.begin(), word.back()); suffix.insert(suffix.begin(), word.back());
word.pop_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 {}; return {};
for (auto &never : settings.autoNever) for (auto &never : settings.autoNever)
if (lower(never) == lower(word)) if (lower(never) == lower(word))
+1 -1
View File
@@ -1072,7 +1072,7 @@ LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
void showAutoOptions(HWND parent, SettingsView *v) void showAutoOptions(HWND parent, SettingsView *v)
{ {
HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic layout correction", autoOptionsProc, 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_HWNDPARENT, (LONG_PTR)parent);
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v}); 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); label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525);
+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.12"; inline constexpr wchar_t AppVersion[] = L"0.6.13";
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;
+18
View File
@@ -334,7 +334,25 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
stage = 21; stage = 21;
break; break;
case 21: case 21:
{
require(text() == L"привет а", "automatic correction preserves immediate next-word typing without a pause"); 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(); finish();
break; break;
} }
+9
View File
@@ -276,6 +276,15 @@ int main(int argc, char **argv)
require(!supplemental.suggest(L"релызик ", 1, layouts, options), require(!supplemental.suggest(L"релызик ", 1, layouts, options),
"never-correct list takes precedence over personal word"); "never-correct list takes precedence over personal word");
options.autoNever.clear(); 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"}; options.cycle = {L"en", L"ru"};
require(!supplemental.suggest(L"релызик ", 1, layouts, options), require(!supplemental.suggest(L"релызик ", 1, layouts, options),
"personal dictionary respects disabled target layouts"); "personal dictionary respects disabled target layouts");