Release v0.6.60: allow apostrophes in custom corrections

This commit is contained in:
agrynco committed 2026-11-09 11:59:49 +02:00
1 parent 4d8d8d47b9
commit 3380e266a6
12 files changed
+49 -18

No files matched your search

+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.59 LANGUAGES CXX RC) project(KSwitcher VERSION 0.6.60 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()
+3 -1
View File
@@ -723,9 +723,11 @@ default and require the main **Automatic correction (experimental)** checkbox to
`првиіт=привіт` or `helo=hello`. Entries are normalized to lowercase; duplicate source words `првиіт=привіт` or `helo=hello`. Entries are normalized to lowercase; duplicate source words
are rejected. The typo must contain 1–32 letters. The correction may contain several words are rejected. The typo must contain 1–32 letters. The correction may contain several words
separated by single spaces, for example `мамамила=мама мила`, up to 32 characters including separated by single spaces, for example `мамамила=мама мила`, up to 32 characters including
spaces. Both sides must use the same supported language, without punctuation; leading, spaces. Corrections also allow apostrophes (`'`, `’`, `ʼ`) between letters, for example
`ревю=рев'ю`. Both sides must use the same supported language, without other punctuation; leading,
trailing and repeated spaces are not allowed. Explicit pairs take priority over dictionary recognition, including 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.
Update every synchronizing PC to v0.6.60 or newer before sharing apostrophe pairs; older clients reject them.
- **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
by inserting, deleting or replacing one letter, or swapping two adjacent letters. Both input by inserting, deleting or replacing one letter, or swapping two adjacent letters. Both input
Binary file not shown.
+8 -6
View File
@@ -1,15 +1,15 @@
# Latest Windows release # Latest Windows release
## v0.6.59 — 2026-11-09 ## v0.6.60 — 2026-11-09
## Changes ## Changes
- Show clear download/upload progress and results, including the server version, applied changes or unchanged local settings. Preserve action errors instead of replacing them with an older status. - Fix custom auto-correction pairs such as `ревю=рев'ю`: correction text now accepts straight (`'`), typographic (`’`) and modifier-letter (`ʼ`) apostrophes between letters, both when saving settings and applying replacements.
- Identify OpenBao HTTP 400 failures as authentication, download or write failures instead of labeling every failure as a rejected write. Failed downloads do not apply remote settings. - Preserve the exact apostrophe, initial capitalization, surrounding punctuation and current layout. Multi-word corrections remain supported, with a maximum of 32 characters including spaces and apostrophes.
- Refresh the already-open Settings window after a successful Download remote, preserving its selected tab. This also works if the Sync dialog was closed while the request was running. Confirmation explains that unsaved Settings edits will be discarded. - Use the same correction validator for settings and runtime. Source words still contain only letters; leading/trailing or adjacent apostrophes, apostrophes next to spaces, other punctuation and mixed-language replacements remain invalid. Update the validation message and user documentation.
- Retain all v0.6.58 LAN, clipboard, screenshot, snippet and chat-profile behavior. No sync schema or compatibility change. - Retain all v0.6.59 sync, LAN, clipboard, screenshot, snippet and chat-profile behavior. No new setting, shortcut or default.
## Validation ## Validation
Passed: 132 sync, 571 input/UI, 341 native, 829 chat-profile/UI, 205 LAN input, 289 LAN clipboard, 43 LAN pairing and 5 settings-save merge checks; dictionary equivalence for 140,939 words; wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress. Regression tests use an isolated mock OpenBao transport, not a live user account. Passed: 349 native checks (including apostrophe validation, correction, capitalization/punctuation and settings roundtrips), 571 input/UI, 829 chat-profile/UI, 132 sync, 205 LAN input, 289 LAN clipboard, 43 LAN pairing and 5 settings-save merge checks; dictionary equivalence for 140,939 words; wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress. Release packaging verifies the manifest, all file hashes and the ZIP checksum.
## Upgrade and compatibility ## Upgrade and compatibility
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.
@@ -17,6 +17,8 @@ For manual installation, exit KSwitcher, close its configurator and screenshot e
**Update every synchronizing PC to v0.6.51 or newer before sharing application-specific snippets. Older versions do not understand application restrictions and may treat scoped snippets as global.** Keep a backup before downgrading. Settings with Telegram Web profiles require v0.6.51 or newer. Chat profiles now synchronize from v0.6.53. Update every synchronizing PC to v0.6.53 or newer before syncing: older clients reject the chat-profile field. Synchronization stores application names, visible chat names and language IDs on your configured OpenBao server. No message contents are included. **Update every synchronizing PC to v0.6.51 or newer before sharing application-specific snippets. Older versions do not understand application restrictions and may treat scoped snippets as global.** Keep a backup before downgrading. Settings with Telegram Web profiles require v0.6.51 or newer. Chat profiles now synchronize from v0.6.53. Update every synchronizing PC to v0.6.53 or newer before syncing: older clients reject the chat-profile field. Synchronization stores application names, visible chat names and language IDs on your configured OpenBao server. No message contents are included.
**Update every synchronizing PC to v0.6.60 or newer before sharing replacement pairs containing apostrophes. Older clients reject these settings.** Existing letter-only and phrase pairs require no migration.
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains no personal settings, credentials or clipboard history. 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
View File
@@ -1 +1 @@
f0d7d15534db21c17b113a25cc07d59a68fa151ecf995ac360ac476751171a9a KSwitcher-win-x64.zip 31e6d5571db106154e3b72391c4f13b856523e602c84a1f8a2935f05c9508854 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,59,0 FILEVERSION 0,6,60,0
PRODUCTVERSION 0,6,59,0 PRODUCTVERSION 0,6,60,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.59" VALUE "FileVersion", "0.6.60"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+1 -1
View File
@@ -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 + L" ") == small.npos) validAutoCorrection(replacement->second, alphabet))
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)
+14
View File
@@ -21,6 +21,20 @@ int snippetAppPriority(const Snippet &snippet, const std::wstring &process)
for (const auto &app : snippet.apps) if (snippetAppName(app) == name && !name.empty()) return 2; for (const auto &app : snippet.apps) if (snippetAppName(app) == name && !name.empty()) return 2;
return 0; return 0;
} }
bool validAutoCorrection(const std::wstring &text, const std::wstring &alphabet)
{
if (text.empty() || text.size() > 32) return false;
auto isLetter = [&](wchar_t c) { return alphabet.find(c) != alphabet.npos; };
for (size_t i = 0; i < text.size(); ++i)
{
if (isLetter(text[i])) continue;
if (text[i] != L' ' && text[i] != L'\'' && text[i] != L'\u2019' && text[i] != L'\u02bc')
return false;
if (i == 0 || i + 1 == text.size() || !isLetter(text[i - 1]) || !isLetter(text[i + 1]))
return false;
}
return true;
}
int HotkeyHook::update(UINT key, bool down, UINT modifiers, int HotkeyHook::update(UINT key, bool down, UINT modifiers,
const std::vector<std::pair<int, Hotkey>> &bindings) const std::vector<std::pair<int, Hotkey>> &bindings)
{ {
+1
View File
@@ -19,6 +19,7 @@ struct Layout
bool hasLetter(wchar_t ch) const; bool hasLetter(wchar_t ch) const;
}; };
bool letter(wchar_t ch); bool letter(wchar_t ch);
bool validAutoCorrection(const std::wstring &text, const std::wstring &alphabet);
std::wstring lower(std::wstring text); std::wstring lower(std::wstring text);
std::wstring upper(std::wstring text); std::wstring upper(std::wstring text);
std::wstring convert(const std::wstring &, const Layout &, const Layout &, bool force = false); std::wstring convert(const std::wstring &, const Layout &, const Layout &, bool force = false);
+2 -3
View File
@@ -138,10 +138,9 @@ void validateSettings(const Settings &s)
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 && from.find_first_not_of(alphabet) == from.npos &&
to.find_first_not_of(std::wstring(alphabet) + L" ") == to.npos && validAutoCorrection(to, alphabet);
to.front() != L' ' && to.back() != L' ' && to.find(L" ") == to.npos;
if (!valid) if (!valid)
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."); 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 words and apostrophes between letters are allowed.");
} }
for (const auto &[language, words] : s.autoWords) for (const auto &[language, words] : s.autoWords)
{ {
+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.59"; inline constexpr wchar_t AppVersion[] = L"0.6.60";
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;
+14 -1
View File
@@ -592,6 +592,16 @@ int main(int argc, char **argv)
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"мама мила"; options.autoReplacements[L"мамамила"] = L"мама мила";
for (const auto *text : {L"рев'ю", L"рев’ю", L"ревʼю", L"моє рев'ю"})
{
options.autoReplacements[L"ревю"] = text;
ks::validateSettings(options);
correction = edited.suggest(L"(Ревю)! ", 2, layouts, options);
auto expected = std::wstring(text);
expected[0] = ks::upper(expected.substr(0, 1))[0];
require(correction && correction->text == L"(" + expected + L")! " && correction->target == 2,
"apostrophe corrections preserve punctuation, capitalization and layout");
}
correction = edited.suggest(L"(Мамамила)! ", 2, layouts, options); correction = edited.suggest(L"(Мамамила)! ", 2, layouts, options);
require(correction && correction->text == L"(Мама мила)! " && correction->target == 2, require(correction && correction->text == L"(Мама мила)! " && correction->target == 2,
"joined words split while preserving capitalization, punctuation and layout"); "joined words split while preserving capitalization, punctuation and layout");
@@ -718,7 +728,8 @@ 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"}, {L"мамамила", L"мама мила"}}; settings.autoReplacements = {{L"првиіт", L"привіт"}, {L"helo", L"hello"}, {L"мамамила", L"мама мила"},
{L"ревю", L"рев'ю"}, {L"пять", L"п’ять"}, {L"імя", L"імʼя"}};
settings.autoMaxRank = 10000; settings.autoMaxRank = 10000;
settings.autoMinLength = 5; settings.autoMinLength = 5;
settings.autoPrefixChars = L"(#"; settings.autoPrefixChars = L"(#";
@@ -739,6 +750,8 @@ int main(int argc, char **argv)
{L"helloworld", L"hello\nworld"}, {L"hello world", L"helloworld"}, {L"helloworld", L"hello\nworld"}, {L"hello world", L"helloworld"},
{L"helloworld", L"hello світ"}, {L"helloworld", L"hello world!"}, {L"helloworld", L"hello світ"}, {L"helloworld", L"hello world!"},
{L"helloworld", L"hello Hello"}, {L"helloworld", L""}, {L"helloworld", L"hello Hello"}, {L"helloworld", L""},
{L"review", L"'review"}, {L"review", L"review’"},
{L"review", L"re''view"}, {L"review", L"reʼ view"},
{L"helloworld", L"abcdefghijklmnopqrstuvwxyz abcdef"}}) {L"helloworld", L"abcdefghijklmnopqrstuvwxyz abcdef"}})
{ {
auto invalid = settings; auto invalid = settings;