Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 176 additions & 48 deletions examples/ted/EditorSettings.cs
Original file line number Diff line number Diff line change
@@ -1,92 +1,149 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Configuration;
using Terminal.Gui.App;
using Terminal.Gui.Configuration;

namespace Ted;

#pragma warning disable CS0618 // Keep legacy CM attributes until Terminal.Gui fully removes CM.

/// <summary>
/// ted's persisted editor settings. These are real Terminal.Gui configuration properties
/// (<see cref="ConfigurationPropertyAttribute" />, <see cref="AppSettingsScope" />), so
/// <see cref="ConfigurationManager" /> is the single authority for <b>reading</b> them: enabling
/// CM (see <c>Program.cs</c>) loads <c>~/.tui/ted.config.json</c> and applies the values to these
/// static properties. ted does no parsing of its own.
/// ted's persisted editor settings. Microsoft.Extensions.Configuration is the primary read path:
/// startup loads <c>~/.tui/ted.config.json</c> and applies the values to these static properties
/// before <see cref="TedApp" /> is constructed. Legacy CM attributes are retained only so older
/// Terminal.Gui builds can still apply the previous <see cref="AppSettingsScope" /> format.
/// <para>
/// <see cref="Save(string)" /> is hand-rolled only because Terminal.Gui exposes no API for
/// writing a user config file. It emits the exact shape CM reads: app-defined
/// (<see cref="AppSettingsScope" />) properties live nested under a top-level
/// <c>"AppSettings"</c> object, keyed <c>DeclaringType.PropertyName</c>. Other top-level keys
/// a user may have added (e.g. <c>"Theme"</c>) are preserved; JSONC comments are not.
/// Any legacy flat root-level <c>"EditorSettings.*"</c> keys from the pre-CM format are
/// dropped on save (migration).
/// <see cref="Save(string)" /> writes the MEC-native shape:
/// <c>"EditorSettings": { "WordWrap": true }</c>. Other top-level keys a user may have added
/// are preserved; JSONC comments are not. Legacy flat root-level
/// <c>"EditorSettings.*"</c> keys and old CM <c>"AppSettings"</c> entries are dropped on save
/// once migrated.
/// </para>
/// </summary>
internal static class EditorSettings
{
internal const string SectionName = "EditorSettings";

[ConfigurationProperty (Scope = typeof (AppSettingsScope))]
public static bool LineNumbers { get; set; } = true;
public static bool LineNumbers
{
get => Defaults.LineNumbers;
set => Defaults.LineNumbers = value;
}

[ConfigurationProperty (Scope = typeof (AppSettingsScope))]
public static bool FoldIndicators { get; set; } = true;
public static bool FoldIndicators
{
get => Defaults.FoldIndicators;
set => Defaults.FoldIndicators = value;
}

[ConfigurationProperty (Scope = typeof (AppSettingsScope))]
public static bool WordWrap { get; set; }
public static bool WordWrap
{
get => Defaults.WordWrap;
set => Defaults.WordWrap = value;
}

[ConfigurationProperty (Scope = typeof (AppSettingsScope))]
public static bool ShowTabs { get; set; }
public static bool ShowTabs
{
get => Defaults.ShowTabs;
set => Defaults.ShowTabs = value;
}

[ConfigurationProperty (Scope = typeof (AppSettingsScope))]
public static int IndentSize { get; set; } = 4;
public static int IndentSize
{
get => Defaults.IndentSize;
set => Defaults.IndentSize = value;
}

[ConfigurationProperty (Scope = typeof (AppSettingsScope))]
public static bool ConvertTabsToSpaces { get; set; } = true;
public static bool ConvertTabsToSpaces
{
get => Defaults.ConvertTabsToSpaces;
set => Defaults.ConvertTabsToSpaces = value;
}

[ConfigurationProperty (Scope = typeof (AppSettingsScope))]
public static bool AutoIndent { get; set; } = true;
public static bool AutoIndent
{
get => Defaults.AutoIndent;
set => Defaults.AutoIndent = value;
}

[ConfigurationProperty (Scope = typeof (AppSettingsScope))]
public static bool Scrollbars { get; set; } = true;
public static bool Scrollbars
{
get => Defaults.Scrollbars;
set => Defaults.Scrollbars = value;
}

[ConfigurationProperty (Scope = typeof (AppSettingsScope))]
public static bool AutoComplete { get; set; }
public static bool AutoComplete
{
get => Defaults.AutoComplete;
set => Defaults.AutoComplete = value;
}

internal static EditorSettingsValues Defaults { get; set; } = new ();

internal static IConfiguration BuildConfiguration (string path)
{
return new ConfigurationBuilder ()
.AddJsonFile (path, true, false)
.Build ();
}

internal static void Load (string path)
{
Apply (BuildConfiguration (path));
}

internal static void Apply (IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull (configuration);

EditorSettingsValues settings = new ();
ApplyLegacyDottedKeys (configuration, settings);
ApplyLegacyDottedKeys (configuration.GetSection ("AppSettings"), settings);
configuration.GetSection (SectionName).Bind (settings);
Defaults = settings;
}

internal static void ResetDefaults ()
{
Defaults = new EditorSettingsValues ();
}

internal static void Save (string path)
{
try
{
JsonObject root = ReadRoot (path);

// Migration: drop legacy flat root-level "EditorSettings.*" keys (the pre-CM
// hand-rolled format). CM reads ted's settings only from the "AppSettings" object.
foreach (var legacyKey in root
.Where (kvp => kvp.Key.StartsWith ("EditorSettings.", StringComparison.Ordinal))
.Select (kvp => kvp.Key)
.ToList ())
{
root.Remove (legacyKey);
}

JsonObject appSettings;
RemoveLegacyDottedKeys (root);

if (root["AppSettings"] is JsonObject existing)
if (root["AppSettings"] is JsonObject appSettings)
{
appSettings = existing;
}
else
{
appSettings = new JsonObject ();
root["AppSettings"] = appSettings;
RemoveLegacyDottedKeys (appSettings);
}

appSettings["EditorSettings.LineNumbers"] = LineNumbers;
appSettings["EditorSettings.FoldIndicators"] = FoldIndicators;
appSettings["EditorSettings.WordWrap"] = WordWrap;
appSettings["EditorSettings.ShowTabs"] = ShowTabs;
appSettings["EditorSettings.IndentSize"] = IndentSize;
appSettings["EditorSettings.ConvertTabsToSpaces"] = ConvertTabsToSpaces;
appSettings["EditorSettings.AutoIndent"] = AutoIndent;
appSettings["EditorSettings.AutoComplete"] = AutoComplete;
appSettings["EditorSettings.Scrollbars"] = Scrollbars;
root[SectionName] = new JsonObject
{
[nameof (LineNumbers)] = LineNumbers,
[nameof (FoldIndicators)] = FoldIndicators,
[nameof (WordWrap)] = WordWrap,
[nameof (ShowTabs)] = ShowTabs,
[nameof (IndentSize)] = IndentSize,
[nameof (ConvertTabsToSpaces)] = ConvertTabsToSpaces,
[nameof (AutoIndent)] = AutoIndent,
[nameof (AutoComplete)] = AutoComplete,
[nameof (Scrollbars)] = Scrollbars
};
Comment on lines +140 to +151
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve unknown keys when rewriting EditorSettings section

Saving settings now replaces the entire EditorSettings object, so any unrecognized keys already present under that section (for example settings written by a newer ted version or another tool) are silently deleted on the next save. The previous save path updated known keys in place and preserved unrelated configuration, so this introduces avoidable config data loss during normal use.

Useful? React with 👍 / 👎.


var directory = Path.GetDirectoryName (path);

Expand Down Expand Up @@ -137,4 +194,75 @@ private static JsonObject ReadRoot (string path)

return node as JsonObject ?? new JsonObject ();
}

private static void ApplyLegacyDottedKeys (IConfiguration configuration, EditorSettingsValues settings)
{
ApplyLegacyBoolean (configuration, nameof (LineNumbers), value => settings.LineNumbers = value);
ApplyLegacyBoolean (configuration, nameof (FoldIndicators), value => settings.FoldIndicators = value);
ApplyLegacyBoolean (configuration, nameof (WordWrap), value => settings.WordWrap = value);
ApplyLegacyBoolean (configuration, nameof (ShowTabs), value => settings.ShowTabs = value);
ApplyLegacyInt32 (configuration, nameof (IndentSize), value => settings.IndentSize = value);
ApplyLegacyBoolean (configuration, nameof (ConvertTabsToSpaces), value => settings.ConvertTabsToSpaces = value);
ApplyLegacyBoolean (configuration, nameof (AutoIndent), value => settings.AutoIndent = value);
ApplyLegacyBoolean (configuration, nameof (Scrollbars), value => settings.Scrollbars = value);
ApplyLegacyBoolean (configuration, nameof (AutoComplete), value => settings.AutoComplete = value);
}

private static void ApplyLegacyBoolean (IConfiguration configuration, string propertyName, Action<bool> apply)
{
var value = configuration[$"{SectionName}.{propertyName}"];

if (value is null)
{
return;
}

apply (bool.Parse (value));
Comment thread
tig marked this conversation as resolved.
Outdated
}

private static void ApplyLegacyInt32 (IConfiguration configuration, string propertyName, Action<int> apply)
{
var value = configuration[$"{SectionName}.{propertyName}"];

if (value is null)
{
return;
}

apply (int.Parse (value, CultureInfo.InvariantCulture));
}
Comment thread
tig marked this conversation as resolved.

private static void RemoveLegacyDottedKeys (JsonObject json)
{
foreach (var legacyKey in json
.Where (kvp => kvp.Key.StartsWith ($"{SectionName}.", StringComparison.Ordinal))
.Select (kvp => kvp.Key)
.ToList ())
{
json.Remove (legacyKey);
}
}

internal sealed class EditorSettingsValues
{
public bool LineNumbers { get; set; } = true;

public bool FoldIndicators { get; set; } = true;

public bool WordWrap { get; set; }

public bool ShowTabs { get; set; }

public int IndentSize { get; set; } = 4;

public bool ConvertTabsToSpaces { get; set; } = true;

public bool AutoIndent { get; set; } = true;

public bool Scrollbars { get; set; } = true;

public bool AutoComplete { get; set; }
}
}

#pragma warning restore CS0618
7 changes: 2 additions & 5 deletions examples/ted/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@

using Ted;
using Terminal.Gui.App;
using Terminal.Gui.Configuration;

// ReSharper disable AccessToDisposedClosure

Hosting.ConfigureLogging ();
Hosting.EnableTracing ();

// ConfigurationManager is the single authority for reading settings: Enable (All) loads
// ~/.tui/ted.config.json (ConfigLocations.AppHome) and applies the values to the
// EditorSettings [ConfigurationProperty] statics before TedApp is constructed.
ConfigurationManager.Enable (ConfigLocations.All);
// Prefer Terminal.Gui's MEC builder when present; fall back to CM for released Terminal.Gui.
TerminalGuiConfigurationBootstrap.Apply ();

using IApplication app = Application.Create ();

Expand Down
64 changes: 64 additions & 0 deletions examples/ted/TerminalGuiConfigurationBootstrap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Terminal.Gui.Configuration;
using Terminal.Gui.Editor.Configuration;
using ConfigurationManager = Terminal.Gui.Configuration.ConfigurationManager;

namespace Ted;

#pragma warning disable CS0618 // ConfigurationManager is a compatibility fallback for released Terminal.Gui.

internal static class TerminalGuiConfigurationBootstrap
{
private const string TuiConfigurationBuilderTypeName =
"Terminal.Gui.Configuration.TuiConfigurationBuilder, Terminal.Gui";

internal static void Apply ()
{
if (TryApplyMec ())
{
return;
}

ConfigurationManager.Enable (ConfigLocations.All);
IConfiguration configuration = EditorSettings.BuildConfiguration (EditorSettings.GetConfigPath ());
EditorConfiguration.Apply (configuration);
EditorSettings.Apply (configuration);
Comment thread
tig marked this conversation as resolved.
Outdated
}

private static bool TryApplyMec ()
{
Type? builderType = Type.GetType (TuiConfigurationBuilderTypeName, false);

if (builderType is null)
{
return false;
}

var builder = Activator.CreateInstance (builderType, "ted")
?? throw new InvalidOperationException ($"Unable to create {builderType.FullName}.");

MethodInfo applyToStaticFacades = builderType.GetMethod ("ApplyToStaticFacades", Type.EmptyTypes)
?? throw new MissingMethodException (builderType.FullName,
"ApplyToStaticFacades");

applyToStaticFacades.Invoke (builder, null);

PropertyInfo configurationProperty = builderType.GetProperty ("Configuration")
?? throw new MissingMemberException (builderType.FullName,
"Configuration");

if (configurationProperty.GetValue (builder) is not IConfiguration configuration)
{
throw new InvalidOperationException (
$"{builderType.FullName}.Configuration did not return IConfiguration.");
}

EditorConfiguration.Apply (configuration);
EditorSettings.Apply (configuration);

return true;
Comment thread
tig marked this conversation as resolved.
Outdated
}
}

#pragma warning restore CS0618
2 changes: 2 additions & 0 deletions examples/ted/ted.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<ProjectReference Include="..\..\src\Terminal.Gui.Editor\Terminal.Gui.Editor.csproj" />
<PackageReference Include="Terminal.Gui" Version="$(TerminalGuiVersion)" />
<PackageReference Include="Serilog" Version="4.3.1" />
Expand Down
Loading
Loading