From 7718ea49cfd862216f35d455e26eb9fa1df36142 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 18:18:01 +0000 Subject: [PATCH 1/7] Initial plan From bc7cf72f394090b3d0831e8bf9d4c7c3fa9d8c2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 18:27:59 +0000 Subject: [PATCH 2/7] Fix AOT hard-coded key binding clone Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/275ee9b0-ace3-402a-a75e-18ca75db4388 Co-authored-by: tig <585482+tig@users.noreply.github.com> --- .../Configuration/ConfigurationManager.cs | 52 +++++++++++++++++- .../Configuration/ConfigurationMangerTests.cs | 54 +++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs index 96b7182dc3..2daca01482 100644 --- a/Terminal.Gui/Configuration/ConfigurationManager.cs +++ b/Terminal.Gui/Configuration/ConfigurationManager.cs @@ -204,7 +204,7 @@ internal static void Initialize () // Some getters (notably ThemeManager.Themes while uninitialized) synthesize composite // objects from other configuration properties, so a single populate-and-clone pass can // observe half-initialized cache entries and persist null/default values into the clone. - hardCodedProperty.Value.PropertyValue = DeepCloner.DeepClone (hardCodedProperty.Value.PropertyValue); + hardCodedProperty.Value.PropertyValue = CloneHardCodedPropertyValue (hardCodedProperty.Value.PropertyValue); hardCodedProperty.Value.Immutable = true; } } @@ -222,6 +222,56 @@ internal static void Initialize () ThemeManager.Themes? [ThemeManager.Theme].Apply (); } + private static object? CloneHardCodedPropertyValue (object? propertyValue) + { + if (propertyValue is Dictionary keyBindings) + { + return CloneKeyBindings (keyBindings); + } + + return DeepCloner.DeepClone (propertyValue); + } + + private static Dictionary CloneKeyBindings (Dictionary source) + { + Dictionary clone = new (source.Comparer); + + foreach (KeyValuePair kvp in source) + { + clone [kvp.Key] = ClonePlatformKeyBinding (kvp.Value); + } + + return clone; + } + + private static PlatformKeyBinding ClonePlatformKeyBinding (PlatformKeyBinding binding) + { + return new () + { + All = CloneKeyArray (binding.All), + Windows = CloneKeyArray (binding.Windows), + Linux = CloneKeyArray (binding.Linux), + Macos = CloneKeyArray (binding.Macos) + }; + } + + private static Key []? CloneKeyArray (Key []? keys) + { + if (keys is null) + { + return null; + } + + Key [] clonedKeys = new Key [keys.Length]; + + for (var i = 0; i < keys.Length; i++) + { + clonedKeys [i] = new (keys [i]); + } + + return clonedKeys; + } + #endregion Initialization #region Enable/Disable diff --git a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs b/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs index cf24139fcf..2cd9ead6ce 100644 --- a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs +++ b/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs @@ -54,6 +54,60 @@ public void HardCodedDefaultCache_Properties_Are_Copies () Disable (true); } + [Fact] + public void HardCodedDefaultCache_KeyBindingDictionaries_Are_Typed_Deep_Copies () + { + // Copilot + AssertKeyBindingDictionaryIsDeepCopy ("Application.DefaultKeyBindings", Application.DefaultKeyBindings!); + AssertKeyBindingDictionaryIsDeepCopy ("View.DefaultKeyBindings", View.DefaultKeyBindings!); + + static void AssertKeyBindingDictionaryIsDeepCopy (string propertyName, Dictionary currentBindings) + { + FrozenDictionary cache = GetHardCodedConfigPropertyCache (); + Dictionary cachedBindings = Assert.IsType> (cache [propertyName].PropertyValue); + + Assert.NotSame (currentBindings, cachedBindings); + Assert.NotEmpty (cachedBindings); + + KeyValuePair cachedEntry = cachedBindings.First (kvp => HasAnyKeys (kvp.Value)); + PlatformKeyBinding currentBinding = currentBindings [cachedEntry.Key]; + + Assert.NotSame (currentBinding, cachedEntry.Value); + AssertKeyArraysAreDeepCopies (currentBinding.All, cachedEntry.Value.All); + AssertKeyArraysAreDeepCopies (currentBinding.Windows, cachedEntry.Value.Windows); + AssertKeyArraysAreDeepCopies (currentBinding.Linux, cachedEntry.Value.Linux); + AssertKeyArraysAreDeepCopies (currentBinding.Macos, cachedEntry.Value.Macos); + } + + static bool HasAnyKeys (PlatformKeyBinding binding) + { + return binding.All is { Length: > 0 } + || binding.Windows is { Length: > 0 } + || binding.Linux is { Length: > 0 } + || binding.Macos is { Length: > 0 }; + } + + static void AssertKeyArraysAreDeepCopies (Key [] currentKeys, Key [] cachedKeys) + { + if (currentKeys is null) + { + Assert.Null (cachedKeys); + + return; + } + + Assert.NotNull (cachedKeys); + Assert.NotSame (currentKeys, cachedKeys); + Assert.Equal (currentKeys.Length, cachedKeys.Length); + + for (var i = 0; i < currentKeys.Length; i++) + { + Assert.NotSame (currentKeys [i], cachedKeys [i]); + Assert.Equal (currentKeys [i], cachedKeys [i]); + } + } + } + [Fact] public void HardCoded_Default_Theme_Uses_Fully_Populated_Cache_Values () { From 60418683fe2e9e0a157654665dd865f9e6f830f0 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 7 May 2026 12:57:42 -0600 Subject: [PATCH 3/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Configuration/ConfigurationMangerTests.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs b/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs index 2cd9ead6ce..b488c08fc3 100644 --- a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs +++ b/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs @@ -58,8 +58,19 @@ public void HardCodedDefaultCache_Properties_Are_Copies () public void HardCodedDefaultCache_KeyBindingDictionaries_Are_Typed_Deep_Copies () { // Copilot - AssertKeyBindingDictionaryIsDeepCopy ("Application.DefaultKeyBindings", Application.DefaultKeyBindings!); - AssertKeyBindingDictionaryIsDeepCopy ("View.DefaultKeyBindings", View.DefaultKeyBindings!); + Assert.False (IsEnabled); + Application.ResetState (true); + + try + { + AssertKeyBindingDictionaryIsDeepCopy ("Application.DefaultKeyBindings", Application.DefaultKeyBindings!); + AssertKeyBindingDictionaryIsDeepCopy ("View.DefaultKeyBindings", View.DefaultKeyBindings!); + } + finally + { + Disable (true); + Application.ResetState (true); + } static void AssertKeyBindingDictionaryIsDeepCopy (string propertyName, Dictionary currentBindings) { From ef54daff0df276dd0f8fa42f660b9d2b2d75750c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:00:31 +0000 Subject: [PATCH 4/7] Match nullable key array test helper Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/d1e76594-a3c8-4c84-b738-3decfdb73987 Co-authored-by: tig <585482+tig@users.noreply.github.com> --- .../Configuration/ConfigurationMangerTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs b/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs index b488c08fc3..ab29345522 100644 --- a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs +++ b/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs @@ -98,7 +98,8 @@ static bool HasAnyKeys (PlatformKeyBinding binding) || binding.Macos is { Length: > 0 }; } - static void AssertKeyArraysAreDeepCopies (Key [] currentKeys, Key [] cachedKeys) +#nullable enable + static void AssertKeyArraysAreDeepCopies (Key []? currentKeys, Key []? cachedKeys) { if (currentKeys is null) { @@ -117,6 +118,7 @@ static void AssertKeyArraysAreDeepCopies (Key [] currentKeys, Key [] cachedKeys) Assert.Equal (currentKeys [i], cachedKeys [i]); } } +#nullable restore } [Fact] From b2ceb74be579b4e8521626c299433b2e1eddd687 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:02:04 +0000 Subject: [PATCH 5/7] Use precise nullable warning suppression Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/d1e76594-a3c8-4c84-b738-3decfdb73987 Co-authored-by: tig <585482+tig@users.noreply.github.com> --- .../Configuration/ConfigurationMangerTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs b/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs index ab29345522..0ec6189397 100644 --- a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs +++ b/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs @@ -98,8 +98,9 @@ static bool HasAnyKeys (PlatformKeyBinding binding) || binding.Macos is { Length: > 0 }; } -#nullable enable +#pragma warning disable CS8632 // Nullable annotations document the nullable PlatformKeyBinding properties in this non-nullable test file. static void AssertKeyArraysAreDeepCopies (Key []? currentKeys, Key []? cachedKeys) +#pragma warning restore CS8632 { if (currentKeys is null) { @@ -118,7 +119,6 @@ static void AssertKeyArraysAreDeepCopies (Key []? currentKeys, Key []? cachedKey Assert.Equal (currentKeys [i], cachedKeys [i]); } } -#nullable restore } [Fact] From 5784607f3b8048f4715275d73c2abbd57755a7fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:05:32 +0000 Subject: [PATCH 6/7] Use record with expression for key binding clone Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/094cf268-64cd-42e9-9179-af0d70e786a2 Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Terminal.Gui/Configuration/ConfigurationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs index 2daca01482..8b86d9eef5 100644 --- a/Terminal.Gui/Configuration/ConfigurationManager.cs +++ b/Terminal.Gui/Configuration/ConfigurationManager.cs @@ -246,7 +246,7 @@ private static Dictionary CloneKeyBindings (Diction private static PlatformKeyBinding ClonePlatformKeyBinding (PlatformKeyBinding binding) { - return new () + return binding with { All = CloneKeyArray (binding.All), Windows = CloneKeyArray (binding.Windows), From bfc9c73282ec72d45d1d3323295d60dfa4b73321 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 7 May 2026 13:26:56 -0600 Subject: [PATCH 7/7] Add AOT-safety comment to CloneHardCodedPropertyValue Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Terminal.Gui/Configuration/ConfigurationManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs index 8b86d9eef5..e005ec3792 100644 --- a/Terminal.Gui/Configuration/ConfigurationManager.cs +++ b/Terminal.Gui/Configuration/ConfigurationManager.cs @@ -222,6 +222,8 @@ internal static void Initialize () ThemeManager.Themes? [ThemeManager.Theme].Apply (); } + // AOT trimming removes the reflective Dictionary<,> constructor that DeepCloner relies on. + // Each non-trivially-cloneable config property type needs a typed clone path here to stay AOT-safe. private static object? CloneHardCodedPropertyValue (object? propertyValue) { if (propertyValue is Dictionary keyBindings)