From 63077d9a7c7cbb0d96e6f646765413c9ddb6ad20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Linares=20Garc=C3=ADa?= Date: Sun, 1 Mar 2026 22:40:31 +1100 Subject: [PATCH 1/6] Prevents config block state bleed Ensures `ElementName` and `MidiNRPN` components correctly manage their internal state by resetting local variables when the associated action type changes. This prevents stale data from being displayed or dispatched when switching between different configuration actions. Adds safety checks to `ElementName` to prevent dispatching updates for non-`ElementName` actions and to avoid creating empty `ElementName` actions in the configuration panel. Refines 14-bit resolution handling in `MidiNRPN` for more immediate updates. Fixes #1430 --- src/renderer/config-blocks/ElementName.svelte | 38 ++++++++++++++-- src/renderer/config-blocks/MidiNRPN.svelte | 44 +++++++++++++------ .../panels/configuration/Configuration.svelte | 5 +++ 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/renderer/config-blocks/ElementName.svelte b/src/renderer/config-blocks/ElementName.svelte index b82533468..7e491840c 100644 --- a/src/renderer/config-blocks/ElementName.svelte +++ b/src/renderer/config-blocks/ElementName.svelte @@ -64,17 +64,34 @@ }; let scriptValue = ""; // local script part + let lastParsedScript = ""; + let isActiveForCurrentAction = false; - $: if (!$action.invalid) { - handleActionChange($action); + $: if (action.short === 'sn') { + if (!$action.invalid && $action.script !== lastParsedScript) { + console.log('[ElementName] Parsing action:', action.id, 'script:', $action.script); + handleActionChange($action); + isActiveForCurrentAction = true; + } + } else { + // Reset state when this component instance is used for a non-ElementName action + if (lastParsedScript !== "") { + console.log('[ElementName] Resetting state, action type:', action.short); + scriptValue = ""; + lastParsedScript = ""; + isActiveForCurrentAction = false; + } } function handleActionChange(data: ActionData) { const matches = data.script.match(information.valueRegex); - scriptValue = matches[1]; + if (matches && matches[1] !== undefined) { + scriptValue = matches[1]; + lastParsedScript = data.script; + } } - $: { + $: if (action.short === 'sn' && isActiveForCurrentAction) { const index = event.config.findIndex((e) => e.id === action.id); if (index === 0 && NumberToEventType(event.type) === EventType.SETUP) { element.name = scriptValue; @@ -83,6 +100,18 @@ } function sendData(e) { + console.log('[ElementName] sendData called:', { + actionId: action.id, + actionShort: action.short, + isActive: isActiveForCurrentAction, + value: e + }); + // Safety check: only dispatch if this is actually an ElementName action + if (action.short !== 'sn' || !isActiveForCurrentAction) { + console.log('[ElementName] sendData blocked - not active for current action'); + return; + } + console.log('[ElementName] Dispatching update-action'); dispatch("update-action", { short: information.short, script: `self:gen("${e}")`, @@ -99,6 +128,7 @@ const { value, validationError } = e.detail; scriptValue = value; validator.value = !validationError; + isActiveForCurrentAction = true; dispatch("validation", { value: validationError }); }} on:change={() => dispatch("sync")} diff --git a/src/renderer/config-blocks/MidiNRPN.svelte b/src/renderer/config-blocks/MidiNRPN.svelte index 7efd158d9..9c45e866d 100644 --- a/src/renderer/config-blocks/MidiNRPN.svelte +++ b/src/renderer/config-blocks/MidiNRPN.svelte @@ -93,15 +93,31 @@ }, ]; - let channel: string; - let msb: string; - let lsb: string; - let nrpnCC: string; - let value: string; - let hiRes: boolean; - - $: if (!$action.invalid) { - handleActionChange($action); + let channel: string = ""; + let msb: string = ""; + let lsb: string = ""; + let nrpnCC: string = ""; + let value: string = ""; + let hiRes: boolean = false; + let lastParsedScript: string = ""; + let isInitialized: boolean = false; + + $: if (action.short === 'gmnp') { + if (!$action.invalid && $action.script !== lastParsedScript) { + handleActionChange($action); + } + } else { + // Reset state when this component instance is used for a non-MidiNRPN action + if (lastParsedScript !== "") { + channel = ""; + msb = ""; + lsb = ""; + nrpnCC = ""; + value = ""; + hiRes = false; + lastParsedScript = ""; + isInitialized = false; + } } function handleActionChange(data: ActionData) { @@ -136,6 +152,8 @@ lsb = midiLSB[0]; nrpnCC = calculateNRPNCC(midiMSB[0], midiLSB[0]); hiRes = midiLSB.length > 1 ? true : false; + lastParsedScript = data.script; + isInitialized = true; } function sendData() { @@ -154,9 +172,7 @@ }); } - $: handleHighResValueChange(hiRes); - - function handleHighResValueChange(hiRes: boolean) { + function handleHighResChange() { sendData(); dispatch("sync"); } @@ -196,7 +212,7 @@ suggestions[3] = [...localDefinitions]; } - $: if ($event) { + $: if (action.short === 'gmnp' && $event) { renderSuggestions(); } @@ -355,7 +371,7 @@ postProcessor={GridScript.shortify} preProcessor={GridScript.humanize} /> - +
diff --git a/src/renderer/main/panels/configuration/Configuration.svelte b/src/renderer/main/panels/configuration/Configuration.svelte index 55ebec0f2..8d22d9f52 100644 --- a/src/renderer/main/panels/configuration/Configuration.svelte +++ b/src/renderer/main/panels/configuration/Configuration.svelte @@ -77,6 +77,11 @@ const setup = element.findEvent(EventTypeToNumber(EventType.SETUP)); if (setup.actionAt(0)?.short !== elementNameInformation.short) { + // Don't create ElementName action if value is empty/undefined + // (this happens when switching to an element without ElementName) + if (!value || value.length === 0) { + return; + } const data = new ActionData( elementNameInformation.short, generateScript(value), From 15ede0d4af6f2f35805709d3d708f16f322d2ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Linares=20Garc=C3=ADa?= Date: Sun, 1 Mar 2026 23:33:08 +1100 Subject: [PATCH 2/6] Fixes #1430: Improves MIDI script parsing robustness Enhances parsing logic and error handling in MIDI configuration blocks, specifically for NRPN and 14-bit MIDI messages. - Prevents runtime errors when parsing incomplete or malformed scripts in `MidiNRPN` and `MidiFourteenBitFace` components by adding explicit checks and safe fallbacks. - Ensures element name parsing in `Configuration` component gracefully handles scripts that do not match the expected pattern, avoiding crashes. - Centralizes state resetting in `MidiNRPN` for cleaner error recovery. --- src/renderer/config-blocks/MidiNRPN.svelte | 92 +++++++++++++------ .../headers/MidiFourteenBitFace.svelte | 27 ++++++ .../panels/configuration/Configuration.svelte | 20 +++- 3 files changed, 105 insertions(+), 34 deletions(-) diff --git a/src/renderer/config-blocks/MidiNRPN.svelte b/src/renderer/config-blocks/MidiNRPN.svelte index 9c45e866d..87ed6cc1c 100644 --- a/src/renderer/config-blocks/MidiNRPN.svelte +++ b/src/renderer/config-blocks/MidiNRPN.svelte @@ -58,7 +58,7 @@ export let action: GridAction; const dispatch = createEventDispatcher(); - let event = action.parent as GridEvent; + let event: GridEvent; const validators = [ { @@ -102,7 +102,10 @@ let lastParsedScript: string = ""; let isInitialized: boolean = false; - $: if (action.short === 'gmnp') { + // Keep the parent event reference in sync with the current action + $: event = action.parent as GridEvent; + + $: if (action.short === "gmnp") { if (!$action.invalid && $action.script !== lastParsedScript) { handleActionChange($action); } @@ -120,40 +123,71 @@ } } - function handleActionChange(data: ActionData) { - // Extract all contents - const matches = []; - const regex = /gms\((.*?[^)])\)(?=\s|$)/g; + function resetLocalState() { + channel = ""; + msb = ""; + lsb = ""; + nrpnCC = ""; + value = ""; + hiRes = false; + } - let match; - while ((match = regex.exec(data.script)) !== null) { - matches.push(`gms(${match[1].trim()})`); // trim to remove any extra spaces + function handleActionChange(data: ActionData) { + if (!data?.script) { + resetLocalState(); + lastParsedScript = ""; + isInitialized = false; + return; } - let midiLSB = []; - let midiMSB = []; + try { + // Extract all gms(...) calls (allowing no spaces between them) + const matches: string[] = []; + const regex = /gms\((.*?)\)/g; - for (let i = 0; i < matches.length; ++i) { - let part = Script.toSegments({ short: "gms", script: matches[i] }); - if (i % 2 === 0) { - midiMSB.push(part[3]); - } else { - midiLSB.push(part[3]); + let match; + while ((match = regex.exec(data.script)) !== null) { + matches.push(`gms(${match[1].trim()})`); // trim to remove any extra spaces } - } - value = midiMSB[1].split("//")[0]; - if (value.startsWith("(") && value.endsWith(")")) { - value = value.slice(1, -1); - } + const midiLSB: string[] = []; + const midiMSB: string[] = []; - channel = Script.toSegments({ short: "gms", script: matches[0] })[0]; - msb = midiMSB[0]; - lsb = midiLSB[0]; - nrpnCC = calculateNRPNCC(midiMSB[0], midiLSB[0]); - hiRes = midiLSB.length > 1 ? true : false; - lastParsedScript = data.script; - isInitialized = true; + for (let i = 0; i < matches.length; ++i) { + const part = Script.toSegments({ short: "gms", script: matches[i] }); + if (i % 2 === 0) { + midiMSB.push(part[3]); + } else { + midiLSB.push(part[3]); + } + } + + // Original parsing logic: value comes from the second MSB entry + let parsedValue = midiMSB[1].split("//")[0]; + if (parsedValue.startsWith("(") && parsedValue.endsWith(")")) { + parsedValue = parsedValue.slice(1, -1); + } + + const channelSegments = Script.toSegments({ + short: "gms", + script: matches[0], + }); + + channel = channelSegments[0]; + msb = midiMSB[0]; + lsb = midiLSB[0]; + value = parsedValue; + nrpnCC = calculateNRPNCC(midiMSB[0], midiLSB[0]); + hiRes = midiLSB.length > 1 ? true : false; + + lastParsedScript = data.script; + isInitialized = true; + } catch (err) { + // If parsing fails for any reason, fall back to a safe empty state + console.error("[MidiNRPN] Failed to parse script", err, data?.script); + resetLocalState(); + isInitialized = false; + } } function sendData() { diff --git a/src/renderer/config-blocks/headers/MidiFourteenBitFace.svelte b/src/renderer/config-blocks/headers/MidiFourteenBitFace.svelte index 08ae7d8e4..0fd41eea0 100644 --- a/src/renderer/config-blocks/headers/MidiFourteenBitFace.svelte +++ b/src/renderer/config-blocks/headers/MidiFourteenBitFace.svelte @@ -18,8 +18,23 @@ } function handleActionChange(data: ActionData) { + if (!data?.script) { + scriptSegments = ["", "", ""]; + midiLSB = ""; + midiMSB = ""; + return; + } + const arr = data.script.split(" gms"); + // Expect at least two gms segments for a valid 14-bit style script + if (arr.length < 2) { + scriptSegments = ["", "", ""]; + midiLSB = ""; + midiMSB = ""; + return; + } + let lsb = whatsInParenthesis.exec(arr[0]); if (lsb !== null) { @@ -36,8 +51,20 @@ } } + // If we failed to extract LSB parameters, bail out safely + if (!midiLSB) { + scriptSegments = ["", "", ""]; + return; + } + let param_array = midiLSB.split(",").map((c) => c.trim()); + // Require at least 4 parameters: channel, status, base, value + if (param_array.length < 4 || !param_array[3]) { + scriptSegments = ["", "", ""]; + return; + } + let value = param_array[3].split("//").slice(0, -1).join("//"); let param_object = { diff --git a/src/renderer/main/panels/configuration/Configuration.svelte b/src/renderer/main/panels/configuration/Configuration.svelte index 8d22d9f52..609199137 100644 --- a/src/renderer/main/panels/configuration/Configuration.svelte +++ b/src/renderer/main/panels/configuration/Configuration.svelte @@ -92,7 +92,14 @@ const action = setup.actionAt(0); const regex = elementNameInformation.valueRegex; - const name = action.script.match(regex)[1]; + const match = action.script.match(regex); + const name = match?.[1]; + + // If the existing ElementName action's script does not match the expected pattern, + // do not attempt to update or remove it to avoid runtime errors. + if (typeof name === "undefined") { + return; + } if (name !== value) { const data = new ActionData( @@ -115,10 +122,13 @@ if (action?.short === elementNameInformation.short) { const regex = elementNameInformation.valueRegex; - const value = action.script.match(regex)[1]; - if (value !== elementName) { - elementName = value; - element.name = value; + const match = action.script.match(regex); + const value = match?.[1]; + if (typeof value !== "undefined") { + if (value !== elementName) { + elementName = value; + element.name = value; + } } } else { elementName = ""; From 802274e6cb6af34746e6b5b9335089432a1ed375 Mon Sep 17 00:00:00 2001 From: sukuwc Date: Fri, 6 Mar 2026 11:35:33 +0100 Subject: [PATCH 3/6] Clean up ElementName: remove debug logs, fix isActiveForCurrentAction - Remove all console.log debug statements left in from development - Move isActiveForCurrentAction = true into handleActionChange so the flag is correctly set when loading from script, not only on user input Co-Authored-By: Claude Sonnet 4.6 --- src/renderer/config-blocks/ElementName.svelte | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/renderer/config-blocks/ElementName.svelte b/src/renderer/config-blocks/ElementName.svelte index 7e491840c..0f380269a 100644 --- a/src/renderer/config-blocks/ElementName.svelte +++ b/src/renderer/config-blocks/ElementName.svelte @@ -69,14 +69,11 @@ $: if (action.short === 'sn') { if (!$action.invalid && $action.script !== lastParsedScript) { - console.log('[ElementName] Parsing action:', action.id, 'script:', $action.script); handleActionChange($action); - isActiveForCurrentAction = true; } } else { // Reset state when this component instance is used for a non-ElementName action if (lastParsedScript !== "") { - console.log('[ElementName] Resetting state, action type:', action.short); scriptValue = ""; lastParsedScript = ""; isActiveForCurrentAction = false; @@ -88,6 +85,7 @@ if (matches && matches[1] !== undefined) { scriptValue = matches[1]; lastParsedScript = data.script; + isActiveForCurrentAction = true; } } @@ -100,18 +98,10 @@ } function sendData(e) { - console.log('[ElementName] sendData called:', { - actionId: action.id, - actionShort: action.short, - isActive: isActiveForCurrentAction, - value: e - }); // Safety check: only dispatch if this is actually an ElementName action if (action.short !== 'sn' || !isActiveForCurrentAction) { - console.log('[ElementName] sendData blocked - not active for current action'); return; } - console.log('[ElementName] Dispatching update-action'); dispatch("update-action", { short: information.short, script: `self:gen("${e}")`, From 9f2926f717d5145c8b5e310e27aa8eb330ed379e Mon Sep 17 00:00:00 2001 From: sukuwc Date: Fri, 6 Mar 2026 11:46:48 +0100 Subject: [PATCH 4/6] SUKU style cleanup --- .../panels/configuration/Configuration.svelte | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/renderer/main/panels/configuration/Configuration.svelte b/src/renderer/main/panels/configuration/Configuration.svelte index 609199137..762fbadeb 100644 --- a/src/renderer/main/panels/configuration/Configuration.svelte +++ b/src/renderer/main/panels/configuration/Configuration.svelte @@ -77,9 +77,7 @@ const setup = element.findEvent(EventTypeToNumber(EventType.SETUP)); if (setup.actionAt(0)?.short !== elementNameInformation.short) { - // Don't create ElementName action if value is empty/undefined - // (this happens when switching to an element without ElementName) - if (!value || value.length === 0) { + if (typeof value === "undefined" || value.length === 0) { return; } const data = new ActionData( @@ -91,12 +89,8 @@ } const action = setup.actionAt(0); - const regex = elementNameInformation.valueRegex; - const match = action.script.match(regex); - const name = match?.[1]; + const name = action.script.match(elementNameInformation.valueRegex)?.[1]; - // If the existing ElementName action's script does not match the expected pattern, - // do not attempt to update or remove it to avoid runtime errors. if (typeof name === "undefined") { return; } @@ -120,18 +114,15 @@ const setup = element.findEvent(EventTypeToNumber(EventType.SETUP)); const action = setup.actionAt(0); - if (action?.short === elementNameInformation.short) { - const regex = elementNameInformation.valueRegex; - const match = action.script.match(regex); - const value = match?.[1]; - if (typeof value !== "undefined") { - if (value !== elementName) { - elementName = value; - element.name = value; - } - } - } else { + if (action?.short !== elementNameInformation.short) { elementName = ""; + return; + } + + const value = action.script.match(elementNameInformation.valueRegex)?.[1]; + if (typeof value !== "undefined" && value !== elementName) { + elementName = value; + element.name = value; } } From 97806d55ceeefa77e45025a7cd605d6550875516 Mon Sep 17 00:00:00 2001 From: sukuwc Date: Fri, 6 Mar 2026 12:48:03 +0100 Subject: [PATCH 5/6] SUKU format fixed --- src/renderer/config-blocks/ElementName.svelte | 6 +++--- src/renderer/config-blocks/MidiNRPN.svelte | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/renderer/config-blocks/ElementName.svelte b/src/renderer/config-blocks/ElementName.svelte index 0f380269a..373ac88bb 100644 --- a/src/renderer/config-blocks/ElementName.svelte +++ b/src/renderer/config-blocks/ElementName.svelte @@ -67,7 +67,7 @@ let lastParsedScript = ""; let isActiveForCurrentAction = false; - $: if (action.short === 'sn') { + $: if (action.short === "sn") { if (!$action.invalid && $action.script !== lastParsedScript) { handleActionChange($action); } @@ -89,7 +89,7 @@ } } - $: if (action.short === 'sn' && isActiveForCurrentAction) { + $: if (action.short === "sn" && isActiveForCurrentAction) { const index = event.config.findIndex((e) => e.id === action.id); if (index === 0 && NumberToEventType(event.type) === EventType.SETUP) { element.name = scriptValue; @@ -99,7 +99,7 @@ function sendData(e) { // Safety check: only dispatch if this is actually an ElementName action - if (action.short !== 'sn' || !isActiveForCurrentAction) { + if (action.short !== "sn" || !isActiveForCurrentAction) { return; } dispatch("update-action", { diff --git a/src/renderer/config-blocks/MidiNRPN.svelte b/src/renderer/config-blocks/MidiNRPN.svelte index 87ed6cc1c..b29971c24 100644 --- a/src/renderer/config-blocks/MidiNRPN.svelte +++ b/src/renderer/config-blocks/MidiNRPN.svelte @@ -246,7 +246,7 @@ suggestions[3] = [...localDefinitions]; } - $: if (action.short === 'gmnp' && $event) { + $: if (action.short === "gmnp" && $event) { renderSuggestions(); } @@ -405,7 +405,11 @@ postProcessor={GridScript.shortify} preProcessor={GridScript.humanize} /> - +
From 47b2f22c380d949592fcf0ead37da7c6be14c0ce Mon Sep 17 00:00:00 2001 From: sukuwc Date: Fri, 6 Mar 2026 12:58:41 +0100 Subject: [PATCH 6/6] SUKU gauard moved to action list --- src/renderer/config-blocks/ElementName.svelte | 24 +++---------------- .../panels/configuration/ActionList.svelte | 2 +- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/renderer/config-blocks/ElementName.svelte b/src/renderer/config-blocks/ElementName.svelte index 373ac88bb..cfbf67953 100644 --- a/src/renderer/config-blocks/ElementName.svelte +++ b/src/renderer/config-blocks/ElementName.svelte @@ -64,32 +64,19 @@ }; let scriptValue = ""; // local script part - let lastParsedScript = ""; - let isActiveForCurrentAction = false; - $: if (action.short === "sn") { - if (!$action.invalid && $action.script !== lastParsedScript) { - handleActionChange($action); - } - } else { - // Reset state when this component instance is used for a non-ElementName action - if (lastParsedScript !== "") { - scriptValue = ""; - lastParsedScript = ""; - isActiveForCurrentAction = false; - } + $: if (!$action.invalid) { + handleActionChange($action); } function handleActionChange(data: ActionData) { const matches = data.script.match(information.valueRegex); if (matches && matches[1] !== undefined) { scriptValue = matches[1]; - lastParsedScript = data.script; - isActiveForCurrentAction = true; } } - $: if (action.short === "sn" && isActiveForCurrentAction) { + $: { const index = event.config.findIndex((e) => e.id === action.id); if (index === 0 && NumberToEventType(event.type) === EventType.SETUP) { element.name = scriptValue; @@ -98,10 +85,6 @@ } function sendData(e) { - // Safety check: only dispatch if this is actually an ElementName action - if (action.short !== "sn" || !isActiveForCurrentAction) { - return; - } dispatch("update-action", { short: information.short, script: `self:gen("${e}")`, @@ -118,7 +101,6 @@ const { value, validationError } = e.detail; scriptValue = value; validator.value = !validationError; - isActiveForCurrentAction = true; dispatch("validation", { value: validationError }); }} on:change={() => dispatch("sync")} diff --git a/src/renderer/main/panels/configuration/ActionList.svelte b/src/renderer/main/panels/configuration/ActionList.svelte index 5ebe639aa..ababe691a 100644 --- a/src/renderer/main/panels/configuration/ActionList.svelte +++ b/src/renderer/main/panels/configuration/ActionList.svelte @@ -170,7 +170,7 @@ in:fade|global={{ delay: 0 }} >
- {#key $latestComponentVersionKeys.get(action.short)} + {#key `${$latestComponentVersionKeys.get(action.short)}-${action.short}`}