Release v0.6.28: typo correction, partial sync and selection diagnostics

This commit is contained in:
2026-11-02 11:19:13 +02:00
parent 738c81dce2
commit 55993c0f90
23 changed files with 676 additions and 76 deletions
+47 -10
View File
@@ -17,7 +17,7 @@ Json configJson(const SyncConfig &c)
{
return {{"enabled", c.enabled}, {"provider", utf8(c.provider)}, {"server", utf8(c.server)},
{"mount", utf8(c.mount)}, {"path", utf8(c.path)}, {"authMount", utf8(c.authMount)},
{"username", utf8(c.username)}};
{"username", utf8(c.username)}, {"keepLocalLayouts", c.keepLocalLayouts}};
}
std::wstring credentialName(const SyncConfig &c)
{
@@ -154,6 +154,7 @@ SyncConfig loadSyncConfig(const std::filesystem::path &root)
if (!syncVisible(root)) return c;
auto j = parse(readFile(root / L"sync.json", 16384));
c.enabled = j.value("enabled", false);
c.keepLocalLayouts = j.value("keepLocalLayouts", false);
c.provider = wide(j.value("provider", "openbao"));
c.server = wide(j.value("server", ""));
c.mount = wide(j.value("mount", "kswitcher"));
@@ -221,30 +222,50 @@ Bytes sharedSyncSettings(const Settings &s)
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock"}) j.erase(key);
return bytes(canonicalProfile(j));
}
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts)
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts, bool keepLocalLayouts)
{
auto j = parse(shared), original = parse(encodeSettings(local));
if (!j.contains("snippetTombstones")) j["snippetTombstones"] = Json::array();
if (!j.contains("autoCorrectReplace")) j["autoCorrectReplace"] = false;
if (!j.contains("autoCorrectTypos")) j["autoCorrectTypos"] = false;
if (!j.contains("autoCorrectReplacements")) j["autoCorrectReplacements"] = Json::object();
auto allowed = parse(sharedSyncSettings(local));
if (!j.is_object() || j.value("format", "") != "kswitcher-settings" || j.value("schemaVersion", 0) != 1)
throw std::runtime_error("Unsupported synchronized settings format.");
for (auto it = j.begin(); it != j.end(); ++it)
if (!allowed.contains(it.key())) throw std::runtime_error("Remote profile contains unsupported or local-only fields.");
if (j.size() != allowed.size()) throw std::runtime_error("Remote profile is incomplete.");
// decodeSettings intentionally ignores hotkeys for missing layouts during import.
// Sync must stop instead, otherwise the next upload would lose those bindings.
std::set<std::string> installed;
for (const auto &layout : layouts) installed.insert(utf8(layout.id));
std::set<std::string> missing;
for (const auto &id : j.at("layouts"))
if (!installed.contains(id.get<std::string>())) throw std::runtime_error("Install the remote profile's keyboard layouts before syncing.");
if (!installed.contains(id.get<std::string>())) missing.insert(id.get<std::string>());
for (const auto &key : j.at("layoutHotkeys"))
if (!installed.contains(key.at("layoutId").get<std::string>())) throw std::runtime_error("Install the remote profile's keyboard layouts before syncing.");
if (!installed.contains(key.at("layoutId").get<std::string>())) missing.insert(key.at("layoutId").get<std::string>());
if (!keepLocalLayouts && !missing.empty())
{
std::wstring message = L"Missing keyboard layouts on this PC: ";
for (const auto &id : missing)
{
auto code = id.starts_with("win:") ? id.substr(4) : id;
wchar_t language[128]{};
if (code.size() == 8 && code.find_first_not_of("0123456789abcdefABCDEF") == code.npos)
GetLocaleInfoW(MAKELCID(LOWORD(std::stoul(code, nullptr, 16)), SORT_DEFAULT), LOCALE_SENGLISHLANGUAGENAME,
language, 128);
if (message.back() != L' ') message += L"; ";
message += (language[0] ? std::wstring(language) : L"Unknown layout") + L" [" + wide(id) + L"]";
}
message += L". Install these exact layouts, or enable 'Keep this PC's layouts' below and sync other settings.";
throw std::runtime_error(utf8(message));
}
if (keepLocalLayouts)
for (auto key : {"layouts", "layoutHotkeys"}) j[key] = original.at(key);
original.update(j);
return decodeSettings(bytes(original), layouts);
}
#include "sync_merge.inl"
SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &local, SyncAction action,
const std::function<void(const Bytes &)> &validate, const std::vector<SyncChoice> &choices)
const std::function<void(const Bytes &)> &validate, const std::vector<SyncChoice> &choices, bool keepLocalLayouts)
{
if (local.size() > Limit) throw std::runtime_error("Settings exceed the sync size limit.");
if (validate) validate(local);
@@ -255,11 +276,25 @@ SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &l
return {true, local, provider.write(local, 0), L"Uploaded new profile."};
}
if (validate && action != SyncAction::Upload) validate(remote.settings);
auto mergeLocal = local, mergeBase = base;
if (keepLocalLayouts)
{
// Never upload a locally filtered projection over another PC's layout setup.
const auto remoteJson = parse(remote.settings);
auto preserve = [&](const Bytes &profile)
{
auto j = parse(profile);
for (auto key : {"layouts", "layoutHotkeys"}) j[key] = remoteJson.at(key);
return bytes(j);
};
mergeLocal = preserve(local);
if (!base.empty()) mergeBase = preserve(base);
}
SyncMerge merged;
if (action == SyncAction::Automatic) merged = mergeSyncProfile(base, local, remote.settings, choices);
if (action == SyncAction::Automatic) merged = mergeSyncProfile(mergeBase, mergeLocal, remote.settings, choices);
else
{
auto selected = bytes(normalizeProfile(parse(action == SyncAction::Download ? remote.settings : local)));
auto selected = bytes(normalizeProfile(parse(action == SyncAction::Download ? remote.settings : mergeLocal)));
merged = {selected, selected, selected, {}};
}
if (validate) { validate(merged.local); validate(merged.remote); }
@@ -308,7 +343,9 @@ Bytes loadSyncBase(const std::filesystem::path &root, const SyncConfig &c)
auto path = root / L"sync-base.bin";
if (!std::filesystem::exists(path)) return {};
auto j = parse(unprotect(readFile(path, Limit + 65536)));
return j.at("connection") == configJson(c) ? bytes(j.at("settings")) : Bytes{};
auto connection = j.at("connection");
if (!connection.contains("keepLocalLayouts")) connection["keepLocalLayouts"] = false;
return connection == configJson(c) ? bytes(j.at("settings")) : Bytes{};
}
void saveSyncBase(const std::filesystem::path &root, const SyncConfig &c, const Bytes &base)
{