feat: release kswitcher 0.6.64

Add mouse button actions and application profiles, with volume controls and per-monitor settings layout fixes.
This commit is contained in:
agrynco committed 2026-11-12 00:39:12 +02:00
1 parent 5a2106899d
commit 14f33012a3
23 files changed
+2233 -107

No files matched your search

+2 -2
View File
@@ -179,8 +179,8 @@ int main()
SendMessageW(automatic,WM_COMMAND,IDOK,0);
require(!saveEnabled(),"reverting auto-correction draft disables Save");
HWND tabs = GetDlgItem(parent, 260);
require(tabs && TabCtrl_GetItemCount(tabs) == 5, "five settings categories");
for (int page = 0; page < 5; ++page)
require(tabs && TabCtrl_GetItemCount(tabs) == 6, "six settings categories");
for (int page = 0; page < TabCtrl_GetItemCount(tabs); ++page)
{
TabCtrl_SetCurSel(tabs, page);
NMHDR notification{tabs, 260, TCN_SELCHANGE};
+224
View File
@@ -125,6 +125,26 @@ struct InputRegressionAccess {
return a.notifiedUpdate;
}
static void click() { MSLLHOOKSTRUCT m{}; m.flags=LLMHF_INJECTED; App::mouseProc(HC_ACTION,WM_LBUTTONDOWN,(LPARAM)&m); }
static bool volumeHoldStopsOnRelease(App &app)
{
constexpr int button = 3;
auto &press = app.mousePresses[button];
press = {app.window, {}, GetTickCount64(), {MouseActionType::VolumeUp}, true, false};
app.beginMouseVolume(button);
const bool onDown = app.mouseActions.size() == 1 && app.volumeButtonHeld();
app.repeatMouseVolume(button);
const bool initialDelay = app.mouseActions.size() == 1;
press.started -= 2100;
app.repeatMouseVolume(button);
const bool repeated = app.mouseActions.size() == 2;
MSLLHOOKSTRUCT mouse{};
mouse.mouseData = MAKEWPARAM(0, XBUTTON1);
const bool consumed = App::mouseProc(HC_ACTION, WM_XBUTTONUP, reinterpret_cast<LPARAM>(&mouse)) == 1;
app.repeatMouseVolume(button);
const bool stopped = !app.volumeButtonHeld() && app.mouseActions.size() == 2;
app.mouseActions.clear();
return onDown && initialDelay && repeated && consumed && stopped;
}
};
}
int main(int argc, char **argv) {
@@ -181,6 +201,12 @@ int main(int argc, char **argv) {
// Message-only window: no hooks, synthetic input, clipboard changes or visible editor.
app.window=CreateWindowExW(0,L"STATIC",L"Input tests",0,0,0,0,0,HWND_MESSAGE,nullptr,nullptr,nullptr);
require(app.window!=nullptr,"message-only test window");
require(ks::InputRegressionAccess::volumeHoldStopsOnRelease(app),
"volume hold repeats after its delay and stops on release without an extra step");
require(ks::mouseVolumeRepeatInterval(350) > ks::mouseVolumeRepeatInterval(1000) &&
ks::mouseVolumeRepeatInterval(1000) > ks::mouseVolumeRepeatInterval(2000) &&
!ks::isMouseVolumeStep(ks::MouseActionType::Mute),
"volume repeat accelerates and excludes mute");
require(!ks::confirmUpdatedStartup(L""),"normal startup does not report update success");
require(!ks::confirmUpdatedStartup(L"Local.Other.Event"),"unrelated event cannot report update success");
auto startedName=L"Local\\KSwitcher.Update.Started.Test."+std::to_wstring(GetCurrentProcessId());
@@ -752,6 +778,204 @@ int main(int argc, char **argv) {
fitted=ks::fitWindowRect({2000,2000,2600,2400},{-1920,0,0,1080});
require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported");
ks::showSettings(app,false);
{
auto settings = app.settingsWindow;
auto tabs = GetDlgItem(settings, 260);
auto capture = GetDlgItem(settings, 737);
auto events = GetDlgItem(settings, 733);
require(!(GetWindowLongPtrW(capture, GWL_STYLE) & WS_VISIBLE),
"mouse detection is hidden on General");
TabCtrl_SetCurSel(tabs, 5);
NMHDR changed{tabs, 260, TCN_SELCHANGE};
SendMessageW(settings, WM_NOTIFY, 260, reinterpret_cast<LPARAM>(&changed));
require(GetWindowLongPtrW(capture, GWL_STYLE) & WS_VISIBLE,
"mouse detection is visible on Mouse");
RECT suggestedSettings{};
GetWindowRect(settings, &suggestedSettings);
suggestedSettings.right -= 12;
suggestedSettings.bottom -= 12;
const UINT settingsDpi = GetDpiForWindow(settings);
SendMessageW(settings, WM_DPICHANGED, MAKEWPARAM(settingsDpi, settingsDpi),
reinterpret_cast<LPARAM>(&suggestedSettings));
require(!(GetWindowLongPtrW(settings, GWL_STYLE) & (WS_HSCROLL | WS_VSCROLL)) &&
!(GetWindowLongPtrW(events, GWL_STYLE) & WS_HSCROLL),
"a DPI resize keeps both settings and the action table free of unnecessary scrollbars");
RECT profilesBounds{}, eventsBounds{};
require(GetWindowRect(GetDlgItem(settings, 731), &profilesBounds) &&
GetWindowRect(events, &eventsBounds) &&
profilesBounds.top == eventsBounds.top && profilesBounds.bottom == eventsBounds.bottom &&
profilesBounds.right - profilesBounds.left == eventsBounds.right - eventsBounds.left,
"mouse profile and action tables have equal dimensions and aligned edges");
SendMessageW(events, WM_KEYDOWN, VK_RETURN, 0);
auto editor = FindWindowW(L"KSwitcher.MouseAction", nullptr);
require(editor && SendDlgItemMessageW(editor, 734, CB_GETCOUNT, 0, 0) == 8,
"Global actions do not include inheritance");
SendMessageW(editor, WM_COMMAND, IDCANCEL, 0);
require(!GetDlgItem(settings, 741) && !GetDlgItem(settings, 732) && !GetDlgItem(settings, 745),
"applications are chosen in the action editor only");
for (auto message : {WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK, WM_XBUTTONUP})
SendMessageW(capture, message, MAKEWPARAM(0, XBUTTON2), 0);
require(ListView_GetNextItem(events, -1, LVNI_SELECTED) == 4,
"mouse button capture selects the second side button");
wchar_t detected[100]{};
GetDlgItemTextW(settings, 738, detected, 100);
require(std::wstring(detected) == L"Window event: XButton2 | Presses: 2",
"detection shows the last button and counts repeated presses");
SendMessageW(capture, WM_MBUTTONDBLCLK, 0, 0);
SendMessageW(capture, WM_MBUTTONUP, 0, 0);
require(ListView_GetNextItem(events, -1, LVNI_SELECTED) == 7,
"a double middle click selects its own event");
require(!IsWindowEnabled(GetDlgItem(settings, IDOK)), "button detection is not a settings edit");
require(ListView_GetItemCount(events) == 8 && Header_GetItemCount(ListView_GetHeader(events)) == 2,
"mouse events have eight rows and two columns");
NMITEMACTIVATE click{};
click.hdr = {events, 733, NM_DBLCLK};
click.iItem = 7;
SendMessageW(settings, WM_NOTIFY, 733, reinterpret_cast<LPARAM>(&click));
editor = FindWindowW(L"KSwitcher.MouseAction", nullptr);
require(editor && !IsWindowEnabled(settings) && SendDlgItemMessageW(editor, 734, CB_GETCOUNT, 0, 0) == 8,
"double-click opens the Global action editor");
SendDlgItemMessageW(editor, 734, CB_SETCURSEL, 1, 0);
SendMessageW(editor, WM_COMMAND, MAKEWPARAM(734, CBN_SELCHANGE), 0);
SendDlgItemMessageW(editor, 747, ks::CaptureHotkeyMessage, VK_TAB, MOD_CONTROL);
require(!GetDlgItem(editor, 748), "the action editor has no application list");
SendMessageW(editor, WM_COMMAND, IDOK, 0);
const auto profiles = GetDlgItem(settings, 731);
require(ListView_GetItemCount(profiles) == 1 &&
IsWindowEnabled(GetDlgItem(settings, IDOK)), "adding a mouse binding enables Save");
wchar_t action[80]{};
ListView_GetItemText(events, 7, 1, action, 80);
require(std::wstring(action) == L"Ctrl+Tab", "the Action column reflects the selected event's binding");
SendMessageW(events, WM_KEYDOWN, VK_F2, 0);
editor = FindWindowW(L"KSwitcher.MouseAction", nullptr);
require(editor != nullptr, "F2 opens the selected mouse action");
SendDlgItemMessageW(editor, 734, CB_SETCURSEL, 0, 0);
SendMessageW(editor, WM_COMMAND, IDOK, 0);
require(!IsWindowEnabled(GetDlgItem(settings, IDOK)),
"removing the draft mouse binding clears pending changes");
SendMessageW(events, WM_KEYDOWN, VK_F2, 0);
editor = FindWindowW(L"KSwitcher.MouseAction", nullptr);
require(editor != nullptr, "opens the action editor to configure volume");
RECT fullCombo{}, compactHeading{};
GetWindowRect(GetDlgItem(editor, 734), &fullCombo);
GetWindowRect(GetDlgItem(editor, IDOK), &compactHeading);
SendDlgItemMessageW(editor, 734, CB_SETCURSEL, static_cast<int>(ks::MouseActionType::VolumeUp), 0);
SendMessageW(editor, WM_COMMAND, MAKEWPARAM(734, CBN_SELCHANGE), 0);
require(IsWindowVisible(GetDlgItem(editor, 759)) &&
GetDlgItemInt(editor, 759, nullptr, FALSE) == ks::DefaultMouseVolumeStep,
"volume actions show the default step");
RECT volumeCombo{}, volumeField{}, volumeSpin{}, volumeHeading{};
GetWindowRect(GetDlgItem(editor, 734), &volumeCombo);
GetWindowRect(GetDlgItem(editor, 759), &volumeField);
GetWindowRect(GetDlgItem(editor, 760), &volumeSpin);
GetWindowRect(GetDlgItem(editor, IDOK), &volumeHeading);
int minimum{}, maximum{};
SendDlgItemMessageW(editor, 760, UDM_GETRANGE32,
reinterpret_cast<WPARAM>(&minimum), reinterpret_cast<LPARAM>(&maximum));
require(IsWindowVisible(GetDlgItem(editor, 760)) && minimum == 1 && maximum == 50 &&
volumeCombo.top == volumeField.top && volumeCombo.right < volumeField.left &&
volumeSpin.top == volumeField.top && volumeSpin.bottom == volumeField.bottom &&
std::abs(volumeCombo.bottom - volumeField.bottom) <= 1 &&
volumeCombo.right < fullCombo.right && volumeHeading.top == compactHeading.top,
"volume step spinner is beside the shortened action dropdown");
RECT suggested{};
GetWindowRect(editor, &suggested);
suggested.right -= 12;
suggested.bottom -= 12;
const UINT editorDpi = GetDpiForWindow(editor);
SendMessageW(editor, WM_DPICHANGED, MAKEWPARAM(editorDpi, editorDpi),
reinterpret_cast<LPARAM>(&suggested));
GetWindowRect(GetDlgItem(editor, 734), &volumeCombo);
GetWindowRect(GetDlgItem(editor, 759), &volumeField);
GetWindowRect(GetDlgItem(editor, 760), &volumeSpin);
require(!(GetWindowLongPtrW(editor, GWL_STYLE) & (WS_HSCROLL | WS_VSCROLL)) &&
volumeCombo.top == volumeField.top && volumeCombo.bottom == volumeField.bottom &&
volumeSpin.top == volumeField.top && volumeSpin.bottom == volumeField.bottom,
"a DPI resize keeps the action row aligned and restores the full client area");
SendDlgItemMessageW(editor, 734, CB_SETCURSEL, static_cast<int>(ks::MouseActionType::Back), 0);
SendMessageW(editor, WM_COMMAND, MAKEWPARAM(734, CBN_SELCHANGE), 0);
RECT backCombo{}, backHeading{};
GetWindowRect(GetDlgItem(editor, 734), &backCombo);
GetWindowRect(GetDlgItem(editor, IDOK), &backHeading);
require(EqualRect(&fullCombo, &backCombo) && EqualRect(&compactHeading, &backHeading) &&
!IsWindowVisible(GetDlgItem(editor, 760)), "back restores full width without an unused row");
SendDlgItemMessageW(editor, 734, CB_SETCURSEL, static_cast<int>(ks::MouseActionType::VolumeUp), 0);
SendMessageW(editor, WM_COMMAND, MAKEWPARAM(734, CBN_SELCHANGE), 0);
SetDlgItemInt(editor, 759, 7, FALSE);
SendMessageW(editor, WM_COMMAND, IDOK, 0);
SendMessageW(events, WM_KEYDOWN, VK_F2, 0);
editor = FindWindowW(L"KSwitcher.MouseAction", nullptr);
require(editor && GetDlgItemInt(editor, 759, nullptr, FALSE) == 7,
"reopening a volume action restores the configured step");
SendDlgItemMessageW(editor, 734, CB_SETCURSEL, 0, 0);
SendMessageW(editor, WM_COMMAND, IDOK, 0);
SendMessageW(settings, WM_COMMAND, 754, 0);
auto profileDialog = FindWindowW(L"KSwitcher.MouseProfileName", nullptr);
require(profileDialog && GetDlgItem(profileDialog, 755), "add profile opens the Name dialog");
SetDlgItemTextW(profileDialog, 755, L"Browsing");
for (const auto *name : {L"chrome.exe", L"msedge.exe"})
{
SetDlgItemTextW(profileDialog, 749, name);
SendMessageW(profileDialog, WM_COMMAND, 750, 0);
}
SendMessageW(profileDialog, WM_COMMAND, IDOK, 0);
wchar_t applications[80]{};
ListView_GetItemText(profiles, 1, 2, applications, 80);
require(std::wstring(applications) == L"chrome.exe, msedge.exe",
"applications are visible in the profile table");
require(ListView_GetNextItem(profiles, -1, LVNI_SELECTED) == 1 &&
IsWindowEnabled(GetDlgItem(settings, IDOK)), "new profile is selected and enables Save");
RECT profileRow{};
require(ListView_GetItemRect(profiles, 1, &profileRow, LVIR_BOUNDS),
"profile row bounds are available");
const int nameWidth = ListView_GetColumnWidth(profiles, 1);
const int rowCenter = (profileRow.top + profileRow.bottom) / 2;
int checkboxX = -1;
for (int x = nameWidth; x < profileRow.right; ++x)
{
LVHITTESTINFO hit{};
hit.pt = {x, rowCenter};
if (ListView_HitTest(profiles, &hit) == 1 && (hit.flags & LVHT_ONITEMSTATEICON))
{
checkboxX = x;
break;
}
}
require(checkboxX >= nameWidth && checkboxX < nameWidth + ListView_GetColumnWidth(profiles, 0) &&
ListView_GetCheckState(profiles, 1),
"the enabled checkbox is hit-testable in the second column");
SendMessageW(profiles, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(checkboxX, rowCenter));
SendMessageW(profiles, WM_LBUTTONUP, 0, MAKELPARAM(checkboxX, rowCenter));
require(!ListView_GetCheckState(profiles, 1), "clicking the row checkbox disables its profile");
ListView_SetItemState(profiles, 0, LVIS_SELECTED, LVIS_SELECTED);
SendMessageW(profiles, WM_LBUTTONDBLCLK, MK_LBUTTON, MAKELPARAM(nameWidth / 2, rowCenter));
profileDialog = FindWindowW(L"KSwitcher.MouseProfileName", nullptr);
require(profileDialog != nullptr, "double-clicking a profile name opens the clicked row");
SendMessageW(profileDialog, WM_COMMAND, IDCANCEL, 0);
const int applicationsX = nameWidth + ListView_GetColumnWidth(profiles, 0) + 4;
SendMessageW(profiles, WM_LBUTTONDBLCLK, MK_LBUTTON, MAKELPARAM(applicationsX, rowCenter));
profileDialog = FindWindowW(L"KSwitcher.MouseProfileName", nullptr);
require(profileDialog && SendDlgItemMessageW(profileDialog, 748, LB_GETCOUNT, 0, 0) == 2,
"double-click opens the profile editor with its applications");
SetDlgItemTextW(profileDialog, 755, L"Work browsing");
SendDlgItemMessageW(profileDialog, 748, LB_SETCURSEL, 0, 0);
SendMessageW(profileDialog, WM_COMMAND, 751, 0);
SendMessageW(profileDialog, WM_COMMAND, IDOK, 0);
wchar_t renamed[80]{};
ListView_GetItemText(profiles, 1, 1, renamed, 80);
ListView_GetItemText(profiles, 1, 2, applications, 80);
require(std::wstring(renamed) == L"Work browsing" &&
std::wstring(applications) == L"msedge.exe" &&
!ListView_GetCheckState(profiles, 1),
"renaming keeps the profile selected and disabled");
const int appsWidth = ListView_GetColumnWidth(profiles, 2);
ListView_SetColumnWidth(profiles, 2, appsWidth + 100);
SendMessageW(settings, WM_SIZE, 0, 0);
require(ListView_GetColumnWidth(profiles, 2) == appsWidth + 100 &&
(GetWindowLongPtrW(profiles, GWL_STYLE) & WS_HSCROLL),
"the Applications column retains its wider size and can scroll");
SendMessageW(settings, ks::ConfigSettingsDownloaded, 0, 0);
}
{
auto window = app.settingsWindow;
auto previous = app.settings;
+84 -1
View File
@@ -1,4 +1,5 @@
#include "settings_merge.h"
#include "mouse_buttons.h"
#include <iostream>
using namespace ks;
int main(){try{
@@ -17,5 +18,87 @@ int main(){try{
draft=base;current=base;draft.chatCycles[L"telegram\nfirst"]={L"win:04090409"};current.chatCycles[L"telegram\nsecond"]={L"win:04220422"};
merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current)),{});
if(merged.chatCycles.size()!=2)throw std::runtime_error("independent nested changes lost");
std::cout<<"Passed 5 settings-save merge checks\n";
draft = base;
current = base;
draft.mouseBindings[L"chrome"][L"XButton1"] = {MouseActionType::Shortcut, {MOD_CONTROL, VK_TAB}};
current.mouseBindings[L"chrome"][L"XButton2"] = {MouseActionType::Shortcut, {MOD_CONTROL | MOD_SHIFT, VK_TAB}};
merged = decodeSettings(mergeConfigurationSettings(encodeSettings(base), encodeSettings(draft),
encodeSettings(current)), {});
if (merged.mouseBindings.at(L"chrome").size() != 2)
throw std::runtime_error("independent mouse bindings lost");
auto shared = sharedSyncSettings(merged);
if (shared != sharedSyncSettings(base) ||
restoreSyncSettings(shared, merged, {}, false).mouseBindings != merged.mouseBindings)
throw std::runtime_error("mouse profiles must stay local during sync");
auto withButtons = merged;
draft = current = withButtons;
draft.mouseBindings.at(L"chrome").erase(L"XButton1");
current.mouseBindings.at(L"chrome")[L"XButton2"] = {MouseActionType::Shortcut, {MOD_CONTROL, VK_F4}};
merged = decodeSettings(mergeConfigurationSettings(encodeSettings(withButtons), encodeSettings(draft),
encodeSettings(current)), {});
const auto &buttons = merged.mouseBindings.at(L"chrome");
if (buttons.size() != 1 || buttons.at(L"XButton2") != MouseAction{MouseActionType::Shortcut, {MOD_CONTROL, VK_F4}})
throw std::runtime_error("mouse button removal lost during merge");
merged.mouseBindings[GlobalMouseProfile][L"Double middle"] = {MouseActionType::Mute};
merged.mouseBindings[L"chrome"][L"Double middle"] = {MouseActionType::Standard};
merged.mouseBindings[L"edge"][L"XButton1"] = {MouseActionType::Launch, {}, L"C:\\Tools\\app.exe"};
merged.mouseBindings[L"edge"][L"XButton2"] = {MouseActionType::Launch, {}, L"calc.exe"};
merged.mouseBindings[GlobalMouseProfile][L"Left"] = {MouseActionType::Shortcut, {MOD_CONTROL, 'W'}, {}, {L"chrome", L"msedge"}};
merged.mouseBindings[GlobalMouseProfile][L"Middle"] = {MouseActionType::Back, {}, {}, {L"chrome"}};
auto restored = decodeSettings(encodeSettings(merged), {});
if (restored.mouseBindings != merged.mouseBindings ||
effectiveMouseAction(restored, L"chrome", 7) ||
!effectiveMouseAction(restored, L"edge", 7) ||
effectiveMouseAction(restored, L"edge", 7)->type != MouseActionType::Mute)
throw std::runtime_error("mouse action override or inheritance failed");
if (!effectiveMouseAction(restored, L"chrome", 0) || !effectiveMouseAction(restored, L"msedge", 0) ||
effectiveMouseAction(restored, L"explorer", 0) || !effectiveMouseAction(restored, L"chrome", 2) ||
effectiveMouseAction(restored, L"msedge", 2) || !effectiveMouseAction(restored, L"explorer", 7))
throw std::runtime_error("application restrictions or unrestricted fallback failed");
draft = current = restored;
draft.mouseBindings[GlobalMouseProfile][L"Middle"].applications.clear();
current.mouseBindings[GlobalMouseProfile][L"Middle"].type = MouseActionType::Mute;
const auto competing = mergeSyncProfile(encodeSettings(restored), encodeSettings(draft), encodeSettings(current));
if (competing.conflicts.size() != 1)
throw std::runtime_error("competing edits to a mouse action and its scope must conflict");
Settings profiles;
profiles.mouseBindings[L"name:Browsing"][L"XButton1"] = {MouseActionType::Forward};
profiles.mouseBindings[L"name:Browsing"][L"XButton2"] = {MouseActionType::Back};
profiles.mouseProfileApplications[L"name:Browsing"] = {L"chrome", L"msedge"};
profiles.mouseBindings[L"name:Empty"];
profiles.mouseBindings[GlobalMouseProfile][L"XButton1"] = {MouseActionType::VolumeUp};
profiles.mouseBindings[GlobalMouseProfile][L"XButton2"] = {MouseActionType::VolumeDown};
profiles.mouseBindings[GlobalMouseProfile][L"XButton2"].volumeStep = 7;
auto resolve = [&](const wchar_t *app, int event, bool foreground = true)
{
const auto *action = effectiveMouseAction(profiles, app, event, foreground);
return action ? action->type : MouseActionType::Standard;
};
profiles = decodeSettings(mergeConfigurationSettings(encodeSettings(base), encodeSettings(profiles), encodeSettings(base)), {});
if (profiles.mouseBindings.at(GlobalMouseProfile).at(L"XButton1").volumeStep != DefaultMouseVolumeStep ||
profiles.mouseBindings.at(GlobalMouseProfile).at(L"XButton2").volumeStep != 7)
throw std::runtime_error("default and custom volume steps must round-trip independently");
auto invalidVolume = profiles;
invalidVolume.mouseBindings[GlobalMouseProfile][L"XButton1"].volumeStep = 0;
rejected = false;
try { validateSettings(invalidVolume); }
catch (const std::runtime_error &) { rejected = true; }
if (!rejected) throw std::runtime_error("invalid volume steps must be rejected");
if (!profiles.mouseBindings.contains(L"name:Empty") ||
resolve(L"chrome", 3) != MouseActionType::Forward || resolve(L"msedge", 4) != MouseActionType::Back ||
resolve(L"chrome", 3, false) != MouseActionType::VolumeUp ||
resolve(L"explorer", 4) != MouseActionType::VolumeDown)
throw std::runtime_error("named profiles must persist and override fallback only for the matching foreground app");
profiles.mouseBindings[L"name:Alternative"][L"XButton1"] = {MouseActionType::Back};
profiles.mouseProfileApplications[L"name:Alternative"] = {L"chrome"};
profiles.disabledMouseProfiles.insert(L"name:Browsing");
profiles = decodeSettings(encodeSettings(profiles), {});
if (resolve(L"chrome", 3) != MouseActionType::Back)
throw std::runtime_error("disabled profiles must not resolve actions");
profiles.disabledMouseProfiles.clear();
rejected = false;
try { validateSettings(profiles); }
catch (const std::runtime_error &e) { rejected = std::string(e.what()).starts_with("Mouse action conflict:"); }
if (!rejected) throw std::runtime_error("overlapping active profiles must be rejected");
std::cout<<"Settings-save merge checks passed\n";
}catch(const std::exception &e){std::cerr<<e.what()<<'\n';return 1;}}