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

This commit is contained in:
Tig
2023-02-21 00:34:18 +13:00
committed by GitHub
parent eb90d6abff
commit 9425b2a720
56 changed files with 5103 additions and 809 deletions

2
.gitignore vendored
View File

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

View File

@@ -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

View File

@@ -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);
}
}
}
}

View File

@@ -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>

View File

@@ -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> {
}
}
}

View File

@@ -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 ();
}
}
}

View File

@@ -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}");
}
}
}
}

View File

@@ -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 ();
}
}
}

View File

@@ -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
// }
//}
}
}

View File

@@ -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 ();
}
}
}

View File

@@ -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 ();
}
}
}

View File

@@ -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 ();
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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"
}
}
}
]
}
}
]
}

View File

@@ -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>

View File

@@ -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));

View File

@@ -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;

View File

@@ -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;

View File

@@ -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.

View File

@@ -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;

View File

@@ -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 ()
{

View File

@@ -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

View File

@@ -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"
}
}
}
]
}
}
]
}

View File

@@ -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 (),

View File

@@ -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 ();
}
}
}

View File

@@ -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;
ConfigChanged ();
if (_colorScheme == null) {
ColorScheme = _colorScheme = Colors.Base;
}
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 sc in Colors.ColorSchemes) {
var item = new MenuItem ();
item.Title = $"_{sc.Key}";
item.Shortcut = Key.AltMask | (Key)sc.Key.Substring (0, 1) [0];
item.CheckType |= MenuItemCheckStyle.Radio;
item.Checked = sc.Value == _colorScheme;
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 += () => {
ColorScheme = _colorScheme = sc.Value;
SetNeedsDisplay ();
foreach (var menuItem in menuItems) {
menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _colorScheme;
}
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 {
Title = $"_{sc.Key}",
Data = sc.Key,
Shortcut = Key.AltMask | (Key)sc.Key [..1] [0]
};
item.CheckType |= MenuItemCheckStyle.Radio;
item.Checked = sc.Key == _topLevelColorScheme;
item.Action += () => {
_topLevelColorScheme = (string)item.Data;
foreach (var schemeMenuItem in schemeMenuItems) {
schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme;
}
ColorScheme = Colors.ColorSchemes [_topLevelColorScheme];
Application.Top.SetNeedsDisplay ();
};
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);
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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" });

View File

@@ -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 ()

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 ());
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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 [] {
"$$",

View File

@@ -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 ();
};

View File

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

View File

@@ -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 ();
};

View File

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

View File

@@ -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 = () => {

View File

@@ -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
docfx/articles/config.md Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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/**",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -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)

View File

@@ -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"
]
}
}
}