Compare commits

...
21 changed files with 144 additions and 34 deletions

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.58 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()
+5 -3
View File
@@ -590,8 +590,8 @@ Nonconflicting changes synchronize immediately, and unresolved conflicts survive
- **Upload local** creates the remote profile or replaces it with this PC's saved shared settings. - **Upload local** creates the remote profile or replaces it with this PC's saved shared settings.
- **Download remote** replaces this PC's shared settings with the server profile. - **Download remote** replaces this PC's shared settings with the server profile.
- If the server has no profile yet, choose **Upload local** to create it. - If the server has no profile yet, choose **Upload local** to create it.
- Unsaved edits in Settings are not synchronized. Close and reopen Settings after a download; - Unsaved edits in Settings are not synchronized. After a successful **Download remote**, the open Settings window refreshes automatically and keeps the selected tab. The download confirmation warns that unsaved edits will be discarded.
the existing stale-editor check prevents an old Settings window overwriting downloaded changes. - The status distinguishes downloading, downloaded settings, no local changes and failure. HTTP 400 errors identify authentication, download or upload; field errors remain visible until the next action.
Once connected, the resident checks at startup, every **10 minutes**, and about two seconds after saving settings, on a separate Once connected, the resident checks at startup, every **10 minutes**, and about two seconds after saving settings, on a separate
network thread. Errors back off for 15 minutes; the Sync button allows a manual retry. Local use network thread. Errors back off for 15 minutes; the Sync button allows a manual retry. Local use
@@ -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 -7
View File
@@ -1,16 +1,15 @@
# Latest Windows release # Latest Windows release
## v0.6.58 — 2026-11-06 ## v0.6.60 — 2026-11-09
## Changes ## Changes
- Keep trusted TCP input listeners and connections alive across missing discovery announcements. Retain the last known address for reconnection; authenticate incoming trusted peers even before discovery sees their address. - 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.
- Show saved pairing separately from actual input connectivity: connected, disabled here, offline or not connected. Background identity verification does not flash the list. - 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.
- Log TCP listener failures with the Windows error and peer connection state changes for troubleshooting. - 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.59 sync, LAN, clipboard, screenshot, snippet and chat-profile behavior. No new setting, shortcut or default.
## Validation ## Validation
Passed: 205 LAN input checks (including missed discovery and authentication before discovery), 289 clipboard transfer, 43 pairing, 829 settings/chat UI, 570 input, 341 native, 126 sync 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. LAN remains experimental; three physical PCs have not been verified together. 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.
Update both PCs to receive these fixes. Preserve each PC's own data directory and pairing identity. Input protocol v2 remains compatible with v0.6.57. A changed instance ID requires pairing again. The computer list refreshes automatically once per second while open.
## 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.
@@ -18,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 @@
16d8138c63442931bf947707b28820d9b89943169ac9f25db3cb50d23c0ff319 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,58,0 FILEVERSION 0,6,60,0
PRODUCTVERSION 0,6,58,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.58" VALUE "FileVersion", "0.6.60"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+16 -2
View File
@@ -13,7 +13,7 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
validateSyncConfig(c); validateSyncConfig(c);
if (action == 4 && (c != conflictConnection || choices.empty())) if (action == 4 && (c != conflictConnection || choices.empty()))
throw std::runtime_error("Connection changed or no choices selected. Sync again."); throw std::runtime_error("Connection changed or no choices selected. Sync again.");
if (busy) return L"Wait for the current text operation to finish."; if (busy) return syncStatus = L"Sync did not start. Wait for the current text operation to finish, then try again.";
syncRetryAfter = 0; // Explicit retries may bypass the automatic error backoff. syncRetryAfter = 0; // Explicit retries may bypass the automatic error backoff.
if (syncWorker.joinable()) syncWorker.join(); if (syncWorker.joinable()) syncWorker.join();
syncConnection = c; syncConnection = c;
@@ -22,7 +22,8 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
auto availableLayouts = layouts; auto availableLayouts = layouts;
syncRunning = true; syncRunning = true;
syncDone = false; syncDone = false;
syncStatus = L"Connecting to OpenBao..."; syncStatus = action == 2 ? L"Downloading remote settings from OpenBao..." :
action == 3 ? L"Uploading local settings to OpenBao..." : L"Connecting to OpenBao...";
nextSync = GetTickCount64() + SyncIntervalMs; nextSync = GetTickCount64() + SyncIntervalMs;
try try
{ {
@@ -42,6 +43,9 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
} }
catch (const nlohmann::json::exception &) { result.status = L"Invalid sync data or server response."; } catch (const nlohmann::json::exception &) { result.status = L"Invalid sync data or server response."; }
catch (const std::exception &e) { result.status = wide(e.what()); } catch (const std::exception &e) { result.status = wide(e.what()); }
if (!result.success)
result.status = std::wstring(action == 2 ? L"Download failed. Local settings were not changed. " :
action == 3 ? L"Upload failed. " : L"Synchronization failed. ") + result.status;
syncResult = std::move(result); syncResult = std::move(result);
syncDone.store(true); syncDone.store(true);
}); });
@@ -76,8 +80,18 @@ void App::pollSync()
{ {
atomicWrite(data / L"settings-before-sync.bin", protect(snapshot)); atomicWrite(data / L"settings-before-sync.bin", protect(snapshot));
applySettings(next); applySettings(next);
syncStatus += L" Changes applied on this PC; local backup saved.";
} }
else syncStatus += result.conflicts.empty() ? L" This PC's settings are already up to date; no local changes needed." :
L" No local settings were changed; conflicts still need your choice.";
saveSyncBase(data, syncConnection, result.base.empty() ? result.settings : result.base); saveSyncBase(data, syncConnection, result.base.empty() ? result.settings : result.base);
if (result.downloaded)
{
if (settingsWindow) SendMessageW(settingsWindow, ConfigSettingsDownloaded, 0, 0);
if (auto config = FindWindowW(L"KSwitcher.Config", nullptr);
config && configurationPeer(config, L"KSwitcher.Config.exe"))
PostMessageW(config, ConfigSettingsDownloaded, 0, 0);
}
pendingSyncConflicts = std::move(result.conflicts); pendingSyncConflicts = std::move(result.conflicts);
conflictConnection = syncConnection; conflictConnection = syncConnection;
SYSTEMTIME time{}; GetLocalTime(&time); SYSTEMTIME time{}; GetLocalTime(&time);
+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)
+13
View File
@@ -52,6 +52,19 @@ LRESULT CALLBACK controller(HWND window, UINT message, WPARAM wp, LPARAM lp)
} }
return FALSE; return FALSE;
} }
if (message == ConfigSettingsDownloaded)
{
try
{
app->settings = decodeSettings(request(*app, ConfigRead), app->layouts);
if (app->settingsWindow) SendMessageW(app->settingsWindow, ConfigSettingsDownloaded, 0, 0);
}
catch (const std::exception &e)
{
MessageBoxW(app->settingsWindow, wide(e.what()).c_str(), L"Unable to refresh downloaded settings", MB_OK | MB_ICONERROR);
}
return 0;
}
if (message == WM_APP + 20) if (message == WM_APP + 20)
{ {
try try
+1
View File
@@ -2,6 +2,7 @@
#include "app.h" #include "app.h"
namespace ks namespace ks
{ {
inline constexpr UINT ConfigSettingsDownloaded = WM_APP + 24;
inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1, inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
ConfigLayouts = ConfigRead + 2, ConfigClearHistory = ConfigRead + 3, ConfigLayouts = ConfigRead + 2, ConfigClearHistory = ConfigRead + 3,
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5, ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
+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)
{ {
+14 -6
View File
@@ -39,7 +39,7 @@ class OpenBao final : public SyncProvider
SyncConfig config; SyncConfig config;
SyncTransport transport; SyncTransport transport;
std::wstring token; std::wstring token;
Json response(unsigned status, const Bytes &result, bool missing) Json response(unsigned status, const Bytes &result, bool missing, const std::wstring &path, const wchar_t *method)
{ {
if (result.size() > Limit) throw std::runtime_error("OpenBao response is too large."); if (result.size() > Limit) throw std::runtime_error("OpenBao response is too large.");
if (status == 404 && missing) if (status == 404 && missing)
@@ -50,7 +50,14 @@ class OpenBao final : public SyncProvider
return nullptr; return nullptr;
} }
if (status == 401 || status == 403) throw std::runtime_error("OpenBao denied access. Check login and profile policy."); if (status == 401 || status == 403) throw std::runtime_error("OpenBao denied access. Check login and profile policy.");
if (status == 400) throw std::runtime_error("OpenBao rejected the write or its version changed. Sync again; check KV v2 configuration."); if (status == 400)
{
if (path.starts_with(L"/v1/auth/"))
throw std::runtime_error("OpenBao rejected the authentication request (HTTP 400). Check username, password and login mount.");
if (std::wstring_view(method) == L"GET")
throw std::runtime_error("OpenBao rejected the download request (HTTP 400). Check KV v2 mount and profile path.");
throw std::runtime_error("OpenBao rejected the write (HTTP 400) or its version changed. Sync again; check KV v2 configuration.");
}
if (status < 200 || status >= 300) throw std::runtime_error("OpenBao HTTP error " + std::to_string(status) + "."); if (status < 200 || status >= 300) throw std::runtime_error("OpenBao HTTP error " + std::to_string(status) + ".");
return result.empty() ? Json::object() : parse(result); return result.empty() ? Json::object() : parse(result);
} }
@@ -59,7 +66,7 @@ class OpenBao final : public SyncProvider
if (transport) if (transport)
{ {
auto reply = transport(method, path, token, body ? bytes(*body) : Bytes{}); auto reply = transport(method, path, token, body ? bytes(*body) : Bytes{});
return response(reply.status, reply.body, missing); return response(reply.status, reply.body, missing, path, method);
} }
URL_COMPONENTS url{sizeof(url)}; URL_COMPONENTS url{sizeof(url)};
url.dwHostNameLength = url.dwUrlPathLength = url.dwExtraInfoLength = (DWORD)-1; url.dwHostNameLength = url.dwUrlPathLength = url.dwExtraInfoLength = (DWORD)-1;
@@ -98,7 +105,7 @@ class OpenBao final : public SyncProvider
if (result.size() + received > Limit) throw std::runtime_error("OpenBao response is too large."); if (result.size() + received > Limit) throw std::runtime_error("OpenBao response is too large.");
result.insert(result.end(), chunk, chunk + received); result.insert(result.end(), chunk, chunk + received);
} }
return response(status, result, missing); return response(status, result, missing, path, method);
} }
public: public:
explicit OpenBao(const SyncConfig &c, SyncTransport t) : config(c), transport(std::move(t)) explicit OpenBao(const SyncConfig &c, SyncTransport t) : config(c), transport(std::move(t))
@@ -306,9 +313,10 @@ SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &l
auto revision = remote.revision; auto revision = remote.revision;
if (parse(merged.remote) != parse(remote.settings) && action != SyncAction::Download) revision = provider.write(merged.remote, revision); if (parse(merged.remote) != parse(remote.settings) && action != SyncAction::Download) revision = provider.write(merged.remote, revision);
return {true, merged.local, revision, return {true, merged.local, revision,
merged.conflicts.empty() ? L"Synchronized. Server version " + std::to_wstring(revision) + L"." : merged.conflicts.empty() ? std::wstring(action == SyncAction::Download ? L"Downloaded remote settings. Server version " :
action == SyncAction::Upload ? L"Uploaded local settings. Server version " : L"Synchronized. Server version ") + std::to_wstring(revision) + L"." :
L"Other changes synchronized. Resolve " + std::to_wstring(merged.conflicts.size()) + L" conflicting setting(s).", L"Other changes synchronized. Resolve " + std::to_wstring(merged.conflicts.size()) + L" conflicting setting(s).",
merged.base, merged.conflicts}; merged.base, merged.conflicts, action == SyncAction::Download};
} }
Bytes encodeSyncConflicts(const std::vector<SyncConflict> &conflicts) Bytes encodeSyncConflicts(const std::vector<SyncConflict> &conflicts)
{ {
+1
View File
@@ -63,6 +63,7 @@ struct SyncResult
std::wstring status; std::wstring status;
Bytes base; Bytes base;
std::vector<SyncConflict> conflicts; std::vector<SyncConflict> conflicts;
bool downloaded{};
}; };
SyncResult synchronize(SyncProvider &, const Bytes &base, const Bytes &local, SyncAction, SyncResult synchronize(SyncProvider &, const Bytes &base, const Bytes &local, SyncAction,
const std::function<void(const Bytes &)> &validate = {}, const std::vector<SyncChoice> &choices = {}, const std::function<void(const Bytes &)> &validate = {}, const std::vector<SyncChoice> &choices = {},
+4
View File
@@ -1688,6 +1688,10 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{ {
switch (msg) switch (msg)
{ {
case ConfigSettingsDownloaded:
v->draft = v->app->settings;
fillSettings(w, v);
return 0;
case WM_ENABLE: case WM_ENABLE:
if (wp) updateSettingsState(w, v); if (wp) updateSettingsState(w, v);
break; break;
+9 -4
View File
@@ -1,5 +1,5 @@
#include "ui_sync_conflicts.inl" #include "ui_sync_conflicts.inl"
struct SyncView { HWND parent; App *app; }; struct SyncView { HWND parent; App *app; bool actionError{}; };
void updateSyncControls(HWND w, App &app) void updateSyncControls(HWND w, App &app)
{ {
bool enabled = checked(w, 601); bool enabled = checked(w, 601);
@@ -17,7 +17,8 @@ void updateSyncControls(HWND w, App &app)
status = L"Configure the connection, then click Save / Sign in / Sync to enable synchronization."; status = L"Configure the connection, then click Save / Sign in / Sync to enable synchronization.";
else else
status = app.syncCommand(); status = app.syncCommand();
if (value(w, 608) != status) value(w, 608, status); auto *view = reinterpret_cast<SyncView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
if ((!view || !view->actionError) && value(w, 608) != status) value(w, 608, status);
} }
SyncConfig syncFields(HWND w) SyncConfig syncFields(HWND w)
{ {
@@ -47,6 +48,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
int id = LOWORD(wp); int id = LOWORD(wp);
if (id == 601 && HIWORD(wp) == BN_CLICKED) if (id == 601 && HIWORD(wp) == BN_CLICKED)
{ {
v->actionError = false;
updateSyncControls(w, *v->app); updateSyncControls(w, *v->app);
return 0; return 0;
} }
@@ -56,6 +58,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (id == IDCANCEL) { DestroyWindow(w); return 0; } if (id == IDCANCEL) { DestroyWindow(w); return 0; }
if (id == 609) if (id == 609)
{ {
v->actionError = false;
auto c = loadSyncConfig(v->app->data); auto c = loadSyncConfig(v->app->data);
c.enabled = false; c.enabled = false;
saveSyncConfig(v->app->data, c); saveSyncConfig(v->app->data, c);
@@ -69,6 +72,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
} }
if (id == IDOK || id == 610 || id == 611) if (id == IDOK || id == 610 || id == 611)
{ {
v->actionError = false;
if (!checked(w, 601)) if (!checked(w, 601))
{ {
if (id != IDOK) return 0; if (id != IDOK) return 0;
@@ -80,7 +84,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
return 0; return 0;
} }
if (id != IDOK && MessageBoxW(w, id == 610 ? if (id != IDOK && MessageBoxW(w, id == 610 ?
L"Replace this PC's shared settings with the remote profile? A local backup will be saved. Unsaved edits in Settings are not included." : L"Replace this PC's shared settings with the remote profile? A local backup will be saved. The open Settings window will refresh automatically; its unsaved edits will be discarded." :
L"Replace the remote profile with this PC's saved settings? OpenBao retains older versions according to its retention policy. Unsaved edits are not included.", L"Replace the remote profile with this PC's saved settings? OpenBao retains older versions according to its retention policy. Unsaved edits are not included.",
L"Resolve synchronization", MB_YESNO | MB_ICONWARNING) != IDYES) return 0; L"Resolve synchronization", MB_YESNO | MB_ICONWARNING) != IDYES) return 0;
auto c = syncFields(w); auto c = syncFields(w);
@@ -100,6 +104,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
v->actionError = true; // Keep this action's error visible across status polling.
// Never display parser exceptions containing connection file contents. // Never display parser exceptions containing connection file contents.
value(w, 608, L"Unable to complete sync action. Check connection fields and saved configuration."); value(w, 608, L"Unable to complete sync action. Check connection fields and saved configuration.");
if (dynamic_cast<const std::runtime_error *>(&e)) if (dynamic_cast<const std::runtime_error *>(&e))
@@ -130,7 +135,7 @@ void showSyncOptions(HWND parent, App &app)
label(w, L"KV v2 &mount", 18, 210, 160); edit(w, 605, 190, 206, 450); value(w, 605, c.mount); label(w, L"KV v2 &mount", 18, 210, 160); edit(w, 605, 190, 206, 450); value(w, 605, c.mount);
label(w, L"Profile pa&th", 18, 250, 160); edit(w, 606, 190, 246, 450); value(w, 606, c.path); label(w, L"Profile pa&th", 18, 250, 160); edit(w, 606, 190, 246, 450); value(w, 606, c.path);
label(w, L"&Login mount", 18, 290, 160); edit(w, 607, 190, 286, 450); value(w, 607, c.authMount); label(w, L"&Login mount", 18, 290, 160); edit(w, 607, 190, 286, 450); value(w, 607, c.authMount);
label(w, L"Sync uses saved settings. Close and reopen Settings after downloading changes.", 18, 330, 622); label(w, L"Sync uses saved settings. Download remote refreshes the open Settings window.", 18, 330, 622);
edit(w, 608, 18, 362, 622, 76, ES_MULTILINE | ES_READONLY | WS_VSCROLL | ES_AUTOVSCROLL); edit(w, 608, 18, 362, 622, 76, ES_MULTILINE | ES_READONLY | WS_VSCROLL | ES_AUTOVSCROLL);
value(w, 608, app.syncCommand()); value(w, 608, app.syncCommand());
checkbox(w, L"&Keep this PC's layouts and layout shortcuts (sync other settings)", 613, 18, 446, 622); checkbox(w, L"&Keep this PC's layouts and layout shortcuts (sync other settings)", 613, 18, 446, 622);
+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.58"; 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;
+16
View File
@@ -1,4 +1,5 @@
#include "app.h" #include "app.h"
#include "configuration.h"
#include "ui_scroll.h" #include "ui_scroll.h"
#include "ui_geometry.h" #include "ui_geometry.h"
#include "screenshot.h" #include "screenshot.h"
@@ -752,6 +753,21 @@ int main(int argc, char **argv) {
require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported"); require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported");
ks::showSettings(app,false); ks::showSettings(app,false);
{ {
auto window = app.settingsWindow;
auto previous = app.settings;
TabCtrl_SetCurSel(GetDlgItem(window,260),3);
app.settings.awake = !previous.awake;
app.settings.maxItems = previous.maxItems + 1;
SendMessageW(window,ks::ConfigSettingsDownloaded,0,0);
wchar_t count[32]{};
GetDlgItemTextW(window,240,count,32);
require(app.settingsWindow == window &&
SendDlgItemMessageW(window,205,BM_GETCHECK,0,0) == (app.settings.awake ? BST_CHECKED : BST_UNCHECKED) &&
std::stoi(count) == app.settings.maxItems && TabCtrl_GetCurSel(GetDlgItem(window,260)) == 3,
"download refreshes existing settings controls while preserving window and active tab");
app.settings = previous;
SendMessageW(window,ks::ConfigSettingsDownloaded,0,0);
TabCtrl_SetCurSel(GetDlgItem(window,260),0);
auto timer = SetTimer(nullptr, 0, 10, answerSettingsClose); auto timer = SetTimer(nullptr, 0, 10, answerSettingsClose);
require(timer != 0, "settings close prompt responder"); require(timer != 0, "settings close prompt responder");
SendMessageW(app.settingsWindow, WM_CLOSE, 0, 0); SendMessageW(app.settingsWindow, WM_CLOSE, 0, 0);
+19 -1
View File
@@ -298,6 +298,9 @@ int main()
require(provider.writes == 1 && result.revision == 1, "no-op avoids versions"); require(provider.writes == 1 && result.revision == 1, "no-op avoids versions");
result = synchronize(provider, base, bytes(left), SyncAction::Download); result = synchronize(provider, base, bytes(left), SyncAction::Download);
require(parse(result.settings) == parse(base) && provider.writes == 1, "download never writes"); require(parse(result.settings) == parse(base) && provider.writes == 1, "download never writes");
require(result.downloaded, "explicit download requests a settings window refresh");
require(result.status.find(L"Downloaded remote settings. Server version 1.") != std::wstring::npos,
"explicit download reports its result and server version");
provider.race = true; provider.race = true;
rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload); }, "racing writer protected"); rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload); }, "racing writer protected");
require(provider.writes == 1 && provider.remote.settings == base, "CAS failure preserves remote"); require(provider.writes == 1 && provider.remote.settings == base, "CAS failure preserves remote");
@@ -395,10 +398,25 @@ int main()
rejects([&] { bao->read(); }, "permission denied is not absent"); rejects([&] { bao->read(); }, "permission denied is not absent");
readStatus = 302; readStatus = 302;
rejects([&] { bao->read(); }, "redirect response rejected"); rejects([&] { bao->read(); }, "redirect response rejected");
readStatus = 400;
try { bao->read(); require(false, "download HTTP 400 must fail"); }
catch (const std::runtime_error &e) {
require(std::string(e.what()).find("download request (HTTP 400)") != std::string::npos,
"download failure is not mislabeled as a write failure");
}
writeStatus = 400; writeStatus = 400;
rejects([&] { bao->write(base, 7); }, "server CAS refusal propagated"); rejects([&] { bao->write(base, 7); }, "server CAS refusal propagated");
} }
require(logins == 1 && revokes == 1 && reads == 5 && writes == 2, "temporary login lifecycle"); require(logins == 1 && revokes == 1 && reads == 6 && writes == 2, "temporary login lifecycle");
try {
makeSyncProvider(config, [](const std::wstring &, const std::wstring &, const std::wstring &, const Bytes &) {
return SyncHttpResponse{400, {}};
});
require(false, "login HTTP 400 must fail");
} catch (const std::runtime_error &e) {
require(std::string(e.what()).find("authentication request (HTTP 400)") != std::string::npos,
"login failure identifies authentication instead of a write");
}
forgetSyncPassword(config); forgetSyncPassword(config); forgetSyncPassword(config); forgetSyncPassword(config);
++checks; ++checks;
std::filesystem::remove_all(root); std::filesystem::remove_all(root);
+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;