From 18ab1acdb1011c160d6d4922deafa395657e549b Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:50:51 +0200 Subject: [PATCH 1/6] refactor(config): move ParseConfig's bind sanitize pass into SanitizeBinds (#475) Move-only: the lines are unchanged and run at the same point. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TdjopXZfsUiesYxYLYHEdH --- src/config.cpp | 65 +++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 2c765b2a..79e27f26 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -123,6 +123,40 @@ int EffectiveSamplingMode(int iniValue, bool mpoDisabledAtBoot, bool mpoDisabled return iniValue; } +// ParseConfig's last pass: unsafe binds and switched-off extra keys read as unbound. +static void SanitizeBinds(Config& c) { + // Reject keybinds to keys Wind must never swallow (see IsForbiddenBindVk). A bound key is + // eaten system-wide, so binding e.g. Backspace or the Windows key would make it unusable + // everywhere; treat a forbidden bind as unbound regardless of how it got into the ini. + // Since #285 every bind goes through the one shared rule set (src/keybind_rules.h), which also + // refuses typing keys alone, Shift/AltGr + a typing key and system-reserved combos: an unsafe bind + // in an ini (hand-edited, or from an older version) reads as unbound. + auto sanitizeKey = [](int& vk, int* mods) { + const int m = mods ? *mods : 0; + if (CheckKeyBind(vk, m) != BindVerdict::Ok) { vk = 0; if (mods) *mods = 0; } + }; + sanitizeKey(c.zoomInVk, &c.zoomInMods); sanitizeKey(c.zoomInVk2, &c.zoomInMods2); + sanitizeKey(c.zoomOutVk, &c.zoomOutMods); sanitizeKey(c.zoomOutVk2, &c.zoomOutMods2); + sanitizeKey(c.recenterVk, &c.recenterMods); + sanitizeKey(c.cursorLockVk, &c.cursorLockMods); + sanitizeKey(c.panLeftVk, &c.panLeftMods); sanitizeKey(c.panRightVk, &c.panRightMods); + sanitizeKey(c.panUpVk, &c.panUpMods); sanitizeKey(c.panDownVk, &c.panDownMods); + sanitizeKey(c.hideCursorVk, &c.hideCursorMods); + sanitizeKey(c.quickZoomVk, &c.quickZoomMods); + auto sanitizeButton = [](int& b, int& mods) { + if (CheckClickBind(b, mods) != BindVerdict::Ok) { b = 0; mods = 0; } + }; + sanitizeButton(c.zoomInButton, c.zoomInButtonMods); sanitizeButton(c.zoomInButton2, c.zoomInButton2Mods); + sanitizeButton(c.zoomOutButton, c.zoomOutButtonMods); sanitizeButton(c.zoomOutButton2, c.zoomOutButton2Mods); + if (c.zoomWheelMods != 0 && CheckWheelBind(c.zoomWheelMods) != BindVerdict::Ok) c.zoomWheelMods = 0; + // Extra-key switches (#318): off = unbound as far as the core is concerned (no hook tracking, no + // swallowing, no RegisterHotKey). The ini keeps the binding; only this parsed copy is cleared. + if (!c.panKeysOn) { c.panLeftVk = c.panRightVk = c.panUpVk = c.panDownVk = 0; + c.panLeftMods = c.panRightMods = c.panUpMods = c.panDownMods = 0; } + if (!c.hideCursorOn) { c.hideCursorVk = 0; c.hideCursorMods = 0; } + if (!c.cursorLockOn) { c.cursorLockVk = 0; c.cursorLockMods = 0; } +} + Config ParseConfig(const std::string& text) { Config c; // A UTF-8 BOM (Notepad's "UTF-8 with BOM") would glue itself to the first key and lose it. @@ -353,36 +387,7 @@ Config ParseConfig(const std::string& text) { // #148 - which the mapper's MPO-aware pan wall now guards at ANY level, so maxLevel is one // shared setting across all models. High levels still cost DWM re-scale time; that is a // perf trade the user owns, not a crash.) - // Reject keybinds to keys Wind must never swallow (see IsForbiddenBindVk). A bound key is - // eaten system-wide, so binding e.g. Backspace or the Windows key would make it unusable - // everywhere; treat a forbidden bind as unbound regardless of how it got into the ini. - // Since #285 every bind goes through the one shared rule set (src/keybind_rules.h), which also - // refuses typing keys alone, Shift/AltGr + a typing key and system-reserved combos: an unsafe bind - // in an ini (hand-edited, or from an older version) reads as unbound. - auto sanitizeKey = [](int& vk, int* mods) { - const int m = mods ? *mods : 0; - if (CheckKeyBind(vk, m) != BindVerdict::Ok) { vk = 0; if (mods) *mods = 0; } - }; - sanitizeKey(c.zoomInVk, &c.zoomInMods); sanitizeKey(c.zoomInVk2, &c.zoomInMods2); - sanitizeKey(c.zoomOutVk, &c.zoomOutMods); sanitizeKey(c.zoomOutVk2, &c.zoomOutMods2); - sanitizeKey(c.recenterVk, &c.recenterMods); - sanitizeKey(c.cursorLockVk, &c.cursorLockMods); - sanitizeKey(c.panLeftVk, &c.panLeftMods); sanitizeKey(c.panRightVk, &c.panRightMods); - sanitizeKey(c.panUpVk, &c.panUpMods); sanitizeKey(c.panDownVk, &c.panDownMods); - sanitizeKey(c.hideCursorVk, &c.hideCursorMods); - sanitizeKey(c.quickZoomVk, &c.quickZoomMods); - auto sanitizeButton = [](int& b, int& mods) { - if (CheckClickBind(b, mods) != BindVerdict::Ok) { b = 0; mods = 0; } - }; - sanitizeButton(c.zoomInButton, c.zoomInButtonMods); sanitizeButton(c.zoomInButton2, c.zoomInButton2Mods); - sanitizeButton(c.zoomOutButton, c.zoomOutButtonMods); sanitizeButton(c.zoomOutButton2, c.zoomOutButton2Mods); - if (c.zoomWheelMods != 0 && CheckWheelBind(c.zoomWheelMods) != BindVerdict::Ok) c.zoomWheelMods = 0; - // Extra-key switches (#318): off = unbound as far as the core is concerned (no hook tracking, no - // swallowing, no RegisterHotKey). The ini keeps the binding; only this parsed copy is cleared. - if (!c.panKeysOn) { c.panLeftVk = c.panRightVk = c.panUpVk = c.panDownVk = 0; - c.panLeftMods = c.panRightMods = c.panUpMods = c.panDownMods = 0; } - if (!c.hideCursorOn) { c.hideCursorVk = 0; c.hideCursorMods = 0; } - if (!c.cursorLockOn) { c.cursorLockVk = 0; c.cursorLockMods = 0; } + SanitizeBinds(c); return c; } } From fdac88ce56df53d17aa071108161113b72c3402b Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:51:09 +0200 Subject: [PATCH 2/6] refactor(config): move ParseConfig's clamp pass into ClampConfig (#475) Move-only: the lines are unchanged and run at the same point, before SanitizeBinds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TdjopXZfsUiesYxYLYHEdH --- src/config.cpp | 141 +++++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 68 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 79e27f26..48e02a24 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -157,6 +157,78 @@ static void SanitizeBinds(Config& c) { if (!c.cursorLockOn) { c.cursorLockVk = 0; c.cursorLockMods = 0; } } +// ParseConfig's range pass, after every key is read. haveRenderScaling: the ini had the key (#456). +static void ClampConfig(Config& c, bool haveRenderScaling) { + // Clamp numeric fields to their documented ranges. The ini is a hand-editable surface, and an + // out-of-range value (e.g. maxLevel=0, which would invert ZoomController's clamp and disable zoom, + // or a negative ramp) would otherwise silently break behavior with no feedback. Ranges mirror the + // config UI sliders / the struct-comment docs. + c.trackMarginPct = (int)clampd(c.trackMarginPct, 0, 40); + c.mouseMarginPct = (int)clampd(c.mouseMarginPct, 0, 40); + // 30x cap (#429): above it the smooth path's half-texel jumps reach 15-25 px and the pointer + // runs off the screen; must be >= the 1.0 min zoom level. + c.maxLevel = clampd(c.maxLevel, 1.0, 30.0); + c.zoomInSpeed = clampd(c.zoomInSpeed, 0.25, 4.0); + c.zoomOutSpeed = clampd(c.zoomOutSpeed, 0.25, 4.0); + c.panSpeed = clampd(c.panSpeed, 0.25, 4.0); + c.smoothZoomAccel = clampd(c.smoothZoomAccel, 1.0, 8.0); + c.smoothZoomRamp = clampd(c.smoothZoomRamp, 0.1, 3.0); + c.cursorSensitivity = clampd(c.cursorSensitivity, 0.25, 4.0); + if (c.panGlideMaxPx < 0) c.panGlideMaxPx = 0; + if (c.txRampMinStep < 0) c.txRampMinStep = 0; + if (c.txRampMinStep > 100) c.txRampMinStep = 100; + if (c.txRampMinFrom < 1.0) c.txRampMinFrom = 1.0; + if (c.panGlideMaxPx > 400) c.panGlideMaxPx = 400; + if (c.gameCursorLagMs < -1) c.gameCursorLagMs = -1; + if (c.gameCursorLagMs > 100) c.gameCursorLagMs = 100; + c.sharpness = clampd(c.sharpness, 0.0, 1.0); + if (!haveRenderScaling) c.renderScaling = c.bilinear == 0 ? 0 : 1; // bilinear retired (#456) + if (c.renderScaling < 0 || c.renderScaling > 2) c.renderScaling = 1; + c.brightness = clampd(c.brightness, 0.5, 1.5); + c.colorWarmPct = (int)clampd(c.colorWarmPct, 0, 100); + c.colorDimPct = (int)clampd(c.colorDimPct, 1, 100); + c.trayPinned = (int)clampd(c.trayPinned, 0, 1); + c.quickZoomDefault = clampd(c.quickZoomDefault, 1.0, 30.0); + if (c.outlineThickness < 1) c.outlineThickness = 1; + if (c.outlineThickness > 40) c.outlineThickness = 40; + c.outlineLowZoomMax = clampd(c.outlineLowZoomMax, 1.0, 50.0); + if (c.trackGlideMs < 0) c.trackGlideMs = 0; // glide time; a negative one would run the spring backwards + if (c.trackGlideMs > 5000) c.trackGlideMs = 5000; + if (c.txMaxStepPct < 0) c.txMaxStepPct = 0; // 0 = uncapped + if (c.txMaxStepPct > 1000) c.txMaxStepPct = 1000; + if (c.gameFpsCap < 0) c.gameFpsCap = 0; // 0 = off + if (c.gameFpsCap > 240) c.gameFpsCap = 240; + if (c.gpuPriority < -1) c.gpuPriority = -1; // tri-state: -1 low / 0 normal / +1 high + if (c.gpuPriority > 1) c.gpuPriority = 1; + if (c.tdrTest < 0) c.tdrTest = 0; // diagnostic harness (issue #148) + if (c.tdrTest > 4) c.tdrTest = 4; + if (c.ixDecimate < 1) c.ixDecimate = 1; // 1 = publish every changed tick + if (c.ixDecimate > 16) c.ixDecimate = 16; + if (c.txRestLevel < 1.0) c.txRestLevel = 1.0; + if (c.txRestLevel > 1.01) c.txRestLevel = 1.01; // visually identity only + if (c.zoomEaseOutMs < 0) c.zoomEaseOutMs = 0; + if (c.zoomEaseOutMs > 300) c.zoomEaseOutMs = 300; + if (c.txWarmMode < 0) c.txWarmMode = 0; + if (c.txWarmMode > 1) c.txWarmMode = 1; // the retired modes 2-4 read as the shipped pulse + c.launchQuiesce = c.launchQuiesce ? 1 : 0; + if (c.txWarmHz < 0) c.txWarmHz = 0; // 0 = every tick + if (c.txWarmHz > 1000) c.txWarmHz = 1000; + if (c.txEdgeMargin < 0.0) c.txEdgeMargin = 0.0; + if (c.txEdgeMargin > 8.0) c.txEdgeMargin = 8.0; // beyond this the lost border is the bug + c.outlineIdleSeconds = clampd(c.outlineIdleSeconds, 0.5, 60.0); + // "transform" is a first-class model again (revived for issue #148: the compositor-internal + // zoom that stays smooth over heavy games); anything unknown falls back to hybrid, the + // product default ("Auto" in the UI) - same fallback as a missing key (struct default). + // "magnify" (the retired Windows Magnifier driver) is unknown too, so an old ini that still + // says so runs Auto. + if (c.model != "render" && c.model != "transform" && c.model != "hybrid") c.model = "hybrid"; + // (The old transform/hybrid maxLevel<=12 clamp is GONE: the "TDR territory above 12x" was + // root-caused 2026-07-26 to NVIDIA's 16-bit MPO plane-programming overflow - see issue + // #148 - which the mapper's MPO-aware pan wall now guards at ANY level, so maxLevel is one + // shared setting across all models. High levels still cost DWM re-scale time; that is a + // perf trade the user owns, not a crash.) +} + Config ParseConfig(const std::string& text) { Config c; // A UTF-8 BOM (Notepad's "UTF-8 with BOM") would glue itself to the first key and lose it. @@ -319,74 +391,7 @@ Config ParseConfig(const std::string& text) { else if (key == "outlineIdleSeconds") c.outlineIdleSeconds = ParseDoubleStrict(val); } catch (...) { /* keep default on bad value */ } } - // Clamp numeric fields to their documented ranges. The ini is a hand-editable surface, and an - // out-of-range value (e.g. maxLevel=0, which would invert ZoomController's clamp and disable zoom, - // or a negative ramp) would otherwise silently break behavior with no feedback. Ranges mirror the - // config UI sliders / the struct-comment docs. - c.trackMarginPct = (int)clampd(c.trackMarginPct, 0, 40); - c.mouseMarginPct = (int)clampd(c.mouseMarginPct, 0, 40); - // 30x cap (#429): above it the smooth path's half-texel jumps reach 15-25 px and the pointer - // runs off the screen; must be >= the 1.0 min zoom level. - c.maxLevel = clampd(c.maxLevel, 1.0, 30.0); - c.zoomInSpeed = clampd(c.zoomInSpeed, 0.25, 4.0); - c.zoomOutSpeed = clampd(c.zoomOutSpeed, 0.25, 4.0); - c.panSpeed = clampd(c.panSpeed, 0.25, 4.0); - c.smoothZoomAccel = clampd(c.smoothZoomAccel, 1.0, 8.0); - c.smoothZoomRamp = clampd(c.smoothZoomRamp, 0.1, 3.0); - c.cursorSensitivity = clampd(c.cursorSensitivity, 0.25, 4.0); - if (c.panGlideMaxPx < 0) c.panGlideMaxPx = 0; - if (c.txRampMinStep < 0) c.txRampMinStep = 0; - if (c.txRampMinStep > 100) c.txRampMinStep = 100; - if (c.txRampMinFrom < 1.0) c.txRampMinFrom = 1.0; - if (c.panGlideMaxPx > 400) c.panGlideMaxPx = 400; - if (c.gameCursorLagMs < -1) c.gameCursorLagMs = -1; - if (c.gameCursorLagMs > 100) c.gameCursorLagMs = 100; - c.sharpness = clampd(c.sharpness, 0.0, 1.0); - if (!haveRenderScaling) c.renderScaling = c.bilinear == 0 ? 0 : 1; // bilinear retired (#456) - if (c.renderScaling < 0 || c.renderScaling > 2) c.renderScaling = 1; - c.brightness = clampd(c.brightness, 0.5, 1.5); - c.colorWarmPct = (int)clampd(c.colorWarmPct, 0, 100); - c.colorDimPct = (int)clampd(c.colorDimPct, 1, 100); - c.trayPinned = (int)clampd(c.trayPinned, 0, 1); - c.quickZoomDefault = clampd(c.quickZoomDefault, 1.0, 30.0); - if (c.outlineThickness < 1) c.outlineThickness = 1; - if (c.outlineThickness > 40) c.outlineThickness = 40; - c.outlineLowZoomMax = clampd(c.outlineLowZoomMax, 1.0, 50.0); - if (c.trackGlideMs < 0) c.trackGlideMs = 0; // glide time; a negative one would run the spring backwards - if (c.trackGlideMs > 5000) c.trackGlideMs = 5000; - if (c.txMaxStepPct < 0) c.txMaxStepPct = 0; // 0 = uncapped - if (c.txMaxStepPct > 1000) c.txMaxStepPct = 1000; - if (c.gameFpsCap < 0) c.gameFpsCap = 0; // 0 = off - if (c.gameFpsCap > 240) c.gameFpsCap = 240; - if (c.gpuPriority < -1) c.gpuPriority = -1; // tri-state: -1 low / 0 normal / +1 high - if (c.gpuPriority > 1) c.gpuPriority = 1; - if (c.tdrTest < 0) c.tdrTest = 0; // diagnostic harness (issue #148) - if (c.tdrTest > 4) c.tdrTest = 4; - if (c.ixDecimate < 1) c.ixDecimate = 1; // 1 = publish every changed tick - if (c.ixDecimate > 16) c.ixDecimate = 16; - if (c.txRestLevel < 1.0) c.txRestLevel = 1.0; - if (c.txRestLevel > 1.01) c.txRestLevel = 1.01; // visually identity only - if (c.zoomEaseOutMs < 0) c.zoomEaseOutMs = 0; - if (c.zoomEaseOutMs > 300) c.zoomEaseOutMs = 300; - if (c.txWarmMode < 0) c.txWarmMode = 0; - if (c.txWarmMode > 1) c.txWarmMode = 1; // the retired modes 2-4 read as the shipped pulse - c.launchQuiesce = c.launchQuiesce ? 1 : 0; - if (c.txWarmHz < 0) c.txWarmHz = 0; // 0 = every tick - if (c.txWarmHz > 1000) c.txWarmHz = 1000; - if (c.txEdgeMargin < 0.0) c.txEdgeMargin = 0.0; - if (c.txEdgeMargin > 8.0) c.txEdgeMargin = 8.0; // beyond this the lost border is the bug - c.outlineIdleSeconds = clampd(c.outlineIdleSeconds, 0.5, 60.0); - // "transform" is a first-class model again (revived for issue #148: the compositor-internal - // zoom that stays smooth over heavy games); anything unknown falls back to hybrid, the - // product default ("Auto" in the UI) - same fallback as a missing key (struct default). - // "magnify" (the retired Windows Magnifier driver) is unknown too, so an old ini that still - // says so runs Auto. - if (c.model != "render" && c.model != "transform" && c.model != "hybrid") c.model = "hybrid"; - // (The old transform/hybrid maxLevel<=12 clamp is GONE: the "TDR territory above 12x" was - // root-caused 2026-07-26 to NVIDIA's 16-bit MPO plane-programming overflow - see issue - // #148 - which the mapper's MPO-aware pan wall now guards at ANY level, so maxLevel is one - // shared setting across all models. High levels still cost DWM re-scale time; that is a - // perf trade the user owns, not a crash.) + ClampConfig(c, haveRenderScaling); SanitizeBinds(c); return c; } From 7c392c4d1a7ebae68c594ecb1723221116e55add Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:51:23 +0200 Subject: [PATCH 3/6] refactor(config): move ParseConfig's key dispatch chains into ApplyValueKey (#475) Move-only (re-indented): both if/else chains keep every key's parser and order. The try/catch stays in ParseConfig, so a throw still skips the rest and keeps the default. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TdjopXZfsUiesYxYLYHEdH --- src/config.cpp | 263 +++++++++++++++++++++++++------------------------ 1 file changed, 135 insertions(+), 128 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 48e02a24..ef40c4d1 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -229,6 +229,140 @@ static void ClampConfig(Config& c, bool haveRenderScaling) { // perf trade the user owns, not a crash.) } +// One non-empty key=value: each key keeps its own parser (ParseIntStrict, ParseDoubleStrict or the +// raw string). A bad number throws and ParseConfig's catch keeps the default. haveRenderScaling is +// set when the renderScaling key is seen (issue #456). +static void ApplyValueKey(Config& c, const std::string& key, const std::string& val, bool& haveRenderScaling) { + if (key == "zoomInButton") c.zoomInButton = ParseIntStrict(val); + else if (key == "zoomOutButton") c.zoomOutButton = ParseIntStrict(val); + else if (key == "zoomInButton2") c.zoomInButton2 = ParseIntStrict(val); + else if (key == "zoomOutButton2") c.zoomOutButton2 = ParseIntStrict(val); + else if (key == "recenterVk") c.recenterVk = ParseIntStrict(val); + else if (key == "cursorLockVk") c.cursorLockVk = ParseIntStrict(val); + else if (key == "recenterMods") c.recenterMods = ParseIntStrict(val); + else if (key == "cursorLockMods") c.cursorLockMods = ParseIntStrict(val); + else if (key == "panLeftVk") c.panLeftVk = ParseIntStrict(val); + else if (key == "panLeftMods") c.panLeftMods = ParseIntStrict(val); + else if (key == "panRightVk") c.panRightVk = ParseIntStrict(val); + else if (key == "panRightMods") c.panRightMods = ParseIntStrict(val); + else if (key == "panUpVk") c.panUpVk = ParseIntStrict(val); + else if (key == "panUpMods") c.panUpMods = ParseIntStrict(val); + else if (key == "panDownVk") c.panDownVk = ParseIntStrict(val); + else if (key == "panDownMods") c.panDownMods = ParseIntStrict(val); + else if (key == "panSpeed") c.panSpeed = ParseDoubleStrict(val); + else if (key == "zoomTrace") c.zoomTrace = ParseIntStrict(val); + else if (key == "hitchLog") c.hitchLog = ParseIntStrict(val); + else if (key == "hitchThresholdPct") c.hitchThresholdPct = std::clamp(ParseIntStrict(val), 110, 1000); + // The chain is split in two: MSVC caps if/else nesting depth (C1061). Keys are unique, + // so a second chain changes nothing. + if (key == "hideCursorVk") c.hideCursorVk = ParseIntStrict(val); + else if (key == "hideCursorMods") c.hideCursorMods = ParseIntStrict(val); + else if (key == "zoomInVk") c.zoomInVk = ParseIntStrict(val); + else if (key == "zoomOutVk") c.zoomOutVk = ParseIntStrict(val); + else if (key == "zoomInVk2") c.zoomInVk2 = ParseIntStrict(val); + else if (key == "zoomOutVk2") c.zoomOutVk2 = ParseIntStrict(val); + else if (key == "zoomInMods") c.zoomInMods = ParseIntStrict(val); + else if (key == "zoomOutMods") c.zoomOutMods = ParseIntStrict(val); + else if (key == "zoomInMods2") c.zoomInMods2 = ParseIntStrict(val); + else if (key == "zoomOutMods2") c.zoomOutMods2 = ParseIntStrict(val); + else if (key == "zoomInButtonMods") c.zoomInButtonMods = ParseIntStrict(val); + else if (key == "zoomOutButtonMods") c.zoomOutButtonMods = ParseIntStrict(val); + else if (key == "zoomInButton2Mods") c.zoomInButton2Mods = ParseIntStrict(val); + else if (key == "zoomOutButton2Mods") c.zoomOutButton2Mods = ParseIntStrict(val); + else if (key == "zoomWheelMods") c.zoomWheelMods = ParseIntStrict(val); + else if (key == "panKeysOn") c.panKeysOn = ParseIntStrict(val) != 0 ? 1 : 0; + else if (key == "hideCursorOn") c.hideCursorOn = ParseIntStrict(val) != 0 ? 1 : 0; + else if (key == "cursorLockOn") c.cursorLockOn = ParseIntStrict(val) != 0 ? 1 : 0; + else if (key == "maxLevel") c.maxLevel = ParseDoubleStrict(val); + else if (key == "zoomInSpeed") c.zoomInSpeed = ParseDoubleStrict(val); + else if (key == "zoomOutSpeed") c.zoomOutSpeed = ParseDoubleStrict(val); + else if (key == "smoothZoom") c.smoothZoom = ParseIntStrict(val); + else if (key == "smoothZoomAccel") c.smoothZoomAccel = ParseDoubleStrict(val); + else if (key == "smoothZoomRamp") c.smoothZoomRamp = ParseDoubleStrict(val); + else if (key == "zoomEaseOutMs") c.zoomEaseOutMs = ParseIntStrict(val); + else if (key == "vsync") c.vsync = ParseIntStrict(val); + else if (key == "dwmFlush") c.dwmFlush = ParseIntStrict(val); + else if (key == "diagnostics") c.diagnostics = ParseIntStrict(val); + else if (key == "cursorSensitivity") c.cursorSensitivity = ParseDoubleStrict(val); + else if (key == "panGlideMaxPx") c.panGlideMaxPx = ParseIntStrict(val); + else if (key == "gameCursorLagMs") c.gameCursorLagMs = ParseIntStrict(val); + else if (key == "cursorConstantSize") c.cursorConstantSize = ParseIntStrict(val); + else if (key == "trayPinned") c.trayPinned = ParseIntStrict(val); + else if (key == "cursorVisibility") c.cursorVisibility = val; + else if (key == "model") c.model = val; + else if (key == "fastPan") c.fastPan = ParseIntStrict(val); + else if (key == "smoothPan") c.smoothPan = ParseIntStrict(val); + else if (key == "magInputTransform") c.magInputTransform = ParseIntStrict(val); + else if (key == "bilinear") c.bilinear = ParseIntStrict(val); + else if (key == "renderScaling") { c.renderScaling = ParseIntStrict(val); haveRenderScaling = true; } + else if (key == "upscaleTiming") c.upscaleTiming = ParseIntStrict(val); + else if (key == "sharpness") c.sharpness = ParseDoubleStrict(val); + else if (key == "zorderBand") c.zorderBand = ParseIntStrict(val); + else if (key == "brightness") c.brightness = ParseDoubleStrict(val); + else if (key == "colorWarmPct") c.colorWarmPct = ParseIntStrict(val); + else if (key == "colorDimPct") c.colorDimPct = ParseIntStrict(val); + else if (key == "hdrTonemap") c.hdrTonemap = ParseIntStrict(val); + else if (key == "multiMonitor") c.multiMonitor = ParseIntStrict(val); + else if (key == "cropCapture") c.cropCapture = ParseIntStrict(val); + else if (key == "lowGpuPriority") c.lowGpuPriority = ParseIntStrict(val); + else if (key == "gpuPriority") c.gpuPriority = ParseIntStrict(val); + else if (key == "gameCrop") c.gameCrop = ParseIntStrict(val); + else if (key == "tdrTest") c.tdrTest = ParseIntStrict(val); + else if (key == "probeClicks") c.probeClicks = ParseIntStrict(val); + else if (key == "desktopTransform") c.desktopTransform = ParseIntStrict(val); + else if (key == "cursorBandAuto") c.cursorBandAuto = ParseIntStrict(val); + else if (key == "trackCaret") c.trackCaret = ParseIntStrict(val); + else if (key == "trackFocus") c.trackFocus = ParseIntStrict(val); + else if (key == "trackAlign") c.trackAlign = ParseIntStrict(val); + else if (key == "mouseAlign") c.mouseAlign = ParseIntStrict(val); + else if (key == "trackGlideMs") c.trackGlideMs = ParseIntStrict(val); + else if (key == "trackMarginPct") c.trackMarginPct = ParseIntStrict(val); + else if (key == "mouseMarginPct") c.mouseMarginPct = ParseIntStrict(val); + else if (key == "trackLog") c.trackLog = ParseIntStrict(val); + else if (key == "ixDecimate") c.ixDecimate = ParseIntStrict(val); + else if (key == "mpoBuster") c.mpoBuster = ParseIntStrict(val); + else if (key == "txSamplingMode") { + // Only 0 (nearest), 1 (smooth) and -1 (leave DWM alone) are meaningful. 2..4 are + // kernel aliases of nearest that would slip past the MPO sampling guard; anything + // else folds to nearest so EffectiveSamplingMode guards it (review 2026-10-09 #23). + const int v = ParseIntStrict(val); + c.txSamplingMode = (v == 1 || v == -1) ? v : 0; + } + else if (key == "txWarmMode") c.txWarmMode = ParseIntStrict(val); + else if (key == "txTrace") c.txTrace = ParseIntStrict(val); + else if (key == "txRestLevel") c.txRestLevel = ParseDoubleStrict(val); + else if (key == "launchQuiesce") c.launchQuiesce = ParseIntStrict(val); + else if (key == "txWarmHz") c.txWarmHz = ParseIntStrict(val); + else if (key == "mpoNearestGuard") c.mpoNearestGuard = ParseIntStrict(val); + else if (key == "txNativeCursor") c.txNativeCursor = ParseIntStrict(val); + else if (key == "txSmoothLadder") c.txSmoothLadder = ParseIntStrict(val); + else if (key == "txRampMinStep") c.txRampMinStep = ParseIntStrict(val); + else if (key == "txRampMinFrom") c.txRampMinFrom = ParseDoubleStrict(val); + else if (key == "mpoGuardTest") c.mpoGuardTest = ParseIntStrict(val); + else if (key == "mpoGuard") c.mpoGuard = ParseIntStrict(val); + else if (key == "mpoGuardLiftWall") c.mpoGuardLiftWall = ParseIntStrict(val); + else if (key == "lockedBallistics") c.lockedBallistics = ParseIntStrict(val); + else if (key == "edgeClip") c.edgeClip = ParseIntStrict(val); + else if (key == "txMaxStepPct") c.txMaxStepPct = ParseIntStrict(val); + else if (key == "warpLock") c.warpLock = ParseIntStrict(val); + else if (key == "lockForce") c.lockForce = ParseIntStrict(val); + else if (key == "txEdgeMargin") c.txEdgeMargin = ParseDoubleStrict(val); + else if (key == "gameFpsCap") c.gameFpsCap = ParseIntStrict(val); + else if (key == "onboarded") c.onboarded = ParseIntStrict(val); + else if (key == "quickZoomDefault") c.quickZoomDefault = ParseDoubleStrict(val); + else if (key == "quickZoomModifier") c.quickZoomModifier = val; + else if (key == "quickZoomHotkeyMode") c.quickZoomHotkeyMode = ParseIntStrict(val); + else if (key == "quickZoomVk") c.quickZoomVk = ParseIntStrict(val); + else if (key == "quickZoomMods") c.quickZoomMods = ParseIntStrict(val); + else if (key == "outline") c.outline = ParseIntStrict(val); + else if (key == "outlineThickness") c.outlineThickness = ParseIntStrict(val); + else if (key == "outlineColor") { c.outlineColor = val; ParseHexColor(val, c.outlineR, c.outlineG, c.outlineB); } + else if (key == "outlineLowZoomOnly") c.outlineLowZoomOnly = ParseIntStrict(val); + else if (key == "outlineLowZoomMax") c.outlineLowZoomMax = ParseDoubleStrict(val); + else if (key == "outlineIdleHide") c.outlineIdleHide = ParseIntStrict(val); + else if (key == "outlineIdleSeconds") c.outlineIdleSeconds = ParseDoubleStrict(val); +} + Config ParseConfig(const std::string& text) { Config c; // A UTF-8 BOM (Notepad's "UTF-8 with BOM") would glue itself to the first key and lose it. @@ -261,134 +395,7 @@ Config ParseConfig(const std::string& text) { if (key == "engineOther") { c.engineOther = val.empty() ? "auto" : val; continue; } if (val.empty()) continue; try { - if (key == "zoomInButton") c.zoomInButton = ParseIntStrict(val); - else if (key == "zoomOutButton") c.zoomOutButton = ParseIntStrict(val); - else if (key == "zoomInButton2") c.zoomInButton2 = ParseIntStrict(val); - else if (key == "zoomOutButton2") c.zoomOutButton2 = ParseIntStrict(val); - else if (key == "recenterVk") c.recenterVk = ParseIntStrict(val); - else if (key == "cursorLockVk") c.cursorLockVk = ParseIntStrict(val); - else if (key == "recenterMods") c.recenterMods = ParseIntStrict(val); - else if (key == "cursorLockMods") c.cursorLockMods = ParseIntStrict(val); - else if (key == "panLeftVk") c.panLeftVk = ParseIntStrict(val); - else if (key == "panLeftMods") c.panLeftMods = ParseIntStrict(val); - else if (key == "panRightVk") c.panRightVk = ParseIntStrict(val); - else if (key == "panRightMods") c.panRightMods = ParseIntStrict(val); - else if (key == "panUpVk") c.panUpVk = ParseIntStrict(val); - else if (key == "panUpMods") c.panUpMods = ParseIntStrict(val); - else if (key == "panDownVk") c.panDownVk = ParseIntStrict(val); - else if (key == "panDownMods") c.panDownMods = ParseIntStrict(val); - else if (key == "panSpeed") c.panSpeed = ParseDoubleStrict(val); - else if (key == "zoomTrace") c.zoomTrace = ParseIntStrict(val); - else if (key == "hitchLog") c.hitchLog = ParseIntStrict(val); - else if (key == "hitchThresholdPct") c.hitchThresholdPct = std::clamp(ParseIntStrict(val), 110, 1000); - // The chain is split in two: MSVC caps if/else nesting depth (C1061). Keys are unique, - // so a second chain changes nothing. - if (key == "hideCursorVk") c.hideCursorVk = ParseIntStrict(val); - else if (key == "hideCursorMods") c.hideCursorMods = ParseIntStrict(val); - else if (key == "zoomInVk") c.zoomInVk = ParseIntStrict(val); - else if (key == "zoomOutVk") c.zoomOutVk = ParseIntStrict(val); - else if (key == "zoomInVk2") c.zoomInVk2 = ParseIntStrict(val); - else if (key == "zoomOutVk2") c.zoomOutVk2 = ParseIntStrict(val); - else if (key == "zoomInMods") c.zoomInMods = ParseIntStrict(val); - else if (key == "zoomOutMods") c.zoomOutMods = ParseIntStrict(val); - else if (key == "zoomInMods2") c.zoomInMods2 = ParseIntStrict(val); - else if (key == "zoomOutMods2") c.zoomOutMods2 = ParseIntStrict(val); - else if (key == "zoomInButtonMods") c.zoomInButtonMods = ParseIntStrict(val); - else if (key == "zoomOutButtonMods") c.zoomOutButtonMods = ParseIntStrict(val); - else if (key == "zoomInButton2Mods") c.zoomInButton2Mods = ParseIntStrict(val); - else if (key == "zoomOutButton2Mods") c.zoomOutButton2Mods = ParseIntStrict(val); - else if (key == "zoomWheelMods") c.zoomWheelMods = ParseIntStrict(val); - else if (key == "panKeysOn") c.panKeysOn = ParseIntStrict(val) != 0 ? 1 : 0; - else if (key == "hideCursorOn") c.hideCursorOn = ParseIntStrict(val) != 0 ? 1 : 0; - else if (key == "cursorLockOn") c.cursorLockOn = ParseIntStrict(val) != 0 ? 1 : 0; - else if (key == "maxLevel") c.maxLevel = ParseDoubleStrict(val); - else if (key == "zoomInSpeed") c.zoomInSpeed = ParseDoubleStrict(val); - else if (key == "zoomOutSpeed") c.zoomOutSpeed = ParseDoubleStrict(val); - else if (key == "smoothZoom") c.smoothZoom = ParseIntStrict(val); - else if (key == "smoothZoomAccel") c.smoothZoomAccel = ParseDoubleStrict(val); - else if (key == "smoothZoomRamp") c.smoothZoomRamp = ParseDoubleStrict(val); - else if (key == "zoomEaseOutMs") c.zoomEaseOutMs = ParseIntStrict(val); - else if (key == "vsync") c.vsync = ParseIntStrict(val); - else if (key == "dwmFlush") c.dwmFlush = ParseIntStrict(val); - else if (key == "diagnostics") c.diagnostics = ParseIntStrict(val); - else if (key == "cursorSensitivity") c.cursorSensitivity = ParseDoubleStrict(val); - else if (key == "panGlideMaxPx") c.panGlideMaxPx = ParseIntStrict(val); - else if (key == "gameCursorLagMs") c.gameCursorLagMs = ParseIntStrict(val); - else if (key == "cursorConstantSize") c.cursorConstantSize = ParseIntStrict(val); - else if (key == "trayPinned") c.trayPinned = ParseIntStrict(val); - else if (key == "cursorVisibility") c.cursorVisibility = val; - else if (key == "model") c.model = val; - else if (key == "fastPan") c.fastPan = ParseIntStrict(val); - else if (key == "smoothPan") c.smoothPan = ParseIntStrict(val); - else if (key == "magInputTransform") c.magInputTransform = ParseIntStrict(val); - else if (key == "bilinear") c.bilinear = ParseIntStrict(val); - else if (key == "renderScaling") { c.renderScaling = ParseIntStrict(val); haveRenderScaling = true; } - else if (key == "upscaleTiming") c.upscaleTiming = ParseIntStrict(val); - else if (key == "sharpness") c.sharpness = ParseDoubleStrict(val); - else if (key == "zorderBand") c.zorderBand = ParseIntStrict(val); - else if (key == "brightness") c.brightness = ParseDoubleStrict(val); - else if (key == "colorWarmPct") c.colorWarmPct = ParseIntStrict(val); - else if (key == "colorDimPct") c.colorDimPct = ParseIntStrict(val); - else if (key == "hdrTonemap") c.hdrTonemap = ParseIntStrict(val); - else if (key == "multiMonitor") c.multiMonitor = ParseIntStrict(val); - else if (key == "cropCapture") c.cropCapture = ParseIntStrict(val); - else if (key == "lowGpuPriority") c.lowGpuPriority = ParseIntStrict(val); - else if (key == "gpuPriority") c.gpuPriority = ParseIntStrict(val); - else if (key == "gameCrop") c.gameCrop = ParseIntStrict(val); - else if (key == "tdrTest") c.tdrTest = ParseIntStrict(val); - else if (key == "probeClicks") c.probeClicks = ParseIntStrict(val); - else if (key == "desktopTransform") c.desktopTransform = ParseIntStrict(val); - else if (key == "cursorBandAuto") c.cursorBandAuto = ParseIntStrict(val); - else if (key == "trackCaret") c.trackCaret = ParseIntStrict(val); - else if (key == "trackFocus") c.trackFocus = ParseIntStrict(val); - else if (key == "trackAlign") c.trackAlign = ParseIntStrict(val); - else if (key == "mouseAlign") c.mouseAlign = ParseIntStrict(val); - else if (key == "trackGlideMs") c.trackGlideMs = ParseIntStrict(val); - else if (key == "trackMarginPct") c.trackMarginPct = ParseIntStrict(val); - else if (key == "mouseMarginPct") c.mouseMarginPct = ParseIntStrict(val); - else if (key == "trackLog") c.trackLog = ParseIntStrict(val); - else if (key == "ixDecimate") c.ixDecimate = ParseIntStrict(val); - else if (key == "mpoBuster") c.mpoBuster = ParseIntStrict(val); - else if (key == "txSamplingMode") { - // Only 0 (nearest), 1 (smooth) and -1 (leave DWM alone) are meaningful. 2..4 are - // kernel aliases of nearest that would slip past the MPO sampling guard; anything - // else folds to nearest so EffectiveSamplingMode guards it (review 2026-10-09 #23). - const int v = ParseIntStrict(val); - c.txSamplingMode = (v == 1 || v == -1) ? v : 0; - } - else if (key == "txWarmMode") c.txWarmMode = ParseIntStrict(val); - else if (key == "txTrace") c.txTrace = ParseIntStrict(val); - else if (key == "txRestLevel") c.txRestLevel = ParseDoubleStrict(val); - else if (key == "launchQuiesce") c.launchQuiesce = ParseIntStrict(val); - else if (key == "txWarmHz") c.txWarmHz = ParseIntStrict(val); - else if (key == "mpoNearestGuard") c.mpoNearestGuard = ParseIntStrict(val); - else if (key == "txNativeCursor") c.txNativeCursor = ParseIntStrict(val); - else if (key == "txSmoothLadder") c.txSmoothLadder = ParseIntStrict(val); - else if (key == "txRampMinStep") c.txRampMinStep = ParseIntStrict(val); - else if (key == "txRampMinFrom") c.txRampMinFrom = ParseDoubleStrict(val); - else if (key == "mpoGuardTest") c.mpoGuardTest = ParseIntStrict(val); - else if (key == "mpoGuard") c.mpoGuard = ParseIntStrict(val); - else if (key == "mpoGuardLiftWall") c.mpoGuardLiftWall = ParseIntStrict(val); - else if (key == "lockedBallistics") c.lockedBallistics = ParseIntStrict(val); - else if (key == "edgeClip") c.edgeClip = ParseIntStrict(val); - else if (key == "txMaxStepPct") c.txMaxStepPct = ParseIntStrict(val); - else if (key == "warpLock") c.warpLock = ParseIntStrict(val); - else if (key == "lockForce") c.lockForce = ParseIntStrict(val); - else if (key == "txEdgeMargin") c.txEdgeMargin = ParseDoubleStrict(val); - else if (key == "gameFpsCap") c.gameFpsCap = ParseIntStrict(val); - else if (key == "onboarded") c.onboarded = ParseIntStrict(val); - else if (key == "quickZoomDefault") c.quickZoomDefault = ParseDoubleStrict(val); - else if (key == "quickZoomModifier") c.quickZoomModifier = val; - else if (key == "quickZoomHotkeyMode") c.quickZoomHotkeyMode = ParseIntStrict(val); - else if (key == "quickZoomVk") c.quickZoomVk = ParseIntStrict(val); - else if (key == "quickZoomMods") c.quickZoomMods = ParseIntStrict(val); - else if (key == "outline") c.outline = ParseIntStrict(val); - else if (key == "outlineThickness") c.outlineThickness = ParseIntStrict(val); - else if (key == "outlineColor") { c.outlineColor = val; ParseHexColor(val, c.outlineR, c.outlineG, c.outlineB); } - else if (key == "outlineLowZoomOnly") c.outlineLowZoomOnly = ParseIntStrict(val); - else if (key == "outlineLowZoomMax") c.outlineLowZoomMax = ParseDoubleStrict(val); - else if (key == "outlineIdleHide") c.outlineIdleHide = ParseIntStrict(val); - else if (key == "outlineIdleSeconds") c.outlineIdleSeconds = ParseDoubleStrict(val); + ApplyValueKey(c, key, val, haveRenderScaling); } catch (...) { /* keep default on bad value */ } } ClampConfig(c, haveRenderScaling); From c00f416f2fb516cc19e5670d77a07ea50899ca1e Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:51:46 +0200 Subject: [PATCH 4/6] refactor(config): move the empty-value list and engine keys into ApplyEmptyableKey (#475) Move-only apart from 'continue' becoming 'return true' (the caller continues on true) and the two comments that said 'below' now naming ParseConfig / ApplyValueKey. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TdjopXZfsUiesYxYLYHEdH --- src/config.cpp | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index ef40c4d1..d44477e7 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -363,6 +363,27 @@ static void ApplyValueKey(Config& c, const std::string& key, const std::string& else if (key == "outlineIdleSeconds") c.outlineIdleSeconds = ParseDoubleStrict(val); } +// Keys that take an empty value; returns true when key is one of them (ParseConfig moves on). +static bool ApplyEmptyableKey(Config& c, const std::string& key, const std::string& val) { + // The exe LISTS must accept an empty value: "key=" with nothing after it is how the user + // clears one, and it is exactly what the config UI writes when the last entry is removed. + // They are handled before the empty-value guard in ParseConfig, which exists because the numeric + // settings cannot take one (stoi("") throws). Without this, clearing a list in the UI wrote + // the ini correctly and the core silently kept the previous value - including the shipped + // defaults, so a list with defaults could never be emptied at all. + if (key == "transformExclude") { c.transformExclude = val; return true; } + if (key == "renderExclude") { c.renderExclude = val; return true; } + if (key == "noSwallowApps") { c.noSwallowApps = val; return true; } + if (key == "lockApps") { c.lockApps = val; return true; } + // Per-window-type engine preferences. Parsed here with the lists rather than in ApplyValueKey, + // because an empty value must mean "back to auto" and the numeric guard would skip it. + if (key == "engineGame") { c.engineGame = val.empty() ? "auto" : val; return true; } + if (key == "engineAcrylic") { c.engineAcrylic = val.empty() ? "auto" : val; return true; } + if (key == "engineDesktop") { c.engineDesktop = val.empty() ? "auto" : val; return true; } + if (key == "engineOther") { c.engineOther = val.empty() ? "auto" : val; return true; } + return false; +} + Config ParseConfig(const std::string& text) { Config c; // A UTF-8 BOM (Notepad's "UTF-8 with BOM") would glue itself to the first key and lose it. @@ -377,22 +398,7 @@ Config ParseConfig(const std::string& text) { std::string key = TrimWs(t.substr(0, eq)); std::string val = TrimWs(t.substr(eq + 1)); if (key.empty()) continue; - // The exe LISTS must accept an empty value: "key=" with nothing after it is how the user - // clears one, and it is exactly what the config UI writes when the last entry is removed. - // They are handled before the empty-value guard below, which exists because the numeric - // settings cannot take one (stoi("") throws). Without this, clearing a list in the UI wrote - // the ini correctly and the core silently kept the previous value - including the shipped - // defaults, so a list with defaults could never be emptied at all. - if (key == "transformExclude") { c.transformExclude = val; continue; } - if (key == "renderExclude") { c.renderExclude = val; continue; } - if (key == "noSwallowApps") { c.noSwallowApps = val; continue; } - if (key == "lockApps") { c.lockApps = val; continue; } - // Per-window-type engine preferences. Parsed here with the lists rather than below, - // because an empty value must mean "back to auto" and the numeric guard would skip it. - if (key == "engineGame") { c.engineGame = val.empty() ? "auto" : val; continue; } - if (key == "engineAcrylic") { c.engineAcrylic = val.empty() ? "auto" : val; continue; } - if (key == "engineDesktop") { c.engineDesktop = val.empty() ? "auto" : val; continue; } - if (key == "engineOther") { c.engineOther = val.empty() ? "auto" : val; continue; } + if (ApplyEmptyableKey(c, key, val)) continue; if (val.empty()) continue; try { ApplyValueKey(c, key, val, haveRenderScaling); From dbf0ed7b8fe193bbd58f7a4af117f99f5a3ac07f Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:52:06 +0200 Subject: [PATCH 5/6] refactor(config): move ParseConfig's line tokenising into SplitIniLine (#475) Move-only apart from 'continue' becoming 'return false' and the key/val declarations becoming assignments to the out parameters (the caller declares them per line, as before). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TdjopXZfsUiesYxYLYHEdH --- src/config.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index d44477e7..4af4c288 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -384,6 +384,18 @@ static bool ApplyEmptyableKey(Config& c, const std::string& key, const std::stri return false; } +// One ini line to a trimmed key and value; false for a blank, comment, '='-less or keyless line. +static bool SplitIniLine(const std::string& line, std::string& key, std::string& val) { + std::string t = TrimWs(line); + if (t.empty() || t[0] == ';' || t[0] == '#') return false; + size_t eq = t.find('='); + if (eq == std::string::npos) return false; + key = TrimWs(t.substr(0, eq)); + val = TrimWs(t.substr(eq + 1)); + if (key.empty()) return false; + return true; +} + Config ParseConfig(const std::string& text) { Config c; // A UTF-8 BOM (Notepad's "UTF-8 with BOM") would glue itself to the first key and lose it. @@ -391,13 +403,8 @@ Config ParseConfig(const std::string& text) { std::string line; bool haveRenderScaling = false; // issue #456: without the key, `bilinear` decides (migration) while (std::getline(in, line)) { - std::string t = TrimWs(line); - if (t.empty() || t[0] == ';' || t[0] == '#') continue; - size_t eq = t.find('='); - if (eq == std::string::npos) continue; - std::string key = TrimWs(t.substr(0, eq)); - std::string val = TrimWs(t.substr(eq + 1)); - if (key.empty()) continue; + std::string key, val; + if (!SplitIniLine(line, key, val)) continue; if (ApplyEmptyableKey(c, key, val)) continue; if (val.empty()) continue; try { From 962f6637d6cb0db6436af48152a7baa6b5ff911f Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:54:33 +0200 Subject: [PATCH 6/6] docs: describe ParseConfig's helper structure in 08 and 09 (#475) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TdjopXZfsUiesYxYLYHEdH --- docs/architecture/08-config-profiles.md | 5 +++++ docs/architecture/09-settings-ui.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/architecture/08-config-profiles.md b/docs/architecture/08-config-profiles.md index 918e8856..f5eba4a0 100644 --- a/docs/architecture/08-config-profiles.md +++ b/docs/architecture/08-config-profiles.md @@ -73,6 +73,11 @@ as an initializer, so a missing or malformed key keeps the default; parsing neve - `model` must be `render`, `transform` or `hybrid`; anything else becomes `hybrid`. - Unsafe binds read as unbound (`src/keybind_rules.h`, [06](06-input.md)). - `LoadConfig` is the I/O wrapper, excluded from the test build by `WIND_TESTS`. +- Structure: per line `SplitIniLine` (key and value), `ApplyEmptyableKey` (the exe lists and + `engine*`, which accept an empty value), then `ApplyValueKey` (one parser per key; a bad value + throws and keeps the default). After the loop `ClampConfig` (ranges, the `renderScaling` + migration, the `model` fallback) and `SanitizeBinds`. A new key goes in `ApplyValueKey`, its + range in `ClampConfig`. ## Hot-reload and the UI-only fingerprint diff --git a/docs/architecture/09-settings-ui.md b/docs/architecture/09-settings-ui.md index ce996d09..76a16deb 100644 --- a/docs/architecture/09-settings-ui.md +++ b/docs/architecture/09-settings-ui.md @@ -106,7 +106,7 @@ and default. `Settings.svelte` is the shell (title bar, sidebar, banner, save ca - **Removed from the UI is not removed from the product.** Many keys have no row (quick zoom setup, outline, `sharpness`, `multiMonitor`, the `tx*` knobs). The core still parses them, and Open settings file is the way to edit them. -- Adding a setting is normally a schema row plus a `ParseConfig` entry. +- Adding a setting is normally a schema row plus a `ParseConfig` entry (`ApplyValueKey`, see 08). - Copy rules (in the schema header): plain language, no toggle label starting with "Enable", no description that restates its label.