Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
Override per-build via -p:TerminalGuiVersion=<x>; use -p:UseLocalTerminalGui=true
to build against the ../Terminal.Gui enlistment instead (see Directory.Build.targets).
-->
<TerminalGuiVersion Condition="'$(TerminalGuiVersion)' == ''">2.4.14-develop.2</TerminalGuiVersion>
<TerminalGuiVersion Condition="'$(TerminalGuiVersion)' == ''">2.4.15</TerminalGuiVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
243 changes: 195 additions & 48 deletions examples/ted/EditorSettings.cs
Original file line number Diff line number Diff line change
@@ -1,92 +1,154 @@
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);

if (appSettings.Count == 0)
{
root.Remove ("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
};

var directory = Path.GetDirectoryName (path);

Expand Down Expand Up @@ -137,4 +199,89 @@ 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;
}

if (bool.TryParse (value, out var parsed))
{
apply (parsed);

return;
}

Logging.Error ($"EditorSettings.Load: invalid legacy {SectionName}.{propertyName} value '{value}'.");
}

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

if (value is null)
{
return;
}

if (int.TryParse (value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
{
apply (parsed);

return;
}

Logging.Error ($"EditorSettings.Load: invalid legacy {SectionName}.{propertyName} value '{value}'.");
}

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
9 changes: 4 additions & 5 deletions examples/ted/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@

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);
// Load settings through Terminal.Gui's Microsoft.Extensions.Configuration builder
// (TuiConfigurationBuilder), applied before TedApp is constructed. Requires Terminal.Gui
// >= 2.4.15 (the TerminalGuiVersion pin); there is no ConfigurationManager fallback.
TerminalGuiConfigurationBootstrap.Apply ();

using IApplication app = Application.Create ();

Expand Down
15 changes: 15 additions & 0 deletions examples/ted/TerminalGuiConfigurationBootstrap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Terminal.Gui.Configuration;
using Terminal.Gui.Editor.Configuration;

namespace Ted;

internal static class TerminalGuiConfigurationBootstrap
{
internal static void Apply ()
{
TuiConfigurationBuilder builder = new ("ted");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate the TuiConfigurationBuilder call on the TG bump

When building ted with the repo's default TerminalGuiVersion pin (2.4.14-develop.2 in Directory.Build.props), this direct reference requires the new Terminal.Gui MEC API before the dependency has been bumped, so examples/ted and the solution fail to compile and the Program.cs comment's CM fallback can never run. Please either bump the Terminal.Gui pin in the same change or keep a real compatibility path, e.g. conditional/reflection fallback to ConfigurationManager, until the new API is available.

Useful? React with 👍 / 👎.

builder.ApplyToStaticFacades ();
EditorConfiguration.Apply (builder.Configuration);
EditorSettings.Apply (builder.Configuration);
}
}
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.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
<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