Release v0.6.47: allow multi-word auto-correction replacements
This commit is contained in:
1 parent
5de5b791bf
commit
e33387c2d5
13 files changed
+48
-29
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.46 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.47 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()
|
||||||
|
|||||||
@@ -599,8 +599,10 @@ default and require the main **Automatic correction (experimental)** checkbox to
|
|||||||
|
|
||||||
- **Use custom replacements**: enter one `typo=correction` pair per line, for example
|
- **Use custom replacements**: enter one `typo=correction` pair per line, for example
|
||||||
`првиіт=привіт` or `helo=hello`. Entries are normalized to lowercase; duplicate source words
|
`првиіт=привіт` or `helo=hello`. Entries are normalized to lowercase; duplicate source words
|
||||||
are rejected. Each side must contain 1–32 letters of the same supported language, without
|
are rejected. The typo must contain 1–32 letters. The correction may contain several words
|
||||||
spaces or punctuation. Explicit pairs take priority over dictionary recognition, including
|
separated by single spaces, for example `мамамила=мама мила`, up to 32 characters including
|
||||||
|
spaces. Both sides must use the same supported language, without punctuation; leading,
|
||||||
|
trailing and repeated spaces are not allowed. Explicit pairs take priority over dictionary recognition, including
|
||||||
already valid words, and do not require dictionary membership or satisfy the frequency limit.
|
already valid words, and do not require dictionary membership or satisfy the frequency limit.
|
||||||
- **Correct spelling typos using dictionaries**: checks unknown words in the current language
|
- **Correct spelling typos using dictionaries**: checks unknown words in the current language
|
||||||
after no layout correction is found. It accepts exactly one dictionary candidate reachable
|
after no layout correction is found. It accepts exactly one dictionary candidate reachable
|
||||||
@@ -616,6 +618,8 @@ ALL CAPS, mixed-case identifiers, numbers, URLs and email addresses are skipped.
|
|||||||
is applied once, without chaining. Click **OK**, then **Save** in Settings; pairs and switches
|
is applied once, without chaining. Click **OK**, then **Save** in Settings; pairs and switches
|
||||||
are included in settings export/import and synchronization. Undo restores the original word
|
are included in settings export/import and synchronization. Undo restores the original word
|
||||||
and suppresses its correction until restart.
|
and suppresses its correction until restart.
|
||||||
|
When synchronizing phrase replacements, update all participating PCs to v0.6.47 or newer;
|
||||||
|
older clients reject spaces in replacement pairs.
|
||||||
|
|
||||||
### Layout correction and shared options
|
### Layout correction and shared options
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
+10
-9
@@ -1,21 +1,22 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.46 — 2026-11-05
|
## v0.6.47 — 2026-11-05
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Bundle **tools/StayHere.exe** and **tools/StayHere-README.md**. Start the activity utility from **Utilities → StayHere**; it does not start automatically with KSwitcher.
|
- Custom auto-correction replacements now accept several words on the correction side, for example `мамамила=мама мила`. The source remains a single word of 1–32 letters; the correction supports up to 32 characters including single spaces between words, in the same supported language.
|
||||||
- StayHere starts enabled. Left-click its tray icon to pause/resume or right-click for Exit. While enabled, it prevents automatic display/system sleep and checks inactivity every 15 seconds; after three minutes without input it sends F15. It does not move the mouse, bypass locking, install autostart or directly control Teams presence. Locked/disconnected sessions receive no simulated activity.
|
- Preserve initial capitalization, surrounding punctuation, the current layout and Undo when splitting joined words. Subsequent typing is preserved. Updated validation messages and the options hint explain support for spaces.
|
||||||
- Include StayHere source and reproducible GCC/CMake builds. CLI status/pause/resume/exit parsing tolerates launcher whitespace. Normal updates replace bundled StayHere and documentation while preserving personal utilities. A running old instance continues until exited; relaunch to use the updated executable.
|
- Enable **Automatic correction (experimental)** and **Use custom replacements**, enter one `typo=correction` pair per line, then click **OK → Save**. Existing language, minimum-length and exclusion settings still apply; defaults are unchanged.
|
||||||
- Retain all v0.6.45 Far snippet, Utilities and tools-folder changes and the v0.6.44 newest-history-selection fix.
|
- Retain bundled StayHere and all earlier fixes. The tray menu remains **Utilities → StayHere**.
|
||||||
- Correct the Far test fixture's working directory: a long temporary path could scroll the command line and invalidate absolute cursor-coordinate assertions. Application snippet behavior is unchanged in this release.
|
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
Passed isolated StayHere start/status/pause/resume/exit checks, Utilities regressions, dictionary equivalence (140,939 words), 105 sync checks, shared wheel tests, 319 native checks, 574 input checks, screenshot UI/save/registration, 54 updater checks and updater progress UI. The updater replaced a running isolated StayHere executable successfully. Final package manifests and downloaded assets are checked before completion. Real-Far spot checks confirmed expansion, caret placement and markers; the uninterrupted full interactive run remains outstanding because unrelated foreground changes cancel it. No additional Far application-code change is shipped in v0.6.46.
|
Passed 332 native regression checks, 574 input/settings checks and focused auto-correction integration checks, including phrase replacement, capitalization/punctuation, Undo and immediate subsequent typing. Also passed 105 synchronization checks and 54 updater regression checks. Release binaries, package manifest and SHA-256 checksums are verified; published assets are read back before completion.
|
||||||
|
|
||||||
## Upgrade
|
## 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.
|
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 its configurator and screenshot editors, and extract the entire ZIP into its installation folder. Keep personal `data/`, customized dictionaries and your own tools. Find `tools/StayHere.exe` beside the executable actually running.
|
For manual installation, exit KSwitcher, close its configurator and screenshot editors, and extract the entire ZIP into its installation folder. Keep personal `data/`, customized dictionaries and your own tools. Keep the three KSwitcher executables together; StayHere runs independently.
|
||||||
|
|
||||||
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. Keep the three KSwitcher executables together; history uses another instance of KSwitcher.exe. StayHere runs independently. The package contains no personal settings, credentials or clipboard history.
|
When synchronizing phrase replacements across PCs, update each participating PC to v0.6.47 or newer: older versions reject spaces in replacement pairs.
|
||||||
|
|
||||||
|
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains no personal settings, credentials or clipboard history.
|
||||||
|
|
||||||
Publish stable `release-vX.Y.Z` tags matching the executable version. Keep `v0.4.2` as the last legacy tag.
|
Publish stable `release-vX.Y.Z` tags matching the executable version. Keep `v0.4.2` as the last legacy tag.
|
||||||
@@ -1 +1 @@
|
|||||||
e6d26d59b18559ad226359b35cd403c68326384f8e1918d31b368cbd1110f6b9 KSwitcher-win-x64.zip
|
c15371f89ea86410267d96c909739b44c207cd76256cd29dc7b5f40b04ac7ffb 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,46,0
|
FILEVERSION 0,6,47,0
|
||||||
PRODUCTVERSION 0,6,46,0
|
PRODUCTVERSION 0,6,47,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.46"
|
VALUE "FileVersion", "0.6.47"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+1
-1
@@ -257,7 +257,7 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
|
|||||||
{
|
{
|
||||||
auto replacement = settings.autoReplacements.find(small);
|
auto replacement = settings.autoReplacements.find(small);
|
||||||
if (replacement != settings.autoReplacements.end() &&
|
if (replacement != settings.autoReplacements.end() &&
|
||||||
replacement->second.find_first_not_of(alphabet) == small.npos)
|
replacement->second.find_first_not_of(alphabet + L" ") == small.npos)
|
||||||
return sameLayout(replacement->second);
|
return sameLayout(replacement->second);
|
||||||
}
|
}
|
||||||
auto personal = [&](const std::wstring &language, const std::wstring &candidate)
|
auto personal = [&](const std::wstring &language, const std::wstring &candidate)
|
||||||
|
|||||||
+4
-2
@@ -118,9 +118,11 @@ void validateSettings(const Settings &s)
|
|||||||
L"абвгдеёжзийклмнопрстуфхцчшщъыьэюя"})
|
L"абвгдеёжзийклмнопрстуфхцчшщъыьэюя"})
|
||||||
valid |= !from.empty() && !to.empty() && from.size() <= 32 && to.size() <= 32 &&
|
valid |= !from.empty() && !to.empty() && from.size() <= 32 && to.size() <= 32 &&
|
||||||
from == lower(from) && to == lower(to) && from != to &&
|
from == lower(from) && to == lower(to) && from != to &&
|
||||||
from.find_first_not_of(alphabet) == from.npos && to.find_first_not_of(alphabet) == to.npos;
|
from.find_first_not_of(alphabet) == from.npos &&
|
||||||
|
to.find_first_not_of(std::wstring(alphabet) + L" ") == to.npos &&
|
||||||
|
to.front() != L' ' && to.back() != L' ' && to.find(L" ") == to.npos;
|
||||||
if (!valid)
|
if (!valid)
|
||||||
throw std::runtime_error("Replacement pairs must be different lowercase words of 1-32 letters in the same language");
|
throw std::runtime_error("Use a lowercase typo of 1-32 letters and a different correction of 1-32 characters in the same language. Single spaces between correction words are allowed.");
|
||||||
}
|
}
|
||||||
for (const auto &[language, words] : s.autoWords)
|
for (const auto &[language, words] : s.autoWords)
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -1285,7 +1285,7 @@ void showAutoOptions(HWND parent, SettingsView *v)
|
|||||||
label(w, L"Enter punctuation without spaces or separators. These characters are kept.", 18, 530, 525);
|
label(w, L"Enter punctuation without spaces or separators. These characters are kept.", 18, 530, 525);
|
||||||
checkbox(w, L"Use custom &replacements (take priority over dictionaries)", 414, 18, 562, 524);
|
checkbox(w, L"Use custom &replacements (take priority over dictionaries)", 414, 18, 562, 524);
|
||||||
check(w, 414, v->draft.autoReplace);
|
check(w, 414, v->draft.autoReplace);
|
||||||
label(w, L"One pair per line: typo=correction (same language, letters only)", 18, 592, 525);
|
label(w, L"One pair per line: typo=correction (same language; correction may contain spaces)", 18, 592, 525);
|
||||||
edit(w, 416, 18, 618, 524, 70, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL | ES_WANTRETURN);
|
edit(w, 416, 18, 618, 524, 70, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL | ES_WANTRETURN);
|
||||||
std::wstring pairs;
|
std::wstring pairs;
|
||||||
for (const auto &[from, to] : v->draft.autoReplacements) pairs += from + L"=" + to + L"\r\n";
|
for (const auto &[from, to] : v->draft.autoReplacements) pairs += from + L"=" + to + L"\r\n";
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.46";
|
inline constexpr wchar_t AppVersion[] = L"0.6.47";
|
||||||
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;
|
||||||
|
|||||||
+2
-2
@@ -901,7 +901,7 @@ int main(int argc, char **argv) {
|
|||||||
"personal dictionary field fits options window");
|
"personal dictionary field fits options window");
|
||||||
}
|
}
|
||||||
SetDlgItemTextW(autoOptions,412,L"Релізик\r\nрелізик, словечко");
|
SetDlgItemTextW(autoOptions,412,L"Релізик\r\nрелізик, словечко");
|
||||||
SetDlgItemTextW(autoOptions,416,L" Првиіт = Привіт\r\nhelo=hello");
|
SetDlgItemTextW(autoOptions,416,L" Првиіт = Привіт\r\nhelo=hello\r\n Мамамила = Мама мила ");
|
||||||
SendDlgItemMessageW(autoOptions,414,BM_SETCHECK,BST_CHECKED,0);
|
SendDlgItemMessageW(autoOptions,414,BM_SETCHECK,BST_CHECKED,0);
|
||||||
SendDlgItemMessageW(autoOptions,415,BM_SETCHECK,BST_CHECKED,0);
|
SendDlgItemMessageW(autoOptions,415,BM_SETCHECK,BST_CHECKED,0);
|
||||||
SendMessageW(autoOptions,WM_COMMAND,IDOK,0);
|
SendMessageW(autoOptions,WM_COMMAND,IDOK,0);
|
||||||
@@ -923,7 +923,7 @@ int main(int argc, char **argv) {
|
|||||||
"personal dictionary normalizes case, newlines and duplicate words");
|
"personal dictionary normalizes case, newlines and duplicate words");
|
||||||
wchar_t pairs[128]{};
|
wchar_t pairs[128]{};
|
||||||
GetDlgItemTextW(autoOptions,416,pairs,128);
|
GetDlgItemTextW(autoOptions,416,pairs,128);
|
||||||
require(std::wstring(pairs)==L"helo=hello\r\nпрвиіт=привіт\r\n" &&
|
require(std::wstring(pairs)==L"helo=hello\r\nмамамила=мама мила\r\nпрвиіт=привіт\r\n" &&
|
||||||
SendDlgItemMessageW(autoOptions,414,BM_GETCHECK,0,0)==BST_CHECKED &&
|
SendDlgItemMessageW(autoOptions,414,BM_GETCHECK,0,0)==BST_CHECKED &&
|
||||||
SendDlgItemMessageW(autoOptions,415,BM_GETCHECK,0,0)==BST_CHECKED,
|
SendDlgItemMessageW(autoOptions,415,BM_GETCHECK,0,0)==BST_CHECKED,
|
||||||
"typo options and normalized replacement pairs survive reopening");
|
"typo options and normalized replacement pairs survive reopening");
|
||||||
|
|||||||
@@ -802,14 +802,14 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
case 22:
|
case 22:
|
||||||
require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma");
|
require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma");
|
||||||
app->settings.autoReplace = true;
|
app->settings.autoReplace = true;
|
||||||
app->settings.autoReplacements = {{L"helo", L"hello"}};
|
app->settings.autoReplacements = {{L"helo", L"hello world"}};
|
||||||
ActivateKeyboardLayout(app->layouts[en].handle, 0);
|
ActivateKeyboardLayout(app->layouts[en].handle, 0);
|
||||||
prepare(L"(Helo)! ", false);
|
prepare(L"(Helo)! ", false);
|
||||||
record(L"(Helo)! ");
|
record(L"(Helo)! ");
|
||||||
app->transform(ks::AutoCorrect);
|
app->transform(ks::AutoCorrect);
|
||||||
break;
|
break;
|
||||||
case 23:
|
case 23:
|
||||||
require(text() == L"(Hello)! ", "explicit typo pair replaces verified text with punctuation and capital");
|
require(text() == L"(Hello world)! ", "explicit phrase pair replaces verified text with punctuation and capital");
|
||||||
app->transform(ks::Undo);
|
app->transform(ks::Undo);
|
||||||
break;
|
break;
|
||||||
case 24:
|
case 24:
|
||||||
@@ -835,7 +835,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
case 27:
|
case 27:
|
||||||
require(text() == L"kswithcer ", "Undo suppresses the rejected spelling correction");
|
require(text() == L"kswithcer ", "Undo suppresses the rejected spelling correction");
|
||||||
app->settings.autoReplace = true;
|
app->settings.autoReplace = true;
|
||||||
app->settings.autoReplacements = {{L"tehh", L"the"}};
|
app->settings.autoReplacements = {{L"tehh", L"the word"}};
|
||||||
app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages);
|
app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages);
|
||||||
prepare(L"", false);
|
prepare(L"", false);
|
||||||
{
|
{
|
||||||
@@ -852,7 +852,7 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 28:
|
case 28:
|
||||||
require(text() == L"the x", "custom replacement through keyboard hook preserves subsequent typing");
|
require(text() == L"the word x", "custom phrase replacement through keyboard hook preserves subsequent typing");
|
||||||
app->settings.autoReplace = false;
|
app->settings.autoReplace = false;
|
||||||
app->settings.autoTypos = false;
|
app->settings.autoTypos = false;
|
||||||
if (typosOnly) { finish(); break; }
|
if (typosOnly) { finish(); break; }
|
||||||
|
|||||||
+14
-2
@@ -590,6 +590,12 @@ int main(int argc, char **argv)
|
|||||||
options.autoNever = {L"hlelo"};
|
options.autoNever = {L"hlelo"};
|
||||||
require(!edited.suggest(L"hlelo ", 0, layouts, options), "never-correct overrides explicit pairs");
|
require(!edited.suggest(L"hlelo ", 0, layouts, options), "never-correct overrides explicit pairs");
|
||||||
options.autoNever.clear();
|
options.autoNever.clear();
|
||||||
|
options.autoReplacements[L"мамамила"] = L"мама мила";
|
||||||
|
correction = edited.suggest(L"(Мамамила)! ", 2, layouts, options);
|
||||||
|
require(correction && correction->text == L"(Мама мила)! " && correction->target == 2,
|
||||||
|
"joined words split while preserving capitalization, punctuation and layout");
|
||||||
|
edited.reject(L"мамамила ", options);
|
||||||
|
require(!edited.suggest(L"мамамила ", 2, layouts, options), "undo suppresses phrase replacements");
|
||||||
edited.reject(L"(Hlelo)! ", options);
|
edited.reject(L"(Hlelo)! ", options);
|
||||||
require(!edited.suggest(L"hlelo ", 0, layouts, options), "undo rejection suppresses custom replacements");
|
require(!edited.suggest(L"hlelo ", 0, layouts, options), "undo rejection suppresses custom replacements");
|
||||||
options.autoReplace = false;
|
options.autoReplace = false;
|
||||||
@@ -711,7 +717,7 @@ int main(int argc, char **argv)
|
|||||||
settings.elevatedAutostart = true;
|
settings.elevatedAutostart = true;
|
||||||
settings.autoCorrect = true;
|
settings.autoCorrect = true;
|
||||||
settings.autoReplace = settings.autoTypos = true;
|
settings.autoReplace = settings.autoTypos = true;
|
||||||
settings.autoReplacements = {{L"првиіт", L"привіт"}, {L"helo", L"hello"}};
|
settings.autoReplacements = {{L"првиіт", L"привіт"}, {L"helo", L"hello"}, {L"мамамила", L"мама мила"}};
|
||||||
settings.autoMaxRank = 10000;
|
settings.autoMaxRank = 10000;
|
||||||
settings.autoMinLength = 5;
|
settings.autoMinLength = 5;
|
||||||
settings.autoPrefixChars = L"(#";
|
settings.autoPrefixChars = L"(#";
|
||||||
@@ -726,7 +732,13 @@ int main(int argc, char **argv)
|
|||||||
require(restored.autoWords == settings.autoWords, "personal dictionaries survive settings roundtrip");
|
require(restored.autoWords == settings.autoWords, "personal dictionaries survive settings roundtrip");
|
||||||
require(restored.autoReplace && restored.autoTypos && restored.autoReplacements == settings.autoReplacements,
|
require(restored.autoReplace && restored.autoTypos && restored.autoReplacements == settings.autoReplacements,
|
||||||
"typo options and Unicode pairs survive settings roundtrip");
|
"typo options and Unicode pairs survive settings roundtrip");
|
||||||
for (const auto &pair : {std::pair{L"hello", L"привіт"}, {L"hello", L"hello"}, {L"hi!", L"hello"}})
|
for (const auto &pair : {std::pair{L"hello", L"привіт"}, {L"hello", L"hello"}, {L"hi!", L"hello"},
|
||||||
|
{L"helloworld", L" hello world"}, {L"helloworld", L"hello world "},
|
||||||
|
{L"helloworld", L"hello world"}, {L"helloworld", L"hello\tworld"},
|
||||||
|
{L"helloworld", L"hello\nworld"}, {L"hello world", L"helloworld"},
|
||||||
|
{L"helloworld", L"hello світ"}, {L"helloworld", L"hello world!"},
|
||||||
|
{L"helloworld", L"hello Hello"}, {L"helloworld", L""},
|
||||||
|
{L"helloworld", L"abcdefghijklmnopqrstuvwxyz abcdef"}})
|
||||||
{
|
{
|
||||||
auto invalid = settings;
|
auto invalid = settings;
|
||||||
invalid.autoReplacements = {{pair.first, pair.second}};
|
invalid.autoReplacements = {{pair.first, pair.second}};
|
||||||
|
|||||||
Reference in new issue
Block a user