Browse Source

Fixes #2181 - (Really) Adds configuration manager (#2365)

Tig 2 years ago
parent
commit
9425b2a720
56 changed files with 5101 additions and 807 deletions
  1. 2 0
      .gitignore
  2. 2 2
      README.md
  3. 0 566
      Terminal.Gui UnitTests/ScenarioTests.cs
  4. 0 57
      Terminal.Gui UnitTests/UnitTests.csproj
  5. 45 0
      Terminal.Gui/Configuration/AppScope.cs
  6. 89 0
      Terminal.Gui/Configuration/AttributeJsonConverter.cs
  7. 76 0
      Terminal.Gui/Configuration/ColorJsonConverter.cs
  8. 89 0
      Terminal.Gui/Configuration/ColorSchemeJsonConverter.cs
  9. 663 0
      Terminal.Gui/Configuration/ConfigurationManager.cs
  10. 41 0
      Terminal.Gui/Configuration/DictionaryJsonConverter.cs
  11. 132 0
      Terminal.Gui/Configuration/KeyJsonConverter.cs
  12. 206 0
      Terminal.Gui/Configuration/Scope.cs
  13. 118 0
      Terminal.Gui/Configuration/SettingsScope.cs
  14. 292 0
      Terminal.Gui/Configuration/ThemeScope.cs
  15. 20 3
      Terminal.Gui/Core/Application.cs
  16. 31 11
      Terminal.Gui/Core/Border.cs
  17. 17 32
      Terminal.Gui/Core/ConsoleDriver.cs
  18. 14 2
      Terminal.Gui/Core/Window.cs
  19. 445 0
      Terminal.Gui/Resources/config.json
  20. 9 0
      Terminal.Gui/Terminal.Gui.csproj
  21. 3 0
      Terminal.Gui/Types/Point.cs
  22. 46 1
      Terminal.Gui/Views/FrameView.cs
  23. 6 3
      Terminal.Gui/Views/ListView.cs
  24. 1 1
      Terminal.Gui/Views/StatusBar.cs
  25. 1 1
      Terminal.Gui/Views/TextField.cs
  26. 27 3
      Terminal.Gui/Windows/Dialog.cs
  27. 16 18
      Terminal.sln
  28. 183 0
      UICatalog/Resources/config.json
  29. 21 2
      UICatalog/Scenario.cs
  30. 223 0
      UICatalog/Scenarios/ConfigurationEditor.cs
  31. 224 66
      UICatalog/UICatalog.cs
  32. 6 0
      UICatalog/UICatalog.csproj
  33. 2 0
      UnitTests/Application/ApplicationTests.cs
  34. 12 16
      UnitTests/Application/StackExtensionsTests.cs
  35. 1 1
      UnitTests/Colors/AttributeTests.cs
  36. 89 0
      UnitTests/Configuration/AppScopeTests.cs
  37. 833 0
      UnitTests/Configuration/ConfigurationMangerTests.cs
  38. 234 0
      UnitTests/Configuration/JsonConverterTests.cs
  39. 92 0
      UnitTests/Configuration/SettingsScopeTests.cs
  40. 82 0
      UnitTests/Configuration/ThemeScopeTests.cs
  41. 176 0
      UnitTests/Configuration/ThemeTests.cs
  42. 6 1
      UnitTests/TestHelpers.cs
  43. 2 1
      UnitTests/Text/CollectionNavigatorTests.cs
  44. 2 2
      UnitTests/Types/DimTests.cs
  45. 1 0
      UnitTests/Types/PointTests.cs
  46. 6 10
      UnitTests/Types/PosTests.cs
  47. 1 0
      UnitTests/Types/RectTests.cs
  48. 1 0
      UnitTests/UICatalog/ScenarioTests.cs
  49. 87 0
      UnitTests/Views/TileViewTests.cs
  50. 110 0
      docfx/articles/config.md
  51. 1 0
      docfx/articles/index.md
  52. 14 5
      docfx/build.ps1
  53. 4 3
      docfx/docfx.json
  54. BIN
      docfx/images/sample.gif
  55. 1 0
      docfx/index.md
  56. 296 0
      docfx/schemas/tui-config-schema.json

+ 2 - 0
.gitignore

@@ -24,3 +24,5 @@ UnitTests/TestResults
 demo.*
 
 *.deb
+
+*.tui/

+ 2 - 2
README.md

@@ -40,10 +40,10 @@ _The Documentation matches the most recent Nuget release from the `main` branch
 * **Cross Platform** - Windows, Mac, and Linux. Terminal drivers for Curses, [Windows Console](https://github.com/gui-cs/Terminal.Gui/issues/27), and the .NET Console mean apps will work well on both color and monochrome terminals. 
 * **Keyboard and Mouse Input** - Both keyboard and mouse input are supported, including support for drag & drop.
 * **[Flexible Layout](https://gui-cs.github.io/Terminal.Gui/articles/overview.html#layout)** - Supports both *Absolute layout* and an innovative *Computed Layout* system. *Computed Layout* makes it easy to layout controls relative to each other and enables dynamic terminal UIs.
+* **[Configuration & Themes](https://gui-cs.github.io/Terminal.Gui/articles/config.html)** - Terminal.Gui supports a rich configuration system that allows end-user customization of how the UI looks (e.g. colors) and behaves (e.g. key-bindings).
 * **Clipboard support** - Cut, Copy, and Paste of text provided through the [`Clipboard`](https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui/Terminal.Gui.Clipboard.html) class.
 * **[Arbitrary Views](https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui/Terminal.Gui.View.html)** - All visible UI elements are subclasses of the `View` class, and these in turn can contain an arbitrary number of sub-views.
-* **Advanced App Features** - The [Mainloop](https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui/Terminal.Gui.MainLoop.html) supports processing events, idle handlers, timers, and monitoring file
-descriptors. Most classes are safe for threading.
+* **Advanced App Features** - The [Mainloop](https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui/Terminal.Gui.MainLoop.html) supports processing events, idle handlers, timers, and monitoring file descriptors. Most classes are safe for threading.
 * **Reactive Extensions** - Use [reactive extensions](https://github.com/dotnet/reactive) and benefit from increased code readability, and the ability to apply the MVVM pattern and [ReactiveUI](https://www.reactiveui.net/) data bindings. See the [source code](https://github.com/gui-cs/Terminal.Gui/tree/master/ReactiveExample) of a sample app in order to learn how to achieve this.
 
 ## Showcase & Examples

+ 0 - 566
Terminal.Gui UnitTests/ScenarioTests.cs

@@ -1,566 +0,0 @@
-using NStack;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using Terminal.Gui;
-using UICatalog;
-using Xunit;
-using Xunit.Abstractions;
-
-// Alias Console to MockConsole so we don't accidentally use Console
-using Console = Terminal.Gui.FakeConsole;
-
-namespace UICatalog {
-	public class ScenarioTests {
-		readonly ITestOutputHelper output;
-
-		public ScenarioTests (ITestOutputHelper output)
-		{
-#if DEBUG_IDISPOSABLE
-			Responder.Instances.Clear ();
-#endif
-			this.output = output;
-		}
-
-		int CreateInput (string input)
-		{
-			// Put a control-q in at the end
-			FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo ('q', ConsoleKey.Q, shift: false, alt: false, control: true));
-			foreach (var c in input.Reverse ()) {
-				if (char.IsLetter (c)) {
-					FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo (char.ToLower (c), (ConsoleKey)char.ToUpper (c), shift: char.IsUpper (c), alt: false, control: false));
-				} else {
-					FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo (c, (ConsoleKey)c, shift: false, alt: false, control: false));
-				}
-			}
-			return FakeConsole.MockKeyPresses.Count;
-		}
-
-
-		/// <summary>
-		/// <para>
-		/// This runs through all Scenarios defined in UI Catalog, calling Init, Setup, and Run.
-		/// </para>
-		/// <para>
-		/// Should find any Scenarios which crash on load or do not respond to <see cref="Application.RequestStop()"/>.
-		/// </para>
-		/// </summary>
-		[Fact]
-		public void Run_All_Scenarios ()
-		{
-			List<Scenario> scenarios = Scenario.GetScenarios ();
-			Assert.NotEmpty (scenarios);
-
-			foreach (var scenario in scenarios) {
-
-				output.WriteLine ($"Running Scenario '{scenario}'");
-
-				Func<MainLoop, bool> closeCallback = (MainLoop loop) => {
-					Application.RequestStop ();
-					return false;
-				};
-
-				Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
-
-				// Close after a short period of time
-				var token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), closeCallback);
-
-				scenario.Init (Colors.Base);
-				scenario.Setup ();
-				scenario.Run ();
-				Application.Shutdown ();
-#if DEBUG_IDISPOSABLE
-				foreach (var inst in Responder.Instances) {
-					Assert.True (inst.WasDisposed);
-				}
-				Responder.Instances.Clear ();
-#endif
-			}
-#if DEBUG_IDISPOSABLE
-			foreach (var inst in Responder.Instances) {
-				Assert.True (inst.WasDisposed);
-			}
-			Responder.Instances.Clear ();
-#endif
-		}
-
-		[Fact]
-		public void Run_Generic ()
-		{
-			List<Scenario> scenarios = Scenario.GetScenarios ();
-			Assert.NotEmpty (scenarios);
-
-			var item = scenarios.FindIndex (s => s.GetName ().Equals ("Generic", StringComparison.OrdinalIgnoreCase));
-			var generic = scenarios [item];
-			// Setup some fake keypresses 
-			// Passing empty string will cause just a ctrl-q to be fired
-			int stackSize = CreateInput ("");
-
-			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
-
-			int iterations = 0;
-			Application.Iteration = () => {
-				iterations++;
-				// Stop if we run out of control...
-				if (iterations == 10) {
-					Application.RequestStop ();
-				}
-			};
-
-			var ms = 1000;
-			var abortCount = 0;
-			Func<MainLoop, bool> abortCallback = (MainLoop loop) => {
-				abortCount++;
-				Application.RequestStop ();
-				return false;
-			};
-			var token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (ms), abortCallback);
-
-			Application.Top.KeyPress += (View.KeyEventEventArgs args) => {
-				Assert.Equal (Key.CtrlMask | Key.Q, args.KeyEvent.Key);
-			};
-
-			generic.Init (Colors.Base);
-			generic.Setup ();
-			// There is no need to call Application.Begin because Init already creates the Application.Top
-			// If Application.RunState is used then the Application.RunLoop must also be used instead Application.Run.
-			//var rs = Application.Begin (Application.Top);
-			generic.Run ();
-
-			//Application.End (rs);
-
-			Assert.Equal (0, abortCount);
-			// # of key up events should match # of iterations
-			Assert.Equal (1, iterations);
-			// Using variable in the left side of Assert.Equal/NotEqual give error. Must be used literals values.
-			//Assert.Equal (stackSize, iterations);
-
-			// Shutdown must be called to safely clean up Application if Init has been called
-			Application.Shutdown ();
-
-#if DEBUG_IDISPOSABLE
-			foreach (var inst in Responder.Instances) {
-				Assert.True (inst.WasDisposed);
-			}
-			Responder.Instances.Clear ();
-#endif
-		}
-
-		[Fact]
-		public void Run_All_Views_Tester_Scenario ()
-		{
-			Window _leftPane;
-			ListView _classListView;
-			FrameView _hostPane;
-
-			Dictionary<string, Type> _viewClasses;
-			View _curView = null;
-
-			// Settings
-			FrameView _settingsPane;
-			CheckBox _computedCheckBox;
-			FrameView _locationFrame;
-			RadioGroup _xRadioGroup;
-			TextField _xText;
-			int _xVal = 0;
-			RadioGroup _yRadioGroup;
-			TextField _yText;
-			int _yVal = 0;
-
-			FrameView _sizeFrame;
-			RadioGroup _wRadioGroup;
-			TextField _wText;
-			int _wVal = 0;
-			RadioGroup _hRadioGroup;
-			TextField _hText;
-			int _hVal = 0;
-			List<string> posNames = new List<String> { "Factor", "AnchorEnd", "Center", "Absolute" };
-			List<string> dimNames = new List<String> { "Factor", "Fill", "Absolute" };
-
-
-			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
-
-			var Top = Application.Top;
-
-			_viewClasses = GetAllViewClassesCollection ()
-				.OrderBy (t => t.Name)
-				.Select (t => new KeyValuePair<string, Type> (t.Name, t))
-				.ToDictionary (t => t.Key, t => t.Value);
-
-			_leftPane = new Window ("Classes") {
-				X = 0,
-				Y = 0,
-				Width = 15,
-				Height = Dim.Fill (1), // for status bar
-				CanFocus = false,
-				ColorScheme = Colors.TopLevel,
-			};
-
-			_classListView = new ListView (_viewClasses.Keys.ToList ()) {
-				X = 0,
-				Y = 0,
-				Width = Dim.Fill (0),
-				Height = Dim.Fill (0),
-				AllowsMarking = false,
-				ColorScheme = Colors.TopLevel,
-			};
-			_leftPane.Add (_classListView);
-
-			_settingsPane = new FrameView ("Settings") {
-				X = Pos.Right (_leftPane),
-				Y = 0, // for menu
-				Width = Dim.Fill (),
-				Height = 10,
-				CanFocus = false,
-				ColorScheme = Colors.TopLevel,
-			};
-			_computedCheckBox = new CheckBox ("Computed Layout", true) { X = 0, Y = 0 };
-			_settingsPane.Add (_computedCheckBox);
-
-			var radioItems = new ustring [] { "Percent(x)", "AnchorEnd(x)", "Center", "At(x)" };
-			_locationFrame = new FrameView ("Location (Pos)") {
-				X = Pos.Left (_computedCheckBox),
-				Y = Pos.Bottom (_computedCheckBox),
-				Height = 3 + radioItems.Length,
-				Width = 36,
-			};
-			_settingsPane.Add (_locationFrame);
-
-			var label = new Label ("x:") { X = 0, Y = 0 };
-			_locationFrame.Add (label);
-			_xRadioGroup = new RadioGroup (radioItems) {
-				X = 0,
-				Y = Pos.Bottom (label),
-			};
-			_xText = new TextField ($"{_xVal}") { X = Pos.Right (label) + 1, Y = 0, Width = 4 };
-			_locationFrame.Add (_xText);
-
-			_locationFrame.Add (_xRadioGroup);
-
-			radioItems = new ustring [] { "Percent(y)", "AnchorEnd(y)", "Center", "At(y)" };
-			label = new Label ("y:") { X = Pos.Right (_xRadioGroup) + 1, Y = 0 };
-			_locationFrame.Add (label);
-			_yText = new TextField ($"{_yVal}") { X = Pos.Right (label) + 1, Y = 0, Width = 4 };
-			_locationFrame.Add (_yText);
-			_yRadioGroup = new RadioGroup (radioItems) {
-				X = Pos.X (label),
-				Y = Pos.Bottom (label),
-			};
-			_locationFrame.Add (_yRadioGroup);
-
-			_sizeFrame = new FrameView ("Size (Dim)") {
-				X = Pos.Right (_locationFrame),
-				Y = Pos.Y (_locationFrame),
-				Height = 3 + radioItems.Length,
-				Width = 40,
-			};
-
-			radioItems = new ustring [] { "Percent(width)", "Fill(width)", "Sized(width)" };
-			label = new Label ("width:") { X = 0, Y = 0 };
-			_sizeFrame.Add (label);
-			_wRadioGroup = new RadioGroup (radioItems) {
-				X = 0,
-				Y = Pos.Bottom (label),
-			};
-			_wText = new TextField ($"{_wVal}") { X = Pos.Right (label) + 1, Y = 0, Width = 4 };
-			_sizeFrame.Add (_wText);
-			_sizeFrame.Add (_wRadioGroup);
-
-			radioItems = new ustring [] { "Percent(height)", "Fill(height)", "Sized(height)" };
-			label = new Label ("height:") { X = Pos.Right (_wRadioGroup) + 1, Y = 0 };
-			_sizeFrame.Add (label);
-			_hText = new TextField ($"{_hVal}") { X = Pos.Right (label) + 1, Y = 0, Width = 4 };
-			_sizeFrame.Add (_hText);
-
-			_hRadioGroup = new RadioGroup (radioItems) {
-				X = Pos.X (label),
-				Y = Pos.Bottom (label),
-			};
-			_sizeFrame.Add (_hRadioGroup);
-
-			_settingsPane.Add (_sizeFrame);
-
-			_hostPane = new FrameView ("") {
-				X = Pos.Right (_leftPane),
-				Y = Pos.Bottom (_settingsPane),
-				Width = Dim.Fill (),
-				Height = Dim.Fill (1), // + 1 for status bar
-				ColorScheme = Colors.Dialog,
-			};
-
-			_classListView.OpenSelectedItem += (a) => {
-				_settingsPane.SetFocus ();
-			};
-			_classListView.SelectedItemChanged += (args) => {
-				ClearClass (_curView);
-				_curView = CreateClass (_viewClasses.Values.ToArray () [_classListView.SelectedItem]);
-			};
-
-			_computedCheckBox.Toggled += (previousState) => {
-				if (_curView != null) {
-					_curView.LayoutStyle = previousState ? LayoutStyle.Absolute : LayoutStyle.Computed;
-					_hostPane.LayoutSubviews ();
-				}
-			};
-
-			_xRadioGroup.SelectedItemChanged += (selected) => DimPosChanged (_curView);
-
-			_xText.TextChanged += (args) => {
-				try {
-					_xVal = int.Parse (_xText.Text.ToString ());
-					DimPosChanged (_curView);
-				} catch {
-
-				}
-			};
-
-			_yText.TextChanged += (args) => {
-				try {
-					_yVal = int.Parse (_yText.Text.ToString ());
-					DimPosChanged (_curView);
-				} catch {
-
-				}
-			};
-
-			_yRadioGroup.SelectedItemChanged += (selected) => DimPosChanged (_curView);
-
-			_wRadioGroup.SelectedItemChanged += (selected) => DimPosChanged (_curView);
-
-			_wText.TextChanged += (args) => {
-				try {
-					_wVal = int.Parse (_wText.Text.ToString ());
-					DimPosChanged (_curView);
-				} catch {
-
-				}
-			};
-
-			_hText.TextChanged += (args) => {
-				try {
-					_hVal = int.Parse (_hText.Text.ToString ());
-					DimPosChanged (_curView);
-				} catch {
-
-				}
-			};
-
-			_hRadioGroup.SelectedItemChanged += (selected) => DimPosChanged (_curView);
-
-			Top.Add (_leftPane, _settingsPane, _hostPane);
-
-			Top.LayoutSubviews ();
-
-			_curView = CreateClass (_viewClasses.First ().Value);
-
-			int iterations = 0;
-
-			Application.Iteration += () => {
-				iterations++;
-
-				if (iterations < _viewClasses.Count) {
-					_classListView.MoveDown ();
-					Assert.Equal (_curView.GetType ().Name,
-						_viewClasses.Values.ToArray () [_classListView.SelectedItem].Name);
-				} else {
-					Application.RequestStop ();
-				}
-			};
-
-			Application.Run ();
-
-			Assert.Equal (_viewClasses.Count, iterations);
-
-			Application.Shutdown ();
-
-
-			void DimPosChanged (View view)
-			{
-				if (view == null) {
-					return;
-				}
-
-				var layout = view.LayoutStyle;
-
-				try {
-					view.LayoutStyle = LayoutStyle.Absolute;
-
-					switch (_xRadioGroup.SelectedItem) {
-					case 0:
-						view.X = Pos.Percent (_xVal);
-						break;
-					case 1:
-						view.X = Pos.AnchorEnd (_xVal);
-						break;
-					case 2:
-						view.X = Pos.Center ();
-						break;
-					case 3:
-						view.X = Pos.At (_xVal);
-						break;
-					}
-
-					switch (_yRadioGroup.SelectedItem) {
-					case 0:
-						view.Y = Pos.Percent (_yVal);
-						break;
-					case 1:
-						view.Y = Pos.AnchorEnd (_yVal);
-						break;
-					case 2:
-						view.Y = Pos.Center ();
-						break;
-					case 3:
-						view.Y = Pos.At (_yVal);
-						break;
-					}
-
-					switch (_wRadioGroup.SelectedItem) {
-					case 0:
-						view.Width = Dim.Percent (_wVal);
-						break;
-					case 1:
-						view.Width = Dim.Fill (_wVal);
-						break;
-					case 2:
-						view.Width = Dim.Sized (_wVal);
-						break;
-					}
-
-					switch (_hRadioGroup.SelectedItem) {
-					case 0:
-						view.Height = Dim.Percent (_hVal);
-						break;
-					case 1:
-						view.Height = Dim.Fill (_hVal);
-						break;
-					case 2:
-						view.Height = Dim.Sized (_hVal);
-						break;
-					}
-				} catch (Exception e) {
-					MessageBox.ErrorQuery ("Exception", e.Message, "Ok");
-				} finally {
-					view.LayoutStyle = layout;
-				}
-				UpdateTitle (view);
-			}
-
-			void UpdateSettings (View view)
-			{
-				var x = view.X.ToString ();
-				var y = view.Y.ToString ();
-				_xRadioGroup.SelectedItem = posNames.IndexOf (posNames.Where (s => x.Contains (s)).First ());
-				_yRadioGroup.SelectedItem = posNames.IndexOf (posNames.Where (s => y.Contains (s)).First ());
-				_xText.Text = $"{view.Frame.X}";
-				_yText.Text = $"{view.Frame.Y}";
-
-				var w = view.Width.ToString ();
-				var h = view.Height.ToString ();
-				_wRadioGroup.SelectedItem = dimNames.IndexOf (dimNames.Where (s => w.Contains (s)).First ());
-				_hRadioGroup.SelectedItem = dimNames.IndexOf (dimNames.Where (s => h.Contains (s)).First ());
-				_wText.Text = $"{view.Frame.Width}";
-				_hText.Text = $"{view.Frame.Height}";
-			}
-
-			void UpdateTitle (View view)
-			{
-				_hostPane.Title = $"{view.GetType ().Name} - {view.X.ToString ()}, {view.Y.ToString ()}, {view.Width.ToString ()}, {view.Height.ToString ()}";
-			}
-
-			List<Type> GetAllViewClassesCollection ()
-			{
-				List<Type> types = new List<Type> ();
-				foreach (Type type in typeof (View).Assembly.GetTypes ()
-				 .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsPublic && myType.IsSubclassOf (typeof (View)))) {
-					types.Add (type);
-				}
-				return types;
-			}
-
-			void ClearClass (View view)
-			{
-				// Remove existing class, if any
-				if (view != null) {
-					view.LayoutComplete -= LayoutCompleteHandler;
-					_hostPane.Remove (view);
-					view.Dispose ();
-					_hostPane.Clear ();
-				}
-			}
-
-			View CreateClass (Type type)
-			{
-				// If we are to create a generic Type
-				if (type.IsGenericType) {
-
-					// For each of the <T> arguments
-					List<Type> typeArguments = new List<Type> ();
-
-					// use <object>
-					foreach (var arg in type.GetGenericArguments ()) {
-						typeArguments.Add (typeof (object));
-					}
-
-					// And change what type we are instantiating from MyClass<T> to MyClass<object>
-					type = type.MakeGenericType (typeArguments.ToArray ());
-				}
-				// Instantiate view
-				var view = (View)Activator.CreateInstance (type);
-
-				//_curView.X = Pos.Center ();
-				//_curView.Y = Pos.Center ();
-				view.Width = Dim.Percent (75);
-				view.Height = Dim.Percent (75);
-
-				// Set the colorscheme to make it stand out if is null by default
-				if (view.ColorScheme == null) {
-					view.ColorScheme = Colors.Base;
-				}
-
-				// If the view supports a Text property, set it so we have something to look at
-				if (view.GetType ().GetProperty ("Text") != null) {
-					try {
-						view.GetType ().GetProperty ("Text")?.GetSetMethod ()?.Invoke (view, new [] { ustring.Make ("Test Text") });
-					} catch (TargetInvocationException e) {
-						MessageBox.ErrorQuery ("Exception", e.InnerException.Message, "Ok");
-						view = null;
-					}
-				}
-
-				// If the view supports a Title property, set it so we have something to look at
-				if (view != null && view.GetType ().GetProperty ("Title") != null) {
-					view?.GetType ().GetProperty ("Title")?.GetSetMethod ()?.Invoke (view, new [] { ustring.Make ("Test Title") });
-				}
-
-				// If the view supports a Source property, set it so we have something to look at
-				if (view != null && view.GetType ().GetProperty ("Source") != null && view.GetType ().GetProperty ("Source").PropertyType == typeof (Terminal.Gui.IListDataSource)) {
-					var source = new ListWrapper (new List<ustring> () { ustring.Make ("Test Text #1"), ustring.Make ("Test Text #2"), ustring.Make ("Test Text #3") });
-					view?.GetType ().GetProperty ("Source")?.GetSetMethod ()?.Invoke (view, new [] { source });
-				}
-
-				// Set Settings
-				_computedCheckBox.Checked = view.LayoutStyle == LayoutStyle.Computed;
-
-				// Add
-				_hostPane.Add (view);
-				//DimPosChanged ();
-				_hostPane.LayoutSubviews ();
-				_hostPane.Clear ();
-				_hostPane.SetNeedsDisplay ();
-				UpdateSettings (view);
-				UpdateTitle (view);
-
-				view.LayoutComplete += LayoutCompleteHandler;
-
-				return view;
-			}
-
-			void LayoutCompleteHandler (View.LayoutEventArgs args)
-			{
-				UpdateTitle (_curView);
-			}
-		}
-	}
-}

+ 0 - 57
Terminal.Gui UnitTests/UnitTests.csproj

@@ -1,57 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-  <PropertyGroup>
-    <TargetFramework>net6.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <UseDataCollector />
-    <!-- Version numbers are automatically updated by gitversion when a release is released -->
-    <!-- In the source tree the version will always be 1.0 for all projects. -->
-    <!-- Do not modify these. -->
-    <AssemblyVersion>1.0</AssemblyVersion>
-    <FileVersion>1.0</FileVersion>
-    <Version>1.0</Version>
-    <InformationalVersion>1.0</InformationalVersion>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
-    <DefineConstants>TRACE</DefineConstants>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
-    <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
-  </PropertyGroup>
-  <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
-    <PackageReference Include="ReportGenerator" Version="5.1.10" />
-    <PackageReference Include="System.Collections" Version="4.3.0" />
-    <PackageReference Include="xunit" Version="2.4.2" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="coverlet.collector" Version="3.2.0">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-    </PackageReference>
-  </ItemGroup>
-  <ItemGroup>
-    <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
-    <ProjectReference Include="..\UICatalog\UICatalog.csproj" />
-  </ItemGroup>
-  <PropertyGroup Label="FineCodeCoverage">
-    <Enabled>
-      True
-    </Enabled>
-    <Exclude>
-      [UICatalog]*
-    </Exclude>
-    <Include></Include>
-    <ExcludeByFile>
-      <!--**/Migrations/*
-      **/Hacks/*.cs-->
-    </ExcludeByFile>
-    <ExcludeByAttribute>
-      <!--MyCustomExcludeFromCodeCoverage-->
-    </ExcludeByAttribute>
-    <IncludeTestAssembly>
-      False
-    </IncludeTestAssembly>
-  </PropertyGroup>
-</Project>

+ 45 - 0
Terminal.Gui/Configuration/AppScope.cs

@@ -0,0 +1,45 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+
+#nullable enable
+
+namespace Terminal.Gui.Configuration {
+
+	public static partial class ConfigurationManager {
+		/// <summary>
+		/// The <see cref="Scope{T}"/> class for application-defined configuration settings.
+		/// </summary>
+		/// <remarks>
+		/// </remarks>
+		/// <example>
+		/// <para>
+		/// Use the <see cref="SerializableConfigurationProperty"/> attribute to mark properties that should be serialized as part
+		/// of application-defined configuration settings.
+		/// </para>
+		/// <code>
+		/// public class MyAppSettings {
+		///	[SerializableConfigurationProperty (Scope = typeof (AppScope))]
+		///	public static bool? MyProperty { get; set; } = true;
+		/// }
+		/// </code>
+		/// <para>
+		/// THe resultant Json will look like this:
+		/// </para>
+		/// <code>
+		///   "AppSettings": {
+		///     "MyAppSettings.MyProperty": true,
+		///     "UICatalog.ShowStatusBar": true
+		///   },
+		/// </code>
+		/// </example> 
+		[JsonConverter (typeof (ScopeJsonConverter<AppScope>))]
+		public class AppScope : Scope<AppScope> {
+		}
+	}
+}

+ 89 - 0
Terminal.Gui/Configuration/AttributeJsonConverter.cs

@@ -0,0 +1,89 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Terminal.Gui;
+
+namespace Terminal.Gui.Configuration {
+	/// <summary>
+	/// Json converter fro the <see cref="Attribute"/> class.
+	/// </summary>
+	public class AttributeJsonConverter : JsonConverter<Attribute> {
+		private static AttributeJsonConverter instance;
+
+		/// <summary>
+		/// 
+		/// </summary>
+		public static AttributeJsonConverter Instance {
+			get {
+				if (instance == null) {
+					instance = new AttributeJsonConverter ();
+				}
+
+				return instance;
+			}
+		}
+
+		/// <inheritdoc/>
+		public override Attribute Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+		{
+			if (reader.TokenType != JsonTokenType.StartObject) {
+				throw new JsonException ($"Unexpected StartObject token when parsing Attribute: {reader.TokenType}.");
+			}
+
+			Attribute attribute = new Attribute ();
+			Color foreground =  (Color)(-1);
+			Color background =  (Color)(-1);
+			while (reader.Read ()) {
+				if (reader.TokenType == JsonTokenType.EndObject) {
+					if (foreground ==  (Color)(-1) || background ==  (Color)(-1)) {
+						throw new JsonException ($"Both Foreground and Background colors must be provided.");
+					}
+					return attribute;
+				}
+
+				if (reader.TokenType != JsonTokenType.PropertyName) {
+					throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}.");
+				}
+
+				string propertyName = reader.GetString ();
+				reader.Read ();
+				string color = $"\"{reader.GetString ()}\"";
+
+				switch (propertyName.ToLower ()) {
+				case "foreground":
+					foreground = JsonSerializer.Deserialize<Color> (color, options);
+					break;
+				case "background":
+					background = JsonSerializer.Deserialize<Color> (color, options);
+					break;
+				//case "Bright":
+				//	attribute.Bright = reader.GetBoolean ();
+				//	break;
+				//case "Underline":
+				//	attribute.Underline = reader.GetBoolean ();
+				//	break;
+				//case "Reverse":
+				//	attribute.Reverse = reader.GetBoolean ();
+				//	break;
+				default:
+					throw new JsonException ($"Unknown Attribute property {propertyName}.");
+				}
+
+				attribute = new Attribute (foreground, background);
+			}
+			throw new JsonException ();
+		}
+
+		/// <inheritdoc/>
+		public override void Write (Utf8JsonWriter writer, Attribute value, JsonSerializerOptions options)
+		{
+			writer.WriteStartObject ();
+			writer.WritePropertyName ("Foreground");
+			ColorJsonConverter.Instance.Write (writer, value.Foreground, options);
+			writer.WritePropertyName ("Background");
+			ColorJsonConverter.Instance.Write (writer, value.Background, options);
+			writer.WriteEndObject ();
+		}
+	}
+}
+

+ 76 - 0
Terminal.Gui/Configuration/ColorJsonConverter.cs

@@ -0,0 +1,76 @@
+using System;
+using System.Text.Json.Serialization;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui.Configuration {
+	/// <summary>
+	/// Json converter for the <see cref="Color"/> class.
+	/// </summary>
+	public class ColorJsonConverter : JsonConverter<Color> {
+		private static ColorJsonConverter instance;
+
+		/// <summary>
+		/// Singleton
+		/// </summary>
+		public static ColorJsonConverter Instance {
+			get {
+				if (instance == null) {
+					instance = new ColorJsonConverter ();
+				}
+
+				return instance;
+			}
+		}
+
+		/// <inheritdoc/>
+		public override Color Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+		{
+			// Check if the value is a string
+			if (reader.TokenType == JsonTokenType.String) {
+				// Get the color string
+				var colorString = reader.GetString ();
+
+				// Check if the color string is a color name
+				if (Enum.TryParse (colorString, ignoreCase: true, out Color color)) {
+					// Return the parsed color
+					return color;
+				} else {
+					// Parse the color string as an RGB value
+					var match = Regex.Match (colorString, @"rgb\((\d+),(\d+),(\d+)\)");
+					if (match.Success) {
+						var r = int.Parse (match.Groups [1].Value);
+						var g = int.Parse (match.Groups [2].Value);
+						var b = int.Parse (match.Groups [3].Value);
+						return new TrueColor (r, g, b).ToConsoleColor ();
+					} else {
+						throw new JsonException ($"Invalid Color: '{colorString}'");
+					}
+				}
+			} else {
+				throw new JsonException ($"Unexpected token when parsing Color: {reader.TokenType}");
+			}
+		}
+
+		/// <inheritdoc/>
+		public override void Write (Utf8JsonWriter writer, Color value, JsonSerializerOptions options)
+		{
+			// Try to get the human readable color name from the map
+			var name = Enum.GetName (typeof (Color), value);
+			if (name != null) {
+				// Write the color name to the JSON
+				writer.WriteStringValue (name);
+			} else {
+				//// If the color is not in the map, look up its RGB values in the consoleDriver.colors array
+				//ConsoleColor consoleColor = (ConsoleDriver [(int)value]);
+				//int r = consoleColor.R;
+				//int g = consoleColor.G;
+				//int b = consoleColor.B;
+
+				//// Write the RGB values as a string to the JSON
+				//writer.WriteStringValue ($"rgb({r},{g},{b})");
+				throw new JsonException ($"Unknown Color value. Cannot serialize to JSON: {value}");
+			}
+		}
+	}
+}

+ 89 - 0
Terminal.Gui/Configuration/ColorSchemeJsonConverter.cs

@@ -0,0 +1,89 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui.Configuration {
+	/// <summary>
+	/// Implements a JSON converter for <see cref="ColorScheme"/>. 
+	/// </summary>
+	public class ColorSchemeJsonConverter : JsonConverter<ColorScheme> {
+		private static ColorSchemeJsonConverter instance;
+
+		/// <summary>
+		/// Singleton
+		/// </summary>
+		public static ColorSchemeJsonConverter Instance {
+			get {
+				if (instance == null) {
+					instance = new ColorSchemeJsonConverter ();
+				}
+				return instance;
+			}
+		}
+		
+		/// <inheritdoc/>
+		public override ColorScheme Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+		{
+			if (reader.TokenType != JsonTokenType.StartObject) {
+				throw new JsonException ($"Unexpected StartObject token when parsing ColorScheme: {reader.TokenType}.");
+			}
+
+			var colorScheme = new ColorScheme ();
+
+			while (reader.Read ()) {
+				if (reader.TokenType == JsonTokenType.EndObject) {
+					return colorScheme;
+				}
+
+				if (reader.TokenType != JsonTokenType.PropertyName) {
+					throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}.");
+				}
+
+				var propertyName = reader.GetString ();
+				reader.Read ();
+				var attribute = JsonSerializer.Deserialize<Attribute> (ref reader, options);
+
+				switch (propertyName.ToLower()) {
+				case "normal":
+					colorScheme.Normal = attribute;
+					break;
+				case "focus":
+					colorScheme.Focus = attribute;
+					break;
+				case "hotnormal":
+					colorScheme.HotNormal = attribute;
+					break;
+				case "hotfocus":
+					colorScheme.HotFocus = attribute;
+					break;
+				case "disabled":
+					colorScheme.Disabled = attribute;
+					break;
+				default:
+					throw new JsonException ($"Unrecognized ColorScheme Attribute name: {propertyName}.");
+				}
+			}
+
+			throw new JsonException ();
+		}
+
+		/// <inheritdoc/>
+		public override void Write (Utf8JsonWriter writer, ColorScheme value, JsonSerializerOptions options)
+		{
+			writer.WriteStartObject ();
+
+			writer.WritePropertyName ("Normal");
+			AttributeJsonConverter.Instance.Write (writer, value.Normal, options);
+			writer.WritePropertyName ("Focus");
+			AttributeJsonConverter.Instance.Write (writer, value.Focus, options);
+			writer.WritePropertyName ("HotNormal");
+			AttributeJsonConverter.Instance.Write (writer, value.HotNormal, options);
+			writer.WritePropertyName ("HotFocus");
+			AttributeJsonConverter.Instance.Write (writer, value.HotFocus, options);
+			writer.WritePropertyName ("Disabled");
+			AttributeJsonConverter.Instance.Write (writer, value.Disabled, options);
+
+			writer.WriteEndObject ();
+		}
+	}
+}

+ 663 - 0
Terminal.Gui/Configuration/ConfigurationManager.cs

@@ -0,0 +1,663 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+
+#nullable enable
+
+namespace Terminal.Gui.Configuration {
+	/// <summary>
+	/// Provides settings and configuration management for Terminal.Gui applications. 
+	/// <para>
+	/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration files.
+	/// The configuration files can be placed in at <c>.tui</c> folder in the user's home directory (e.g. <c>C:/Users/username/.tui</c>, 
+	/// or <c>/usr/username/.tui</c>),
+	/// the folder where the Terminal.Gui application was launched from (e.g. <c>./.tui</c>), or as a resource
+	/// within the Terminal.Gui application's main assembly. 
+	/// </para>
+	/// <para>
+	/// Settings are defined in JSON format, according to this schema: 
+	///	https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
+	/// </para>
+	/// <para>
+	/// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>. Settings 
+	/// that will apply to a specific Terminal.Gui application reside in files named <c>appname.config.json</c>,
+	/// where <c>appname</c> is the assembly name of the application (e.g. <c>UICatalog.config.json</c>).
+	/// </para>
+	/// Settings are applied using the following precedence (higher precedence settings
+	/// overwrite lower precedence settings):
+	/// <para>
+	///	1. Application configuration found in the users's home directory (<c>~/.tui/appname.config.json</c>) -- Highest precedence 
+	/// </para>
+	/// <para>
+	///	2. Application configuration found in the directory the app was launched from (<c>./.tui/appname.config.json</c>).
+	/// </para>
+	/// <para>
+	///	3. Application configuration found in the applications's resources (<c>Resources/config.json</c>). 
+	/// </para>
+	/// <para>
+	///	4. Global configuration found in the the user's home directory (<c>~/.tui/config.json</c>).
+	/// </para>
+	/// <para>
+	///	5. Global configuration found in the directory the app was launched from (<c>./.tui/config.json</c>).
+	/// </para>
+	/// <para>
+	///     6. Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
+	/// </para>
+	/// </summary>
+	public static partial class ConfigurationManager {
+
+		private static readonly string _configFilename = "config.json";
+
+		private static readonly JsonSerializerOptions serializerOptions = new JsonSerializerOptions {
+			ReadCommentHandling = JsonCommentHandling.Skip,
+			PropertyNameCaseInsensitive = true,
+			DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+			WriteIndented = true,
+			Converters = {
+				// No need to set converterss - the ConfigRootConverter uses property attributes apply the correct
+				// Converter.
+			},
+		};
+
+		/// <summary>
+		/// An attribute that can be applied to a property to indicate that it should included in the configuration file.
+		/// </summary>
+		/// <example>
+		/// 	[SerializableConfigurationProperty(Scope = typeof(Configuration.ThemeManager.ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
+		///	public static BorderStyle DefaultBorderStyle {
+		///	...
+		/// </example>
+		[AttributeUsage (AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+		public class SerializableConfigurationProperty : System.Attribute {
+			/// <summary>
+			/// Specifies the scope of the property. 
+			/// </summary>
+			public Type? Scope { get; set; }
+
+			/// <summary>
+			/// If <see langword="true"/>, the property will be serialized to the configuration file using only the property name
+			/// as the key. If <see langword="false"/>, the property will be serialized to the configuration file using the
+			/// property name pre-pended with the classname (e.g. <c>Application.UseSystemConsole</c>).
+			/// </summary>
+			public bool OmitClassName { get; set; }
+		}
+
+		/// <summary>
+		/// Holds a property's value and the <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> 
+		/// to get and set the property's value.
+		/// </summary>
+		/// <remarks>
+		/// Configuration properties must be <see langword="public"/> and <see langword="static"/> 
+		/// and have the <see cref="SerializableConfigurationProperty"/>
+		/// attribute. If the type of the property requires specialized JSON serialization, 
+		/// a <see cref="JsonConverter"/> must be provided using 
+		/// the <see cref="JsonConverterAttribute"/> attribute.
+		/// </remarks>
+		public class ConfigProperty {
+			private object? propertyValue;
+
+			/// <summary>
+			/// Describes the property.
+			/// </summary>
+			public PropertyInfo? PropertyInfo { get; set; }
+
+			/// <summary>
+			/// Helper to get either the Json property named (specified by [JsonPropertyName(name)]
+			/// or the actual property name.
+			/// </summary>
+			/// <param name="pi"></param>
+			/// <returns></returns>
+			public static string GetJsonPropertyName (PropertyInfo pi)
+			{
+				var jpna = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
+				return jpna?.Name ?? pi.Name;
+			}
+
+			/// <summary>
+			/// Holds the property's value as it was either read from the class's implementation or from a config file. 
+			/// If the property has not been set (e.g. because no configuration file specified a value), 
+			/// this will be <see langword="null"/>.
+			/// </summary>
+			/// <remarks>
+			/// On <see langword="set"/>, performs a sparse-copy of the new value to the existing value (only copies elements of 
+			/// the object that are non-null).
+			/// </remarks>
+			public object? PropertyValue {
+				get => propertyValue;
+				set {
+					propertyValue = value;
+				}
+			}
+
+			internal object? UpdateValueFrom (object source)
+			{
+				if (source == null) {
+					return PropertyValue;
+				}
+
+				var ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
+				if (source.GetType () != PropertyInfo!.PropertyType && (ut != null && source.GetType () != ut)) {
+					throw new ArgumentException ($"The source object ({PropertyInfo!.DeclaringType}.{PropertyInfo!.Name}) is not of type {PropertyInfo!.PropertyType}.");
+				}
+				if (PropertyValue != null && source != null) {
+					PropertyValue = DeepMemberwiseCopy (source, PropertyValue);
+				} else {
+					PropertyValue = source;
+				}
+
+				return PropertyValue;
+			}
+
+			/// <summary>
+			/// Retrieves (using reflection) the value of the static property described in <see cref="PropertyInfo"/>
+			/// into <see cref="PropertyValue"/>.
+			/// </summary>
+			/// <returns></returns>
+			public object? RetrieveValue ()
+			{
+				return PropertyValue = PropertyInfo!.GetValue (null);
+			}
+
+			/// <summary>
+			/// Applies the <see cref="PropertyValue"/> to the property described by <see cref="PropertyInfo"/>.
+			/// </summary>
+			/// <returns></returns>
+			public bool Apply ()
+			{
+				if (PropertyValue != null) {
+					PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
+				}
+				return PropertyValue != null;
+			}
+		}
+
+		/// <summary>
+		/// A dictionary of all properties in the Terminal.Gui project that are decorated with the <see cref="SerializableConfigurationProperty"/> attribute.
+		/// The keys are the property names pre-pended with the class that implements the property (e.g. <c>Application.UseSystemConsole</c>).
+		/// The values are instances of <see cref="ConfigProperty"/> which hold the property's value and the
+		/// <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> to get and set the property's value.
+		/// </summary>
+		/// <remarks>
+		/// Is <see langword="null"/> until <see cref="Initialize"/> is called. 
+		/// </remarks>
+		private static Dictionary<string, ConfigProperty>? _allConfigProperties;
+
+		/// <summary>
+		/// The backing property for <see cref="Settings"/>. 
+		/// </summary>
+		/// <remarks>
+		/// Is <see langword="null"/> until <see cref="Reset"/> is called. Gets set to a new instance by
+		/// deserializtion (see <see cref="Load"/>).
+		/// </remarks>
+		private static SettingsScope? _settings;
+
+		/// <summary>
+		/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the <see cref="SettingsScope"/>
+		/// attribute value.
+		/// </summary>
+		public static SettingsScope? Settings {
+			get {
+				if (_settings == null) {
+					throw new InvalidOperationException ("ConfigurationManager has not been initialized. Call ConfigurationManager.Reset() before accessing the Settings property.");
+				}
+				return _settings;
+			}
+			set {
+				_settings = value!;
+			}
+		}
+
+		/// <summary>
+		/// The root object of Terminal.Gui themes manager. Contains only properties with the <see cref="ThemeScope"/>
+		/// attribute value.
+		/// </summary>
+		public static ThemeManager? Themes => ThemeManager.Instance;
+
+		/// <summary>
+		/// Aplication-specific configuration settings scope.
+		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("AppSettings")]
+		public static AppScope? AppSettings { get; set; }
+
+		/// <summary>
+		/// Initializes the internal state of ConfiguraitonManager. Nominally called once as part of application
+		/// startup to initilaize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
+		/// </summary>
+		internal static void Initialize ()
+		{
+			_allConfigProperties = new Dictionary<string, ConfigProperty> ();
+			_settings = null;
+
+			Dictionary<string, Type> classesWithConfigProps = new Dictionary<string, Type> (StringComparer.InvariantCultureIgnoreCase);
+			// Get Terminal.Gui.dll classes
+
+			var types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
+				    from type in assembly.GetTypes ()
+				    where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null)
+				    select type;
+
+			foreach (var classWithConfig in types) {
+				classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
+			}
+
+			Debug.WriteLine ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} clases:");
+			classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($"  Class: {x.Key}"));
+
+			foreach (var p in from c in classesWithConfigProps
+					  let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop =>
+						prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty)
+					  let enumerable = props
+					  from p in enumerable
+					  select p) {
+				if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp) {
+					if (p.GetGetMethod (true)!.IsStatic) {
+						// If the class name is ommited, JsonPropertyName is allowed. 
+						_allConfigProperties!.Add (scp.OmitClassName ? ConfigProperty.GetJsonPropertyName (p) : $"{p.DeclaringType?.Name}.{p.Name}", new ConfigProperty {
+							PropertyInfo = p,
+							PropertyValue = null
+						});
+					} else {
+						throw new Exception ($"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static.");
+					}
+				}
+			}
+
+			_allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key).ToDictionary (x => x.Key, x => x.Value, StringComparer.InvariantCultureIgnoreCase);
+
+			Debug.WriteLine ($"ConfigManager.Initialize found {_allConfigProperties.Count} properties:");
+			_allConfigProperties.ToList ().ForEach (x => Debug.WriteLine ($"  Property: {x.Key}"));
+
+			AppSettings = new AppScope ();
+		}
+
+		/// <summary>
+		/// Creates a JSON document with the configuration specified. 
+		/// </summary>
+		/// <returns></returns>
+		internal static string ToJson ()
+		{
+			Debug.WriteLine ($"ConfigurationManager.ToJson()");
+			return JsonSerializer.Serialize<SettingsScope> (Settings!, serializerOptions);
+		}
+
+		internal static Stream ToStream ()
+		{
+			var json = JsonSerializer.Serialize<SettingsScope> (Settings!, serializerOptions);
+			// turn it into a stream
+			var stream = new MemoryStream ();
+			var writer = new StreamWriter (stream);
+			writer.Write (json);
+			writer.Flush ();
+			stream.Position = 0;
+			return stream;
+		}
+
+		/// <summary>
+		/// Event arguments for the <see cref="ConfigurationManager"/> events.
+		/// </summary>
+		public class ConfigurationManagerEventArgs : EventArgs {
+
+			/// <summary>
+			/// Initializes a new instance of <see cref="ConfigurationManagerEventArgs"/>
+			/// </summary>
+			public ConfigurationManagerEventArgs ()
+			{
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters 
+		/// an error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the 
+		/// console when <see cref="Application.Shutdown"/> is called. 
+		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+		public static bool? ThrowOnJsonErrors { get; set; } = false;
+
+		internal static StringBuilder jsonErrors = new StringBuilder ();
+
+		private static void AddJsonError (string error)
+		{
+			Debug.WriteLine ($"ConfigurationManager: {error}");
+			jsonErrors.AppendLine (error);
+		}
+
+		/// <summary>
+		/// Prints any Json deserialization errors that occurred during deserialization to the console.
+		/// </summary>
+		public static void PrintJsonErrors ()
+		{
+			if (jsonErrors.Length > 0) {
+				Console.WriteLine ($"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:");
+				Console.WriteLine (jsonErrors.ToString ());
+			}
+		}
+
+		private static void ClearJsonErrors ()
+		{
+			jsonErrors.Clear ();
+		}
+
+		/// <summary>
+		/// Called when the configuration has been updated from a configuration file. Invokes the <see cref="Updated"/>
+		/// event.
+		/// </summary>
+		public static void OnUpdated ()
+		{
+			Debug.WriteLine ($"ConfigurationManager.OnApplied()");
+			Updated?.Invoke (new ConfigurationManagerEventArgs ());
+		}
+
+		/// <summary>
+		/// Event fired when the configuration has been upddated from a configuration source.  
+		/// application.
+		/// </summary>
+		public static event Action<ConfigurationManagerEventArgs>? Updated;
+
+		/// <summary>
+		/// Resets the state of <see cref="ConfigurationManager"/>. Should be called whenever a new app session
+		/// (e.g. in <see cref="Application.Init(ConsoleDriver, IMainLoopDriver)"/> starts. Called by <see cref="Load"/>
+		/// if the <c>reset</c> parameter is <see langword="true"/>.
+		/// </summary>
+		/// <remarks>
+		/// 
+		/// </remarks>
+		public static void Reset ()
+		{
+			Debug.WriteLine ($"ConfigurationManager.Reset()");
+			if (_allConfigProperties == null) {
+				ConfigurationManager.Initialize ();
+			}
+
+			ClearJsonErrors ();
+			
+			Settings = new SettingsScope ();
+			ThemeManager.Reset ();
+			AppSettings = new AppScope ();
+
+			// To enable some unit tests, we only load from resources if the flag is set
+			if (Locations.HasFlag (ConfigLocations.DefaultOnly)) Settings.UpdateFromResource (typeof (ConfigurationManager).Assembly, $"Terminal.Gui.Resources.{_configFilename}");
+
+			Apply ();
+			ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
+			AppSettings?.Apply ();
+		}
+
+		/// <summary>
+		/// Retrieves the hard coded default settings from the Terminal.Gui library implementation. Used in development of
+		/// the library to generate the default configuration file. Before calling Application.Init, make sure
+		/// <see cref="Locations"/> is set to <see cref="ConfigLocations.None"/>.
+		/// </summary>
+		/// <remarks>
+		/// <para>
+		/// This method is only really useful when using ConfigurationManagerTests
+		/// to generate the JSON doc that is embedded into Terminal.Gui (during development). 
+		/// </para>
+		/// <para>
+		/// WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting defintions (Themes)
+		/// that are NOT generated by this function. If you use this function to regenerate <c>Terminal.Gui.Resources.config.json</c>,
+		/// make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
+		/// </para>		
+		/// </remarks>
+		internal static void GetHardCodedDefaults ()
+		{
+			if (_allConfigProperties == null) {
+				throw new InvalidOperationException ("Initialize must be called first.");
+			}
+			Settings = new SettingsScope ();
+			ThemeManager.GetHardCodedDefaults ();
+			AppSettings?.RetrieveValues ();
+			foreach (var p in Settings!.Where (cp => cp.Value.PropertyInfo != null)) {
+				Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
+			}
+		}
+
+		/// <summary>
+		/// Applies the configuration settings to the running <see cref="Application"/> instance.
+		/// </summary>
+		public static void Apply ()
+		{
+			bool settings = Settings?.Apply () ?? false;
+			bool themes = ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false;
+			bool appsettings = AppSettings?.Apply () ?? false;
+			if (settings || themes || appsettings) {
+				OnApplied ();
+			}
+		}
+
+		/// <summary>
+		/// Called when an updated configuration has been applied to the  
+		/// application. Fires the <see cref="Applied"/> event.
+		/// </summary>
+		public static void OnApplied ()
+		{
+			Debug.WriteLine ($"ConfigurationManager.OnApplied()");
+			Applied?.Invoke (new ConfigurationManagerEventArgs ());
+		}
+
+		/// <summary>
+		/// Event fired when an updated configuration has been applied to the  
+		/// application.
+		/// </summary>
+		public static event Action<ConfigurationManagerEventArgs>? Applied;
+
+		/// <summary>
+		/// Name of the running application. By default this property is set to the application's assembly name.
+		/// </summary>
+		public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
+
+		/// <summary>
+		/// Describes the location of the configuration files. The constancts can be
+		/// combined (bitwise) to specify multiple locations.
+		/// </summary>
+		[Flags]
+		public enum ConfigLocations {
+			/// <summary>
+			/// No configuration will be loaded.
+			/// </summary>
+			/// <remarks>
+			///  Used for development and testing only. For Terminal,Gui to function properly, at least
+			///  <see cref="DefaultOnly"/> should be set.
+			/// </remarks>
+			None = 0,
+
+			/// <summary>
+			/// Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
+			/// </summary>
+			DefaultOnly,
+
+			/// <summary>
+			/// This constant is a combination of all locations
+			/// </summary>
+			All = -1
+
+		}
+
+		/// <summary>
+		/// Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files.
+		/// The value is <see cref="ConfigLocations.All"/>.
+		/// </summary>
+		public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
+
+		/// <summary>
+		/// Loads all settings found in the various configuraiton storage locations to 
+		/// the <see cref="ConfigurationManager"/>. Optionally,
+		/// resets all settings attributed with <see cref="SerializableConfigurationProperty"/> to the defaults 
+		/// defined in <see cref="LoadAppResources"/>.
+		/// </summary>
+		/// <remarks>
+		/// Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.
+		/// </remarks>
+		/// <param name="reset">If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will
+		/// be reset to the defaults defined in <see cref="LoadAppResources"/>.</param>
+		public static void Load (bool reset = false)
+		{
+			Debug.WriteLine ($"ConfigurationManager.Load()");
+
+			if (reset) Reset ();
+
+			// LibraryResoruces is always loaded by Reset
+			if (Locations == ConfigLocations.All) {
+				var embeddedStylesResourceName = Assembly.GetEntryAssembly ()?
+					.GetManifestResourceNames ().FirstOrDefault (x => x.EndsWith (_configFilename));
+				if (string.IsNullOrEmpty(embeddedStylesResourceName)) {
+					embeddedStylesResourceName = _configFilename;
+				}
+
+				Settings = Settings?
+					// Global current directory
+					.Update ($"./.tui/{_configFilename}")?
+					// Global home directory
+					.Update ($"~/.tui/{_configFilename}")?
+					// App resources
+					.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!)?
+					// App current directory
+					.Update ($"./.tui/{AppName}.{_configFilename}")?
+					// App home directory
+					.Update ($"~/.tui/{AppName}.{_configFilename}");
+			}
+		}
+
+		/// <summary>
+		/// Returns an empty Json document with just the $schema tag.
+		/// </summary>
+		/// <returns></returns>
+		public static string GetEmptyJson ()
+		{
+			var emptyScope = new SettingsScope ();
+			emptyScope.Clear ();
+			return JsonSerializer.Serialize<SettingsScope> (emptyScope, serializerOptions);
+		}
+
+		/// <summary>
+		/// System.Text.Json does not support copying a deserialized object to an existing instance.
+		/// To work around this, we implement a 'deep, memberwise copy' method. 
+		/// </summary>
+		/// <remarks>
+		/// TOOD: When System.Text.Json implements `PopulateObject` revisit
+		///	https://github.com/dotnet/corefx/issues/37627
+		/// </remarks>
+		/// <param name="source"></param>
+		/// <param name="destination"></param>
+		/// <returns><paramref name="destination"/> updated from <paramref name="source"/></returns>
+		internal static object? DeepMemberwiseCopy (object? source, object? destination)
+		{
+			if (destination == null) {
+				throw new ArgumentNullException (nameof (destination));
+			}
+
+			if (source == null) {
+				return null!;
+			}
+
+			if (source.GetType () == typeof (SettingsScope)) {
+				return ((SettingsScope)destination).Update ((SettingsScope)source);
+			}
+			if (source.GetType () == typeof (ThemeScope)) {
+				return ((ThemeScope)destination).Update ((ThemeScope)source);
+			}
+			if (source.GetType () == typeof (AppScope)) {
+				return ((AppScope)destination).Update ((AppScope)source);
+			}
+
+			// If value type, just use copy constructor.
+			if (source.GetType ().IsValueType || source.GetType () == typeof (string)) {
+				return source;
+			}
+
+			// Dictionary
+			if (source.GetType ().IsGenericType && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>))) {
+				foreach (var srcKey in ((IDictionary)source).Keys) {
+					if (((IDictionary)destination).Contains (srcKey))
+						((IDictionary)destination) [srcKey] = DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
+					else {
+						((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
+					}
+				}
+				return destination;
+			}
+
+			// ALl other object types
+			var sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
+			var destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
+			foreach (var (sourceProp, destProp) in
+				from sourceProp in sourceProps
+				where destProps.Any (x => x.Name == sourceProp.Name)
+				let destProp = destProps.First (x => x.Name == sourceProp.Name)
+				where destProp.CanWrite
+				select (sourceProp, destProp)) {
+
+				var sourceVal = sourceProp.GetValue (source);
+				var destVal = destProp.GetValue (destination);
+				if (sourceVal != null) {
+					if (destVal != null) {
+						// Recurse
+						destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
+					} else {
+						destProp.SetValue (destination, sourceVal);
+					}
+				}
+			}
+			return destination!;
+		}
+
+		//public class ConfiguraitonLocation
+		//{
+		//	public string Name { get; set; } = string.Empty;
+
+		//	public string? Path { get; set; }
+
+		//	public async Task<SettingsScope> UpdateAsync (Stream stream)
+		//	{
+		//		var scope = await JsonSerializer.DeserializeAsync<SettingsScope> (stream, serializerOptions);
+		//		if (scope != null) {
+		//			ConfigurationManager.Settings?.UpdateFrom (scope);
+		//			return scope;
+		//		}
+		//		return new SettingsScope ();
+		//	}
+
+		//}
+
+		//public class StreamConfiguration {
+		//	private bool _reset;
+
+		//	public StreamConfiguration (bool reset)
+		//	{
+		//		_reset = reset;
+		//	}
+
+		//	public StreamConfiguration UpdateAppResources ()
+		//	{
+		//		if (Locations.HasFlag (ConfigLocations.AppResources)) LoadAppResources ();
+		//		return this;
+		//	}
+
+		//	public StreamConfiguration UpdateAppDirectory ()
+		//	{
+		//		if (Locations.HasFlag (ConfigLocations.AppDirectory)) LoadAppDirectory ();
+		//		return this;
+		//	}
+
+		//	// Additional update methods for each location here
+
+		//	private void LoadAppResources ()
+		//	{
+		//		// Load AppResources logic here
+		//	}
+
+		//	private void LoadAppDirectory ()
+		//	{
+		//		// Load AppDirectory logic here
+		//	}
+		//}
+	}
+}

+ 41 - 0
Terminal.Gui/Configuration/DictionaryJsonConverter.cs

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using System.Text.Json;
+
+namespace Terminal.Gui.Configuration {
+
+	class DictionaryJsonConverter<T> : JsonConverter<Dictionary<string, T>> {
+		public override Dictionary<string, T> Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+		{
+			var dictionary = new Dictionary<string, T> ();
+			while (reader.Read ()) {
+				if (reader.TokenType == JsonTokenType.StartObject) {
+					reader.Read ();
+					if (reader.TokenType == JsonTokenType.PropertyName) {
+						string key = reader.GetString ();
+						reader.Read ();
+						T value = JsonSerializer.Deserialize<T> (ref reader, options);
+						dictionary.Add (key, value);
+					}
+				} else if (reader.TokenType == JsonTokenType.EndArray)
+					break;
+			}
+			return dictionary;
+		}
+
+
+		public override void Write (Utf8JsonWriter writer, Dictionary<string, T> value, JsonSerializerOptions options)
+		{
+			writer.WriteStartArray ();
+			foreach (var item in value) {
+				writer.WriteStartObject ();
+				//writer.WriteString (item.Key, item.Key);
+				writer.WritePropertyName (item.Key);
+				JsonSerializer.Serialize (writer, item.Value, options);
+				writer.WriteEndObject ();
+			}
+			writer.WriteEndArray ();
+		}
+	}
+}

+ 132 - 0
Terminal.Gui/Configuration/KeyJsonConverter.cs

@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui.Configuration {
+	/// <summary>
+	/// Json converter for the <see cref="Key"/> class.
+	/// </summary>
+	public class KeyJsonConverter : JsonConverter<Key> {
+		/// <inheritdoc/>
+		public override Key Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+		{
+			if (reader.TokenType == JsonTokenType.StartObject) {
+				Key key = Key.Unknown;
+				Dictionary<string, Key> modifierDict = new Dictionary<string, Key> (comparer: StringComparer.InvariantCultureIgnoreCase) {
+					{ "Shift", Key.ShiftMask },
+					{ "Ctrl", Key.CtrlMask },
+					{ "Alt", Key.AltMask }
+				};
+
+				List<Key> modifiers = new List<Key> ();
+
+				while (reader.Read ()) {
+					if (reader.TokenType == JsonTokenType.EndObject) {
+						break;
+					}
+
+					if (reader.TokenType == JsonTokenType.PropertyName) {
+						string propertyName = reader.GetString ();
+						reader.Read ();
+
+						switch (propertyName.ToLowerInvariant ()) {
+						case "key":
+							if (reader.TokenType == JsonTokenType.String) {
+								if (Enum.TryParse (reader.GetString (), false, out key)) {
+									break;
+								}
+								
+								// The enum uses "D0..D9" for the number keys
+								if (Enum.TryParse (reader.GetString ().TrimStart ('D', 'd'), false, out key)) {
+									break;
+								}
+
+								if (key == Key.Unknown || key == Key.Null) {
+									throw new JsonException ($"The value \"{reader.GetString ()}\" is not a valid Key.");
+								}
+
+							} else if (reader.TokenType == JsonTokenType.Number) {
+								try {
+									key = (Key)reader.GetInt32 ();
+								} catch (InvalidOperationException ioe) {
+									throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe);
+								} catch (FormatException ioe) {
+									throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe);
+								}
+								break;
+							}
+							break;
+
+						case "modifiers":
+							if (reader.TokenType == JsonTokenType.StartArray) {
+								while (reader.Read ()) {
+									if (reader.TokenType == JsonTokenType.EndArray) {
+										break;
+									}
+									var mod = reader.GetString ();
+									try {
+										modifiers.Add (modifierDict [mod]);
+									} catch (KeyNotFoundException e) {
+										throw new JsonException ($"The value \"{mod}\" is not a valid modifier.", e);
+									}
+								}
+							} else {
+								throw new JsonException ($"Expected an array of modifiers, but got \"{reader.TokenType}\".");
+							}
+							break;
+
+						default:
+							throw new JsonException ($"Unexpected Key property \"{propertyName}\".");
+						}
+					}
+				}
+
+				foreach (var modifier in modifiers) {
+					key |= modifier;
+				}
+
+				return key;
+			}
+			throw new JsonException ($"Unexpected StartObject token when parsing Key: {reader.TokenType}.");
+		}
+
+		/// <inheritdoc/>
+		public override void Write (Utf8JsonWriter writer, Key value, JsonSerializerOptions options)
+		{
+			writer.WriteStartObject ();
+
+			var keyName = (value & ~Key.CtrlMask & ~Key.ShiftMask & ~Key.AltMask).ToString ();
+			if (keyName != null) {
+				writer.WriteString ("Key", keyName);
+			} else {
+				writer.WriteNumber ("Key", (uint)(value & ~Key.CtrlMask & ~Key.ShiftMask & ~Key.AltMask));
+			}
+
+			Dictionary<string, Key> modifierDict = new Dictionary<string, Key>
+			{
+				{ "Shift", Key.ShiftMask },
+				{ "Ctrl", Key.CtrlMask },
+				{ "Alt", Key.AltMask }
+			    };
+
+			List<string> modifiers = new List<string> ();
+			foreach (var pair in modifierDict) {
+				if ((value & pair.Value) == pair.Value) {
+					modifiers.Add (pair.Key);
+				}
+			}
+
+			if (modifiers.Count > 0) {
+				writer.WritePropertyName ("Modifiers");
+				writer.WriteStartArray ();
+				foreach (var modifier in modifiers) {
+					writer.WriteStringValue (modifier);
+				}
+				writer.WriteEndArray ();
+			}
+
+			writer.WriteEndObject ();
+		}
+	}
+}

+ 206 - 0
Terminal.Gui/Configuration/Scope.cs

@@ -0,0 +1,206 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+
+
+#nullable enable
+
+namespace Terminal.Gui.Configuration {
+	public static partial class ConfigurationManager {
+
+		/// <summary>
+		/// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define
+		/// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings.
+		/// </summary>
+		public class Scope<T> : Dictionary<string, ConfigProperty> { //, IScope<Scope<T>> {
+			/// <summary>
+			/// Crates a new instance.
+			/// </summary>
+			public Scope () : base (StringComparer.InvariantCultureIgnoreCase)
+			{
+				foreach (var p in GetScopeProperties ()) {
+					Add (p.Key, new ConfigProperty () { PropertyInfo = p.Value.PropertyInfo, PropertyValue = null });
+				}
+			}
+
+			private IEnumerable<KeyValuePair<string, ConfigProperty>> GetScopeProperties ()
+			{
+				return ConfigurationManager._allConfigProperties!.Where (cp =>
+					(cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty))
+					as SerializableConfigurationProperty)?.Scope == GetType ());
+			}
+
+			/// <summary>
+			/// Updates this instance from the specified source scope.
+			/// </summary>
+			/// <param name="source"></param>
+			/// <returns>The updated scope (this).</returns>
+			public Scope<T>? Update (Scope<T> source)
+			{
+				foreach (var prop in source) {
+					if (ContainsKey (prop.Key))
+						this [prop.Key].PropertyValue = this [prop.Key].UpdateValueFrom (prop.Value.PropertyValue!);
+					else {
+						this [prop.Key].PropertyValue = prop.Value.PropertyValue;
+					}
+				}
+				return this;
+			}
+
+			/// <summary>
+			/// Retrieves the values of the properties of this scope from their corresponding static properties.
+			/// </summary>
+			public void RetrieveValues ()
+			{
+				foreach (var p in this.Where (cp => cp.Value.PropertyInfo != null)) {
+					p.Value.RetrieveValue ();
+				}
+			}
+
+			/// <summary>
+			/// Applies the values of the properties of this scope to their corresponding static properties.
+			/// </summary>
+			/// <returns></returns>
+			internal virtual bool Apply ()
+			{
+				bool set = false;
+				foreach (var p in this.Where (t => t.Value != null && t.Value.PropertyValue != null)) {
+					if (p.Value.Apply ()) {
+						set = true;
+					}
+				}
+				return set;
+			}
+		}
+
+		/// <summary>
+		/// Converts <see cref="Scope{T}"/> instances to/from JSON. Does all the heavy lifting of reading/writing
+		/// config data to/from <see cref="ConfigurationManager"/> JSON documents.
+		/// </summary>
+		/// <typeparam name="scopeT"></typeparam>
+		public class ScopeJsonConverter<scopeT> : JsonConverter<scopeT> where scopeT : Scope<scopeT> {
+			// See: https://stackoverflow.com/questions/60830084/how-to-pass-an-argument-by-reference-using-reflection
+			internal abstract class ReadHelper {
+				public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
+			}
+
+			internal class ReadHelper<converterT> : ReadHelper {
+				private readonly ReadDelegate _readDelegate;
+				private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
+				public ReadHelper (object converter)
+					=> _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read");
+				public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
+					=> _readDelegate.Invoke (ref reader, type, options);
+			}
+
+			/// <inheritdoc/>
+			public override scopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+			{
+				if (reader.TokenType != JsonTokenType.StartObject) {
+					throw new JsonException ($"Expected a JSON object, but got \"{reader.TokenType}\".");
+				}
+
+				var scope = (scopeT)Activator.CreateInstance (typeof (scopeT))!;
+				while (reader.Read ()) {
+					if (reader.TokenType == JsonTokenType.EndObject) {
+						return scope!;
+					}
+					if (reader.TokenType != JsonTokenType.PropertyName) {
+						throw new JsonException ($"Expected a JSON property name, but got \"{reader.TokenType}\".");
+					}
+					var propertyName = reader.GetString ();
+					reader.Read ();
+
+					if (propertyName != null && scope!.TryGetValue (propertyName, out var configProp)) {
+						// This property name was found in the Scope's ScopeProperties dictionary
+						// Figure out if it needs a JsonConverter and if so, create one
+						var propertyType = configProp?.PropertyInfo?.PropertyType!;
+						if (configProp?.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) {
+							var converter = Activator.CreateInstance (jca.ConverterType!)!;
+							if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) {
+								var factory = (JsonConverterFactory)converter;
+								if (propertyType != null && factory.CanConvert (propertyType)) {
+									converter = factory.CreateConverter (propertyType, options);
+								}
+							}
+							var readHelper = Activator.CreateInstance ((Type?)typeof (ReadHelper<>).MakeGenericType (typeof (scopeT), propertyType!)!, converter) as ReadHelper;
+							scope! [propertyName].PropertyValue = readHelper?.Read (ref reader, propertyType!, options);
+						} else {
+							scope! [propertyName].PropertyValue = JsonSerializer.Deserialize (ref reader, propertyType!, options);
+						}
+					} else {
+						// It is not a config property. Maybe it's just a property on the Scope with [JsonInclude]
+						// like ScopeSettings.$schema...
+						var property = scope!.GetType ().GetProperties ().Where (p => {
+							var jia = p.GetCustomAttribute (typeof (JsonIncludeAttribute)) as JsonIncludeAttribute;
+							if (jia != null) {
+								var jpna = p.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
+								if (jpna?.Name == propertyName) {
+									// Bit of a hack, modifying propertyName in an enumerator...
+									propertyName = p.Name;
+									return true;
+								}
+
+								return p.Name == propertyName;
+							}
+							return false;
+						}).FirstOrDefault ();
+
+						if (property != null) {
+							var prop = scope.GetType ().GetProperty (propertyName!)!;
+							prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, options));
+						} else {
+							// Unknown property
+							throw new JsonException ($"Unknown property name \"{propertyName}\".");
+						}
+					}
+				}
+				throw new JsonException ();
+			}
+
+			/// <inheritdoc/>
+			public override void Write (Utf8JsonWriter writer, scopeT scope, JsonSerializerOptions options)
+			{
+				writer.WriteStartObject ();
+
+				var properties = scope!.GetType ().GetProperties ().Where (p => p.GetCustomAttribute (typeof (JsonIncludeAttribute)) != null);
+				foreach (var p in properties) {
+					writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p));
+					JsonSerializer.Serialize (writer, scope.GetType ().GetProperty (p.Name)?.GetValue (scope), options);
+				}
+
+				foreach (var p in from p in scope
+						  .Where (cp =>
+							cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is
+							SerializableConfigurationProperty scp && scp?.Scope == typeof (scopeT))
+						  where p.Value.PropertyValue != null
+						  select p) {
+
+					writer.WritePropertyName (p.Key);
+					var propertyType = p.Value.PropertyInfo?.PropertyType;
+
+					if (propertyType != null && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) {
+						var converter = Activator.CreateInstance (jca.ConverterType!)!;
+						if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) {
+							var factory = (JsonConverterFactory)converter;
+							if (factory.CanConvert (propertyType)) {
+								converter = factory.CreateConverter (propertyType, options)!;
+							}
+						}
+						if (p.Value.PropertyValue != null) {
+							converter.GetType ().GetMethod ("Write")?.Invoke (converter, new object [] { writer, p.Value.PropertyValue, options });
+						}
+					} else {
+						JsonSerializer.Serialize (writer, p.Value.PropertyValue, options);
+					}
+				}
+				writer.WriteEndObject ();
+			}
+		}
+	}
+}

+ 118 - 0
Terminal.Gui/Configuration/SettingsScope.cs

@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+
+#nullable enable
+
+namespace Terminal.Gui.Configuration {
+	public static partial class ConfigurationManager {
+		/// <summary>
+		/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties 
+		/// attributed with  <see cref="SettingsScope"/>.
+		/// </summary>
+		/// <example><code>
+		///  {
+		///    "$schema" : "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
+		///    "Application.UseSystemConsole" : true,
+		///    "Theme" : "Default",
+		///    "Themes": {
+		///    },
+		///  },
+		/// </code></example>
+		/// <remarks>
+		/// </remarks>
+		[JsonConverter (typeof (ScopeJsonConverter<SettingsScope>))]
+		public class SettingsScope : Scope<SettingsScope> {
+			/// <summary>
+			/// Points to our JSON schema.
+			/// </summary>
+			[JsonInclude, JsonPropertyName ("$schema")]
+			public string Schema { get; set; } = "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json";
+
+			public List<string> Sources = new List<string> ();
+
+			/// <summary>
+			/// Updates the <see cref="SettingsScope"/> with the settings in a JSON string.
+			/// </summary>
+			/// <param name="stream">Json document to update the settings with.</param>
+			/// <param name="source">The source (filename/resource name) the Json document was read from.</param>
+			public SettingsScope? Update (Stream stream, string source)
+			{
+				// Update the existing settings with the new settings.
+				try {
+					Update (JsonSerializer.Deserialize<SettingsScope> (stream, serializerOptions)!);
+					OnUpdated ();
+					Debug.WriteLine ($"ConfigurationManager: Read configuration from \"{source}\"");
+					Sources.Add (source);
+					return this;
+				} catch (JsonException e) {
+					if (ThrowOnJsonErrors ?? false) {
+						throw;
+					} else {
+						AddJsonError ($"Error deserializing {source}: {e.Message}");
+					}
+				}
+				return this;
+			}
+			
+			/// <summary>
+			/// Updates the <see cref="SettingsScope"/> with the settings in a JSON file.
+			/// </summary>
+			/// <param name="filePath"></param>
+			public SettingsScope? Update (string filePath)
+			{
+				var realPath = filePath.Replace("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
+				if (!File.Exists (realPath)) {
+					Debug.WriteLine ($"ConfigurationManager: Configuration file \"{realPath}\" does not exist.");
+					Sources.Add (filePath);
+					return this;
+				}
+
+				var stream = File.OpenRead (realPath);
+				return Update (stream, filePath);
+			}
+
+			/// <summary>
+			/// Updates the <see cref="SettingsScope"/> with the settings from a Json resource.
+			/// </summary>
+			/// <param name="assembly"></param>
+			/// <param name="resourceName"></param>
+			public SettingsScope? UpdateFromResource (Assembly assembly, string resourceName)
+			{
+				if (resourceName == null || string.IsNullOrEmpty (resourceName)) {
+					Debug.WriteLine ($"ConfigurationManager: Resource \"{resourceName}\" does not exist in \"{assembly.GetName ().Name}\".");
+					return this;
+				}
+				
+				using Stream? stream = assembly.GetManifestResourceStream (resourceName)!;
+				if (stream == null) {
+					Debug.WriteLine ($"ConfigurationManager: Failed to read resource \"{resourceName}\" from \"{assembly.GetName ().Name}\".");
+					return this;
+				}
+
+				return Update (stream, $"resource://[{assembly.GetName().Name}]/{resourceName}");
+			}
+
+			/// <summary>
+			/// Updates the <see cref="SettingsScope"/> with the settings in a JSON string.
+			/// </summary>
+			/// <param name="json">Json document to update the settings with.</param>
+			/// <param name="source">The source (filename/resource name) the Json document was read from.</param>
+			public SettingsScope? Update (string json, string source)
+			{
+				var stream = new MemoryStream ();
+				var writer = new StreamWriter (stream);
+				writer.Write (json);
+				writer.Flush ();
+				stream.Position = 0;
+
+				return Update (stream, source);
+			}
+		}
+	}
+}

+ 292 - 0
Terminal.Gui/Configuration/ThemeScope.cs

@@ -0,0 +1,292 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+
+#nullable enable
+
+namespace Terminal.Gui.Configuration {
+
+	public static partial class ConfigurationManager {
+		/// <summary>
+		/// The root object for a Theme. A Theme is a set of settings that are applied to the running <see cref="Application"/>
+		/// as a group.
+		/// </summary>
+		/// <remarks>
+		/// <para>
+		/// </para>
+		/// </remarks>
+		/// <example><code>
+		/// 	"Default": {
+		/// 		"ColorSchemes": [
+		/// 		{
+		/// 		"TopLevel": {
+		/// 		"Normal": {
+		/// 			"Foreground": "BrightGreen",
+		/// 			"Background": "Black"
+		/// 		},
+		/// 		"Focus": {
+		/// 		"Foreground": "White",
+		/// 			"Background": "Cyan"
+		/// 
+		/// 		},
+		/// 		"HotNormal": {
+		/// 			"Foreground": "Brown",
+		/// 			"Background": "Black"
+		/// 
+		/// 		},
+		/// 		"HotFocus": {
+		/// 			"Foreground": "Blue",
+		/// 			"Background": "Cyan"
+		/// 		},
+		/// 		"Disabled": {
+		/// 			"Foreground": "DarkGray",
+		/// 			"Background": "Black"
+		/// 
+		/// 		}
+		/// 	}
+		/// </code></example> 
+		[JsonConverter (typeof (ScopeJsonConverter<ThemeScope>))]
+		public class ThemeScope : Scope<ThemeScope> {
+
+			/// <inheritdoc/>
+			internal override bool Apply ()
+			{
+				var ret = base.Apply ();
+				Application.Driver?.InitalizeColorSchemes ();
+				return ret;
+			}
+		}
+
+		/// <summary>
+		/// Contains a dictionary of the <see cref="ThemeManager.Theme"/>s for a Terminal.Gui application.
+		/// </summary>
+		/// <remarks>
+		/// <para>
+		/// A Theme is a collection of settings that are named. The default theme is named "Default".
+		/// </para>
+		/// <para>
+		/// The <see cref="ThemeManager.Theme"/> property is used to detemrine the currently active theme. 
+		/// </para>
+		/// </remarks>
+		/// <para>
+		/// <see cref="ThemeManager"/> is a singleton class. It is created when the first <see cref="ThemeManager"/> property is accessed.
+		/// Accessing <see cref="ThemeManager.Instance"/> is the same as accessing <see cref="ConfigurationManager.Themes"/>.
+		/// </para>
+		/// <example><code>
+		/// 	"Themes": [
+		/// 	{
+		/// 		"Default": {
+		/// 			"ColorSchemes": [
+		/// 			{
+		/// 			"TopLevel": {
+		/// 			"Normal": {
+		/// 				"Foreground": "BrightGreen",
+		/// 				"Background": "Black"
+		/// 			},
+		/// 			"Focus": {
+		/// 			"Foreground": "White",
+		/// 				"Background": "Cyan"
+		/// 
+		/// 			},
+		/// 			"HotNormal": {
+		/// 				"Foreground": "Brown",
+		/// 				"Background": "Black"
+		/// 
+		/// 			},
+		/// 			"HotFocus": {
+		/// 				"Foreground": "Blue",
+		/// 				"Background": "Cyan"
+		/// 			},
+		/// 			"Disabled": {
+		/// 				"Foreground": "DarkGray",
+		/// 				"Background": "Black"
+		/// 
+		/// 			}
+		/// 		}
+		/// 	}
+		/// </code></example> 
+		public class ThemeManager : IDictionary<string, ThemeScope> {
+			private static readonly ThemeManager _instance = new ThemeManager ();
+			static ThemeManager () { } // Make sure it's truly lazy
+			private ThemeManager () { } // Prevent instantiation outside
+
+			/// <summary>
+			/// Class is a singleton...
+			/// </summary>
+			public static ThemeManager Instance { get { return _instance; } }
+
+			private static string theme = string.Empty;
+
+			/// <summary>
+			/// The currently selected theme. This is the internal version; see <see cref="Theme"/>.
+			/// </summary>
+			[JsonInclude, SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("Theme")]
+			internal static string SelectedTheme {
+				get => theme;
+				set {
+					var oldTheme = theme;
+					theme = value;
+					if (oldTheme != theme &&
+						ConfigurationManager.Settings! ["Themes"]?.PropertyValue is Dictionary<string, ThemeScope> themes &&
+						themes.ContainsKey (theme)) {
+						ConfigurationManager.Settings! ["Theme"].PropertyValue = theme;
+						Instance.OnThemeChanged (oldTheme);
+					}
+				}
+			}
+
+			/// <summary>
+			/// Gets or sets the currently selected theme. The value is persisted to the "Theme"
+			/// property.
+			/// </summary>
+			[JsonIgnore]
+			public string Theme {
+				get => ThemeManager.SelectedTheme;
+				set {
+					ThemeManager.SelectedTheme = value;
+				}
+			}
+
+			/// <summary>
+			/// Event arguments for the <see cref="ThemeManager"/> events.
+			/// </summary>
+			public class ThemeManagerEventArgs : EventArgs {
+				/// <summary>
+				/// The name of the new active theme..
+				/// </summary>
+				public string NewTheme { get; set; } = string.Empty;
+
+				/// <summary>
+				/// Initializes a new instance of <see cref="ThemeManagerEventArgs"/>
+				/// </summary>
+				public ThemeManagerEventArgs (string newTheme)
+				{
+					NewTheme = newTheme;
+				}
+			}
+
+			/// <summary>
+			/// Called when the selected theme has changed. Fires the <see cref="ThemeChanged"/> event.
+			/// </summary>
+			internal void OnThemeChanged (string theme)
+			{
+				Debug.WriteLine ($"Themes.OnThemeChanged({theme}) -> {Theme}");
+				ThemeChanged?.Invoke (new ThemeManagerEventArgs (theme));
+			}
+
+			/// <summary>
+			/// Event fired he selected theme has changed.
+			/// application.
+			/// </summary>
+			public event Action<ThemeManagerEventArgs>? ThemeChanged;
+
+			/// <summary>
+			/// Holds the <see cref="ThemeScope"/> definitions. 
+			/// </summary>
+			[JsonInclude, JsonConverter (typeof (DictionaryJsonConverter<ThemeScope>))]
+			[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
+			public static Dictionary<string, ThemeScope>? Themes {
+				get => Settings? ["Themes"]?.PropertyValue as Dictionary<string, ThemeScope>; // themes ?? new Dictionary<string, ThemeScope> ();
+				set {
+					//if (themes == null || value == null) {
+					//	themes = value;
+					//} else {
+					//	themes = (Dictionary<string, ThemeScope>)DeepMemberwiseCopy (value!, themes!)!;
+					//}
+					Settings! ["Themes"].PropertyValue = value;
+				}
+			}
+
+			internal static void Reset ()
+			{
+				Debug.WriteLine ($"Themes.Reset()");
+
+				Themes?.Clear ();
+				SelectedTheme = string.Empty;
+			}
+
+			internal static void GetHardCodedDefaults ()
+			{
+				Debug.WriteLine ($"Themes.GetHardCodedDefaults()");
+				var theme = new ThemeScope ();
+				theme.RetrieveValues ();
+
+				Themes = new Dictionary<string, ThemeScope> (StringComparer.InvariantCultureIgnoreCase) { { "Default", theme } };
+				SelectedTheme = "Default";
+			}
+
+			#region IDictionary
+			/// <inheritdoc/>
+			public ICollection<string> Keys => ((IDictionary<string, ThemeScope>)Themes!).Keys;
+			/// <inheritdoc/>
+			public ICollection<ThemeScope> Values => ((IDictionary<string, ThemeScope>)Themes!).Values;
+			/// <inheritdoc/>
+			public int Count => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Count;
+			/// <inheritdoc/>
+			public bool IsReadOnly => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).IsReadOnly;
+			/// <inheritdoc/>
+			public ThemeScope this [string key] { get => ((IDictionary<string, ThemeScope>)Themes!) [key]; set => ((IDictionary<string, ThemeScope>)Themes!) [key] = value; }
+			/// <inheritdoc/>
+			public void Add (string key, ThemeScope value)
+			{
+				((IDictionary<string, ThemeScope>)Themes!).Add (key, value);
+			}
+			/// <inheritdoc/>
+			public bool ContainsKey (string key)
+			{
+				return ((IDictionary<string, ThemeScope>)Themes!).ContainsKey (key);
+			}
+			/// <inheritdoc/>
+			public bool Remove (string key)
+			{
+				return ((IDictionary<string, ThemeScope>)Themes!).Remove (key);
+			}
+			/// <inheritdoc/>
+			public bool TryGetValue (string key, out ThemeScope value)
+			{
+				return ((IDictionary<string, ThemeScope>)Themes!).TryGetValue (key, out value!);
+			}
+			/// <inheritdoc/>
+			public void Add (KeyValuePair<string, ThemeScope> item)
+			{
+				((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Add (item);
+			}
+			/// <inheritdoc/>
+			public void Clear ()
+			{
+				((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Clear ();
+			}
+			/// <inheritdoc/>
+			public bool Contains (KeyValuePair<string, ThemeScope> item)
+			{
+				return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Contains (item);
+			}
+			/// <inheritdoc/>
+			public void CopyTo (KeyValuePair<string, ThemeScope> [] array, int arrayIndex)
+			{
+				((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).CopyTo (array, arrayIndex);
+			}
+			/// <inheritdoc/>
+			public bool Remove (KeyValuePair<string, ThemeScope> item)
+			{
+				return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Remove (item);
+			}
+			/// <inheritdoc/>
+			public IEnumerator<KeyValuePair<string, ThemeScope>> GetEnumerator ()
+			{
+				return ((IEnumerable<KeyValuePair<string, ThemeScope>>)Themes!).GetEnumerator ();
+			}
+
+			IEnumerator IEnumerable.GetEnumerator ()
+			{
+				return ((IEnumerable)Themes!).GetEnumerator ();
+			}
+			#endregion
+		}
+	}
+}

+ 20 - 3
Terminal.Gui/Core/Application.cs

@@ -20,6 +20,9 @@ using System.ComponentModel;
 using System.Globalization;
 using System.Reflection;
 using System.IO;
+using Terminal.Gui.Configuration;
+using System.Text.Json.Serialization;
+using static Terminal.Gui.Configuration.ConfigurationManager;
 
 namespace Terminal.Gui {
 
@@ -117,6 +120,7 @@ namespace Terminal.Gui {
 		/// The current <see cref="ConsoleDriver.HeightAsBuffer"/> used in the terminal.
 		/// </summary>
 		/// 
+		[SerializableConfigurationProperty (Scope = typeof(SettingsScope))]
 		public static bool HeightAsBuffer {
 			get {
 				if (Driver == null) {
@@ -139,6 +143,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.
 		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof(SettingsScope)), JsonConverter(typeof(KeyJsonConverter))]
 		public static Key AlternateForwardKey {
 			get => alternateForwardKey;
 			set {
@@ -162,6 +167,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.
 		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof(SettingsScope)), JsonConverter (typeof (KeyJsonConverter))]
 		public static Key AlternateBackwardKey {
 			get => alternateBackwardKey;
 			set {
@@ -185,6 +191,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets the key to quit the application.
 		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof(SettingsScope)), JsonConverter (typeof (KeyJsonConverter))]
 		public static Key QuitKey {
 			get => quitKey;
 			set {
@@ -220,6 +227,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Disable or enable the mouse. The mouse is enabled by default.
 		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
 		public static bool IsMouseDisabled { get; set; }
 
 		/// <summary>
@@ -313,6 +321,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// If <see langword="true"/>, forces the use of the System.Console-based (see <see cref="NetDriver"/>) driver. The default is <see langword="false"/>.
 		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
 		public static bool UseSystemConsole { get; set; } = false;
 
 		// For Unit testing - ignores UseSystemConsole
@@ -392,6 +401,14 @@ namespace Terminal.Gui {
 				Driver = driver;
 			}
 
+			// Start the process of configuration management.
+			// Note that we end up calling LoadConfigurationFromAllSources
+			// mulitlple times. We need to do this because some settings are only
+			// valid after a Driver is loaded. In this cases we need just 
+			// `Settings` so we can determine which driver to use.
+			ConfigurationManager.Load (true);
+			ConfigurationManager.Apply ();
+
 			if (Driver == null) {
 				var p = Environment.OSVersion.Platform;
 				if (ForceFakeConsole) {
@@ -779,9 +796,7 @@ namespace Terminal.Gui {
 			}
 
 			if (mouseGrabView != null) {
-				if (view == null) {
-					view = mouseGrabView;
-				}
+				view ??= mouseGrabView;
 
 				var newxy = mouseGrabView.ScreenToView (me.X, me.Y);
 				var nme = new MouseEvent () {
@@ -1070,6 +1085,8 @@ namespace Terminal.Gui {
 		public static void Shutdown ()
 		{
 			ResetState ();
+
+			ConfigurationManager.PrintJsonErrors ();
 		}
 
 		// Encapsulate all setting of initial state for Application; Having

+ 31 - 11
Terminal.Gui/Core/Border.cs

@@ -1,6 +1,7 @@
 using NStack;
 using System;
 using Terminal.Gui.Graphs;
+using System.Text.Json.Serialization;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -34,18 +35,22 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets the width, in integers, of the left side of the bounding rectangle.
 		/// </summary>
+		[JsonInclude]
 		public int Left;
 		/// <summary>
 		/// Gets or sets the width, in integers, of the upper side of the bounding rectangle.
 		/// </summary>
+		[JsonInclude]
 		public int Top;
 		/// <summary>
 		/// Gets or sets the width, in integers, of the right side of the bounding rectangle.
 		/// </summary>
+		[JsonInclude]
 		public int Right;
 		/// <summary>
 		/// Gets or sets the width, in integers, of the lower side of the bounding rectangle.
 		/// </summary>
+		[JsonInclude]
 		public int Bottom;
 
 		/// <summary>
@@ -330,6 +335,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Specifies the <see cref="Gui.BorderStyle"/> for a view.
 		/// </summary>
+		[JsonInclude, JsonConverter (typeof(JsonStringEnumConverter))]
 		public BorderStyle BorderStyle {
 			get => borderStyle;
 			set {
@@ -345,6 +351,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets if a margin frame is drawn around the <see cref="Child"/> regardless the <see cref="BorderStyle"/>
 		/// </summary>
+		[JsonInclude]
 		public bool DrawMarginFrame {
 			get => drawMarginFrame;
 			set {
@@ -362,6 +369,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets the relative <see cref="Thickness"/> of a <see cref="Border"/>.
 		/// </summary>
+		[JsonInclude]
 		public Thickness BorderThickness {
 			get => borderThickness;
 			set {
@@ -373,6 +381,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets the <see cref="Color"/> that draws the outer border color.
 		/// </summary>
+		[JsonInclude, JsonConverter (typeof (Configuration.ColorJsonConverter))]
 		public Color BorderBrush {
 			get => borderBrush;
 			set {
@@ -384,6 +393,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets the <see cref="Color"/> that fills the area between the bounds of a <see cref="Border"/>.
 		/// </summary>
+		[JsonInclude, JsonConverter (typeof (Configuration.ColorJsonConverter))]
 		public Color Background {
 			get => background;
 			set {
@@ -396,6 +406,7 @@ namespace Terminal.Gui {
 		/// Gets or sets a <see cref="Thickness"/> value that describes the amount of space between a
 		///  <see cref="Border"/> and its child element.
 		/// </summary>
+		[JsonInclude]
 		public Thickness Padding {
 			get => padding;
 			set {
@@ -407,6 +418,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets the rendered width of this element.
 		/// </summary>
+		[JsonIgnore]
 		public int ActualWidth {
 			get {
 				var driver = Application.Driver;
@@ -420,6 +432,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets the rendered height of this element.
 		/// </summary>
+		[JsonIgnore]
 		public int ActualHeight {
 			get {
 				var driver = Application.Driver;
@@ -434,21 +447,25 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets the single child element of a <see cref="View"/>.
 		/// </summary>
+		[JsonIgnore]
 		public View Child { get; set; }
 
 		/// <summary>
 		/// Gets the parent <see cref="Child"/> parent if any.
 		/// </summary>
+		[JsonIgnore]
 		public View Parent { get => Child?.SuperView; }
 
 		/// <summary>
 		/// Gets or private sets by the <see cref="ToplevelContainer"/>
 		/// </summary>
+		[JsonIgnore]
 		public ToplevelContainer ChildContainer { get; private set; }
 
 		/// <summary>
 		/// Gets or sets the 3D effect around the <see cref="Border"/>.
 		/// </summary>
+		[JsonInclude]
 		public bool Effect3D {
 			get => effect3D;
 			set {
@@ -460,6 +477,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Get or sets the offset start position for the <see cref="Effect3D"/>
 		/// </summary>
+		[JsonInclude]
 		public Point Effect3DOffset {
 			get => effect3DOffset;
 			set {
@@ -470,8 +488,16 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets the color for the <see cref="Border"/>
 		/// </summary>
+		[JsonInclude, JsonConverter (typeof (Configuration.AttributeJsonConverter))]
 		public Attribute? Effect3DBrush {
-			get => effect3DBrush;
+			get {
+				if (effect3DBrush == null && effect3D) {
+					return effect3DBrush = new Attribute (Color.Gray, Color.DarkGray);
+				} else {
+					return effect3DBrush;
+				}
+			}
+
 			set {
 				effect3DBrush = value;
 				OnBorderChanged ();
@@ -481,6 +507,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The title to be displayed for this view.
 		/// </summary>
+		[JsonIgnore]
 		public ustring Title {
 			get => title;
 			set {
@@ -551,7 +578,7 @@ namespace Terminal.Gui {
 
 			// Draw 3D effects
 			if (Effect3D) {
-				driver.SetAttribute (GetEffect3DBrush ());
+				driver.SetAttribute ((Attribute)Effect3DBrush);
 
 				var effectBorder = new Rect () {
 					X = borderRect.X + Effect3DOffset.X,
@@ -740,7 +767,7 @@ namespace Terminal.Gui {
 			}
 
 			if (Effect3D) {
-				driver.SetAttribute (GetEffect3DBrush ());
+				driver.SetAttribute ((Attribute)Effect3DBrush);
 
 				// Draw the upper Effect3D
 				for (int r = frame.Y - drawMarginFrame - sumThickness.Top + effect3DOffset.Y;
@@ -895,7 +922,7 @@ namespace Terminal.Gui {
 			}
 
 			if (Effect3D) {
-				driver.SetAttribute (GetEffect3DBrush ());
+				driver.SetAttribute ((Attribute)Effect3DBrush);
 
 				// Draw the upper Effect3D
 				for (int r = Math.Max (frame.Y + effect3DOffset.Y, 0);
@@ -940,13 +967,6 @@ namespace Terminal.Gui {
 			driver.SetAttribute (savedAttribute);
 		}
 
-		private Attribute GetEffect3DBrush ()
-		{
-			return Effect3DBrush == null
-				? new Attribute (Color.Gray, Color.DarkGray)
-				: (Attribute)Effect3DBrush;
-		}
-
 		private void AddRuneAt (ConsoleDriver driver, int col, int row, Rune ch)
 		{
 			if (col < driver.Cols && row < driver.Rows && col > 0 && driver.Contents [row, col, 2] == 0

+ 17 - 32
Terminal.Gui/Core/ConsoleDriver.cs

@@ -8,7 +8,10 @@ using System.ComponentModel;
 using System.Diagnostics;
 using System.Linq;
 using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
 using System.Threading.Tasks;
+using Terminal.Gui.Configuration;
+using static Terminal.Gui.Configuration.ConfigurationManager;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -17,6 +20,7 @@ namespace Terminal.Gui {
 	/// <remarks>
 	/// The <see cref="Attribute.HasValidColors"/> value indicates either no-color has been set or the color is invalid.
 	/// </remarks>
+	[JsonConverter (typeof (ColorJsonConverter))]
 	public enum Color {
 		/// <summary>
 		/// The black color.
@@ -170,22 +174,26 @@ namespace Terminal.Gui {
 	///   They encode both the foreground and the background color and are used in the <see cref="ColorScheme"/>
 	///   class to define color schemes that can be used in an application.
 	/// </remarks>
+	[JsonConverter (typeof (AttributeJsonConverter))]
 	public struct Attribute {
 		/// <summary>
 		/// The <see cref="ConsoleDriver"/>-specific color attribute value. If <see cref="Initialized"/> is <see langword="false"/> 
 		/// the value of this property is invalid (typically because the Attribute was created before a driver was loaded)
 		/// and the attribute should be re-made (see <see cref="Make(Color, Color)"/>) before it is used.
 		/// </summary>
+		[JsonIgnore (Condition = JsonIgnoreCondition.Always)]
 		public int Value { get; }
 
 		/// <summary>
 		/// The foreground color.
 		/// </summary>
+		[JsonConverter (typeof (Configuration.ColorJsonConverter))]
 		public Color Foreground { get; }
 
 		/// <summary>
 		/// The background color.
 		/// </summary>
+		[JsonConverter (typeof (Configuration.ColorJsonConverter))]
 		public Color Background { get; }
 
 		/// <summary>
@@ -303,12 +311,14 @@ namespace Terminal.Gui {
 		/// <remarks>
 		/// Attributes that have not been initialized must eventually be initialized before being passed to a driver.
 		/// </remarks>
+		[JsonIgnore]
 		public bool Initialized { get; internal set; }
 
 		/// <summary>
 		/// Returns <see langword="true"/> if the Attribute is valid (both foreground and background have valid color values).
 		/// </summary>
 		/// <returns></returns>
+		[JsonIgnore]
 		public bool HasValidColors { get => (int)Foreground > -1 && (int)Background > -1; }
 	}
 
@@ -320,6 +330,7 @@ namespace Terminal.Gui {
 	/// <remarks>
 	/// See also: <see cref="Colors.ColorSchemes"/>.
 	/// </remarks>
+	[JsonConverter (typeof (ColorSchemeJsonConverter))]
 	public class ColorScheme : IEquatable<ColorScheme> {
 		Attribute _normal = new Attribute (Color.White, Color.Black);
 		Attribute _focus = new Attribute (Color.White, Color.Black);
@@ -586,6 +597,8 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Provides the defined <see cref="ColorScheme"/>s.
 		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof(ThemeScope), OmitClassName = true)]
+		[JsonConverter(typeof(DictionaryJsonConverter<ColorScheme>))]
 		public static Dictionary<string, ColorScheme> ColorSchemes { get; private set; }
 	}
 
@@ -1463,6 +1476,10 @@ namespace Terminal.Gui {
 		/// Ensures all <see cref="Attribute"/>s in <see cref="Colors.ColorSchemes"/> are correctly 
 		/// initialized by the driver.
 		/// </summary>
+		/// <remarks>
+		/// This method was previsouly named CreateColors. It was reanmed to InitalizeColorSchemes when
+		/// <see cref="ConfigurationManager"/> was enabled.
+		/// </remarks>
 		/// <param name="supportsColors">Flag indicating if colors are supported (not used).</param>
 		public void InitalizeColorSchemes (bool supportsColors = true)
 		{
@@ -1475,38 +1492,6 @@ namespace Terminal.Gui {
 				return;
 			}
 
-
-			// Define the default color theme only if the user has not defined one.
-
-			Colors.TopLevel.Normal = MakeColor (Color.BrightGreen, Color.Black);
-			Colors.TopLevel.Focus = MakeColor (Color.White, Color.Cyan);
-			Colors.TopLevel.HotNormal = MakeColor (Color.Brown, Color.Black);
-			Colors.TopLevel.HotFocus = MakeColor (Color.Blue, Color.Cyan);
-			Colors.TopLevel.Disabled = MakeColor (Color.DarkGray, Color.Black);
-
-			Colors.Base.Normal = MakeColor (Color.White, Color.Blue);
-			Colors.Base.Focus = MakeColor (Color.Black, Color.Gray);
-			Colors.Base.HotNormal = MakeColor (Color.BrightCyan, Color.Blue);
-			Colors.Base.HotFocus = MakeColor (Color.BrightBlue, Color.Gray);
-			Colors.Base.Disabled = MakeColor (Color.DarkGray, Color.Blue);
-
-			Colors.Dialog.Normal = MakeColor (Color.Black, Color.Gray);
-			Colors.Dialog.Focus = MakeColor (Color.White, Color.DarkGray);
-			Colors.Dialog.HotNormal = MakeColor (Color.Blue, Color.Gray);
-			Colors.Dialog.HotFocus = MakeColor (Color.BrightYellow, Color.DarkGray);
-			Colors.Dialog.Disabled = MakeColor (Color.Gray, Color.DarkGray);
-
-			Colors.Menu.Normal = MakeColor (Color.White, Color.DarkGray);
-			Colors.Menu.Focus = MakeColor (Color.White, Color.Black);
-			Colors.Menu.HotNormal = MakeColor (Color.BrightYellow, Color.DarkGray);
-			Colors.Menu.HotFocus = MakeColor (Color.BrightYellow, Color.Black);
-			Colors.Menu.Disabled = MakeColor (Color.Gray, Color.DarkGray);
-
-			Colors.Error.Normal = MakeColor (Color.Red, Color.White);
-			Colors.Error.Focus = MakeColor (Color.Black, Color.BrightRed);
-			Colors.Error.HotNormal = MakeColor (Color.Black, Color.White);
-			Colors.Error.HotFocus = MakeColor (Color.White, Color.BrightRed);
-			Colors.Error.Disabled = MakeColor (Color.DarkGray, Color.White);
 		}
 	}
 

+ 14 - 2
Terminal.Gui/Core/Window.cs

@@ -11,7 +11,10 @@
 
 using System;
 using System.Collections;
+using System.Text.Json.Serialization;
 using NStack;
+using Terminal.Gui.Configuration;
+using static Terminal.Gui.Configuration.ConfigurationManager;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -170,6 +173,15 @@ namespace Terminal.Gui {
 			Initialize (title, Rect.Empty, padding, border);
 		}
 
+		/// <summary>
+		/// The default <see cref="BorderStyle"/> for <see cref="FrameView"/>. The default is <see cref="BorderStyle.Single"/>.
+		/// </summary>
+		/// <remarks>
+		/// This property can be set in a Theme to change the default <see cref="BorderStyle"/> for all <see cref="Window"/>s. 
+		/// </remarks>
+		///[SerializableConfigurationProperty (Scope = typeof (ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
+		public static BorderStyle DefaultBorderStyle { get; set; } = BorderStyle.Single;
+
 		void Initialize (ustring title, Rect frame, int padding = 0, Border border = null)
 		{
 			CanFocus = true;
@@ -178,7 +190,7 @@ namespace Terminal.Gui {
 			Title = title;
 			if (border == null) {
 				Border = new Border () {
-					BorderStyle = BorderStyle.Single,
+					BorderStyle = DefaultBorderStyle,
 					Padding = new Thickness (padding),
 					BorderBrush = ColorScheme.Normal.Background
 				};
@@ -338,7 +350,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// An <see cref="EventArgs"/> which allows passing a cancelable new <see cref="Title"/> value event.
+		/// Event arguments for <see cref="Title"/> chane events.
 		/// </summary>
 		public class TitleEventArgs : EventArgs {
 			/// <summary>

+ 445 - 0
Terminal.Gui/Resources/config.json

@@ -0,0 +1,445 @@
+{
+  // This document specifies the "source of truth" for default values for all Terminal.GUi settings managed by
+  // ConfigurationManager. It is automatically loaded, and applied, each time Application.Init
+  // is run (via the ConfiguraitonManager.Reset method). 
+  //
+  // In otherwords, initial values set in the the codebase are always overwritten by the contents of this 
+  // file.
+  //
+  // The Unit Test method "TestConfigurationManagerSaveDefaults" can be used to re-create the base of this file, but
+  // note that not all values here will be recreated (e.g. the Light and Dark themes and any property initialized
+  // null.
+  //
+  "$schema": "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
+  "Application.AlternateBackwardKey": {
+    "Key": "PageUp",
+    "Modifiers": [
+      "Ctrl"
+    ]
+  },
+  "Application.AlternateForwardKey": {
+    "Key": "PageDown",
+    "Modifiers": [
+      "Ctrl"
+    ]
+  },
+  "Application.HeightAsBuffer": false,
+  "Application.QuitKey": {
+    "Key": "Q",
+    "Modifiers": [
+      "Ctrl"
+    ]
+  },
+  "Application.UseSystemConsole": false,
+  "Application.IsMouseDisabled": false,
+  "Theme": "Default",
+  "Themes": [
+    {
+      "Default": {
+        "Dialog.DefaultBorder": {
+          "BorderStyle": "Single",
+          "DrawMarginFrame": true,
+          "BorderThickness": {
+            "Left": 0,
+            "Top": 0,
+            "Right": 0,
+            "Bottom": 0
+          },
+          "BorderBrush": "Black",
+          "Background": "Black",
+          "Padding": {
+            "Left": 0,
+            "Top": 0,
+            "Right": 0,
+            "Bottom": 0
+          },
+          "Effect3D": true,
+          "Effect3DOffset": {
+            "X": 1,
+            "Y": 1
+          },
+          "Effect3DBrush": {
+            "Foreground": "Gray",
+            "Background": "DarkGray"
+          }
+        },
+        "Dialog.DefaultButtonAlignment": "Center",
+        "FrameView.DefaultBorderStyle": "Single",
+        "ColorSchemes": [
+          {
+            "TopLevel": {
+              "Normal": {
+                "Foreground": "BrightGreen",
+                "Background": "Black"
+              },
+              "Focus": {
+                "Foreground": "White",
+                "Background": "Cyan"
+              },
+              "HotNormal": {
+                "Foreground": "Brown",
+                "Background": "Black"
+              },
+              "HotFocus": {
+                "Foreground": "Blue",
+                "Background": "Cyan"
+              },
+              "Disabled": {
+                "Foreground": "DarkGray",
+                "Background": "Black"
+              }
+            }
+          },
+          {
+            "Base": {
+              "Normal": {
+                "Foreground": "White",
+                "Background": "Blue"
+              },
+              "Focus": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              },
+              "HotNormal": {
+                "Foreground": "BrightCyan",
+                "Background": "Blue"
+              },
+              "HotFocus": {
+                "Foreground": "BrightBlue",
+                "Background": "Gray"
+              },
+              "Disabled": {
+                "Foreground": "DarkGray",
+                "Background": "Blue"
+              }
+            }
+          },
+          {
+            "Dialog": {
+              "Normal": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              },
+              "Focus": {
+                "Foreground": "White",
+                "Background": "DarkGray"
+              },
+              "HotNormal": {
+                "Foreground": "Blue",
+                "Background": "Gray"
+              },
+              "HotFocus": {
+                "Foreground": "BrightYellow",
+                "Background": "DarkGray"
+              },
+              "Disabled": {
+                "Foreground": "Gray",
+                "Background": "DarkGray"
+              }
+            }
+          },
+          {
+            "Menu": {
+              "Normal": {
+                "Foreground": "White",
+                "Background": "DarkGray"
+              },
+              "Focus": {
+                "Foreground": "White",
+                "Background": "Black"
+              },
+              "HotNormal": {
+                "Foreground": "BrightYellow",
+                "Background": "DarkGray"
+              },
+              "HotFocus": {
+                "Foreground": "BrightYellow",
+                "Background": "Black"
+              },
+              "Disabled": {
+                "Foreground": "Gray",
+                "Background": "DarkGray"
+              }
+            }
+          },
+          {
+            "Error": {
+              "Normal": {
+                "Foreground": "Red",
+                "Background": "White"
+              },
+              "Focus": {
+                "Foreground": "Black",
+                "Background": "BrightRed"
+              },
+              "HotNormal": {
+                "Foreground": "Black",
+                "Background": "White"
+              },
+              "HotFocus": {
+                "Foreground": "White",
+                "Background": "BrightRed"
+              },
+              "Disabled": {
+                "Foreground": "DarkGray",
+                "Background": "White"
+              }
+            }
+          }
+        ]
+      }
+    },
+    {
+      "Dark": {
+        "ColorSchemes": [
+          {
+            "TopLevel": {
+              "Normal": {
+                "Foreground": "Gray",
+                "Background": "Black"
+              },
+              "Focus": {
+                "Foreground": "White",
+                "Background": "BrightGreen"
+              },
+              "HotNormal": {
+                "Foreground": "BrightGreen",
+                "Background": "Black"
+              },
+              "HotFocus": {
+                "Foreground": "Cyan",
+                "Background": "Black"
+              },
+              "Disabled": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              }
+            }
+          },
+          {
+            "Base": {
+              "Normal": {
+                "Foreground": "Gray",
+                "Background": "Black"
+              },
+              "Focus": {
+                "Foreground": "White",
+                "Background": "DarkGray"
+              },
+              "HotNormal": {
+                "Foreground": "BrightYellow",
+                "Background": "Black"
+              },
+              "HotFocus": {
+                "Foreground": "Cyan",
+                "Background": "Black"
+              },
+              "Disabled": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              }
+            }
+          },
+          {
+            "Dialog": {
+              "Normal": {
+                "Foreground": "Gray",
+                "Background": "Black"
+              },
+              "Focus": {
+                "Foreground": "BrightCyan",
+                "Background": "Black"
+              },
+              "HotNormal": {
+                "Foreground": "White",
+                "Background": "Black"
+              },
+              "HotFocus": {
+                "Foreground": "White",
+                "Background": "Black"
+              },
+              "Disabled": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              }
+            }
+          },
+          {
+            "Menu": {
+              "Normal": {
+                "Foreground": "White",
+                "Background": "DarkGray"
+              },
+              "Focus": {
+                "Foreground": "White",
+                "Background": "Black"
+              },
+              "HotNormal": {
+                "Foreground": "Gray",
+                "Background": "DarkGray"
+              },
+              "HotFocus": {
+                "Foreground": "White",
+                "Background": "Black"
+              },
+              "Disabled": {
+                "Foreground": "Gray",
+                "Background": "Black"
+              }
+            }
+          },
+          {
+            "Error": {
+              "Normal": {
+                "Foreground": "BrightYellow",
+                "Background": "DarkGray"
+              },
+              "Focus": {
+                "Foreground": "DarkGray",
+                "Background": "BrightYellow"
+              },
+              "HotNormal": {
+                "Foreground": "BrightYellow",
+                "Background": "DarkGray"
+              },
+              "HotFocus": {
+                "Foreground": "Red",
+                "Background": "BrightYellow"
+              },
+              "Disabled": {
+                "Foreground": "DarkGray",
+                "Background": "Gray"
+              }
+            }
+          }
+        ]
+      }
+    },
+    {
+      "Light": {
+        "ColorSchemes": [
+          {
+            "TopLevel": {
+              "Normal": {
+                "Foreground": "DarkGray",
+                "Background": "White"
+              },
+              "Focus": {
+                "Foreground": "Black",
+                "Background": "White"
+              },
+              "HotNormal": {
+                "Foreground": "BrightGreen",
+                "Background": "White"
+              },
+              "HotFocus": {
+                "Foreground": "Cyan",
+                "Background": "White"
+              },
+              "Disabled": {
+                "Foreground": "Gray",
+                "Background": "White"
+              }
+            }
+          },
+          {
+            "Base": {
+              "Normal": {
+                "Foreground": "DarkGray",
+                "Background": "White"
+              },
+              "Focus": {
+                "Foreground": "BrightRed",
+                "Background": "Gray"
+              },
+              "HotNormal": {
+                "Foreground": "Red",
+                "Background": "White"
+              },
+              "HotFocus": {
+                "Foreground": "Cyan",
+                "Background": "DarkGray"
+              },
+              "Disabled": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              }
+            }
+          },
+          {
+            "Dialog": {
+              "Normal": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              },
+              "Focus": {
+                "Foreground": "Blue",
+                "Background": "Gray"
+              },
+              "HotNormal": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              },
+              "HotFocus": {
+                "Foreground": "BrightBlue",
+                "Background": "Gray"
+              },
+              "Disabled": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              }
+            }
+          },
+          {
+            "Menu": {
+              "Normal": {
+                "Foreground": "DarkGray",
+                "Background": "White"
+              },
+              "Focus": {
+                "Foreground": "DarkGray",
+                "Background": "Gray"
+              },
+              "HotNormal": {
+                "Foreground": "BrightRed",
+                "Background": "White"
+              },
+              "HotFocus": {
+                "Foreground": "BrightRed",
+                "Background": "Gray"
+              },
+              "Disabled": {
+                "Foreground": "Gray",
+                "Background": "White"
+              }
+            }
+          },
+          {
+            "Error": {
+              "Normal": {
+                "Foreground": "BrightYellow",
+                "Background": "DarkGray"
+              },
+              "Focus": {
+                "Foreground": "DarkGray",
+                "Background": "BrightYellow"
+              },
+              "HotNormal": {
+                "Foreground": "BrightYellow",
+                "Background": "DarkGray"
+              },
+              "HotFocus": {
+                "Foreground": "Red",
+                "Background": "BrightYellow"
+              },
+              "Disabled": {
+                "Foreground": "DarkGray",
+                "Background": "Gray"
+              }
+            }
+          }
+        ]
+      }
+    }
+  ]
+}

+ 9 - 0
Terminal.Gui/Terminal.Gui.csproj

@@ -15,6 +15,12 @@
     <Version>1.0</Version>
     <InformationalVersion>1.0</InformationalVersion>
   </PropertyGroup>
+  <ItemGroup>
+    <None Remove="Resources\config.json" />
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Resources\config.json" />
+  </ItemGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
@@ -53,11 +59,13 @@
   <!-- Enable Nuget Source Link for github -->
   <ItemGroup>
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
+    <PackageReference Include="System.Text.Json" Version="7.0.1" />
   </ItemGroup>
   <PropertyGroup>
     <TargetFrameworks>net472;netstandard2.0;net6.0</TargetFrameworks>
     <RootNamespace>Terminal.Gui</RootNamespace>
     <AssemblyName>Terminal.Gui</AssemblyName>
+    <LangVersion>8</LangVersion>
     <DocumentationFile>bin\Release\Terminal.Gui.xml</DocumentationFile>
     <GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
     <!--<GeneratePackageOnBuild Condition=" '$(Configuration)' == 'Release' ">true</GeneratePackageOnBuild>-->
@@ -83,4 +91,5 @@
       See: https://github.com/gui-cs/Terminal.Gui/releases
     </PackageReleaseNotes>
   </PropertyGroup>
+  <ProjectExtensions><VisualStudio><UserProperties resources_4config_1json__JsonSchema="" /></VisualStudio></ProjectExtensions>
 </Project>

+ 3 - 0
Terminal.Gui/Types/Point.cs

@@ -21,11 +21,13 @@ namespace Terminal.Gui
 		/// <summary>
 		/// Gets or sets the x-coordinate of this Point.
 		/// </summary>
+		[System.Text.Json.Serialization.JsonInclude]
 		public int X;
 
 		/// <summary>
 		/// Gets or sets the y-coordinate of this Point.
 		/// </summary>
+		[System.Text.Json.Serialization.JsonInclude]
 		public int Y;
 
 		// -----------------------
@@ -159,6 +161,7 @@ namespace Terminal.Gui
 		/// <remarks>
 		///	Indicates if both X and Y are zero.
 		/// </remarks>		
+		[System.Text.Json.Serialization.JsonIgnore]
 		public bool IsEmpty {
 			get {
 				return ((X == 0) && (Y == 0));

+ 46 - 1
Terminal.Gui/Views/FrameView.cs

@@ -11,15 +11,51 @@
 
 using System;
 using System.Linq;
+using System.Text.Json.Serialization;
 using NStack;
 using Terminal.Gui.Graphs;
+using static Terminal.Gui.Configuration.ConfigurationManager;
 
 namespace Terminal.Gui {
+
 	/// <summary>
 	/// The FrameView is a container frame that draws a frame around the contents. It is similar to
 	/// a GroupBox in Windows.
 	/// </summary>
 	public class FrameView : View {
+
+		//internal class FrameViewConfig : Configuration.Config<FrameViewConfig> {
+
+		//	/// <summary>
+		//	/// 
+		//	/// </summary>
+		//	/// 
+		//	[JsonConverter (typeof (JsonStringEnumConverter))]
+		//	public BorderStyle? DefaultBorderStyle { get; set; }
+
+		//	public override void Apply ()
+		//	{
+		//		if (DefaultBorderStyle.HasValue) {
+		//			FrameView.DefaultBorderStyle = DefaultBorderStyle.Value;
+		//		}
+		//	}
+
+		//	public override void CopyUpdatedProperitesFrom (FrameViewConfig changedConfig)
+		//	{
+		//		if (changedConfig.DefaultBorderStyle.HasValue) {
+		//			DefaultBorderStyle = changedConfig.DefaultBorderStyle;
+		//		}
+		//	}
+
+		//	public override void GetHardCodedDefaults ()
+		//	{
+		//		DefaultBorderStyle = FrameView.DefaultBorderStyle;
+		//	}
+		//}
+
+		//[Configuration.ConfigProperty]
+		//internal static FrameViewConfig Config { get; set; } = new FrameViewConfig ();
+
 		View contentView;
 		ustring title;
 
@@ -108,13 +144,22 @@ namespace Terminal.Gui {
 		/// </summary>
 		public FrameView () : this (title: string.Empty) { }
 
+		/// <summary>
+		/// The default <see cref="BorderStyle"/> for <see cref="FrameView"/>. The default is <see cref="BorderStyle.Single"/>.
+		/// </summary>
+		/// <remarks>
+		/// This property can be set in a Theme to change the default <see cref="BorderStyle"/> for all <see cref="FrameView"/>s. 
+		/// </remarks>
+		[SerializableConfigurationProperty (Scope = typeof (ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
+		public static BorderStyle DefaultBorderStyle { get; set; } = BorderStyle.Single;
+
 		void Initialize (Rect frame, ustring title, View [] views = null, Border border = null)
 		{
 			if (title == null) title = ustring.Empty;
 			this.Title = title;
 			if (border == null) {
 				Border = new Border () {
-					BorderStyle = BorderStyle.Single
+					BorderStyle = DefaultBorderStyle
 				};
 			} else {
 				Border = border;

+ 6 - 3
Terminal.Gui/Views/ListView.cs

@@ -76,8 +76,8 @@ namespace Terminal.Gui {
 	/// </para>
 	/// <para>
 	///   <see cref="ListView"/> can display any object that implements the <see cref="IList"/> interface.
-	///   <see cref="string"/> values are converted into <see cref="ustring"/> values before rendering, and other values are
-	///   converted into <see cref="string"/> by calling <see cref="object.ToString"/> and then converting to <see cref="ustring"/> .
+	///   <see cref="string"/> values are converted into <see cref="NStack.ustring"/> values before rendering, and other values are
+	///   converted into <see cref="string"/> by calling <see cref="object.ToString"/> and then converting to <see cref="NStack.ustring"/> .
 	/// </para>
 	/// <para>
 	///   To change the contents of the ListView, set the <see cref="Source"/> property (when 
@@ -815,7 +815,10 @@ namespace Terminal.Gui {
 		}
 	}
 
-	/// <inheritdoc/>
+	/// <summary>
+	/// Provides a default implementation of <see cref="IListDataSource"/> that renders
+	/// <see cref="ListView"/> items using <see cref="object.ToString()"/>.
+	/// </summary>
 	public class ListWrapper : IListDataSource {
 		IList src;
 		BitArray marks;

+ 1 - 1
Terminal.Gui/Views/StatusBar.cs

@@ -37,7 +37,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets the global shortcut to invoke the action on the menu.
 		/// </summary>
-		public Key Shortcut { get; }
+		public Key Shortcut { get; set; }
 
 		/// <summary>
 		/// Gets or sets the title.

+ 1 - 1
Terminal.Gui/Views/TextField.cs

@@ -51,7 +51,7 @@ namespace Terminal.Gui {
 		///   This event is raised when the <see cref="Text"/> changes. 
 		/// </remarks>
 		/// <remarks>
-		///   The passed <see cref="EventArgs"/> is a <see cref="ustring"/> containing the old value. 
+		///   The passed <see cref="EventArgs"/> is a <see cref="NStack.ustring"/> containing the old value. 
 		/// </remarks>
 		public event Action<ustring> TextChanged;
 

+ 27 - 3
Terminal.Gui/Windows/Dialog.cs

@@ -7,7 +7,10 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Text.Json.Serialization;
 using NStack;
+using Terminal.Gui.Configuration;
+using static Terminal.Gui.Configuration.ConfigurationManager;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -20,6 +23,26 @@ namespace Terminal.Gui {
 	///  or buttons added to the dialog calls <see cref="Application.RequestStop"/>.
 	/// </remarks>
 	public class Dialog : Window {
+		/// <summary>
+		/// The default <see cref="ButtonAlignments"/> for <see cref="Dialog"/>. 
+		/// </summary>
+		/// <remarks>
+		/// This property can be set in a Theme.
+		/// </remarks>
+		[SerializableConfigurationProperty (Scope = typeof (ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
+		public static ButtonAlignments DefaultButtonAlignment { get; set; } = ButtonAlignments.Center;
+
+		/// <summary>
+		/// Defines the default border styling for <see cref="Dialog"/>. Can be configured via <see cref="ConfigurationManager"/>.
+		/// </summary>
+		[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
+		public static Border DefaultBorder { get; set; } = new Border () {
+			BorderStyle = BorderStyle.Single,
+			DrawMarginFrame = false,
+			Effect3D = true,
+			Effect3DOffset = new Point (1, 1),
+		};
+
 		internal List<Button> buttons = new List<Button> ();
 		const int padding = 0;
 
@@ -54,7 +77,8 @@ namespace Terminal.Gui {
 
 			ColorScheme = Colors.Dialog;
 			Modal = true;
-			Border.Effect3D = true;
+			ButtonAlignment = DefaultButtonAlignment;
+			Border = DefaultBorder;
 
 			if (buttons != null) {
 				foreach (var b in buttons) {
@@ -117,6 +141,7 @@ namespace Terminal.Gui {
 			}
 			return buttons.Select (b => b.Bounds.Width).Sum ();
 		}
+
 		/// <summary>
 		/// Determines the horizontal alignment of the Dialog buttons.
 		/// </summary>
@@ -142,13 +167,12 @@ namespace Terminal.Gui {
 			Right
 		}
 
-		private ButtonAlignments buttonAlignment = Dialog.ButtonAlignments.Center;
 
 		/// <summary>
 		/// Determines how the <see cref="Dialog"/> <see cref="Button"/>s are aligned along the 
 		/// bottom of the dialog. 
 		/// </summary>
-		public ButtonAlignments ButtonAlignment { get => buttonAlignment; set => buttonAlignment = value; }
+		public ButtonAlignments ButtonAlignment { get; set; }
 
 		void LayoutStartedHandler ()
 		{

+ 16 - 18
Terminal.sln

@@ -31,23 +31,6 @@ Global
 		Release|x86 = Release|x86
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
-		{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|x86.ActiveCfg = Debug|Any CPU
-		{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|x86.Build.0 = Debug|Any CPU
-		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|Any CPU.Build.0 = Release|Any CPU
-		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|x86.ActiveCfg = Release|Any CPU
-		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|x86.Build.0 = Release|Any CPU
-
-		{B0A602CD-E176-449D-8663-64238D54F857}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{B0A602CD-E176-449D-8663-64238D54F857}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{B0A602CD-E176-449D-8663-64238D54F857}.Debug|x86.ActiveCfg = Debug|Any CPU
-		{B0A602CD-E176-449D-8663-64238D54F857}.Debug|x86.Build.0 = Debug|Any CPU
-		{B0A602CD-E176-449D-8663-64238D54F857}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{B0A602CD-E176-449D-8663-64238D54F857}.Release|Any CPU.Build.0 = Release|Any CPU
-		{B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.ActiveCfg = Release|Any CPU
-		{B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.Build.0 = Release|Any CPU
 		{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -56,7 +39,14 @@ Global
 		{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|Any CPU.Build.0 = Release|Any CPU
 		{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|x86.ActiveCfg = Release|Any CPU
 		{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|x86.Build.0 = Release|Any CPU
-
+		{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|x86.Build.0 = Debug|Any CPU
+		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|Any CPU.Build.0 = Release|Any CPU
+		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|x86.ActiveCfg = Release|Any CPU
+		{88979F89-9A42-448F-AE3E-3060145F6375}.Release|x86.Build.0 = Release|Any CPU
 		{8B901EDE-8974-4820-B100-5226917E2990}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{8B901EDE-8974-4820-B100-5226917E2990}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{8B901EDE-8974-4820-B100-5226917E2990}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -73,6 +63,14 @@ Global
 		{44E15B48-0DB2-4560-82BD-D3B7989811C3}.Release|Any CPU.Build.0 = Release|Any CPU
 		{44E15B48-0DB2-4560-82BD-D3B7989811C3}.Release|x86.ActiveCfg = Release|Any CPU
 		{44E15B48-0DB2-4560-82BD-D3B7989811C3}.Release|x86.Build.0 = Release|Any CPU
+		{B0A602CD-E176-449D-8663-64238D54F857}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B0A602CD-E176-449D-8663-64238D54F857}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B0A602CD-E176-449D-8663-64238D54F857}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{B0A602CD-E176-449D-8663-64238D54F857}.Debug|x86.Build.0 = Debug|Any CPU
+		{B0A602CD-E176-449D-8663-64238D54F857}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B0A602CD-E176-449D-8663-64238D54F857}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.ActiveCfg = Release|Any CPU
+		{B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 183 - 0
UICatalog/Resources/config.json

@@ -0,0 +1,183 @@
+{
+  "$schema": "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
+  "Application.QuitKey": {
+    "Key": "Esc"
+  },
+  "AppSettings": {
+    "UICatalog.StatusBar": true,
+    "ConfigurationEditor.EditorColorScheme": {
+      "Normal": {
+        "Foreground": "Red",
+        "Background": "White"
+      },
+      "Focus": {
+        "Foreground": "Green",
+        "Background": "White"
+      },
+      "HotNormal": {
+        "Foreground": "Black",
+        "Background": "White"
+      },
+      "HotFocus": {
+        "Foreground": "White",
+        "Background": "BrightRed"
+      },
+      "Disabled": {
+        "Foreground": "DarkGray",
+        "Background": "White"
+      }
+    }
+  },
+  "Themes": [
+    {
+      "UI Catalog Theme": {
+        "ColorSchemes": [
+          {
+            "UI Catalog Scheme": {
+              "Normal": {
+                "Foreground": "White",
+                "Background": "Green"
+              },
+              "Focus": {
+                "Foreground": "Green",
+                "Background": "White"
+              },
+              "HotNormal": {
+                "Foreground": "Blue",
+                "Background": "Green"
+              },
+              "HotFocus": {
+                "Foreground": "BrightRed",
+                "Background": "White"
+              },
+              "Disabled": {
+                "Foreground": "BrightGreen",
+                "Background": "Gray"
+              }
+            }
+          },
+          {
+            "TopLevel": {
+              "Normal": {
+                "Foreground": "DarkGray",
+                "Background": "White"
+              },
+              "Focus": {
+                "Foreground": "Black",
+                "Background": "White"
+              },
+              "HotNormal": {
+                "Foreground": "BrightGreen",
+                "Background": "White"
+              },
+              "HotFocus": {
+                "Foreground": "Cyan",
+                "Background": "White"
+              },
+              "Disabled": {
+                "Foreground": "Gray",
+                "Background": "White"
+              }
+            }
+          },
+          {
+            "Base": {
+              "Normal": {
+                "Foreground": "White",
+                "Background": "Green"
+              },
+              "Focus": {
+                "Foreground": "Green",
+                "Background": "White"
+              },
+              "HotNormal": {
+                "Foreground": "Blue",
+                "Background": "Green"
+              },
+              "HotFocus": {
+                "Foreground": "BrightRed",
+                "Background": "White"
+              },
+              "Disabled": {
+                "Foreground": "BrightGreen",
+                "Background": "Gray"
+              }
+            }
+          },
+          {
+            "Dialog": {
+              "Normal": {
+                "Foreground": "Gray",
+                "Background": "Green"
+              },
+              "Focus": {
+                "Foreground": "Green",
+                "Background": "White"
+              },
+              "HotNormal": {
+                "Foreground": "Blue",
+                "Background": "Green"
+              },
+              "HotFocus": {
+                "Foreground": "Black",
+                "Background": "White"
+              },
+              "Disabled": {
+                "Foreground": "BrightGreen",
+                "Background": "Gray"
+              }
+            }
+          },
+          {
+            "Menu": {
+              "Normal": {
+                "Foreground": "Black",
+                "Background": "Gray"
+              },
+              "Focus": {
+                "Foreground": "Green",
+                "Background": "DarkGray"
+              },
+              "HotNormal": {
+                "Foreground": "Green",
+                "Background": "Gray"
+              },
+              "HotFocus": {
+                "Foreground": "DarkGray",
+                "Background": "DarkGray"
+              },
+              "Disabled": {
+                "Foreground": "Gray",
+                "Background": "White"
+              }
+            }
+          },
+          {
+            "Error": {
+              "Normal": {
+                "Foreground": "BrightRed",
+                "Background": "BrightYellow"
+              },
+              "Focus": {
+                "Foreground": "Black",
+                "Background": "BrightYellow"
+              },
+              "HotNormal": {
+                "Foreground": "DarkGray",
+                "Background": "BrightYellow"
+              },
+              "HotFocus": {
+                "Foreground": "Red",
+                "Background": "BrightYellow"
+              },
+              "Disabled": {
+                "Foreground": "BrightGreen",
+                "Background": "Gray"
+              }
+            }
+          }
+        ]
+      }
+    }
+  ]
+}

+ 21 - 2
UICatalog/Scenario.cs

@@ -3,6 +3,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Terminal.Gui;
+using Terminal.Gui.Configuration;
 
 namespace UICatalog {
 	/// <summary>
@@ -14,7 +15,7 @@ namespace UICatalog {
 	///  <item><description>Annotate the <see cref="Scenario"/> derived class with a <see cref="Scenario.ScenarioMetadata"/> attribute specifying the scenario's name and description.</description></item>
 	///  <item><description>Add one or more <see cref="Scenario.ScenarioCategory"/> attributes to the class specifying which categories the scenario belongs to. If you don't specify a category the scenario will show up in "_All".</description></item>
 	///  <item><description>Implement the <see cref="Setup"/> override which will be called when a user selects the scenario to run.</description></item>
-	///  <item><description>Optionally, implement the <see cref="Init(Toplevel, ColorScheme)"/> and/or <see cref="Run"/> overrides to provide a custom implementation.</description></item>
+	///  <item><description>Optionally, implement the <see cref="Init(ColorScheme)"/> and/or <see cref="Run"/> overrides to provide a custom implementation.</description></item>
 	///  </list>
 	/// </para>
 	/// <para>
@@ -71,9 +72,27 @@ namespace UICatalog {
 		/// </remarks>
 		public virtual void Init (ColorScheme colorScheme)
 		{
+			//ConfigurationManager.Applied += (a) => {
+			//	if (Application.Top == null) {
+			//		return;
+			//	}
+
+			//	//// Apply changes that apply to either UICatalogTopLevel or a Scenario
+			//	//if (Application.Top.MenuBar != null) {
+			//	//	Application.Top.MenuBar.ColorScheme = Colors.ColorSchemes ["Menu"];
+			//	//	Application.Top.MenuBar.SetNeedsDisplay ();
+			//	//}
+
+			//	//if (Application.Top.StatusBar != null) {
+			//	//	Application.Top.StatusBar.ColorScheme = Colors.ColorSchemes ["Menu"];
+			//	//	Application.Top.StatusBar.SetNeedsDisplay ();
+			//	//}
+			//	//Application.Top.SetNeedsDisplay ();
+			//};
+			
 			Application.Init ();
 
-			Win = new Window ($"CTRL-Q to Close - Scenario: {GetName ()}") {
+			Win = new Window ($"{Application.QuitKey} to Close - Scenario: {GetName ()}") {
 				X = 0,
 				Y = 0,
 				Width = Dim.Fill (),

+ 223 - 0
UICatalog/Scenarios/ConfigurationEditor.cs

@@ -0,0 +1,223 @@
+using NStack;
+using System;
+using System.Diagnostics.Metrics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using Terminal.Gui;
+using Terminal.Gui.Configuration;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+using Attribute = Terminal.Gui.Attribute;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "Configuration Editor", Description: "Edits Terminal.Gui Config Files.")]
+	[ScenarioCategory ("TabView"), ScenarioCategory ("Colors"), ScenarioCategory ("Files and IO"), ScenarioCategory ("TextView")]
+	public class ConfigurationEditor : Scenario {
+		TileView _tileView;
+		StatusItem _lenStatusItem;
+
+		private static ColorScheme _editorColorScheme = new ColorScheme () {
+			Normal = new Attribute (Color.Red, Color.White),
+			Focus = new Attribute (Color.Red, Color.Black),
+			HotFocus = new Attribute (Color.BrightRed, Color.Black),
+			HotNormal = new Attribute (Color.Magenta, Color.White)
+		};
+
+		[SerializableConfigurationProperty (Scope = typeof (AppScope))]
+		public static ColorScheme EditorColorScheme {
+			get => _editorColorScheme;
+			set {
+				_editorColorScheme = value;
+				_editorColorSchemeChanged?.Invoke ();
+			}
+		}
+
+		private static Action _editorColorSchemeChanged;
+
+		// Don't create a Window, just return the top-level view
+		public override void Init (ColorScheme colorScheme)
+		{
+			Application.Init ();
+			Application.Top.ColorScheme = colorScheme;
+		}
+
+		public override void Setup ()
+		{
+			_tileView = new TileView (0) {
+				Width = Dim.Fill (),
+				Height = Dim.Fill (1),
+				Orientation = Terminal.Gui.Graphs.Orientation.Vertical,
+				Border = new Border () { BorderStyle = BorderStyle.Single }
+			};
+
+			Application.Top.Add (_tileView);
+
+			_lenStatusItem = new StatusItem (Key.CharMask, "Len: ", null);
+			var statusBar = new StatusBar (new StatusItem [] {
+				new StatusItem(Application.QuitKey, $"{Application.QuitKey} Quit", () => Quit()),
+				new StatusItem(Key.F5, "~F5~ Reload", () => Reload()),
+				new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()),
+				_lenStatusItem,
+			});
+
+			Application.Top.Add (statusBar);
+
+			Open ();
+
+			ConfigurationEditor._editorColorSchemeChanged += () => {
+				foreach (var t in _tileView.Tiles) {
+					t.ContentView.ColorScheme = ConfigurationEditor.EditorColorScheme;
+					t.ContentView.SetNeedsDisplay ();
+				};
+			};
+
+			ConfigurationEditor._editorColorSchemeChanged.Invoke ();
+
+		}
+
+		private class ConfigTextView : TextView {
+			internal TileView.Tile Tile { get; set; }
+			internal FileInfo FileInfo { get; set; }
+
+			internal ConfigTextView ()
+			{
+				ContentsChanged += (obj) => {
+					if (IsDirty) {
+						if (!Tile.Title.EndsWith ('*')) {
+							Tile.Title += '*';
+						} else {
+							Tile.Title = Tile.Title.TrimEnd ('*');
+						}
+					}
+
+				};
+			}
+
+			internal void Read ()
+			{
+				Assembly assembly = null;
+				if (FileInfo.FullName.Contains ("[Terminal.Gui]")) {
+					// Library resources
+					assembly = typeof (ConfigurationManager).Assembly;
+				} else if (FileInfo.FullName.Contains ("[UICatalog]")) {
+					assembly = Assembly.GetEntryAssembly ();
+				}
+				if (assembly != null) {
+					string name = assembly
+						.GetManifestResourceNames ()
+						.FirstOrDefault (x => x.EndsWith ("config.json"));
+					using Stream stream = assembly.GetManifestResourceStream (name);
+					using StreamReader reader = new StreamReader (stream);
+					Text = reader.ReadToEnd ();
+					ReadOnly = true;
+					Enabled = true;
+					return;
+				}
+
+				if (!FileInfo.Exists) {
+					// Create empty config file
+					Text = ConfigurationManager.GetEmptyJson ();
+				} else {
+					Text = File.ReadAllText (FileInfo.FullName);
+				}
+				Tile.Title = Tile.Title.TrimEnd ('*');
+			}
+
+			internal void Save ()
+			{
+				if (!Directory.Exists (FileInfo.DirectoryName)) {
+					// Create dir
+					Directory.CreateDirectory (FileInfo.DirectoryName!);
+				}
+				using var writer = File.CreateText (FileInfo.FullName);
+				writer.Write (Text.ToString ());
+				writer.Close ();
+				Tile.Title = Tile.Title.TrimEnd ('*');
+				//IsDirty = false;
+			}
+		}
+
+		private void Open ()
+		{
+			var subMenu = new MenuBarItem () {
+				Title = "_View",
+			};
+
+			foreach (var configFile in ConfigurationManager.Settings.Sources) {
+
+				var homeDir = $"{Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)}";
+				FileInfo fileInfo = new FileInfo (configFile.Replace ("~", homeDir));
+
+				var tile = _tileView.InsertTile (_tileView.Tiles.Count);
+				tile.Title = configFile.StartsWith ("resource://") ? fileInfo.Name : configFile;
+
+				var textView = new ConfigTextView () {
+					X = 0,
+					Y = 0,
+					Width = Dim.Fill (),
+					Height = Dim.Fill (),
+					FileInfo = fileInfo,
+					Tile = tile
+				};
+
+				tile.ContentView.Add (textView);
+
+				textView.Read ();
+
+				textView.Enter += (a) => {
+					_lenStatusItem.Title = $"Len:{textView.Text.Length}";
+				};
+
+				//var mi = new MenuItem () {
+				//	Title = tile.Title,
+				//	CheckType = MenuItemCheckStyle.Checked,
+				//	Checked = true,
+				//};
+				//mi.Action += () => {
+				//	mi.Checked =! mi.Checked;
+				//	_tileView.SetNeedsDisplay ();
+				//};
+
+				//subMenu.Children = subMenu.Children.Append (mi).ToArray ();
+			}
+
+			//var menu = new MenuBar (new MenuBarItem [] { subMenu });
+			//Application.Top.Add (menu);
+
+		}
+
+		private void Reload ()
+		{
+			if (_tileView.MostFocused is ConfigTextView editor) {
+				editor.Read ();
+			}
+		}
+
+		public void Save ()
+		{
+			if (_tileView.MostFocused is ConfigTextView editor) {
+				editor.Save ();
+			}
+		}
+
+		private void Quit ()
+		{
+			foreach (var tile in _tileView.Tiles) {
+				ConfigTextView editor = tile.ContentView.Subviews [0] as ConfigTextView;
+				if (editor.IsDirty) {
+					int result = MessageBox.Query ("Save Changes", $"Save changes to {editor.FileInfo.FullName}", "Yes", "No", "Cancel");
+					if (result == -1 || result == 2) {
+						// user cancelled
+					}
+					if (result == 0) {
+						editor.Save ();
+					}
+				}
+
+			}
+
+			Application.RequestStop ();
+		}
+	}
+}

+ 224 - 66
UICatalog/UICatalog.cs

@@ -7,8 +7,12 @@ using System.Linq;
 using System.Runtime.InteropServices;
 using System.Text;
 using Terminal.Gui;
-using Microsoft.DotNet.PlatformAbstractions;
-using Rune = System.Rune;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+using Terminal.Gui.Configuration;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+using System.Text.Json.Serialization;
 
 /// <summary>
 /// UI Catalog is a comprehensive sample library for Terminal.Gui. It provides a simple UI for adding to the catalog of scenarios.
@@ -45,6 +49,15 @@ namespace UICatalog {
 	/// UI Catalog is a comprehensive sample app and scenario library for <see cref="Terminal.Gui"/>
 	/// </summary>
 	class UICatalogApp {
+		//[SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true), JsonPropertyName ("UICatalog.StatusBar")]
+		//public static bool ShowStatusBar { get; set; } = true;
+
+		[SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true), JsonPropertyName("UICatalog.StatusBar")]
+		public static bool ShowStatusBar { get; set; } = true;
+
+		static readonly FileSystemWatcher _currentDirWatcher = new FileSystemWatcher ();
+		static readonly FileSystemWatcher _homeDirWatcher = new FileSystemWatcher ();
+
 		static void Main (string [] args)
 		{
 			Console.OutputEncoding = Encoding.Default;
@@ -62,6 +75,8 @@ namespace UICatalog {
 				args = args.Where (val => val != "-usc").ToArray ();
 			}
 
+			StartConfigFileWatcher ();
+
 			// If a Scenario name has been provided on the commandline
 			// run it and exit when done.
 			if (args.Length > 0) {
@@ -69,7 +84,7 @@ namespace UICatalog {
 				_selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ());
 				Application.UseSystemConsole = _useSystemConsole;
 				Application.Init ();
-				_selectedScenario.Init (_colorScheme);
+				_selectedScenario.Init (Colors.ColorSchemes [_topLevelColorScheme]);
 				_selectedScenario.Setup ();
 				_selectedScenario.Run ();
 				_selectedScenario = null;
@@ -92,7 +107,7 @@ namespace UICatalog {
 			Scenario scenario;
 			while ((scenario = RunUICatalogTopLevel ()) != null) {
 				VerifyObjectsWereDisposed ();
-				scenario.Init (_colorScheme);
+				scenario.Init (Colors.ColorSchemes [_topLevelColorScheme]);
 				scenario.Setup ();
 				scenario.Run ();
 
@@ -102,9 +117,66 @@ namespace UICatalog {
 
 				VerifyObjectsWereDisposed ();
 			}
+
+			StopConfigFileWatcher ();
 			VerifyObjectsWereDisposed ();
 		}
 
+		private static void StopConfigFileWatcher() {
+			_currentDirWatcher.EnableRaisingEvents = false;
+			_currentDirWatcher.Changed -= ConfigFileChanged;
+			_currentDirWatcher.Created -= ConfigFileChanged;
+
+			_homeDirWatcher.EnableRaisingEvents = false;
+			_homeDirWatcher.Changed -= ConfigFileChanged;
+			_homeDirWatcher.Created -= ConfigFileChanged;
+		}
+
+		private static void StartConfigFileWatcher()
+		{
+			// Setup a file system watcher for `./.tui/`
+			_currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
+			var f = new FileInfo (Assembly.GetExecutingAssembly ().Location);
+			var tuiDir = Path.Combine (f.Directory.FullName, ".tui");
+
+			if (!Directory.Exists (tuiDir)) {
+				Directory.CreateDirectory (tuiDir);
+			}
+			_currentDirWatcher.Path = tuiDir;
+			_currentDirWatcher.Filter = "*config.json";
+
+			// Setup a file system watcher for `~/.tui/`
+			_homeDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
+			f = new FileInfo (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
+			tuiDir = Path.Combine (f.FullName, ".tui");
+
+			if (!Directory.Exists (tuiDir)) {
+				Directory.CreateDirectory (tuiDir);
+			}
+			_homeDirWatcher.Path = tuiDir;
+			_homeDirWatcher.Filter = "*config.json";
+
+			_currentDirWatcher.Changed += ConfigFileChanged;
+			//_currentDirWatcher.Created += ConfigFileChanged;
+			_currentDirWatcher.EnableRaisingEvents = true;
+
+			_homeDirWatcher.Changed += ConfigFileChanged;
+			//_homeDirWatcher.Created += ConfigFileChanged;
+			_homeDirWatcher.EnableRaisingEvents = true;
+		}
+
+		private static void ConfigFileChanged (object sender, FileSystemEventArgs e)
+		{
+			if (Application.Top == null) {
+				return;
+			}
+
+			// TOOD: THis is a hack. Figure out how to ensure that the file is fully written before reading it.
+			Thread.Sleep (500);
+			ConfigurationManager.Load ();
+			ConfigurationManager.Apply ();
+		}
+
 		/// <summary>
 		/// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the
 		/// UI Catalog main app UI is killed and the Scenario is run as though it were Application.Top. 
@@ -139,15 +211,17 @@ namespace UICatalog {
 
 		static bool _useSystemConsole = false;
 		static ConsoleDriver.DiagnosticFlags _diagnosticFlags;
-		static bool _heightAsBuffer = false;
 		static bool _isFirstRunning = true;
-		static ColorScheme _colorScheme;
+		static string _topLevelColorScheme;
+
+		static MenuItem [] _themeMenuItems;
+		static MenuBarItem _themeMenuBarItem;
 
 		/// <summary>
 		/// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on 
 		/// the command line) and each time a Scenario ends.
 		/// </summary>
-		class UICatalogTopLevel : Toplevel {
+		public class UICatalogTopLevel : Toplevel {
 			public MenuItem miIsMouseDisabled;
 			public MenuItem miHeightAsBuffer;
 
@@ -163,12 +237,13 @@ namespace UICatalog {
 
 			public UICatalogTopLevel ()
 			{
-				ColorScheme = _colorScheme = Colors.Base;
+				_themeMenuItems = CreateThemeMenuItems ();
+				_themeMenuBarItem = new MenuBarItem ("_Themes", _themeMenuItems);
 				MenuBar = new MenuBar (new MenuBarItem [] {
 					new MenuBarItem ("_File", new MenuItem [] {
-						new MenuItem ("_Quit", "Quit UI Catalog", () => RequestStop(), null, null, Key.Q | Key.CtrlMask)
+						new MenuItem ("_Quit", "Quit UI Catalog", () => RequestStop(), null, null)
 					}),
-					new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()),
+					_themeMenuBarItem,
 					new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems()),
 					new MenuBarItem ("_Help", new MenuItem [] {
 						new MenuItem ("_gui.cs API Overview", "", () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui/articles/overview.html"), null, null, Key.F1),
@@ -185,10 +260,11 @@ namespace UICatalog {
 				OS = new StatusItem (Key.CharMask, "OS:", null);
 
 				StatusBar = new StatusBar () {
-					Visible = true,
+					Visible = UICatalogApp.ShowStatusBar					
 				};
+
 				StatusBar.Items = new StatusItem [] {
-					new StatusItem(Key.Q | Key.CtrlMask, "~CTRL-Q~ Quit", () => {
+					new StatusItem(Application.QuitKey, $"~{Application.QuitKey} to quit", () => {
 						if (_selectedScenario is null){
 							// This causes GetScenarioToRun to return null
 							_selectedScenario = null;
@@ -232,7 +308,7 @@ namespace UICatalog {
 				};
 				CategoryListView.SelectedItemChanged += CategoryListView_SelectedChanged;
 
-				ContentPane.Tiles.ElementAt(0).Title = "Categories";
+				ContentPane.Tiles.ElementAt (0).Title = "Categories";
 				ContentPane.Tiles.ElementAt (0).MinSize = 2;
 				ContentPane.Tiles.ElementAt (0).ContentView.Add (CategoryListView);
 
@@ -258,22 +334,19 @@ namespace UICatalog {
 				Add (StatusBar);
 
 				Loaded += LoadedHandler;
+				Unloaded += UnloadedHandler;
 
 				// Restore previous selections
 				CategoryListView.SelectedItem = _cachedCategoryIndex;
 				ScenarioListView.SelectedItem = _cachedScenarioIndex;
+
+				ConfigurationManager.Applied += ConfigAppliedHandler;
 			}
  
 			void LoadedHandler ()
 			{
-				Application.HeightAsBuffer = _heightAsBuffer;
-
-				if (_colorScheme == null) {
-					ColorScheme = _colorScheme = Colors.Base;
-				}
+				ConfigChanged ();
 
-				miIsMouseDisabled.Checked = Application.IsMouseDisabled;
-				miHeightAsBuffer.Checked = Application.HeightAsBuffer;
 				DriverName.Title = $"Driver: {Driver.GetType ().Name}";
 				OS.Title = $"OS: {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystem} {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystemVersion}";
 
@@ -284,9 +357,30 @@ namespace UICatalog {
 				if (!_isFirstRunning) {
 					ScenarioListView.SetFocus ();
 				}
+
+				StatusBar.VisibleChanged += () => {
+					UICatalogApp.ShowStatusBar = StatusBar.Visible;
+
+					var height = (StatusBar.Visible ? 1 : 0);// + (MenuBar.Visible ? 1 : 0);
+					ContentPane.Height = Dim.Fill (height);
+					LayoutSubviews ();
+					SetChildNeedsDisplay ();
+				};
+
 				Loaded -= LoadedHandler;
 			}
 
+			private void UnloadedHandler ()
+			{
+				ConfigurationManager.Applied -= ConfigAppliedHandler;
+				Unloaded -= UnloadedHandler;
+			}
+			
+			void ConfigAppliedHandler (ConfigurationManagerEventArgs a)
+			{
+				ConfigChanged ();
+			}
+
 			/// <summary>
 			/// Launches the selected scenario, setting the global _selectedScenario
 			/// </summary>
@@ -307,20 +401,22 @@ namespace UICatalog {
 
 			List<MenuItem []> CreateDiagnosticMenuItems ()
 			{
-				List<MenuItem []> menuItems = new List<MenuItem []> ();
-				menuItems.Add (CreateDiagnosticFlagsMenuItems ());
-				menuItems.Add (new MenuItem [] { null });
-				menuItems.Add (CreateHeightAsBufferMenuItems ());
-				menuItems.Add (CreateDisabledEnabledMouseItems ());
-				menuItems.Add (CreateKeybindingsMenuItems ());
+				List<MenuItem []> menuItems = new List<MenuItem []> {
+					CreateDiagnosticFlagsMenuItems (),
+					new MenuItem [] { null },
+					CreateHeightAsBufferMenuItems (),
+					CreateDisabledEnabledMouseItems (),
+					CreateKeybindingsMenuItems ()
+				};
 				return menuItems;
 			}
 
 			MenuItem [] CreateDisabledEnabledMouseItems ()
 			{
 				List<MenuItem> menuItems = new List<MenuItem> ();
-				miIsMouseDisabled = new MenuItem ();
-				miIsMouseDisabled.Title = "_Disable Mouse";
+				miIsMouseDisabled = new MenuItem {
+					Title = "_Disable Mouse"
+				};
 				miIsMouseDisabled.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miIsMouseDisabled.Title.ToString ().Substring (1, 1) [0];
 				miIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked;
 				miIsMouseDisabled.Action += () => {
@@ -334,9 +430,10 @@ namespace UICatalog {
 			MenuItem [] CreateKeybindingsMenuItems ()
 			{
 				List<MenuItem> menuItems = new List<MenuItem> ();
-				var item = new MenuItem ();
-				item.Title = "_Key Bindings";
-				item.Help = "Change which keys do what";
+				var item = new MenuItem {
+					Title = "_Key Bindings",
+					Help = "Change which keys do what"
+				};
 				item.Action += () => {
 					var dlg = new KeyBindingsDialog ();
 					Application.Run (dlg);
@@ -351,8 +448,9 @@ namespace UICatalog {
 			MenuItem [] CreateHeightAsBufferMenuItems ()
 			{
 				List<MenuItem> menuItems = new List<MenuItem> ();
-				miHeightAsBuffer = new MenuItem ();
-				miHeightAsBuffer.Title = "_Height As Buffer";
+				miHeightAsBuffer = new MenuItem {
+					Title = "_Height As Buffer"
+				};
 				miHeightAsBuffer.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miHeightAsBuffer.Title.ToString ().Substring (1, 1) [0];
 				miHeightAsBuffer.CheckType |= MenuItemCheckStyle.Checked;
 				miHeightAsBuffer.Action += () => {
@@ -373,9 +471,10 @@ namespace UICatalog {
 
 				List<MenuItem> menuItems = new List<MenuItem> ();
 				foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) {
-					var item = new MenuItem ();
-					item.Title = GetDiagnosticsTitle (diag);
-					item.Shortcut = Key.AltMask + index.ToString () [0];
+					var item = new MenuItem {
+						Title = GetDiagnosticsTitle (diag),
+						Shortcut = Key.AltMask + index.ToString () [0]
+					};
 					index++;
 					item.CheckType |= MenuItemCheckStyle.Checked;
 					if (GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off) == item.Title) {
@@ -417,26 +516,21 @@ namespace UICatalog {
 
 				string GetDiagnosticsTitle (Enum diag)
 				{
-					switch (Enum.GetName (_diagnosticFlags.GetType (), diag)) {
-					case "Off":
-						return OFF;
-					case "FrameRuler":
-						return FRAME_RULER;
-					case "FramePadding":
-						return FRAME_PADDING;
-					}
-					return "";
+					return Enum.GetName (_diagnosticFlags.GetType (), diag) switch {
+						"Off" => OFF,
+						"FrameRuler" => FRAME_RULER,
+						"FramePadding" => FRAME_PADDING,
+						_ => "",
+					};
 				}
 
 				Enum GetDiagnosticsEnumValue (ustring title)
 				{
-					switch (title.ToString ()) {
-					case FRAME_RULER:
-						return ConsoleDriver.DiagnosticFlags.FrameRuler;
-					case FRAME_PADDING:
-						return ConsoleDriver.DiagnosticFlags.FramePadding;
-					}
-					return null;
+					return title.ToString () switch {
+						FRAME_RULER => ConsoleDriver.DiagnosticFlags.FrameRuler,
+						FRAME_PADDING => ConsoleDriver.DiagnosticFlags.FramePadding,
+						_ => null,
+					};
 				}
 
 				void SetDiagnosticsFlag (Enum diag, bool add)
@@ -463,27 +557,91 @@ namespace UICatalog {
 				}
 			}
 
-			MenuItem [] CreateColorSchemeMenuItems ()
+			public MenuItem [] CreateThemeMenuItems ()
 			{
 				List<MenuItem> menuItems = new List<MenuItem> ();
+				foreach (var theme in ConfigurationManager.Themes) {
+					var item = new MenuItem {
+						Title = theme.Key,
+						Shortcut = Key.AltMask + theme.Key [0]
+					};
+					item.CheckType |= MenuItemCheckStyle.Checked;
+					item.Checked = theme.Key == ConfigurationManager.Themes.Theme;
+					item.Action += () => {
+						ConfigurationManager.Themes.Theme = theme.Key;
+						ConfigurationManager.Apply ();
+					};
+					menuItems.Add (item);
+				}
+
+				var schemeMenuItems = new List<MenuItem> ();
 				foreach (var sc in Colors.ColorSchemes) {
-					var item = new MenuItem ();
-					item.Title = $"_{sc.Key}";
-					item.Shortcut = Key.AltMask | (Key)sc.Key.Substring (0, 1) [0];
+					var item = new MenuItem {
+						Title = $"_{sc.Key}",
+						Data = sc.Key,
+						Shortcut = Key.AltMask | (Key)sc.Key [..1] [0]
+					};
 					item.CheckType |= MenuItemCheckStyle.Radio;
-					item.Checked = sc.Value == _colorScheme;
+					item.Checked = sc.Key == _topLevelColorScheme;
 					item.Action += () => {
-						ColorScheme = _colorScheme = sc.Value;
-						SetNeedsDisplay ();
-						foreach (var menuItem in menuItems) {
-							menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _colorScheme;
+						_topLevelColorScheme = (string)item.Data;
+						foreach (var schemeMenuItem in schemeMenuItems) {
+							schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme;
 						}
+						ColorScheme = Colors.ColorSchemes [_topLevelColorScheme];
+						Application.Top.SetNeedsDisplay ();
 					};
-					menuItems.Add (item);
+					schemeMenuItems.Add (item);
 				}
+				menuItems.Add (null);
+				var mbi = new MenuBarItem ("_Color Scheme for Application.Top", schemeMenuItems.ToArray ());
+				menuItems.Add (mbi);
+
 				return menuItems.ToArray ();
 			}
 
+			public void ConfigChanged ()
+			{
+				if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) {
+					_topLevelColorScheme = "Base";
+				}
+				
+				_themeMenuItems = ((UICatalogTopLevel)Application.Top).CreateThemeMenuItems ();
+				_themeMenuBarItem.Children = _themeMenuItems;
+
+				var checkedThemeMenu = _themeMenuItems.Where (m => (bool)m.Checked).FirstOrDefault ();
+				if (checkedThemeMenu != null) {
+					checkedThemeMenu.Checked = false;
+				}
+				checkedThemeMenu = _themeMenuItems.Where (m => m != null && m.Title == ConfigurationManager.Themes.Theme).FirstOrDefault ();
+				if (checkedThemeMenu != null) {
+					ConfigurationManager.Themes.Theme = checkedThemeMenu.Title.ToString ();
+					checkedThemeMenu.Checked = true;
+				}
+				var schemeMenuItems = ((MenuBarItem)_themeMenuItems.Where (i => i is MenuBarItem).FirstOrDefault ()).Children;
+				foreach (var schemeMenuItem in schemeMenuItems) {
+					schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme;
+				}
+
+				ColorScheme = Colors.ColorSchemes [_topLevelColorScheme];
+
+				ContentPane.Border.BorderStyle = FrameView.DefaultBorderStyle;
+
+				MenuBar.Menus [0].Children [0].Shortcut = Application.QuitKey;
+				StatusBar.Items [0].Shortcut = Application.QuitKey;
+				StatusBar.Items [0].Title = $"~{Application.QuitKey} to quit";
+
+				miIsMouseDisabled.Checked = Application.IsMouseDisabled;
+				miHeightAsBuffer.Checked = Application.HeightAsBuffer;
+
+				var height = (UICatalogApp.ShowStatusBar ? 1 : 0);// + (MenuBar.Visible ? 1 : 0);
+				ContentPane.Height = Dim.Fill (height);
+
+				StatusBar.Visible = UICatalogApp.ShowStatusBar;
+
+				Application.Top.SetNeedsDisplay ();
+			}
+
 			void KeyDownHandler (View.KeyEventEventArgs a)
 			{
 				if (a.KeyEvent.IsCapslock) {
@@ -533,6 +691,7 @@ namespace UICatalog {
 			// after a scenario was selected to run. This proves the main UI Catalog
 			// 'app' closed cleanly.
 			foreach (var inst in Responder.Instances) {
+				
 				Debug.Assert (inst.WasDisposed);
 			}
 			Responder.Instances.Clear ();
@@ -554,7 +713,7 @@ namespace UICatalog {
 					url = url.Replace ("&", "^&");
 					Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true });
 				} else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) {
-					using (var process = new Process {
+					using var process = new Process {
 						StartInfo = new ProcessStartInfo {
 							FileName = "xdg-open",
 							Arguments = url,
@@ -563,9 +722,8 @@ namespace UICatalog {
 							CreateNoWindow = true,
 							UseShellExecute = false
 						}
-					}) {
-						process.Start ();
-					}
+					};
+					process.Start ();
 				} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
 					Process.Start ("open", url);
 				}

+ 6 - 0
UICatalog/UICatalog.csproj

@@ -18,6 +18,12 @@
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
   </PropertyGroup>
+  <ItemGroup>
+    <None Remove="Resources\config.json" />
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Resources\config.json" />
+  </ItemGroup>
   <ItemGroup>
   <None Update="./Scenarios/Spinning_globe_dark_small.gif" CopyToOutputDirectory="PreserveNewest" />
   </ItemGroup>

+ 2 - 0
UnitTests/Application/ApplicationTests.cs

@@ -24,6 +24,8 @@ namespace Terminal.Gui.ApplicationTests {
 			Assert.Null (Application.Driver);
 			Assert.Null (Application.Top);
 			Assert.Null (Application.Current);
+			// removed below as HeightAsBuffer now works without a driver loaded
+			//Assert.Throws<ArgumentNullException> (() => Application.HeightAsBuffer == true);
 			Assert.Null (Application.MainLoop);
 			Assert.Null (Application.Iteration);
 			Assert.Null (Application.RootMouseEvent);

+ 12 - 16
UnitTests/Application/StackExtensionsTests.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using Terminal.Gui;
 using Xunit;
 
 namespace Terminal.Gui.ApplicationTests {
@@ -11,11 +12,8 @@ namespace Terminal.Gui.ApplicationTests {
 
 			int index = toplevels.Count - 1;
 			foreach (var top in toplevels) {
-				if (top.GetType () == typeof (Toplevel)) {
-					Assert.Equal ("Top", top.Id);
-				} else {
-					Assert.Equal ($"w{index}", top.Id);
-				}
+				if (top.GetType () == typeof (Toplevel)) 					Assert.Equal ("Top", top.Id);
+else 					Assert.Equal ($"w{index}", top.Id);
 				index--;
 			}
 
@@ -35,7 +33,7 @@ namespace Terminal.Gui.ApplicationTests {
 
 			var valueToReplace = new Window () { Id = "w1" };
 			var valueToReplaceWith = new Window () { Id = "new" };
-			ToplevelEqualityComparer comparer = new ToplevelEqualityComparer ();
+			var comparer = new ToplevelEqualityComparer ();
 
 			toplevels.Replace (valueToReplace, valueToReplaceWith, comparer);
 
@@ -55,7 +53,7 @@ namespace Terminal.Gui.ApplicationTests {
 
 			var valueToSwapFrom = new Window () { Id = "w3" };
 			var valueToSwapTo = new Window () { Id = "w1" };
-			ToplevelEqualityComparer comparer = new ToplevelEqualityComparer ();
+			var comparer = new ToplevelEqualityComparer ();
 			toplevels.Swap (valueToSwapFrom, valueToSwapTo, comparer);
 
 			var tops = toplevels.ToArray ();
@@ -105,18 +103,16 @@ namespace Terminal.Gui.ApplicationTests {
 			Stack<Toplevel> toplevels = CreateToplevels ();
 
 			// Only allows unique keys
-			HashSet<int> hCodes = new HashSet<int> ();
+			var hCodes = new HashSet<int> ();
 
-			foreach (var top in toplevels) {
-				Assert.True (hCodes.Add (top.GetHashCode ()));
-			}
+			foreach (var top in toplevels) 				Assert.True (hCodes.Add (top.GetHashCode ()));
 		}
 
 		[Fact]
 		public void Stack_Toplevels_FindDuplicates ()
 		{
 			Stack<Toplevel> toplevels = CreateToplevels ();
-			ToplevelEqualityComparer comparer = new ToplevelEqualityComparer ();
+			var comparer = new ToplevelEqualityComparer ();
 
 			toplevels.Push (new Toplevel () { Id = "w4" });
 			toplevels.Push (new Toplevel () { Id = "w1" });
@@ -131,7 +127,7 @@ namespace Terminal.Gui.ApplicationTests {
 		public void Stack_Toplevels_Contains ()
 		{
 			Stack<Toplevel> toplevels = CreateToplevels ();
-			ToplevelEqualityComparer comparer = new ToplevelEqualityComparer ();
+			var comparer = new ToplevelEqualityComparer ();
 
 			Assert.True (toplevels.Contains (new Window () { Id = "w2" }, comparer));
 			Assert.False (toplevels.Contains (new Toplevel () { Id = "top2" }, comparer));
@@ -143,7 +139,7 @@ namespace Terminal.Gui.ApplicationTests {
 			Stack<Toplevel> toplevels = CreateToplevels ();
 
 			var valueToMove = new Window () { Id = "w1" };
-			ToplevelEqualityComparer comparer = new ToplevelEqualityComparer ();
+			var comparer = new ToplevelEqualityComparer ();
 
 			toplevels.MoveTo (valueToMove, 1, comparer);
 
@@ -162,7 +158,7 @@ namespace Terminal.Gui.ApplicationTests {
 			Stack<Toplevel> toplevels = CreateToplevels ();
 
 			var valueToMove = new Window () { Id = "Top" };
-			ToplevelEqualityComparer comparer = new ToplevelEqualityComparer ();
+			var comparer = new ToplevelEqualityComparer ();
 
 			toplevels.MoveTo (valueToMove, 0, comparer);
 
@@ -178,7 +174,7 @@ namespace Terminal.Gui.ApplicationTests {
 
 		private Stack<Toplevel> CreateToplevels ()
 		{
-			Stack<Toplevel> toplevels = new Stack<Toplevel> ();
+			var toplevels = new Stack<Toplevel> ();
 
 			toplevels.Push (new Toplevel () { Id = "Top" });
 			toplevels.Push (new Window () { Id = "w1" });

+ 1 - 1
UnitTests/Drivers/AttributeTests.cs → UnitTests/Colors/AttributeTests.cs

@@ -7,7 +7,7 @@ using Xunit;
 // Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
 
-namespace Terminal.Gui.DriverTests {
+namespace Terminal.Gui.ColorTests {
 	public class AttributeTests {
 		[Fact]
 		public void Constuctors_Constuct ()

+ 89 - 0
UnitTests/Configuration/AppScopeTests.cs

@@ -0,0 +1,89 @@
+using Xunit;
+using Terminal.Gui.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Text.Json;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+
+namespace Terminal.Gui.ConfigurationTests {
+	public class AppScopeTests {
+		public static readonly JsonSerializerOptions _jsonOptions = new () {
+			Converters = {
+				//new AttributeJsonConverter (),
+				//new ColorJsonConverter ()
+				}
+		};
+
+		public class AppSettingsTestClass {
+			[SerializableConfigurationProperty (Scope = typeof (AppScope))]
+			public static bool? TestProperty { get; set; } = null;
+		}
+
+		[Fact]
+		public void TestNullable ()
+		{
+			AppSettingsTestClass.TestProperty = null;
+			Assert.Null (AppSettingsTestClass.TestProperty);
+
+			ConfigurationManager.Initialize ();
+			ConfigurationManager.GetHardCodedDefaults ();
+			ConfigurationManager.Apply ();
+			Assert.Null (AppSettingsTestClass.TestProperty);
+
+			AppSettingsTestClass.TestProperty = true;
+			ConfigurationManager.Initialize ();
+			ConfigurationManager.GetHardCodedDefaults ();
+			Assert.NotNull (AppSettingsTestClass.TestProperty);
+			ConfigurationManager.Apply ();
+			Assert.NotNull (AppSettingsTestClass.TestProperty);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Apply_ShouldApplyUpdatedProperties ()
+		{
+			ConfigurationManager.Reset ();
+			Assert.Null (AppSettingsTestClass.TestProperty);
+			Assert.NotEmpty (ConfigurationManager.AppSettings);
+			Assert.Null (ConfigurationManager.AppSettings ["AppSettingsTestClass.TestProperty"].PropertyValue);
+			
+			AppSettingsTestClass.TestProperty = true;
+			ConfigurationManager.Reset ();
+			Assert.True (AppSettingsTestClass.TestProperty);
+			Assert.NotEmpty (ConfigurationManager.AppSettings);
+			Assert.Null (ConfigurationManager.AppSettings ["AppSettingsTestClass.TestProperty"].PropertyValue as bool?);
+			
+			ConfigurationManager.AppSettings ["AppSettingsTestClass.TestProperty"].PropertyValue = false;
+			Assert.False (ConfigurationManager.AppSettings ["AppSettingsTestClass.TestProperty"].PropertyValue as bool?);
+			
+			// ConfigurationManager.Settings should NOT apply theme settings
+			ConfigurationManager.Settings.Apply ();
+			Assert.True (AppSettingsTestClass.TestProperty);
+
+			// ConfigurationManager.Themes should NOT apply theme settings
+			ConfigurationManager.ThemeManager.Themes! [ThemeManager.SelectedTheme]!.Apply ();
+			Assert.True (AppSettingsTestClass.TestProperty);
+
+			// ConfigurationManager.AppSettings should NOT apply theme settings
+			ConfigurationManager.AppSettings.Apply ();
+			Assert.False (AppSettingsTestClass.TestProperty);
+
+		}
+
+		[Fact]
+		public void TestSerialize_RoundTrip ()
+		{
+			ConfigurationManager.Reset ();
+
+			var initial = ConfigurationManager.AppSettings;
+
+			var serialized = JsonSerializer.Serialize<AppScope> (ConfigurationManager.AppSettings, _jsonOptions);
+			var deserialized = JsonSerializer.Deserialize<AppScope> (serialized, _jsonOptions);
+
+			Assert.NotEqual (initial, deserialized);
+			Assert.Equal (deserialized.Count, initial.Count);
+		}
+	}
+}

+ 833 - 0
UnitTests/Configuration/ConfigurationMangerTests.cs

@@ -0,0 +1,833 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Text.Json;
+using Terminal.Gui.Configuration;
+using Xunit;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+
+namespace Terminal.Gui.ConfigurationTests {
+	public class ConfigurationManagerTests {
+
+		public static readonly JsonSerializerOptions _jsonOptions = new() {
+			Converters = {
+				new AttributeJsonConverter (),
+				new ColorJsonConverter (),
+				}
+		};
+
+		[Fact ()]
+		public void DeepMemberwiseCopyTest ()
+		{
+			// Value types
+			var stringDest = "Destination";
+			var stringSrc = "Source";
+			var stringCopy = DeepMemberwiseCopy (stringSrc, stringDest);
+			Assert.Equal (stringSrc, stringCopy);
+
+			stringDest = "Destination";
+			stringSrc = "Destination";
+			stringCopy = DeepMemberwiseCopy (stringSrc, stringDest);
+			Assert.Equal (stringSrc, stringCopy);
+
+			stringDest = "Destination";
+			stringSrc = null;
+			stringCopy = DeepMemberwiseCopy (stringSrc, stringDest);
+			Assert.Equal (stringSrc, stringCopy);
+
+			stringDest = "Destination";
+			stringSrc = string.Empty;
+			stringCopy = DeepMemberwiseCopy (stringSrc, stringDest);
+			Assert.Equal (stringSrc, stringCopy);
+
+			var boolDest = true;
+			var boolSrc = false;
+			var boolCopy = DeepMemberwiseCopy (boolSrc, boolDest);
+			Assert.Equal (boolSrc, boolCopy);
+
+			boolDest = false;
+			boolSrc = true;
+			boolCopy = DeepMemberwiseCopy (boolSrc, boolDest);
+			Assert.Equal (boolSrc, boolCopy);
+
+			boolDest = true;
+			boolSrc = true;
+			boolCopy = DeepMemberwiseCopy (boolSrc, boolDest);
+			Assert.Equal (boolSrc, boolCopy);
+
+			boolDest = false;
+			boolSrc = false;
+			boolCopy = DeepMemberwiseCopy (boolSrc, boolDest);
+			Assert.Equal (boolSrc, boolCopy);
+
+			// Structs
+			var attrDest = new Attribute (1);
+			var attrSrc = new Attribute (2);
+			var attrCopy = DeepMemberwiseCopy (attrSrc, attrDest);
+			Assert.Equal (attrSrc, attrCopy);
+
+			// Classes
+			var colorschemeDest = new ColorScheme () { Disabled = new Attribute (1) };
+			var colorschemeSrc = new ColorScheme () { Disabled = new Attribute (2) };
+			var colorschemeCopy = DeepMemberwiseCopy (colorschemeSrc, colorschemeDest);
+			Assert.Equal (colorschemeSrc, colorschemeCopy);
+
+			// Dictionaries
+			var dictDest = new Dictionary<string, Attribute> () { { "Disabled", new Attribute (1) } };
+			var dictSrc = new Dictionary<string, Attribute> () { { "Disabled", new Attribute (2) } };
+			var dictCopy = (Dictionary<string, Attribute>)DeepMemberwiseCopy (dictSrc, dictDest);
+			Assert.Equal (dictSrc, dictCopy);
+
+			dictDest = new Dictionary<string, Attribute> () { { "Disabled", new Attribute (1) } };
+			dictSrc = new Dictionary<string, Attribute> () { { "Disabled", new Attribute (2) }, { "Normal", new Attribute (3) } };
+			dictCopy = (Dictionary<string, Attribute>)DeepMemberwiseCopy (dictSrc, dictDest);
+			Assert.Equal (dictSrc, dictCopy);
+
+			// src adds an item
+			dictDest = new Dictionary<string, Attribute> () { { "Disabled", new Attribute (1) } };
+			dictSrc = new Dictionary<string, Attribute> () { { "Disabled", new Attribute (2) }, { "Normal", new Attribute (3) } };
+			dictCopy = (Dictionary<string, Attribute>)DeepMemberwiseCopy (dictSrc, dictDest);
+			Assert.Equal (2, dictCopy.Count);
+			Assert.Equal (dictSrc ["Disabled"], dictCopy ["Disabled"]);
+			Assert.Equal (dictSrc ["Normal"], dictCopy ["Normal"]);
+
+			// src updates only one item
+			dictDest = new Dictionary<string, Attribute> () { { "Disabled", new Attribute (1) }, { "Normal", new Attribute (2) } };
+			dictSrc = new Dictionary<string, Attribute> () { { "Disabled", new Attribute (3) } };
+			dictCopy = (Dictionary<string, Attribute>)DeepMemberwiseCopy (dictSrc, dictDest);
+			Assert.Equal (2, dictCopy.Count);
+			Assert.Equal (dictSrc ["Disabled"], dictCopy ["Disabled"]);
+			Assert.Equal (dictDest ["Normal"], dictCopy ["Normal"]);
+
+
+		}
+
+		//[Fact ()]
+		//public void LoadFromJsonTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void ToJsonTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void UpdateConfigurationTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void UpdateConfigurationFromFileTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void SaveHardCodedDefaultsTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void LoadGlobalFromLibraryResourceTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void LoadGlobalFromAppDirectoryTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void LoadGlobalFromHomeDirectoryTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void LoadAppFromAppResourcesTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void LoadAppFromAppDirectoryTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void LoadAppFromHomeDirectoryTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+		//[Fact ()]
+		//public void LoadTest ()
+		//{
+		//	Assert.True (false, "This test needs an implementation");
+		//}
+
+
+		/// <summary>
+		/// Save the `config.json` file; this can be used to update the file in `Terminal.Gui.Resources.config.json'.
+		/// </summary>
+		/// <remarks>
+		/// IMPORTANT: For the file generated to be valid, this must be the ONLY test run. Conifg Properties
+		/// are all satic and thus can be overwritten by other tests.</remarks>
+		[Fact]
+		public void SaveDefaults ()
+		{
+			ConfigurationManager.Initialize ();
+
+			// Get the hard coded settings
+			ConfigurationManager.GetHardCodedDefaults ();
+
+			// Serialize to a JSON string
+			string json = ConfigurationManager.ToJson ();
+
+			// Write the JSON string to the file 
+			File.WriteAllText ("config.json", json);
+		}
+
+		[Fact]
+		public void UseWithoutResetAsserts ()
+		{
+			ConfigurationManager.Initialize ();
+			Assert.Throws<InvalidOperationException> (() => _ = ConfigurationManager.Settings);
+		}
+
+		[Fact]
+		public void Reset_Resets()
+		{
+			ConfigurationManager.Locations = ConfigLocations.DefaultOnly;
+			ConfigurationManager.Reset ();
+			Assert.NotEmpty (ConfigurationManager.Themes);
+			Assert.Equal ("Default", ConfigurationManager.Themes.Theme);
+		}
+
+		[Fact]
+		public void Reset_and_ResetLoadWithLibraryResourcesOnly_are_same ()
+		{
+			ConfigurationManager.Locations = ConfigLocations.DefaultOnly;
+			// arrange
+			ConfigurationManager.Reset ();
+			ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q;
+			ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F;
+			ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B;
+			ConfigurationManager.Settings ["Application.UseSystemConsole"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.HeightAsBuffer"].PropertyValue = true;
+			ConfigurationManager.Settings.Apply ();
+
+			// assert apply worked
+			Assert.Equal (Key.Q, Application.QuitKey);
+			Assert.Equal (Key.F, Application.AlternateForwardKey);
+			Assert.Equal (Key.B, Application.AlternateBackwardKey);
+			Assert.True (Application.UseSystemConsole);
+			Assert.True (Application.IsMouseDisabled);
+			Assert.True (Application.HeightAsBuffer);
+
+			//act
+			ConfigurationManager.Reset ();
+
+			// assert
+			Assert.NotEmpty (ConfigurationManager.Themes);
+			Assert.Equal ("Default", ConfigurationManager.Themes.Theme);
+			Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey);
+			Assert.Equal (Key.PageDown | Key.CtrlMask, Application.AlternateForwardKey);
+			Assert.Equal (Key.PageUp | Key.CtrlMask, Application.AlternateBackwardKey);
+			Assert.False (Application.UseSystemConsole);
+			Assert.False (Application.IsMouseDisabled);
+			Assert.False (Application.HeightAsBuffer);
+
+			// arrange
+			ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q;
+			ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F;
+			ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B;
+			ConfigurationManager.Settings ["Application.UseSystemConsole"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.HeightAsBuffer"].PropertyValue = true;
+			ConfigurationManager.Settings.Apply ();
+
+
+			ConfigurationManager.Locations = ConfigLocations.DefaultOnly;
+
+			// act
+			ConfigurationManager.Reset ();
+			ConfigurationManager.Load ();
+
+			// assert
+			Assert.NotEmpty (ConfigurationManager.Themes);
+			Assert.Equal ("Default", ConfigurationManager.Themes.Theme);
+			Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey);
+			Assert.Equal (Key.PageDown | Key.CtrlMask, Application.AlternateForwardKey);
+			Assert.Equal (Key.PageUp | Key.CtrlMask, Application.AlternateBackwardKey);
+			Assert.False (Application.UseSystemConsole);
+			Assert.False (Application.IsMouseDisabled);
+			Assert.False (Application.HeightAsBuffer);
+
+		}
+
+
+		[Fact]
+		public void TestConfigProperties ()
+		{
+			ConfigurationManager.Locations = ConfigLocations.All;
+			ConfigurationManager.Reset ();
+
+			Assert.NotEmpty (ConfigurationManager.Settings);
+			// test that all ConfigProperites have our attribute
+			Assert.All (ConfigurationManager.Settings, item => Assert.NotEmpty (item.Value.PropertyInfo.CustomAttributes.Where (a => a.AttributeType == typeof (SerializableConfigurationProperty))));
+			Assert.Empty (ConfigurationManager.Settings.Where (cp => cp.Value.PropertyInfo.GetCustomAttribute (typeof (SerializableConfigurationProperty)) == null));
+
+			// Application is a static class
+			PropertyInfo pi = typeof (Application).GetProperty ("UseSystemConsole");
+			Assert.Equal (pi, ConfigurationManager.Settings ["Application.UseSystemConsole"].PropertyInfo);
+
+			// FrameView is not a static class and DefaultBorderStyle is Scope.Scheme
+			pi = typeof (FrameView).GetProperty ("DefaultBorderStyle");
+			Assert.False (ConfigurationManager.Settings.ContainsKey ("FrameView.DefaultBorderStyle"));
+			Assert.True (ConfigurationManager.Themes ["Default"].ContainsKey ("FrameView.DefaultBorderStyle"));
+		}
+
+		[Fact]
+		public void TestConfigPropertyOmitClassName ()
+		{
+			// Color.ColorShemes is serialzied as "ColorSchemes", not "Colors.ColorSchemes"
+			PropertyInfo pi = typeof (Colors).GetProperty ("ColorSchemes");
+			var scp = ((SerializableConfigurationProperty)pi.GetCustomAttribute (typeof (SerializableConfigurationProperty)));
+			Assert.True (scp.Scope == typeof (ThemeScope));
+			Assert.True (scp.OmitClassName);
+
+			ConfigurationManager.Reset ();
+			Assert.Equal (pi, ConfigurationManager.Themes ["Default"] ["ColorSchemes"].PropertyInfo);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestConfigurationManagerToJson ()
+		{
+			ConfigurationManager.GetHardCodedDefaults ();
+			var stream = ConfigurationManager.ToStream ();
+
+			
+			ConfigurationManager.Settings.Update (stream, "TestConfigurationManagerToJson");
+
+		}
+
+		[Fact, AutoInitShutdown (configLocation: ConfigLocations.None)]
+		public void TestConfigurationManagerInitDriver_NoLocations ()
+		{
+
+			
+		}
+
+		[Fact, AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+		public void TestConfigurationManagerInitDriver ()
+		{
+			Assert.Equal ("Default", ConfigurationManager.Themes.Theme);
+			Assert.True (ConfigurationManager.Themes.ContainsKey ("Default"));
+
+			Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey);
+
+			Assert.Equal (Color.White, Colors.ColorSchemes ["Base"].Normal.Foreground);
+			Assert.Equal (Color.Blue, Colors.ColorSchemes ["Base"].Normal.Background);
+
+			// Change Base
+			var json = ConfigurationManager.ToStream ();
+			
+			ConfigurationManager.Settings.Update (json, "TestConfigurationManagerInitDriver");
+
+			var colorSchemes = ((Dictionary<string, ColorScheme>)ConfigurationManager.Themes [ConfigurationManager.Themes.Theme] ["ColorSchemes"].PropertyValue);
+			Assert.Equal (Colors.Base, colorSchemes ["Base"]);
+			Assert.Equal (Colors.TopLevel, colorSchemes ["TopLevel"]);
+			Assert.Equal (Colors.Error, colorSchemes ["Error"]);
+			Assert.Equal (Colors.Dialog, colorSchemes ["Dialog"]);
+			Assert.Equal (Colors.Menu, colorSchemes ["Menu"]);
+
+			Colors.Base = colorSchemes ["Base"];
+			Colors.TopLevel = colorSchemes ["TopLevel"];
+			Colors.Error = colorSchemes ["Error"];
+			Colors.Dialog = colorSchemes ["Dialog"];
+			Colors.Menu = colorSchemes ["Menu"];
+
+			Assert.Equal (colorSchemes ["Base"], Colors.Base);
+			Assert.Equal (colorSchemes ["TopLevel"], Colors.TopLevel);
+			Assert.Equal (colorSchemes ["Error"], Colors.Error);
+			Assert.Equal (colorSchemes ["Dialog"], Colors.Dialog);
+			Assert.Equal (colorSchemes ["Menu"], Colors.Menu);
+		}
+
+		[Fact]
+		public void TestConfigurationManagerUpdateFromJson ()
+		{
+			// Arrange
+			string json = @"
+{
+  ""$schema"": ""https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json"",
+  ""Application.QuitKey"": {
+    ""Key"": ""Z"",
+    ""Modifiers"": [
+      ""Alt""
+    ]
+  },
+  ""Theme"": ""Default"",
+  ""Themes"": [
+    {
+      ""Default"": {
+        ""ColorSchemes"": [
+          {
+            ""TopLevel"": {
+              ""Normal"": {
+                ""Foreground"": ""BrightGreen"",
+                ""Background"": ""Black""
+              },
+              ""Focus"": {
+                ""Foreground"": ""White"",
+                ""Background"": ""Cyan""
+              },
+              ""HotNormal"": {
+                ""Foreground"": ""Brown"",
+                ""Background"": ""Black""
+              },
+              ""HotFocus"": {
+                ""Foreground"": ""Blue"",
+                ""Background"": ""Cyan""
+              },
+              ""Disabled"": {
+                ""Foreground"": ""DarkGray"",
+                ""Background"": ""Black""
+              }
+            }
+          },
+          {
+            ""Base"": {
+              ""Normal"": {
+                ""Foreground"": ""White"",
+                ""Background"": ""Blue""
+              },
+              ""Focus"": {
+                ""Foreground"": ""Black"",
+                ""Background"": ""Gray""
+              },
+              ""HotNormal"": {
+                ""Foreground"": ""BrightCyan"",
+                ""Background"": ""Blue""
+              },
+              ""HotFocus"": {
+                ""Foreground"": ""BrightBlue"",
+                ""Background"": ""Gray""
+              },
+              ""Disabled"": {
+                ""Foreground"": ""DarkGray"",
+                ""Background"": ""Blue""
+              }
+            }
+          },
+          {
+            ""Dialog"": {
+              ""Normal"": {
+                ""Foreground"": ""Black"",
+                ""Background"": ""Gray""
+              },
+              ""Focus"": {
+                ""Foreground"": ""White"",
+                ""Background"": ""DarkGray""
+              },
+              ""HotNormal"": {
+                ""Foreground"": ""Blue"",
+                ""Background"": ""Gray""
+              },
+              ""HotFocus"": {
+                ""Foreground"": ""BrightYellow"",
+                ""Background"": ""DarkGray""
+              },
+              ""Disabled"": {
+                ""Foreground"": ""Gray"",
+                ""Background"": ""DarkGray""
+              }
+            }
+          },
+          {
+            ""Menu"": {
+              ""Normal"": {
+                ""Foreground"": ""White"",
+                ""Background"": ""DarkGray""
+              },
+              ""Focus"": {
+                ""Foreground"": ""White"",
+                ""Background"": ""Black""
+              },
+              ""HotNormal"": {
+                ""Foreground"": ""BrightYellow"",
+                ""Background"": ""DarkGray""
+              },
+              ""HotFocus"": {
+                ""Foreground"": ""BrightYellow"",
+                ""Background"": ""Black""
+              },
+              ""Disabled"": {
+                ""Foreground"": ""Gray"",
+                ""Background"": ""DarkGray""
+              }
+            }
+          },
+          {
+            ""Error"": {
+              ""Normal"": {
+                ""Foreground"": ""Red"",
+                ""Background"": ""White""
+              },
+              ""Focus"": {
+                ""Foreground"": ""Black"",
+                ""Background"": ""BrightRed""
+              },
+              ""HotNormal"": {
+                ""Foreground"": ""Black"",
+                ""Background"": ""White""
+              },
+              ""HotFocus"": {
+                ""Foreground"": ""White"",
+                ""Background"": ""BrightRed""
+              },
+              ""Disabled"": {
+                ""Foreground"": ""DarkGray"",
+                ""Background"": ""White""
+              }
+            }
+          }
+        ],
+        ""Dialog.DefaultButtonAlignment"": ""Center""
+      }
+    }
+  ]
+}					
+			";
+
+			ConfigurationManager.Reset ();
+			ConfigurationManager.ThrowOnJsonErrors = true;
+			
+			ConfigurationManager.Settings.Update (json, "TestConfigurationManagerUpdateFromJson");
+
+			Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey);
+			Assert.Equal (Key.Z | Key.AltMask, ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue);
+
+			Assert.Equal ("Default", ConfigurationManager.Themes.Theme);
+
+			Assert.Equal (Color.White, Colors.ColorSchemes ["Base"].Normal.Foreground);
+			Assert.Equal (Color.Blue, Colors.ColorSchemes ["Base"].Normal.Background);
+
+			var colorSchemes = (Dictionary<string, ColorScheme>)Themes.First().Value ["ColorSchemes"].PropertyValue;
+			Assert.Equal (Color.White, colorSchemes ["Base"].Normal.Foreground);
+			Assert.Equal (Color.Blue, colorSchemes ["Base"].Normal.Background);
+
+			// Now re-apply
+			ConfigurationManager.Apply ();
+
+			Assert.Equal (Key.Z | Key.AltMask, Application.QuitKey);
+			Assert.Equal ("Default", ConfigurationManager.Themes.Theme);
+
+			Assert.Equal (Color.White, Colors.ColorSchemes ["Base"].Normal.Foreground);
+			Assert.Equal (Color.Blue, Colors.ColorSchemes ["Base"].Normal.Background);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestConfigurationManagerInvalidJsonThrows ()
+		{
+			ConfigurationManager.ThrowOnJsonErrors = true;
+			// "yellow" is not a color
+			string json = @"
+			{
+				""Themes"" : {
+					""ThemeDefinitions"" : [ 
+                                        {
+						""Default"" : {
+							""ColorSchemes"": [
+							{
+								""UserDefined"": {
+									""hotNormal"": {
+										""foreground"": ""yellow"",
+										""background"": ""1234""
+									}
+								}
+							}
+							]
+						}
+					}
+					]
+				}
+			}";
+
+			JsonException jsonException = Assert.Throws<JsonException> (() => ConfigurationManager.Settings.Update (json, "test"));
+			Assert.Equal ("Invalid Color: 'yellow'", jsonException.Message);
+
+			// AbNormal is not a ColorScheme attribute
+			json = @"
+			{
+				""Themes"" : {
+					""ThemeDefinitions"" : [ 
+                                        {
+						""Default"" : {
+							""ColorSchemes"": [
+							{
+								""UserDefined"": {
+									""AbNormal"": {
+										""foreground"": ""green"",
+										""background"": ""1234""
+									}
+								}
+							}
+							]
+						}
+					}
+					]
+				}
+			}";
+
+			jsonException = Assert.Throws<JsonException> (() => ConfigurationManager.Settings.Update (json, "test"));
+			Assert.Equal ("Unrecognized ColorScheme Attribute name: AbNormal.", jsonException.Message);
+
+			// Modify hotNormal background only 
+			json = @"
+			{
+				""Themes"" : {
+					""ThemeDefinitions"" : [ 
+                                        {
+						""Default"" : {
+							""ColorSchemes"": [
+							{
+								""UserDefined"": {
+									""hotNormal"": {
+										""background"": ""cyan""
+									}
+								}
+							}
+							]
+						}
+					}
+					]
+				}
+			}";
+
+			jsonException = Assert.Throws<JsonException> (() => ConfigurationManager.Settings.Update (json, "test"));
+			Assert.Equal ("Both Foreground and Background colors must be provided.", jsonException.Message);
+
+
+			// Unknown proeprty
+			json = @"
+			{
+				""Unknown"" : ""Not known""
+			}";
+
+			jsonException = Assert.Throws<JsonException> (() => ConfigurationManager.Settings.Update (json, "test"));
+			Assert.StartsWith ("Unknown property", jsonException.Message);
+			
+			Assert.Equal (0, ConfigurationManager.jsonErrors.Length);
+
+			ConfigurationManager.ThrowOnJsonErrors = false;
+		}
+
+		[Fact]
+		public void TestConfigurationManagerInvalidJsonLogs ()
+		{
+			Application.Init (new FakeDriver ());
+
+			ConfigurationManager.ThrowOnJsonErrors = false;
+			// "yellow" is not a color
+			string json = @"
+			{
+				""Themes"" : {
+					""ThemeDefinitions"" : [ 
+                                        {
+						""Default"" : {
+							""ColorSchemes"": [
+							{
+								""UserDefined"": {
+									""hotNormal"": {
+										""foreground"": ""yellow"",
+										""background"": ""1234""
+									}
+								}
+							}
+							]
+						}
+					}
+					]
+				}
+			}";
+
+			ConfigurationManager.Settings.Update (json, "test");
+
+			// AbNormal is not a ColorScheme attribute
+			json = @"
+			{
+				""Themes"" : {
+					""ThemeDefinitions"" : [ 
+                                        {
+						""Default"" : {
+							""ColorSchemes"": [
+							{
+								""UserDefined"": {
+									""AbNormal"": {
+										""foreground"": ""green"",
+										""background"": ""1234""
+									}
+								}
+							}
+							]
+						}
+					}
+					]
+				}
+			}";
+
+			ConfigurationManager.Settings.Update (json, "test");
+
+			// Modify hotNormal background only 
+			json = @"
+			{
+				""Themes"" : {
+					""ThemeDefinitions"" : [ 
+                                        {
+						""Default"" : {
+							""ColorSchemes"": [
+							{
+								""UserDefined"": {
+									""hotNormal"": {
+										""background"": ""cyan""
+									}
+								}
+							}
+							]
+						}
+					}
+					]
+				}
+			}";
+
+			ConfigurationManager.Settings.Update (json, "test");
+
+			ConfigurationManager.Settings.Update ("{}}", "test");
+			
+			Assert.NotEqual (0, ConfigurationManager.jsonErrors.Length);
+
+			Application.Shutdown ();
+
+			ConfigurationManager.ThrowOnJsonErrors = false;
+		}
+
+		[Fact, AutoInitShutdown]
+		public void LoadConfigurationFromAllSources_ShouldLoadSettingsFromAllSources ()
+		{
+			//var _configFilename = "config.json";
+			//// Arrange
+			//// Create a mock of the configuration files in all sources
+			//// Home directory
+			//string homeDir = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), ".tui");
+			//if (!Directory.Exists (homeDir)) {
+			//	Directory.CreateDirectory (homeDir);
+			//}
+			//string globalConfigFile = Path.Combine (homeDir, _configFilename);
+			//string appSpecificConfigFile = Path.Combine (homeDir, "appname.config.json");
+			//File.WriteAllText (globalConfigFile, "{\"Settings\": {\"TestSetting\":\"Global\"}}");
+			//File.WriteAllText (appSpecificConfigFile, "{\"Settings\": {\"TestSetting\":\"AppSpecific\"}}");
+
+			//// App directory
+			//string appDir = Directory.GetCurrentDirectory ();
+			//string appDirGlobalConfigFile = Path.Combine (appDir, _configFilename);
+			//string appDirAppSpecificConfigFile = Path.Combine (appDir, "appname.config.json");
+			//File.WriteAllText (appDirGlobalConfigFile, "{\"Settings\": {\"TestSetting\":\"GlobalAppDir\"}}");
+			//File.WriteAllText (appDirAppSpecificConfigFile, "{\"Settings\": {\"TestSetting\":\"AppSpecificAppDir\"}}");
+
+			//// App resources
+			//// ...
+
+			//// Act
+			//ConfigurationManager.Locations = ConfigurationManager.ConfigLocation.All;
+			//ConfigurationManager.Load ();
+
+			//// Assert
+			//// Check that the settings from the highest precedence source are loaded
+			//Assert.Equal ("AppSpecific", ConfigurationManager.Config.Settings.TestSetting);
+		}
+
+
+		[Fact]
+		public void Load_FiresUpdated ()
+		{
+			ConfigurationManager.Reset ();
+			
+			ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q;
+			ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F;
+			ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B;
+			ConfigurationManager.Settings ["Application.UseSystemConsole"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.HeightAsBuffer"].PropertyValue = true;
+
+			ConfigurationManager.Updated += ConfigurationManager_Updated;
+			bool fired = false;
+			void ConfigurationManager_Updated (ConfigurationManager.ConfigurationManagerEventArgs obj)
+			{
+				fired = true;
+				// assert
+				Assert.Equal (Key.Q | Key.CtrlMask, ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue);
+				Assert.Equal (Key.PageDown | Key.CtrlMask, ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue);
+				Assert.Equal (Key.PageUp | Key.CtrlMask, ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue);
+				Assert.False ((bool)ConfigurationManager.Settings ["Application.UseSystemConsole"].PropertyValue);
+				Assert.False ((bool)ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue);
+				Assert.False ((bool)ConfigurationManager.Settings ["Application.HeightAsBuffer"].PropertyValue);
+			}
+
+			ConfigurationManager.Load (true);
+
+			// assert
+			Assert.True (fired);
+
+			ConfigurationManager.Updated -= ConfigurationManager_Updated;
+		}
+
+		[Fact]
+		public void Apply_FiresApplied ()
+		{
+			ConfigurationManager.Reset ();
+			ConfigurationManager.Applied += ConfigurationManager_Applied;
+			bool fired = false;
+			void ConfigurationManager_Applied (ConfigurationManager.ConfigurationManagerEventArgs obj)
+			{
+				fired = true;
+				// assert
+				Assert.Equal (Key.Q, Application.QuitKey);
+				Assert.Equal (Key.F, Application.AlternateForwardKey);
+				Assert.Equal (Key.B, Application.AlternateBackwardKey);
+				Assert.True (Application.UseSystemConsole);
+				Assert.True (Application.IsMouseDisabled);
+				Assert.True (Application.HeightAsBuffer);
+			}
+
+			// act
+			ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q;
+			ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F;
+			ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B;
+			ConfigurationManager.Settings ["Application.UseSystemConsole"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.HeightAsBuffer"].PropertyValue = true;
+
+			ConfigurationManager.Apply ();
+
+			// assert
+			Assert.True (fired);
+
+			ConfigurationManager.Applied -= ConfigurationManager_Applied;
+		}
+	}
+}
+

+ 234 - 0
UnitTests/Configuration/JsonConverterTests.cs

@@ -0,0 +1,234 @@
+using Xunit;
+using Terminal.Gui.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Text.Json;
+
+namespace Terminal.Gui.ConfigurationTests {
+	public class ColorJsonConverterTests {
+
+		[Theory]
+		[InlineData ("Black", Color.Black)]
+		[InlineData ("Blue", Color.Blue)]
+		[InlineData ("BrightBlue", Color.BrightBlue)]
+		[InlineData ("BrightCyan", Color.BrightCyan)]
+		[InlineData ("BrightGreen", Color.BrightGreen)]
+		[InlineData ("BrightMagenta", Color.BrightMagenta)]
+		[InlineData ("BrightRed", Color.BrightRed)]
+		[InlineData ("BrightYellow", Color.BrightYellow)]
+		[InlineData ("Brown", Color.Brown)]
+		[InlineData ("Cyan", Color.Cyan)]
+		[InlineData ("DarkGray", Color.DarkGray)]
+		[InlineData ("Gray", Color.Gray)]
+		[InlineData ("Green", Color.Green)]
+		[InlineData ("Magenta", Color.Magenta)]
+		[InlineData ("Red", Color.Red)]
+		[InlineData ("White", Color.White)]
+		public void TestColorDeserializationFromHumanReadableColorNames (string colorName, Color expectedColor)
+		{
+			// Arrange
+			string json = $"\"{colorName}\"";
+
+			// Act
+			Color actualColor = JsonSerializer.Deserialize<Color> (json, ConfigurationManagerTests._jsonOptions);
+
+			// Assert
+			Assert.Equal (expectedColor, actualColor);
+		}
+
+
+		[Theory]
+		[InlineData (Color.Black, "Black")]
+		[InlineData (Color.Blue, "Blue")]
+		[InlineData (Color.Green, "Green")]
+		[InlineData (Color.Cyan, "Cyan")]
+		[InlineData (Color.Gray, "Gray")]
+		[InlineData (Color.Red, "Red")]
+		[InlineData (Color.Magenta, "Magenta")]
+		[InlineData (Color.Brown, "Brown")]
+		[InlineData (Color.DarkGray, "DarkGray")]
+		[InlineData (Color.BrightBlue, "BrightBlue")]
+		[InlineData (Color.BrightGreen, "BrightGreen")]
+		[InlineData (Color.BrightCyan, "BrightCyan")]
+		[InlineData (Color.BrightRed, "BrightRed")]
+		[InlineData (Color.BrightMagenta, "BrightMagenta")]
+		[InlineData (Color.BrightYellow, "BrightYellow")]
+		[InlineData (Color.White, "White")]
+		public void SerializesEnumValuesAsStrings (Color color, string expectedJson)
+		{
+			var converter = new ColorJsonConverter ();
+			var options = new JsonSerializerOptions { Converters = { converter } };
+
+			var serialized = JsonSerializer.Serialize (color, options);
+
+			Assert.Equal ($"\"{expectedJson}\"", serialized);
+		}
+
+		[Fact]
+		public void TestSerializeColor_Black ()
+		{
+			// Arrange
+			var color = Color.Black;
+			var expectedJson = "\"Black\"";
+
+			// Act
+			var json = JsonSerializer.Serialize (color, new JsonSerializerOptions {
+				Converters = { new ColorJsonConverter () }
+			});
+
+			// Assert
+			Assert.Equal (expectedJson, json);
+		}
+
+		[Fact]
+		public void TestSerializeColor_BrightRed ()
+		{
+			// Arrange
+			var color = Color.BrightRed;
+			var expectedJson = "\"BrightRed\"";
+
+			// Act
+			var json = JsonSerializer.Serialize (color, new JsonSerializerOptions {
+				Converters = { new ColorJsonConverter () }
+			});
+
+			// Assert
+			Assert.Equal (expectedJson, json);
+		}
+
+		[Fact]
+		public void TestDeserializeColor_Black ()
+		{
+			// Arrange
+			var json = "\"Black\"";
+			var expectedColor = Color.Black;
+
+			// Act
+			var color = JsonSerializer.Deserialize<Color> (json, new JsonSerializerOptions {
+				Converters = { new ColorJsonConverter () }
+			});
+
+			// Assert
+			Assert.Equal (expectedColor, color);
+		}
+
+		[Fact]
+		public void TestDeserializeColor_BrightRed ()
+		{
+			// Arrange
+			var json = "\"BrightRed\"";
+			var expectedColor = Color.BrightRed;
+
+			// Act
+			var color = JsonSerializer.Deserialize<Color> (json, new JsonSerializerOptions {
+				Converters = { new ColorJsonConverter () }
+			});
+
+			// Assert
+			Assert.Equal (expectedColor, color);
+		}
+	}
+
+	public class AttributeJsonConverterTests {
+		[Fact, AutoInitShutdown]
+		public void TestDeserialize ()
+		{
+			// Test deserializing from human-readable color names
+			var json = "{\"Foreground\":\"Blue\",\"Background\":\"Green\"}";
+			var attribute = JsonSerializer.Deserialize<Attribute> (json, ConfigurationManagerTests._jsonOptions);
+			Assert.Equal (Color.Blue, attribute.Foreground);
+			Assert.Equal (Color.Green, attribute.Background);
+
+			// Test deserializing from RGB values
+			json = "{\"Foreground\":\"rgb(255,0,0)\",\"Background\":\"rgb(0,255,0)\"}";
+			attribute = JsonSerializer.Deserialize<Attribute> (json, ConfigurationManagerTests._jsonOptions);
+			Assert.Equal (Color.BrightRed, attribute.Foreground);
+			Assert.Equal (Color.BrightGreen, attribute.Background);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestSerialize ()
+		{
+			// Test serializing to human-readable color names
+			var attribute = new Attribute (Color.Blue, Color.Green);
+			var json = JsonSerializer.Serialize<Attribute> (attribute, ConfigurationManagerTests._jsonOptions);
+			Assert.Equal ("{\"Foreground\":\"Blue\",\"Background\":\"Green\"}", json);
+		}
+	}
+
+	public class ColorSchemeJsonConverterTests {
+		//string json = @"
+		//	{
+		//	""ColorSchemes"": {
+		//		""Base"": {
+		//			""normal"": {
+		//				""foreground"": ""White"",
+		//				""background"": ""Blue""
+		//   		            },
+		//			""focus"": {
+		//				""foreground"": ""Black"",
+		//				""background"": ""Gray""
+		//			    },
+		//			""hotNormal"": {
+		//				""foreground"": ""BrightCyan"",
+		//				""background"": ""Blue""
+		//			    },
+		//			""hotFocus"": {
+		//				""foreground"": ""BrightBlue"",
+		//				""background"": ""Gray""
+		//			    },
+		//			""disabled"": {
+		//				""foreground"": ""DarkGray"",
+		//				""background"": ""Blue""
+		//			    }
+		//		}
+		//		}
+		//	}";
+		[Fact, AutoInitShutdown]
+		public void TestColorSchemesSerialization ()
+		{
+			// Arrange
+			var expectedColorScheme = new ColorScheme {
+				Normal = Attribute.Make (Color.White, Color.Blue),
+				Focus = Attribute.Make (Color.Black, Color.Gray),
+				HotNormal = Attribute.Make (Color.BrightCyan, Color.Blue),
+				HotFocus = Attribute.Make (Color.BrightBlue, Color.Gray),
+				Disabled = Attribute.Make (Color.DarkGray, Color.Blue)
+			};
+			var serializedColorScheme = JsonSerializer.Serialize<ColorScheme> (expectedColorScheme, ConfigurationManagerTests._jsonOptions);
+
+			// Act
+			var actualColorScheme = JsonSerializer.Deserialize<ColorScheme> (serializedColorScheme, ConfigurationManagerTests._jsonOptions);
+
+			// Assert
+			Assert.Equal (expectedColorScheme, actualColorScheme);
+		}
+	}
+
+	public class KeyJsonConverterTests {
+		[Theory, AutoInitShutdown]
+		[InlineData (Key.A, "A")]
+		[InlineData (Key.a | Key.ShiftMask, "a, ShiftMask")]
+		[InlineData (Key.A | Key.CtrlMask, "A, CtrlMask")]
+		[InlineData (Key.a | Key.AltMask | Key.CtrlMask, "a, CtrlMask, AltMask")]
+		[InlineData (Key.Delete | Key.AltMask | Key.CtrlMask, "Delete, CtrlMask, AltMask")]
+		[InlineData (Key.D4, "D4")]
+		[InlineData (Key.Esc, "Esc")]
+		public void TestKeyRoundTripConversion (Key key, string expectedStringTo)
+		{
+			// Arrange
+			var options = new JsonSerializerOptions ();
+			options.Converters.Add (new KeyJsonConverter ());
+
+			// Act
+			var json = JsonSerializer.Serialize (key, options);
+			var deserializedKey = JsonSerializer.Deserialize<Key> (json, options);
+
+			// Assert
+			Assert.Equal (expectedStringTo, deserializedKey.ToString ());
+		}
+	}
+}

+ 92 - 0
UnitTests/Configuration/SettingsScopeTests.cs

@@ -0,0 +1,92 @@
+using Xunit;
+using Terminal.Gui.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+
+namespace Terminal.Gui.ConfigurationTests {
+	public class SettingsScopeTests {
+
+		[Fact]
+		public void GetHardCodedDefaults_ShouldSetProperties ()
+		{
+			ConfigurationManager.Reset ();
+
+			Assert.Equal (3, ((Dictionary<string, ConfigurationManager.ThemeScope>)ConfigurationManager.Settings ["Themes"].PropertyValue).Count);
+
+			ConfigurationManager.GetHardCodedDefaults ();
+			Assert.NotEmpty (ConfigurationManager.Themes);
+			Assert.Equal ("Default", ConfigurationManager.Themes.Theme);
+
+			Assert.True (ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue is Key);
+			Assert.True (ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue is Key);
+			Assert.True (ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue is Key);
+			Assert.True (ConfigurationManager.Settings ["Application.UseSystemConsole"].PropertyValue is bool);
+			Assert.True (ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue is bool);
+			Assert.True (ConfigurationManager.Settings ["Application.HeightAsBuffer"].PropertyValue is bool);
+
+			Assert.True (ConfigurationManager.Settings ["Theme"].PropertyValue is string);
+			Assert.Equal ("Default", ConfigurationManager.Settings ["Theme"].PropertyValue as string);
+
+			Assert.True (ConfigurationManager.Settings ["Themes"].PropertyValue is Dictionary<string, ConfigurationManager.ThemeScope>);
+			Assert.Single (((Dictionary<string, ConfigurationManager.ThemeScope>)ConfigurationManager.Settings ["Themes"].PropertyValue));
+
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Apply_ShouldApplyProperties ()
+		{
+			// arrange
+			Assert.Equal (Key.Q | Key.CtrlMask, (Key)ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue);
+			Assert.Equal (Key.PageDown | Key.CtrlMask, (Key)ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue);
+			Assert.Equal (Key.PageUp | Key.CtrlMask, (Key)ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue);
+			Assert.False ((bool)ConfigurationManager.Settings ["Application.UseSystemConsole"].PropertyValue);
+			Assert.False ((bool)ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue);
+			Assert.False ((bool)ConfigurationManager.Settings ["Application.HeightAsBuffer"].PropertyValue);
+
+			// act
+			ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q;
+			ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F;
+			ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B;
+			ConfigurationManager.Settings ["Application.UseSystemConsole"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true;
+			ConfigurationManager.Settings ["Application.HeightAsBuffer"].PropertyValue = true;
+
+			ConfigurationManager.Settings.Apply ();
+
+			// assert
+			Assert.Equal (Key.Q, Application.QuitKey);
+			Assert.Equal (Key.F, Application.AlternateForwardKey);
+			Assert.Equal (Key.B, Application.AlternateBackwardKey);
+			Assert.True (Application.UseSystemConsole);
+			Assert.True (Application.IsMouseDisabled);
+			Assert.True (Application.HeightAsBuffer);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void CopyUpdatedProperitesFrom_ShouldCopyChangedPropertiesOnly ()
+		{
+			ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.End;
+
+			var updatedSettings = new SettingsScope ();
+
+			///Don't set Quitkey
+			updatedSettings["Application.AlternateForwardKey"].PropertyValue = Key.F;
+			updatedSettings["Application.AlternateBackwardKey"].PropertyValue = Key.B;
+			updatedSettings["Application.UseSystemConsole"].PropertyValue = true;
+			updatedSettings["Application.IsMouseDisabled"].PropertyValue = true;
+			updatedSettings["Application.HeightAsBuffer"].PropertyValue = true;
+
+			ConfigurationManager.Settings.Update (updatedSettings);
+			Assert.Equal (Key.End, ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue);
+			Assert.Equal (Key.F, updatedSettings ["Application.AlternateForwardKey"].PropertyValue);
+			Assert.Equal (Key.B, updatedSettings ["Application.AlternateBackwardKey"].PropertyValue);
+			Assert.True ((bool)updatedSettings ["Application.UseSystemConsole"].PropertyValue);
+			Assert.True ((bool)updatedSettings ["Application.IsMouseDisabled"].PropertyValue);
+			Assert.True ((bool)updatedSettings ["Application.HeightAsBuffer"].PropertyValue);
+		}
+	}
+}

+ 82 - 0
UnitTests/Configuration/ThemeScopeTests.cs

@@ -0,0 +1,82 @@
+using Xunit;
+using Terminal.Gui.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Text.Json;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+
+namespace Terminal.Gui.ConfigurationTests {
+	public class ThemeScopeTests {
+		public static readonly JsonSerializerOptions _jsonOptions = new() {
+			Converters = {
+				//new AttributeJsonConverter (),
+				//new ColorJsonConverter ()
+				}
+		};
+
+
+		[Fact]
+		public void ThemeManager_ClassMethodsWork ()
+		{
+			ConfigurationManager.Reset ();
+			Assert.Equal (ConfigurationManager.ThemeManager.Instance, ConfigurationManager.Themes);
+			Assert.NotEmpty (ConfigurationManager.ThemeManager.Themes);
+
+			ConfigurationManager.ThemeManager.SelectedTheme = "foo";
+			Assert.Equal ("foo", ConfigurationManager.ThemeManager.SelectedTheme);
+			ConfigurationManager.ThemeManager.Reset ();
+			Assert.Equal (string.Empty, ConfigurationManager.ThemeManager.SelectedTheme);
+
+			Assert.Empty (ConfigurationManager.ThemeManager.Themes);
+		}
+
+		[Fact]
+		public void AllThemesPresent()
+		{
+			ConfigurationManager.Reset ();
+			Assert.True (ConfigurationManager.Themes.ContainsKey ("Default"));
+			Assert.True (ConfigurationManager.Themes.ContainsKey ("Dark"));
+			Assert.True (ConfigurationManager.Themes.ContainsKey ("Light"));
+		}
+
+		[Fact]
+		public void GetHardCodedDefaults_ShouldSetProperties ()
+		{
+			ConfigurationManager.Reset ();
+			ConfigurationManager.GetHardCodedDefaults ();
+			Assert.NotEmpty (ConfigurationManager.Themes);
+			Assert.Equal ("Default", ConfigurationManager.Themes.Theme);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Apply_ShouldApplyUpdatedProperties ()
+		{
+			ConfigurationManager.Reset ();
+			Assert.NotEmpty (ConfigurationManager.Themes);
+			Assert.Equal (Dialog.ButtonAlignments.Center, Dialog.DefaultButtonAlignment);
+
+			ConfigurationManager.Themes ["Default"] ["Dialog.DefaultButtonAlignment"].PropertyValue = Dialog.ButtonAlignments.Right;
+
+			ConfigurationManager.ThemeManager.Themes! [ThemeManager.SelectedTheme]!.Apply ();
+			Assert.Equal (Dialog.ButtonAlignments.Right, Dialog.DefaultButtonAlignment);
+		}
+		
+
+		[Fact]
+		public void TestSerialize_RoundTrip ()
+		{
+			ConfigurationManager.Reset ();
+
+			var initial = ConfigurationManager.ThemeManager.Themes;
+			
+			var serialized = JsonSerializer.Serialize<IDictionary<string, ThemeScope>> (ConfigurationManager.Themes, _jsonOptions);
+			var deserialized = JsonSerializer.Deserialize<IDictionary<string, ThemeScope>> (serialized, _jsonOptions);
+
+			Assert.NotEqual (initial, deserialized);
+			Assert.Equal (deserialized.Count, initial.Count);
+		}
+	}
+}

+ 176 - 0
UnitTests/Configuration/ThemeTests.cs

@@ -0,0 +1,176 @@
+using Xunit;
+using Terminal.Gui.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+using System.Text.Json;
+using static Terminal.Gui.Configuration.ConfigurationManager;
+
+namespace Terminal.Gui.ConfigurationTests {
+	public class ThemeTests {
+		public static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions () {
+			Converters = {
+				new AttributeJsonConverter (),
+				new ColorJsonConverter ()
+				}
+		};
+
+		[Fact]
+		public void TestApply_UpdatesColors ()
+		{
+			// Arrange
+			ConfigurationManager.Reset ();
+
+			Assert.False (Colors.ColorSchemes.ContainsKey ("test"));
+
+			var theme = new ThemeScope ();
+			Assert.NotEmpty (theme);
+
+			Themes.Add ("testTheme", theme);
+
+			var colorScheme = new ColorScheme { Normal = new Attribute (Color.Red, Color.Green) };
+
+			theme ["ColorSchemes"].PropertyValue = new Dictionary<string, ColorScheme> () {
+				{ "test",  colorScheme }
+			};
+
+			Assert.Equal (Color.Red, ((Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue) ["test"].Normal.Foreground);
+			Assert.Equal (Color.Green, ((Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue) ["test"].Normal.Background);
+
+			// Act
+			Themes.Theme = "testTheme";
+			Themes! [ThemeManager.SelectedTheme]!.Apply ();
+
+			// Assert
+			var updatedScheme = Colors.ColorSchemes ["test"];
+			Assert.Equal (Color.Red, updatedScheme.Normal.Foreground);
+			Assert.Equal (Color.Green, updatedScheme.Normal.Background);
+		}
+
+		[Fact]
+		public void TestApply ()
+		{
+			ConfigurationManager.Reset ();
+
+			var theme = new ThemeScope ();
+			Assert.NotEmpty (theme);
+
+			Themes.Add ("testTheme", theme);
+
+			Assert.True (Dialog.DefaultBorder.Effect3D);
+			Assert.Equal (typeof (Border), theme ["Dialog.DefaultBorder"].PropertyInfo.PropertyType);
+			theme ["Dialog.DefaultBorder"].PropertyValue = new Border () { Effect3D = false }; // default is true
+
+			Themes.Theme = "testTheme";
+			Themes! [ThemeManager.SelectedTheme]!.Apply ();
+
+			Assert.False (Dialog.DefaultBorder.Effect3D);
+		}
+
+		[Fact]
+		public void TestUpdatFrom_Change ()
+		{
+			// arrange
+			ConfigurationManager.Reset ();
+
+			var theme = new ThemeScope ();
+			Assert.NotEmpty (theme);
+
+			var colorScheme = new ColorScheme {
+				// note: ColorScheme's can't be partial; default for each attribute
+				// is always White/Black
+				Normal = new Attribute (Color.Red, Color.Green),
+				Focus = new Attribute (Color.Cyan, Color.BrightCyan),
+				HotNormal = new Attribute (Color.Brown, Color.BrightYellow),
+				HotFocus = new Attribute (Color.Green, Color.BrightGreen),
+				Disabled = new Attribute (Color.Gray, Color.DarkGray),
+			};
+			theme ["ColorSchemes"].PropertyValue = Colors.Create ();
+			((Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue) ["test"] = colorScheme;
+
+			var colorSchemes = (Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue;
+			Assert.Equal (colorScheme.Normal, colorSchemes ["Test"].Normal);
+			Assert.Equal (colorScheme.Focus, colorSchemes ["Test"].Focus);
+
+			// Change just Normal
+			var newTheme = new ThemeScope ();
+			var newColorScheme = new ColorScheme {
+				Normal = new Attribute (Color.Blue, Color.BrightBlue),
+				
+				Focus = colorScheme.Focus,
+				HotNormal =colorScheme.HotNormal,
+				HotFocus = colorScheme.HotFocus,
+				Disabled = colorScheme.Disabled,
+			};
+			newTheme ["ColorSchemes"].PropertyValue = Colors.Create ();
+			((Dictionary<string, ColorScheme>)newTheme ["ColorSchemes"].PropertyValue) ["test"] = newColorScheme;
+
+			// Act
+			theme.Update (newTheme);
+
+			// Assert
+			colorSchemes = (Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue;
+			// Normal should have changed
+			Assert.Equal (Color.Blue, colorSchemes ["Test"].Normal.Foreground);
+			Assert.Equal (Color.BrightBlue, colorSchemes ["Test"].Normal.Background);
+			Assert.Equal (Color.Cyan, colorSchemes ["Test"].Focus.Foreground);
+			Assert.Equal (Color.BrightCyan, colorSchemes ["Test"].Focus.Background);
+		}
+
+		[Fact]
+		public void TestUpdatFrom_Add ()
+		{
+			// arrange
+			ConfigurationManager.Reset ();
+
+			var theme = new ThemeScope ();
+			Assert.NotEmpty (theme);
+
+			theme ["ColorSchemes"].PropertyValue = Colors.Create ();
+			var colorSchemes = (Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue;
+			Assert.Equal (Colors.ColorSchemes.Count, colorSchemes.Count);
+
+			var newTheme = new ThemeScope ();
+			var colorScheme = new ColorScheme {
+				// note: ColorScheme's can't be partial; default for each attribute
+				// is always White/Black
+				Normal = new Attribute (Color.Red, Color.Green),
+				Focus = new Attribute (Color.Cyan, Color.BrightCyan),
+				HotNormal = new Attribute (Color.Brown, Color.BrightYellow),
+				HotFocus = new Attribute (Color.Green, Color.BrightGreen),
+				Disabled = new Attribute (Color.Gray, Color.DarkGray),
+			};
+
+			newTheme ["ColorSchemes"].PropertyValue = Colors.Create ();
+			// add a new ColorScheme to the newTheme
+			((Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue) ["test"] = colorScheme;
+
+			colorSchemes = (Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue;
+			Assert.Equal (Colors.ColorSchemes.Count + 1, colorSchemes.Count);
+
+			// Act
+			theme.Update (newTheme);
+
+			// Assert
+			colorSchemes = (Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue;
+			Assert.Equal (colorSchemes ["Test"].Normal, colorScheme.Normal);
+			Assert.Equal (colorSchemes ["Test"].Focus, colorScheme.Focus);
+		}
+
+		[Fact]
+		public void TestSerialize_RoundTrip ()
+		{
+			var theme = new ThemeScope ();
+			theme ["Dialog.DefaultButtonAlignment"].PropertyValue = Dialog.ButtonAlignments.Right;
+
+			var json = JsonSerializer.Serialize (theme, _jsonOptions);
+
+			var deserialized = JsonSerializer.Deserialize<ThemeScope> (json, _jsonOptions);
+
+			Assert.Equal (Dialog.ButtonAlignments.Right, (Dialog.ButtonAlignments)deserialized ["Dialog.DefaultButtonAlignment"].PropertyValue);
+		}
+	}
+}

+ 6 - 1
UnitTests/TestHelpers.cs

@@ -11,6 +11,7 @@ using Attribute = Terminal.Gui.Attribute;
 using System.Text.RegularExpressions;
 using System.Reflection;
 using System.Diagnostics;
+using Terminal.Gui.Configuration;
 
 
 // This class enables test functions annotated with the [AutoInitShutdown] attribute to 
@@ -36,11 +37,14 @@ public class AutoInitShutdownAttribute : Xunit.Sdk.BeforeAfterTestAttribute {
 	/// Only valid if <see cref="consoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.</param>
 	/// <param name="fakeClipboardIsSupportedAlwaysTrue">Only valid if <paramref name="autoInit"/> is true.
 	/// Only valid if <see cref="consoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.</param>
+	/// <param name="configLocation">Determines what config file locations <see cref="ConfigurationManager"/> will 
+	/// load from.</param>
 	public AutoInitShutdownAttribute (bool autoInit = true, bool autoShutdown = true,
 		Type consoleDriverType = null,
 		bool useFakeClipboard = false,
 		bool fakeClipboardAlwaysThrowsNotSupportedException = false,
-		bool fakeClipboardIsSupportedAlwaysTrue = false)
+		bool fakeClipboardIsSupportedAlwaysTrue = false,
+		ConfigurationManager.ConfigLocations configLocation = ConfigurationManager.ConfigLocations.DefaultOnly)
 	{
 		//Assert.True (autoInit == false && consoleDriverType == null);
 
@@ -50,6 +54,7 @@ public class AutoInitShutdownAttribute : Xunit.Sdk.BeforeAfterTestAttribute {
 		FakeDriver.FakeBehaviors.UseFakeClipboard = useFakeClipboard;
 		FakeDriver.FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException;
 		FakeDriver.FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue;
+		ConfigurationManager.Locations = configLocation;
 	}
 
 	static bool _init = false;

+ 2 - 1
UnitTests/Text/CollectionNavigatorTests.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Threading;
+using Terminal.Gui;
 using Xunit;
 
 namespace Terminal.Gui.TextTests {
@@ -339,7 +340,7 @@ namespace Terminal.Gui.TextTests {
 		}
 
 		[Fact]
-		public void  MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches ()
+		public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches ()
 		{
 			var strings = new string [] {
 				"$$",

+ 2 - 2
UnitTests/Types/DimTests.cs

@@ -645,7 +645,7 @@ namespace Terminal.Gui.TypeTests {
 			};
 
 			Application.Iteration += () => {
-				while (count < 20) 					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
+				while (count < 20) field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
 
 				Application.RequestStop ();
 			};
@@ -1088,7 +1088,7 @@ namespace Terminal.Gui.TypeTests {
 			};
 
 			Application.Iteration += () => {
-				while (count > 0) 					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
+				while (count > 0) field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
 
 				Application.RequestStop ();
 			};

+ 1 - 0
UnitTests/Types/PointTests.cs

@@ -1,4 +1,5 @@
 using System;
+using Terminal.Gui;
 using Xunit;
 
 namespace Terminal.Gui.TypeTests {

+ 6 - 10
UnitTests/Types/PosTests.cs

@@ -127,7 +127,7 @@ namespace Terminal.Gui.TypeTests {
 			var win = new Window ();
 
 			var label = new Label ("This should be the last line.") {
-				TextAlignment = Terminal.Gui.TextAlignment.Centered,
+				TextAlignment = TextAlignment.Centered,
 				ColorScheme = Colors.Menu,
 				Width = Dim.Fill (),
 				X = Pos.Center (),
@@ -173,7 +173,7 @@ namespace Terminal.Gui.TypeTests {
 			var win = new Window ();
 
 			var label = new Label ("This should be the last line.") {
-				TextAlignment = Terminal.Gui.TextAlignment.Centered,
+				TextAlignment = TextAlignment.Centered,
 				ColorScheme = Colors.Menu,
 				Width = Dim.Fill (),
 				X = Pos.Center (),
@@ -220,7 +220,7 @@ namespace Terminal.Gui.TypeTests {
 			var win = new Window ();
 
 			var label = new Label ("This should be the last line.") {
-				TextAlignment = Terminal.Gui.TextAlignment.Centered,
+				TextAlignment = TextAlignment.Centered,
 				ColorScheme = Colors.Menu,
 				Width = Dim.Fill (),
 				X = Pos.Center (),
@@ -283,7 +283,7 @@ namespace Terminal.Gui.TypeTests {
 			var win = new Window ();
 
 			var label = new Label ("This should be the last line.") {
-				TextAlignment = Terminal.Gui.TextAlignment.Centered,
+				TextAlignment = TextAlignment.Centered,
 				ColorScheme = Colors.Menu,
 				Width = Dim.Fill (),
 				X = Pos.Center (),
@@ -891,9 +891,7 @@ namespace Terminal.Gui.TypeTests {
 			};
 
 			Application.Iteration += () => {
-				while (count < 20) {
-					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
-				}
+				while (count < 20) field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
 
 				Application.RequestStop ();
 			};
@@ -951,9 +949,7 @@ namespace Terminal.Gui.TypeTests {
 			};
 
 			Application.Iteration += () => {
-				while (count > 0) {
-					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
-				}
+				while (count > 0) field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
 
 				Application.RequestStop ();
 			};

+ 1 - 0
UnitTests/Types/RectTests.cs

@@ -1,4 +1,5 @@
 using System;
+using Terminal.Gui;
 using Xunit;
 
 namespace Terminal.Gui.TypeTests {

+ 1 - 0
UnitTests/UICatalog/ScenarioTests.cs

@@ -98,6 +98,7 @@ namespace UICatalog.Tests {
 			int stackSize = CreateInput ("");
 
 			Application.Init (new FakeDriver ());
+			Application.QuitKey = Key.CtrlMask | Key.Q; // Config manager may have set this to a different key
 
 			int iterations = 0;
 			Application.Iteration = () => {

+ 87 - 0
UnitTests/Views/TileViewTests.cs

@@ -2096,6 +2096,93 @@ namespace Terminal.Gui.ViewTests {
 		}
 
 
+		[Fact,AutoInitShutdown]
+		public void TestDisposal_NoEarlyDisposalsOfUsersViews_DuringRebuildForTileCount ()
+		{
+			var tv = GetTileView (20,10);
+
+			var myReusableView = new DisposeCounter ();
+
+			// I want my view in the first tile
+			tv.Tiles.ElementAt (0).ContentView.Add (myReusableView);
+			Assert.Equal (0, myReusableView.DisposalCount);
+
+			// I've changed my mind, I want 3 tiles now
+			tv.RebuildForTileCount (3);
+
+			// but I still want my view in the first tile
+			tv.Tiles.ElementAt (0).ContentView.Add (myReusableView);
+			Assert.Multiple (
+				()=>Assert.Equal (0, myReusableView.DisposalCount)
+				,()=> {
+					tv.Dispose ();
+					Assert.Equal (1, myReusableView.DisposalCount); 
+				});
+		}
+		[Fact, AutoInitShutdown]
+		public void TestDisposal_NoEarlyDisposalsOfUsersViews_DuringInsertTile ()
+		{
+			var tv = GetTileView (20, 10);
+
+			var myReusableView = new DisposeCounter ();
+
+			// I want my view in the first tile
+			tv.Tiles.ElementAt (0).ContentView.Add (myReusableView);
+			Assert.Equal (0, myReusableView.DisposalCount);
+
+			// I've changed my mind, I want 3 tiles now
+			tv.InsertTile (0);
+			tv.InsertTile (2);
+
+			// but I still want my view in the first tile
+			tv.Tiles.ElementAt (0).ContentView.Add (myReusableView);
+			Assert.Multiple (
+				() => Assert.Equal (0, myReusableView.DisposalCount)
+				, () => {
+					tv.Dispose ();
+
+					// TODO seems to be double disposed ?!
+					Assert.True (myReusableView.DisposalCount >= 1);
+				});
+		}
+		[Theory, AutoInitShutdown]
+		[InlineData(0)]
+		[InlineData (1)]
+		public void TestDisposal_NoEarlyDisposalsOfUsersViews_DuringRemoveTile(int idx)
+		{
+			var tv = GetTileView (20, 10);
+
+			var myReusableView = new DisposeCounter ();
+
+			// I want my view in the first tile
+			tv.Tiles.ElementAt (0).ContentView.Add (myReusableView);
+			Assert.Equal (0, myReusableView.DisposalCount);
+
+			tv.RemoveTile (idx);
+
+			// but I still want my view in the first tile
+			tv.Tiles.ElementAt (0).ContentView.Add (myReusableView);
+			Assert.Multiple (
+				() => Assert.Equal (0, myReusableView.DisposalCount)
+				, () => {
+					tv.Dispose ();
+
+					// TODO seems to be double disposed ?!
+					Assert.True (myReusableView.DisposalCount >= 1);
+				});
+		}
+
+		private class DisposeCounter : View
+		{
+			public int DisposalCount;
+			protected override void Dispose (bool disposing)
+			{
+				DisposalCount++;
+				base.Dispose (disposing);
+			}
+
+		}
+
 		/// <summary>
 		/// Creates a vertical orientation root container with left pane split into
 		/// two (with horizontal splitter line).

+ 110 - 0
docfx/articles/config.md

@@ -0,0 +1,110 @@
+# Configuration Management
+
+Terminal.Gui provides configuration and theme management for Terminal.Gui applications via the [`ConfigurationManager`](~/api/Terminal.Gui/Terminal.Gui.Configuration.
+
+1) **Settings**. Settings are applied to the [`Application`](~/api/Terminal.Gui/Terminal.Gui.Application.yml) class. Settings are accessed via the `Settings` property of the [`ConfigurationManager`](~/api/Terminal.Gui/Terminal.Gui.Configuration.ConfigurationManager.yml) class.
+2) **Themes**. Themes are a named collection of settings impacting how applications look. The default theme is named "Default". The built-in configuration stored within the Terminal.Gui library defines two additional themes: "Dark", and "Light". Additional themes can be defined in the configuration files.
+3) **AppSettings**. AppSettings allow applicaitons to use the  [`ConfigurationManager`](~/api/Terminal.Gui/Terminal.Gui.Configuration.ConfigurationManager.yml) to store and retrieve application-specific settings.
+
+The The [`ConfigurationManager`](~/api/Terminal.Gui/Terminal.Gui.Configuration.ConfigurationManager.yml) will look for configuration files in the `.tui` folder in the user's home directory (e.g. `C:/Users/username/.tui` or `/usr/username/.tui`), the folder where the Terminal.Gui application was launched from (e.g. `./.tui`), or as a resource within the Terminal.Gui application's main assembly.
+
+Settings that will apply to all applications (global settings) reside in files named config.json. Settings that will apply to a specific Terminal.Gui application reside in files named appname.config.json, where appname is the assembly name of the application (e.g. `UICatalog.config.json`).
+
+Settings are applied using the following precedence (higher precedence settings overwrite lower precedence settings):
+
+1. App specific settings found in the users's home directory (`~/.tui/appname.config.json`). -- Highest precedence.
+
+2. App specific settings found in the directory the app was launched from (`./.tui/appname.config.json`).
+
+3. App settings in app resources (`Resources/config.json`).
+
+4. Global settings found in the the user's home directory (`~/.tui/config.json`).
+
+5. Global settings found in the directory the app was launched from (`./.tui/config.json`).
+
+6. Default settings defined in the Terminal.Gui assembly -- Lowest precedence.
+
+The `UI Catalog` application provides an example of how to use the [`ConfigurationManager`](~/api/Terminal.Gui/Terminal.Gui.Configuration.ConfigurationManager.yml) class to load and save configuration files. The `Configuration Editor` scenario provides an editor that allows users to edit the configuration files. UI Catalog also uses a file system watcher to detect changes to the configuration files to tell [`ConfigurationManager`](~/api/Terminal.Gui/Terminal.Gui.Configuration.ConfigurationManager.yml) to reaload them; allowing users to change settings without having to restart the application.
+
+# What Can Be Configured
+
+## Settings
+
+Settings for the [`Application`](~/api/Terminal.Gui/Terminal.Gui.Application.yml) class.
+    * [QuitKey](~/api/Terminal.Gui/Terminal.Gui.Application.yml#QuitKey)
+    * [AlternateForwardKey](~/api/Terminal.Gui/Terminal.Gui.Application.yml#AlternateForwardKey)
+    * [AlternateBackwardKey](~/api/Terminal.Gui/Terminal.Gui.Application.yml#AlternateBackwardKey)
+    * [UseSystemConsole](~/api/Terminal.Gui/Terminal.Gui.Application.yml#UseSystemConsole)
+    * [IsMouseDisabled](~/api/Terminal.Gui/Terminal.Gui.Application.yml#IsMouseDisabled)
+    * [HeightAsBuffer](~/api/Terminal.Gui/Terminal.Gui.Application.yml#HeightAsBuffer)
+
+## Themes
+
+A Theme is a named collection of settings that impact the visual style of Terminal.Gui applications. The default theme is named "Default". The built-in configuration stored within the Terminal.Gui library defines two more themes: "Dark", and "Light". Additional themes can be defined in the configuration files. 
+
+The Json property `Theme` defines the name of the theme that will be used. If the theme is not found, the default theme will be used.
+
+Themes support defining ColorSchemes as well as various default settings for Views. Both the default color schemes and user defined color schemes can be configured. See [ColorSchemes](~/api/Terminal.Gui/Terminal.Gui.Colors.yml) for more information.
+
+# Example Configuration File
+
+```json
+{
+  "$schema": "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
+  "Application.QuitKey": {
+    "Key": "Esc"
+  },
+  "AppSettings": {
+    "UICatalog.StatusBar": false
+  },
+  "Theme": "UI Catalog Theme",
+  "Themes": [
+    {
+      "UI Catalog Theme": {
+        "ColorSchemes": [
+          {
+            "UI Catalog Scheme": {
+              "Normal": {
+                "Foreground": "White",
+                "Background": "Green"
+              },
+              "Focus": {
+                "Foreground": "Green",
+                "Background": "White"
+              },
+              "HotNormal": {
+                "Foreground": "Blue",
+                "Background": "Green"
+              },
+              "HotFocus": {
+                "Foreground": "BrightRed",
+                "Background": "White"
+              },
+              "Disabled": {
+                "Foreground": "BrightGreen",
+                "Background": "Gray"
+              }
+            }
+          },
+          {
+            "TopLevel": {
+              "Normal": {
+                "Foreground": "DarkGray",
+                "Background": "White"
+              ...
+              }
+            }
+          }
+        ],
+        "Dialog.DefaultEffect3D": false
+      }
+    }
+  ]
+}
+```
+
+# Configuration File Schema
+
+Settings are defined in JSON format, according to the schema found here: 
+
+https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json

+ 1 - 0
docfx/articles/index.md

@@ -5,5 +5,6 @@
 * [Keyboard Event Processing](keyboard.md)
 * [Event Processing and the Application Main Loop](mainloop.md)
 * [Cross-platform Driver Model](drivers.md)
+* [Configuration and Theme Manager](config.md)
 * [TableView Deep Dive](tableview.md)
 * [TreeView Deep Dive](treeview.md)

+ 14 - 5
docfx/build.ps1

@@ -1,11 +1,20 @@
 # Builds the Terminal.gui API documentation using docfx
 
-dotnet build --configuration Release ../Terminal.sln
+$prevPwd = $PWD; Set-Location -ErrorAction Stop -LiteralPath $PSScriptRoot
 
-rm ../docs -Recurse -Force -ErrorAction SilentlyContinue
+try {
+    $PWD  # output the current location 
 
-$env:DOCFX_SOURCE_BRANCH_NAME="main"
+    dotnet build --configuration Release ../Terminal.sln
 
-docfx --metadata
+    rm ../docs -Recurse -Force -ErrorAction SilentlyContinue
+
+    $env:DOCFX_SOURCE_BRANCH_NAME="main"
+
+    docfx --metadata --serve --force
+}
+finally {
+  # Restore the previous location.
+  $prevPwd | Set-Location
+}
 
-docfx --serve --force

+ 4 - 3
docfx/docfx.json

@@ -16,7 +16,7 @@
       "dest": "api/Terminal.Gui",
       "shouldSkipMarkup": true,
       "properties": {
-          "TargetFramework": "net6.0"
+          "TargetFramework": "net7.0"
       }
     },
     {
@@ -35,7 +35,7 @@
       "dest": "api/UICatalog",
       "shouldSkipMarkup": false,
       "properties": {
-          "TargetFramework": "net6.0"
+          "TargetFramework": "net7.0"
       }
     }
   ],
@@ -67,7 +67,8 @@
     "resource": [
       {
         "files": [
-          "images/**"
+          "images/**",
+          "schemas/**"
         ],
         "exclude": [
           "obj/**",

BIN
docfx/images/sample.gif


+ 1 - 0
docfx/index.md

@@ -14,6 +14,7 @@ A toolkit for building rich console apps for .NET, .NET Core, and Mono that work
 * [Keyboard Event Processing](~/articles/keyboard.md)
 * [Event Processing and the Application Main Loop](~/articles/mainloop.md)
 * [Cross-platform Driver Model](~/articles/drivers.md)
+* [Configuration and Theme Manager](~/articles/config.md)
 * [TableView Deep Dive](~/articles/tableview.md)
 * [TreeView Deep Dive](~/articles/treeview.md)
 

+ 296 - 0
docfx/schemas/tui-config-schema.json

@@ -0,0 +1,296 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "description": "The JSON schema for the Terminal.Gui Configuration Manager (https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json).",
+  "type": "object",
+  "properties": {
+    "Application.HeightAsBuffer": {
+      "description": "See HeightAsBuffer API documentation.",
+      "type": "boolean"
+    },
+    "Application.AlternateForwardKey": {
+      "description": "Alternative key for navigating forwards through views. SCtrl+Tab is the primary key.",
+      "$ref": "#/definitions/Key"
+    },
+    "Application.AlternateBackwardKey": {
+      "description": "Alternative key for navigating backwards through views. Shift+Ctrl+Tab is the primary key.",
+      "$ref": "#/definitions/Key"
+    },
+    "Application.QuitKey": {
+      "description": "The key to quit the application. Ctrl+Q is the default.",
+      "$ref": "#/definitions/Key"
+    },
+    "Application.IsMouseDisabled": {
+      "description": "Disable or enable the mouse. The mouse is enabled by default.",
+      "type": "boolean"
+    },
+    "Application.UseSystemConsole": {
+      "description": "If true, forces the use of the System.Console-based (aka NetDriver) driver. The default is false.",
+      "type": "boolean"
+    },
+    "Theme": {
+      "description": "The currently selected theme. The default is 'Default'.",
+      "type": "string"
+    },
+    "Themes": {
+      "description": "An array of Theme objects. Each Theme specifies a set of settings for an application. Set Theme to the name of the active theme.",
+      "type": "array",
+      "properties": {
+        "Themes": {
+          "$ref": "#/definitions/Theme"
+        }
+      },
+      "additionalProperties": {
+        "$ref": "#/definitions/ColorScheme"
+      }
+    }
+  },
+  "definitions": {
+    "Theme": {
+      "description": "A Theme is a collection of settings that are named.",
+      "type": "object",
+      "properties": {
+        "ColorSchemes": {
+          "description": "The ColorSchemes defined for this Theme.",
+          "$ref": "#/definitions/ColorSchemes"
+        }
+      }
+    },
+    "ColorSchemes": {
+      "description": "A list of ColorSchemes. Each ColorScheme specifies a set of Attributes (Foreground & Background).",
+      "type": "array",
+      "properties": {
+        "TopLevel": {
+          "$ref": "#/definitions/ColorScheme"
+        },
+        "Base": {
+          "$ref": "#/definitions/ColorScheme"
+        },
+        "Dialog": {
+          "$ref": "#/definitions/ColorScheme"
+        },
+        "Menu": {
+          "$ref": "#/definitions/ColorScheme"
+        },
+        "Error": {
+          "$ref": "#/definitions/ColorScheme"
+        }
+      },
+      "additionalProperties": {
+        "$ref": "#/definitions/ColorScheme"
+      }
+    },
+    "ColorScheme": {
+      "description": "A Terminal.Gui ColorScheme. Specifies the Foreground & Background colors for modes of an Terminal.Gui app.",
+      "type": "object",
+      "properties": {
+        "Normal": {
+          "description": "The foreground and background color for text when the view is not focused, hot, or disabled.",
+          "$ref": "#/definitions/Attribute"
+        },
+        "Focus": {
+          "description": "The foreground and background color for text when the view has focus.",
+          "$ref": "#/definitions/Attribute"
+        },
+        "HotNormal": {
+          "description": "The foreground and background color for text when the view is highlighted (hot).",
+          "$ref": "#/definitions/Attribute"
+        },
+        "HotFocus": {
+          "description": "The foreground and background color for text when the view is highlighted (hot) and has focus.",
+          "$ref": "#/definitions/Attribute"
+        },
+        "Disabled": {
+          "description": "The foreground and background color for text when the view disabled.",
+          "$ref": "#/definitions/Attribute"
+        }
+      }
+    },
+    "Attribute": {
+      "description": "A Terminal.Gui color attribute. Specifies the Foreground & Background colors for Terminal.Gui output.",
+      "type": "object",
+      "properties": {
+        "Foreground": {
+          "$ref": "#/definitions/Color"
+        },
+        "Background": {
+          "$ref": "#/definitions/Color"
+        }
+      },
+      "required": [
+        "Foreground",
+        "Background"
+      ]
+    },
+    "Color": {
+      "description": "One be either one of 16 standard color names or an rgb(r,g,b) tuple.",
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "type": "string",
+      "properties": {
+        "color": {
+          "oneOf": [
+            {
+              "type": "string",
+              "enum": [
+                "Black",
+                "Blue",
+                "Green",
+                "Cyan",
+                "Red",
+                "Magenta",
+                "Brown",
+                "Gray",
+                "DarkGray",
+                "BrightBlue",
+                "BrightGreen",
+                "BrightCyan",
+                "BrightRed",
+                "BrightMagenta",
+                "BrightYellow",
+                "White"
+              ]
+            },
+            {
+              "type": "string",
+              "pattern": "^rgb\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*\\)$"
+            }
+          ]
+        }
+      }
+    },
+    "Key": {
+      "description": "A key pressed on the keyboard.",
+      "type": "object",
+      "properties": {
+        "Key": {
+          "description": "A key name (e.g. A, b, 1, 2, Enter, Esc, F5, etc.) or an integer value (e.g. 65, 66, 67, etc.).",
+          "oneOf": [
+            {
+              "type": "string",
+              "enum": [
+                "Null",
+                "Backspace",
+                "Tab",
+                "Enter",
+                "Clear",
+                "Esc",
+                "Space",
+                "D0",
+                "D1",
+                "D2",
+                "D3",
+                "D4",
+                "D5",
+                "D6",
+                "D7",
+                "D8",
+                "D9",
+                "0",
+                "1",
+                "2",
+                "3",
+                "4",
+                "5",
+                "6",
+                "7",
+                "8",
+                "9",
+                "a",
+                "b",
+                "c",
+                "d",
+                "e",
+                "f",
+                "g",
+                "h",
+                "i",
+                "j",
+                "k",
+                "l",
+                "m",
+                "n",
+                "o",
+                "p",
+                "q",
+                "r",
+                "s",
+                "t",
+                "u",
+                "v",
+                "w",
+                "x",
+                "y",
+                "z",
+                "A",
+                "B",
+                "C",
+                "D",
+                "E",
+                "F",
+                "G",
+                "H",
+                "I",
+                "J",
+                "K",
+                "L",
+                "M",
+                "N",
+                "O",
+                "P",
+                "Q",
+                "R",
+                "S",
+                "T",
+                "U",
+                "V",
+                "W",
+                "X",
+                "Y",
+                "Z",
+                "F1",
+                "F2",
+                "F3",
+                "F4",
+                "F5",
+                "F6",
+                "F7",
+                "F8",
+                "F9",
+                "F10",
+                "F11",
+                "F12",
+                "Insert",
+                "Delete",
+                "Home",
+                "End",
+                "PageUp",
+                "PageDown",
+                "Up",
+                "Down",
+                "Left",
+                "Right"
+              ]
+            },
+            {
+              "type": "integer"
+            }
+          ]
+        },
+        "Modifiers": {
+          "description": "A keyboard modifier (e.g. Ctrl, Alt, or Shift).",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "enum": [
+              "Ctrl",
+              "Alt",
+              "Shift"
+            ]
+          }
+        }
+      },
+      "required": [
+        "Key"
+      ]
+    }
+  }
+}