diff --git a/examples/ted/EditorSettings.cs b/examples/ted/EditorSettings.cs
new file mode 100644
index 0000000..69e828c
--- /dev/null
+++ b/examples/ted/EditorSettings.cs
@@ -0,0 +1,280 @@
+using System.Text.RegularExpressions;
+using Terminal.Gui.App;
+using Terminal.Gui.Configuration;
+
+namespace Ted;
+
+internal sealed class TedSettingsScope;
+
+internal static class EditorSettings
+{
+ [ConfigurationProperty (Scope = typeof (TedSettingsScope))]
+ public static bool LineNumbers { get; set; } = true;
+
+ [ConfigurationProperty (Scope = typeof (TedSettingsScope))]
+ public static bool FoldIndicators { get; set; } = true;
+
+ [ConfigurationProperty (Scope = typeof (TedSettingsScope))]
+ public static bool WordWrap { get; set; }
+
+ [ConfigurationProperty (Scope = typeof (TedSettingsScope))]
+ public static bool ShowTabs { get; set; }
+
+ [ConfigurationProperty (Scope = typeof (TedSettingsScope))]
+ public static bool UseThemeBackground { get; set; } = true;
+
+ [ConfigurationProperty (Scope = typeof (TedSettingsScope))]
+ public static int IndentSize { get; set; } = 4;
+
+ [ConfigurationProperty (Scope = typeof (TedSettingsScope))]
+ public static bool ConvertTabsToSpaces { get; set; } = true;
+
+ [ConfigurationProperty (Scope = typeof (TedSettingsScope))]
+ public static bool AutoIndent { get; set; } = true;
+
+ ///
+ /// Loads settings from the config file at .
+ /// Called once at startup before constructing .
+ ///
+ internal static void Load ()
+ {
+ Load (GetConfigPath ());
+ }
+
+ internal static void Load (string path)
+ {
+ if (!File.Exists (path))
+ {
+ return;
+ }
+
+ try
+ {
+ string text = File.ReadAllText (path);
+
+ LineNumbers = ReadBool (text, "EditorSettings.LineNumbers", LineNumbers);
+ FoldIndicators = ReadBool (text, "EditorSettings.FoldIndicators", FoldIndicators);
+ WordWrap = ReadBool (text, "EditorSettings.WordWrap", WordWrap);
+ ShowTabs = ReadBool (text, "EditorSettings.ShowTabs", ShowTabs);
+ UseThemeBackground = ReadBool (text, "EditorSettings.UseThemeBackground", UseThemeBackground);
+ IndentSize = ReadInt (text, "EditorSettings.IndentSize", IndentSize);
+ ConvertTabsToSpaces = ReadBool (text, "EditorSettings.ConvertTabsToSpaces", ConvertTabsToSpaces);
+ AutoIndent = ReadBool (text, "EditorSettings.AutoIndent", AutoIndent);
+ }
+ catch (Exception ex)
+ {
+ Logging.Error ($"EditorSettings.Load: {ex.GetType ().Name}: {ex.Message}");
+ }
+ }
+
+ internal static void Save ()
+ {
+ Save (GetConfigPath ());
+ }
+
+ internal static void Save (string path)
+ {
+ try
+ {
+ EnsureConfigFile (path);
+ string text = File.ReadAllText (path);
+ Dictionary entries = new ()
+ {
+ ["EditorSettings.LineNumbers"] = ToJson (LineNumbers),
+ ["EditorSettings.FoldIndicators"] = ToJson (FoldIndicators),
+ ["EditorSettings.WordWrap"] = ToJson (WordWrap),
+ ["EditorSettings.ShowTabs"] = ToJson (ShowTabs),
+ ["EditorSettings.UseThemeBackground"] = ToJson (UseThemeBackground),
+ ["EditorSettings.IndentSize"] = IndentSize.ToString (),
+ ["EditorSettings.ConvertTabsToSpaces"] = ToJson (ConvertTabsToSpaces),
+ ["EditorSettings.AutoIndent"] = ToJson (AutoIndent)
+ };
+
+ List toInsert = [];
+
+ foreach ((string key, string value) in entries)
+ {
+ Regex pattern = new (
+ $@"^(?\s*""{Regex.Escape (key)}""\s*:\s*)(?:true|false|-?\d+)(?\s*,?\s*(?://.*)?)$",
+ RegexOptions.Multiline);
+ bool replaced = false;
+ text = pattern.Replace (
+ text,
+ match =>
+ {
+ replaced = true;
+
+ return $"{match.Groups["prefix"].Value}{value}{match.Groups["suffix"].Value}";
+ },
+ 1);
+
+ if (replaced)
+ {
+ continue;
+ }
+
+ toInsert.Add ($" \"{key}\": {value}");
+ }
+
+ if (toInsert.Count > 0)
+ {
+ int lastBrace = FindRootClosingBrace (text);
+
+ if (lastBrace >= 0)
+ {
+ int insertCommaAfter = FindLastObjectMemberCharacterPosition (text, lastBrace);
+
+ if (insertCommaAfter >= 0 && text[insertCommaAfter] != ',' && text[insertCommaAfter] != '{')
+ {
+ text = text.Insert (insertCommaAfter + 1, ",");
+ lastBrace = FindRootClosingBrace (text);
+ }
+
+ string insertion = $"\n\n{string.Join (",\n", toInsert)}\n";
+ text = text.Insert (lastBrace, insertion);
+ }
+ }
+
+ File.WriteAllText (path, text);
+ }
+ catch (Exception ex)
+ {
+ Logging.Error ($"EditorSettings.Save: {ex.GetType ().Name}: {ex.Message}");
+ }
+ }
+
+ internal static string GetConfigPath ()
+ {
+ string home =
+ Environment.GetEnvironmentVariable ("HOME")
+ ?? Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)
+ ?? Directory.GetCurrentDirectory ();
+
+ return Path.Combine (home, ".tui", "ted.config.json");
+ }
+
+ private static void EnsureConfigFile (string path)
+ {
+ string? directory = Path.GetDirectoryName (path);
+
+ if (!string.IsNullOrWhiteSpace (directory))
+ {
+ Directory.CreateDirectory (directory);
+ }
+
+ if (!File.Exists (path))
+ {
+ File.WriteAllText (path, "{}");
+ }
+ }
+
+ private static string ToJson (bool value)
+ {
+ return value ? "true" : "false";
+ }
+
+ private static bool ReadBool (string json, string key, bool defaultValue)
+ {
+ // Match key only at a JSON property position: line starts with optional whitespace,
+ // then the key. Negative lookahead skips // comment lines.
+ Match m = Regex.Match (
+ json,
+ $@"^(?!\s*//)\s*""{Regex.Escape (key)}""\s*:\s*(?true|false)",
+ RegexOptions.IgnoreCase | RegexOptions.Multiline);
+
+ return m.Success ? string.Equals (m.Groups["v"].Value, "true", StringComparison.OrdinalIgnoreCase) : defaultValue;
+ }
+
+ private static int ReadInt (string json, string key, int defaultValue)
+ {
+ Match m = Regex.Match (
+ json,
+ $@"^(?!\s*//)\s*""{Regex.Escape (key)}""\s*:\s*(?-?\d+)",
+ RegexOptions.Multiline);
+
+ return m.Success && int.TryParse (m.Groups["v"].Value, out int v) ? v : defaultValue;
+ }
+
+ ///
+ /// Finds the last '}' that is NOT inside a // comment.
+ /// Scans backwards, skipping any '}' on a line whose non-whitespace content starts with //.
+ ///
+ private static int FindRootClosingBrace (string text)
+ {
+ int i = text.Length - 1;
+
+ while (i >= 0)
+ {
+ i = text.LastIndexOf ('}', i);
+
+ if (i < 0)
+ {
+ return -1;
+ }
+
+ // Check if this '}' is on a comment line
+ int lineStart = text.LastIndexOf ('\n', i) + 1;
+ string lineBeforeBrace = text[lineStart..i];
+
+ if (lineBeforeBrace.TrimStart ().StartsWith ("//", StringComparison.Ordinal))
+ {
+ i--;
+
+ continue;
+ }
+
+ return i;
+ }
+
+ return -1;
+ }
+
+ private static int FindLastObjectMemberCharacterPosition (string text, int braceIndex)
+ {
+ int i = braceIndex - 1;
+
+ while (i >= 0)
+ {
+ char c = text[i];
+
+ if (char.IsWhiteSpace (c))
+ {
+ i--;
+
+ continue;
+ }
+
+ int lineStart = text.LastIndexOf ('\n', i) + 1;
+ string line = text[lineStart..(i + 1)];
+ string trimmedLine = line.TrimStart ();
+
+ if (trimmedLine.StartsWith ("//", StringComparison.Ordinal))
+ {
+ i = lineStart - 1;
+
+ continue;
+ }
+
+ int commentStart = line.IndexOf ("//", StringComparison.Ordinal);
+
+ if (commentStart >= 0)
+ {
+ string withoutComment = line[..commentStart];
+ int lastNonWhitespace = withoutComment.TrimEnd ().Length - 1;
+
+ if (lastNonWhitespace >= 0)
+ {
+ return lineStart + lastNonWhitespace;
+ }
+
+ i = lineStart - 1;
+
+ continue;
+ }
+
+ return i;
+ }
+
+ return -1;
+ }
+}
diff --git a/examples/ted/EditorSettingsDialog.cs b/examples/ted/EditorSettingsDialog.cs
new file mode 100644
index 0000000..4287cab
--- /dev/null
+++ b/examples/ted/EditorSettingsDialog.cs
@@ -0,0 +1,121 @@
+using Terminal.Gui.App;
+using Terminal.Gui.Editor;
+using Terminal.Gui.Input;
+using Terminal.Gui.Text.Indentation;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+
+namespace Ted;
+
+internal sealed class EditorSettingsDialog : Dialog
+{
+ private readonly NumericUpDown _indentSize;
+ private readonly CheckBox _convertTabsCheck;
+ private readonly CheckBox _autoIndentCheck;
+
+ internal bool WasAccepted { get; private set; }
+
+ internal EditorSettingsDialog (Editor editor)
+ {
+ Title = "Settings";
+ Width = Dim.Percent (60);
+ Height = 16;
+
+ View tabSettingsTab = new ()
+ {
+ Title = "_Tab Settings",
+ Width = Dim.Fill (),
+ Height = Dim.Fill ()
+ };
+
+ _indentSize = new ()
+ {
+ X = 20,
+ Y = 1,
+ Value = editor.IndentationSize,
+ Width = 8
+ };
+ _indentSize.ValueChanging += (_, e) =>
+ {
+ if (e.NewValue is < 1)
+ {
+ e.Handled = true;
+ }
+ };
+
+ _convertTabsCheck = new ()
+ {
+ X = 1,
+ Y = 3,
+ Title = "Con_vert Tabs to Spaces",
+ Value = editor.ConvertTabsToSpaces ? CheckState.Checked : CheckState.UnChecked
+ };
+
+ _autoIndentCheck = new ()
+ {
+ X = 1,
+ Y = 5,
+ Title = "_Auto Indent",
+ Value = editor.IndentationStrategy is not null ? CheckState.Checked : CheckState.UnChecked
+ };
+
+ tabSettingsTab.Add (
+ new Label () { X = 1, Y = 1, Text = "_Indent size:" },
+ _indentSize,
+ _convertTabsCheck,
+ _autoIndentCheck);
+
+ View configTab = new ()
+ {
+ Title = "_Config",
+ Width = Dim.Fill (),
+ Height = Dim.Fill ()
+ };
+
+ configTab.Add (new Label () { X = 1, Y = 1, Text = "No settings yet." });
+
+ Tabs tabs = new ()
+ {
+ X = 0,
+ Y = 0,
+ Width = Dim.Fill (),
+ Height = Dim.Fill (2)
+ };
+
+ tabs.InsertTab (0, configTab);
+ tabs.InsertTab (1, tabSettingsTab);
+
+ Button okBtn = new ()
+ {
+ Text = "OK",
+ X = Pos.Center () - 6,
+ Y = Pos.Bottom (tabs),
+ IsDefault = true
+ };
+
+ Button cancelBtn = new ()
+ {
+ Text = "Cancel",
+ X = Pos.Right (okBtn) + 2,
+ Y = Pos.Bottom (tabs)
+ };
+
+ okBtn.Accepting += (_, _) =>
+ {
+ WasAccepted = true;
+ RequestStop ();
+ };
+
+ cancelBtn.Accepting += (_, _) => RequestStop ();
+ Add (tabs, okBtn, cancelBtn);
+ }
+
+ internal void ApplyTo (Editor editor)
+ {
+ editor.IndentationSize = Math.Max (1, _indentSize.Value);
+ editor.ConvertTabsToSpaces = _convertTabsCheck.Value == CheckState.Checked;
+ editor.IndentationStrategy = _autoIndentCheck.Value == CheckState.Checked
+ ? new DefaultIndentationStrategy ()
+ : null;
+ }
+}
diff --git a/examples/ted/Program.cs b/examples/ted/Program.cs
index dd2301a..f209413 100644
--- a/examples/ted/Program.cs
+++ b/examples/ted/Program.cs
@@ -10,6 +10,7 @@
Hosting.EnableTracing ();
ConfigurationManager.Enable (ConfigLocations.All);
+EditorSettings.Load ();
using IApplication app = Application.Create ();
diff --git a/examples/ted/TedApp.MarkdownPreview.cs b/examples/ted/TedApp.MarkdownPreview.cs
index 7ecbb01..3397a2c 100644
--- a/examples/ted/TedApp.MarkdownPreview.cs
+++ b/examples/ted/TedApp.MarkdownPreview.cs
@@ -10,7 +10,7 @@ public sealed partial class TedApp
private Markdown? _markdownPreview;
private bool _syncingScroll;
- /// The status-bar checkbox that toggles the Markdown preview pane.
+ /// Toggle state used by the View menu item that shows or hides the Markdown preview pane.
public CheckBox PreviewCheckBox { get; } = new ()
{
AllowCheckStateNone = false,
@@ -30,6 +30,7 @@ internal void UpdatePreviewVisibility ()
{
var isMd = IsMarkdownFile;
PreviewCheckBox.Visible = isMd;
+ _previewMarkdownMenuItem.Enabled = isMd;
if (!isMd && _markdownPreview is not null)
{
@@ -41,6 +42,10 @@ internal void UpdatePreviewVisibility ()
// Document was swapped while preview was open — refresh content and re-hook.
RefreshPreviewDocument ();
}
+
+ _previewMarkdownMenuItem.Title = ToggleTitle (
+ PreviewCheckBox.Value == CheckState.Checked,
+ "_Preview Markdown");
}
private void ToggleMarkdownPreview ()
@@ -70,7 +75,8 @@ private void ShowMarkdownPreview ()
Height = Editor.Height,
Text = Editor.Document?.Text ?? string.Empty,
ViewportSettings = ViewportSettingsFlags.HasScrollBars,
- SyntaxHighlighter = new TextMateSyntaxHighlighter (ThemeName.DarkPlus)
+ SyntaxHighlighter = new TextMateSyntaxHighlighter (ThemeName.DarkPlus),
+ UseThemeBackground = Editor.UseThemeBackground
};
// Editor takes the left half, preview takes the right half.
diff --git a/examples/ted/TedApp.cs b/examples/ted/TedApp.cs
index 35194f4..ba9348d 100644
--- a/examples/ted/TedApp.cs
+++ b/examples/ted/TedApp.cs
@@ -22,6 +22,7 @@ public sealed partial class TedApp : Window
{
private readonly BraceFoldingStrategy _braceFoldingStrategy;
private readonly Shortcut _fileNameShortcut;
+ private readonly MenuItem _previewMarkdownMenuItem;
/// Initializes a new .
public TedApp (bool readOnly = false)
@@ -33,12 +34,30 @@ public TedApp (bool readOnly = false)
// Editor's KeyBindings (any commands the editor doesn't claim fall back to Application).
Editor = new Editor
{
- GutterOptions = GutterOptions.LineNumbers | GutterOptions.Folding,
- ConvertTabsToSpaces = true,
+ ConvertTabsToSpaces = EditorSettings.ConvertTabsToSpaces,
+ IndentationSize = EditorSettings.IndentSize,
+ WordWrap = EditorSettings.WordWrap,
+ ShowTabs = EditorSettings.ShowTabs,
+ UseThemeBackground = EditorSettings.UseThemeBackground,
ReadOnly = readOnly,
ViewportSettings = ViewportSettingsFlags.HasScrollBars
};
+ GutterOptions initialGutter = GutterOptions.None;
+
+ if (EditorSettings.LineNumbers)
+ {
+ initialGutter |= GutterOptions.LineNumbers;
+ }
+
+ if (EditorSettings.FoldIndicators)
+ {
+ initialGutter |= GutterOptions.Folding;
+ }
+
+ Editor.GutterOptions = initialGutter;
+ Editor.IndentationStrategy = EditorSettings.AutoIndent ? new DefaultIndentationStrategy () : null;
+
// Enable brace-based folding. The strategy re-scans on each document change.
_braceFoldingStrategy = new BraceFoldingStrategy ();
InstallFolding ();
@@ -68,34 +87,6 @@ public TedApp (bool readOnly = false)
Value = Editor.GutterOptions.HasFlag (GutterOptions.Folding) ? CheckState.Checked : CheckState.UnChecked
};
- CheckBox convertTabsToSpacesCheckBox = new ()
- {
- AllowCheckStateNone = false,
- CanFocus = false,
- Text = "_Convert Tabs To Spaces",
- Value = Editor.ConvertTabsToSpaces ? CheckState.Checked : CheckState.UnChecked
- };
-
- convertTabsToSpacesCheckBox.ValueChanged += (_, e) =>
- {
- Editor.ConvertTabsToSpaces = e.NewValue == CheckState.Checked;
- };
-
- CheckBox autoIndentCheckBox = new ()
- {
- AllowCheckStateNone = false,
- CanFocus = false,
- Text = "_Auto Indent",
- Value = Editor.IndentationStrategy is not null ? CheckState.Checked : CheckState.UnChecked
- };
-
- autoIndentCheckBox.ValueChanged += (_, e) =>
- {
- Editor.IndentationStrategy = e.NewValue == CheckState.Checked
- ? new DefaultIndentationStrategy ()
- : null;
- };
-
CheckBox useThemeBackgroundCheckBox = new ()
{
AllowCheckStateNone = false,
@@ -111,52 +102,32 @@ public TedApp (bool readOnly = false)
Text = "_Word Wrap",
Value = Editor.WordWrap ? CheckState.Checked : CheckState.UnChecked
};
-
- wordWrapCheckBox.ValueChanged += (_, e) =>
- {
- Editor.WordWrap = e.NewValue == CheckState.Checked;
- };
-
- LanguageShortcut = new Shortcut (Key.Empty, "Plain Text", null) { MouseHighlightStates = MouseState.None };
-
- IndentationSizeUpDown = new NumericUpDown
+ _previewMarkdownMenuItem = new MenuItem
{
- Value = Editor.IndentationSize,
- Increment = 1
+ Title = ToggleTitle (false, "_Preview Markdown"),
+ Enabled = false
};
-
- IndentationSizeUpDown.ValueChanged += (_, e) =>
+ _previewMarkdownMenuItem.Action = () =>
{
- if (Editor.IndentationSize == e.NewValue)
+ if (PreviewCheckBox.Visible)
{
- return;
+ PreviewCheckBox.Value = PreviewCheckBox.Value == CheckState.Checked
+ ? CheckState.UnChecked
+ : CheckState.Checked;
}
-
- Editor.IndentationSize = e.NewValue;
- };
-
- ShowTabsCheckBox = new CheckBox
- {
- AllowCheckStateNone = false,
- CanFocus = false,
- Title = "↹",
- Value = Editor.ShowTabs ? CheckState.Checked : CheckState.UnChecked
};
- ShowTabsCheckBox.ValueChanged += (_, e) =>
+ LanguageShortcut = new Shortcut (Key.Empty, "Plain Text", null) { MouseHighlightStates = MouseState.None };
+ ShowTabsCheckBox.Value = Editor.ShowTabs ? CheckState.Checked : CheckState.UnChecked;
+ PreviewCheckBox.ValueChanged += (_, e) =>
{
- Editor.ShowTabs = e.NewValue == CheckState.Checked;
+ ToggleMarkdownPreview ();
+ _previewMarkdownMenuItem.Title = ToggleTitle (e.NewValue == CheckState.Checked, "_Preview Markdown");
};
- PreviewCheckBox.ValueChanged += (_, _) => ToggleMarkdownPreview ();
-
StatusBar statusBar =
new ([
new Shortcut { Title = "Language", CommandView = LanguageShortcut },
- new Shortcut
- { Text = "Indent", CommandView = IndentationSizeUpDown, MouseHighlightStates = MouseState.None },
- new Shortcut { CommandView = ShowTabsCheckBox },
- new Shortcut { CommandView = PreviewCheckBox },
LocShortcut = new Shortcut (Key.Empty, FormatLoc (1, 1), null)
{ MouseHighlightStates = MouseState.None }
])
@@ -189,13 +160,15 @@ public TedApp (bool readOnly = false)
new MenuItem { Command = Command.Quit, Action = Quit, Key = KeyFor (Command.Quit) }
]),
new MenuBarItem (Strings.menuEdit, CreateEditMenuItems ()),
- new MenuBarItem ("_Options",
+ new MenuBarItem ("_View",
[
new MenuItem
{
Action = () =>
{
- if (lineNumbersCheckBox.Value == CheckState.Checked)
+ bool shouldEnableLineNumbers = !Editor.GutterOptions.HasFlag (GutterOptions.LineNumbers);
+
+ if (shouldEnableLineNumbers)
{
Editor.GutterOptions |= GutterOptions.LineNumbers;
}
@@ -204,7 +177,9 @@ public TedApp (bool readOnly = false)
Editor.GutterOptions &= ~GutterOptions.LineNumbers;
}
+ lineNumbersCheckBox.Value = shouldEnableLineNumbers ? CheckState.Checked : CheckState.UnChecked;
Editor.SetNeedsDraw ();
+ SaveViewSettings ();
},
CommandView = lineNumbersCheckBox,
HelpText = "Show line numbers"
@@ -213,7 +188,9 @@ public TedApp (bool readOnly = false)
{
Action = () =>
{
- if (foldIndicatorsCheckBox.Value == CheckState.Checked)
+ bool shouldEnableFoldIndicators = !Editor.GutterOptions.HasFlag (GutterOptions.Folding);
+
+ if (shouldEnableFoldIndicators)
{
Editor.GutterOptions |= GutterOptions.Folding;
}
@@ -222,36 +199,59 @@ public TedApp (bool readOnly = false)
Editor.GutterOptions &= ~GutterOptions.Folding;
}
+ foldIndicatorsCheckBox.Value = shouldEnableFoldIndicators ? CheckState.Checked : CheckState.UnChecked;
Editor.SetNeedsDraw ();
+ SaveViewSettings ();
},
CommandView = foldIndicatorsCheckBox,
HelpText = "Show fold indicators in the gutter"
},
new MenuItem
{
- CommandView = convertTabsToSpacesCheckBox,
- HelpText = "Insert spaces when Tab is pressed"
- },
- new MenuItem
- {
- CommandView = autoIndentCheckBox,
- HelpText = "Copy indentation from the previous line on Enter"
+ Action = () =>
+ {
+ bool wordWrapEnabled = !Editor.WordWrap;
+ Editor.WordWrap = wordWrapEnabled;
+ wordWrapCheckBox.Value = wordWrapEnabled ? CheckState.Checked : CheckState.UnChecked;
+ SaveViewSettings ();
+ },
+ CommandView = wordWrapCheckBox,
+ HelpText = "Soft-wrap long lines at viewport edge"
},
new MenuItem
{
Action = () =>
{
- Editor.UseThemeBackground = useThemeBackgroundCheckBox.Value == CheckState.Checked;
+ bool useThemeBackgroundEnabled = !Editor.UseThemeBackground;
+ Editor.UseThemeBackground = useThemeBackgroundEnabled;
+ useThemeBackgroundCheckBox.Value = useThemeBackgroundEnabled ? CheckState.Checked : CheckState.UnChecked;
+
+ if (_markdownPreview is not null)
+ {
+ _markdownPreview.UseThemeBackground = Editor.UseThemeBackground;
+ }
+
+ SaveViewSettings ();
},
CommandView = useThemeBackgroundCheckBox,
HelpText = "Use theme background for highlighted text"
},
new MenuItem
{
- CommandView = wordWrapCheckBox,
- HelpText = "Soft-wrap long lines at viewport edge"
- }
+ Action = () =>
+ {
+ bool showTabsEnabled = !Editor.ShowTabs;
+ Editor.ShowTabs = showTabsEnabled;
+ ShowTabsCheckBox.Value = showTabsEnabled ? CheckState.Checked : CheckState.UnChecked;
+ SaveViewSettings ();
+ },
+ CommandView = ShowTabsCheckBox,
+ HelpText = "Show tab glyphs"
+ },
+ _previewMarkdownMenuItem
]),
+ new MenuBarItem ("_Options",
+ [new MenuItem ("_Settings...", string.Empty, ShowSettingsDialog)]),
new MenuBarItem (Strings.menuHelp,
[new MenuItem ("_About", "About ted", ShowAboutDialog)]),
_fileNameShortcut = new Shortcut (Key.Empty, "", Open)
@@ -281,11 +281,13 @@ [new MenuItem ("_About", "About ted", ShowAboutDialog)]),
/// The status-bar shortcut that displays the current syntax-highlighting language name.
public Shortcut LanguageShortcut { get; }
- /// The indentation-size selector shown in the status bar.
- public NumericUpDown IndentationSizeUpDown { get; }
-
- /// The status-bar checkbox that toggles visible tab glyphs.
- public CheckBox ShowTabsCheckBox { get; }
+ /// The settings checkbox state for visible tab glyphs.
+ public CheckBox ShowTabsCheckBox { get; } = new ()
+ {
+ AllowCheckStateNone = false,
+ CanFocus = false,
+ Title = "Show _Tabs"
+ };
///
/// The status-bar shortcut that mirrors the editor's caret position. Both line and column are
@@ -384,7 +386,44 @@ private void UpdateLocShortcut ()
private static string FormatLoc (int line, int column)
{
- return $"Ln: {line}, Ch: {column}";
+ return $"Ln {line}, Col {column}";
+ }
+
+ private static string ToggleTitle (bool on, string label)
+ {
+ return on ? $"✓ {label}" : $" {label}";
+ }
+
+ private void SaveViewSettings ()
+ {
+ EditorSettings.LineNumbers = Editor.GutterOptions.HasFlag (GutterOptions.LineNumbers);
+ EditorSettings.FoldIndicators = Editor.GutterOptions.HasFlag (GutterOptions.Folding);
+ EditorSettings.WordWrap = Editor.WordWrap;
+ EditorSettings.ShowTabs = Editor.ShowTabs;
+ EditorSettings.UseThemeBackground = Editor.UseThemeBackground;
+ EditorSettings.IndentSize = Editor.IndentationSize;
+ EditorSettings.ConvertTabsToSpaces = Editor.ConvertTabsToSpaces;
+ EditorSettings.AutoIndent = Editor.IndentationStrategy is not null;
+ EditorSettings.Save ();
+ }
+
+ private void ShowSettingsDialog ()
+ {
+ if (App is null)
+ {
+ throw new InvalidOperationException ("ted must be running before showing settings.");
+ }
+
+ EditorSettingsDialog dialog = new (Editor);
+ App.Run (dialog);
+
+ if (dialog.WasAccepted)
+ {
+ dialog.ApplyTo (Editor);
+ SaveViewSettings ();
+ }
+
+ dialog.Dispose ();
}
///
diff --git a/examples/ted/ted.csproj b/examples/ted/ted.csproj
index 30a4e96..9086d64 100644
--- a/examples/ted/ted.csproj
+++ b/examples/ted/ted.csproj
@@ -7,6 +7,10 @@
false
+
+
+
+
diff --git a/tests/Terminal.Gui.Editor.IntegrationTests/TedAppTests.cs b/tests/Terminal.Gui.Editor.IntegrationTests/TedAppTests.cs
index 89bd695..2a7178a 100644
--- a/tests/Terminal.Gui.Editor.IntegrationTests/TedAppTests.cs
+++ b/tests/Terminal.Gui.Editor.IntegrationTests/TedAppTests.cs
@@ -4,6 +4,7 @@
using Ted;
using Terminal.Gui.Editor.IntegrationTests.Testing;
using Terminal.Gui.Input;
+using Terminal.Gui.Text.Indentation;
using Terminal.Gui.Testing;
using Terminal.Gui.Editor;
using Terminal.Gui.Views;
@@ -304,11 +305,11 @@ public async Task Renders_OptionsMenu_Header ()
}
[Fact]
- public async Task Renders_Indent_Size_StatusBar_Item ()
+ public async Task Renders_ViewMenu_Header ()
{
await using AppFixture fx = new (() => new TedApp ());
- DriverAssert.ContentsContains (fx.Driver, "Indent");
+ DriverAssert.ContentsContains (fx.Driver, "View");
}
[Fact]
@@ -323,12 +324,12 @@ public async Task Constructor_Defaults_To_Plain_Text_Highlighting ()
[Fact]
public async Task Highlighting_Auto_Detects_From_File_Extension ()
{
- var filePath = Path.Combine (Path.GetTempPath (), $"ted-highlight-{Guid.NewGuid ():N}.xml");
+ var tempXmlFilePath = Path.Combine (Path.GetTempPath (), $"ted-highlight-{Guid.NewGuid ():N}.xml");
try
{
await using AppFixture fx = new (() => new TedApp ());
- fx.Top.OpenMissingFile (filePath);
+ fx.Top.OpenMissingFile (tempXmlFilePath);
Assert.NotNull (fx.Top.Editor.HighlightingDefinition);
Assert.Equal ("XML", fx.Top.Editor.HighlightingDefinition!.Name);
@@ -336,36 +337,44 @@ public async Task Highlighting_Auto_Detects_From_File_Extension ()
}
finally
{
- DeleteIfExists (filePath);
+ DeleteIfExists (tempXmlFilePath);
}
}
[Fact]
- public async Task IndentationSize_StatusBar_NumericUpDown_Changes_Editor_IndentationSize ()
+ public async Task OptionsMenu_Contains_Settings_Item ()
{
await using AppFixture fx = new (() => new TedApp ());
- fx.Top.IndentationSizeUpDown.Value = 8;
+ InputInjectionOptions options = new () { Mode = InputInjectionMode.Direct };
+ fx.Injector.InjectKey (Key.O.WithAlt, options);
+ fx.Render ();
- Assert.Equal (8, fx.Top.Editor.IndentationSize);
+ DriverAssert.ContentsContains (fx.Driver, "Settings...");
}
[Fact]
- public async Task ShowTabs_StatusBar_CheckBox_Changes_Editor_ShowTabs ()
+ public void Constructor_ReadOnly_Sets_Editor_ReadOnly ()
{
- await using AppFixture fx = new (() => new TedApp ());
+ TedApp app = new (true);
- fx.Top.ShowTabsCheckBox.Value = CheckState.Checked;
+ Assert.True (app.Editor.ReadOnly);
+ }
- Assert.True (fx.Top.Editor.ShowTabs);
+ [Fact]
+ public void Constructor_Defaults_UseThemeBackground_To_True ()
+ {
+ TedApp app = new ();
+
+ Assert.True (app.Editor.UseThemeBackground);
}
[Fact]
- public void Constructor_ReadOnly_Sets_Editor_ReadOnly ()
+ public void Constructor_Defaults_AutoIndent_To_Enabled ()
{
- TedApp app = new (true);
+ TedApp app = new ();
- Assert.True (app.Editor.ReadOnly);
+ Assert.IsType (app.Editor.IndentationStrategy);
}
[Fact]
@@ -373,7 +382,7 @@ public async Task Loc_StatusBar_Shortcut_Initially_Shows_Line_1_Column_1 ()
{
await using AppFixture fx = new (() => new TedApp ());
- Assert.Equal ("Ln: 1, Ch: 1", fx.Top.LocShortcut.Title);
+ Assert.Equal ("Ln 1, Col 1", fx.Top.LocShortcut.Title);
}
[Fact]
@@ -390,7 +399,7 @@ public async Task Loc_StatusBar_Shortcut_Tracks_Caret_Movement ()
// Caret at offset 8 → "beta": line 2, column 3 ('t').
fx.Top.Editor.CaretOffset = 8;
- Assert.Equal ("Ln: 2, Ch: 3", fx.Top.LocShortcut.Title);
+ Assert.Equal ("Ln 2, Col 3", fx.Top.LocShortcut.Title);
}
[Fact]
@@ -409,7 +418,7 @@ public async Task Loc_StatusBar_Shortcut_Updates_When_Document_Edit_Shifts_Caret
// Inserting before the caret shifts it to the right; the loc shortcut must follow.
fx.Top.Editor.Document!.Insert (0, ">>>");
- Assert.Equal ("Ln: 1, Ch: 5", fx.Top.LocShortcut.Title);
+ Assert.Equal ("Ln 1, Col 5", fx.Top.LocShortcut.Title);
}
[Fact]
@@ -428,25 +437,25 @@ public async Task FileMenu_OpensViaKeyboard_AltF ()
}
[Fact]
- public async Task OptionsMenu_TogglesLineNumbers_ViaKeyboard ()
+ public async Task ViewMenu_TogglesLineNumbers_ViaKeyboard ()
{
await using AppFixture fx = new (() => new TedApp ());
Assert.True (fx.Top.Editor.GutterOptions.HasFlag (GutterOptions.LineNumbers));
InputInjectionOptions options = new () { Mode = InputInjectionMode.Direct };
- fx.Injector.InjectKey (Key.O.WithAlt, options);
+ fx.Injector.InjectKey (Key.V.WithAlt, options);
fx.Render ();
DriverAssert.ContentsContains (fx.Driver, "Line Numbers");
DriverAssert.ContentsContains (fx.Driver, "☒ Line Numbers");
- DriverAssert.ContentsContains (fx.Driver, "Convert Tabs To Spaces");
+ DriverAssert.ContentsContains (fx.Driver, "Show Tabs");
fx.Injector.InjectKey (Key.Enter, options);
fx.Render ();
Assert.False (fx.Top.Editor.GutterOptions.HasFlag (GutterOptions.LineNumbers));
- fx.Injector.InjectKey (Key.O.WithAlt, options);
+ fx.Injector.InjectKey (Key.V.WithAlt, options);
fx.Render ();
DriverAssert.ContentsContains (fx.Driver, "☐ Line Numbers");
diff --git a/tests/Terminal.Gui.Editor.IntegrationTests/TedSettingsPersistenceTests.cs b/tests/Terminal.Gui.Editor.IntegrationTests/TedSettingsPersistenceTests.cs
new file mode 100644
index 0000000..a3c7b18
--- /dev/null
+++ b/tests/Terminal.Gui.Editor.IntegrationTests/TedSettingsPersistenceTests.cs
@@ -0,0 +1,458 @@
+// Claude - gpt-5
+
+using System.Drawing;
+using System.Reflection;
+using Ted;
+using Terminal.Gui.Input;
+using Terminal.Gui.Views;
+using Terminal.Gui.Editor.IntegrationTests.Testing;
+using Terminal.Gui.Testing;
+using Xunit;
+
+namespace Terminal.Gui.Editor.IntegrationTests;
+
+[CollectionDefinition (nameof (TedSettingsPersistenceCollection), DisableParallelization = true)]
+public sealed class TedSettingsPersistenceCollection;
+
+[Collection (nameof (TedSettingsPersistenceCollection))]
+public class TedSettingsPersistenceTests
+{
+ [Fact]
+ public void SaveViewSettings_Creates_ConfigFile_And_Persists_IndentSize ()
+ {
+ using ConfigPathScope scope = new ();
+ TedApp app = new ();
+ app.Editor.IndentationSize = 7;
+
+ InvokeSaveViewSettings (app);
+
+ Assert.True (File.Exists (scope.ConfigPath));
+ Assert.Contains ("\"EditorSettings.IndentSize\": 7", File.ReadAllText (scope.ConfigPath));
+ }
+
+ [Fact]
+ public void SaveViewSettings_Updates_Existing_IndentSize_Value ()
+ {
+ using ConfigPathScope scope = new ();
+ TedApp app = new ();
+
+ app.Editor.IndentationSize = 2;
+ InvokeSaveViewSettings (app);
+
+ app.Editor.IndentationSize = 8;
+ InvokeSaveViewSettings (app);
+
+ string text = File.ReadAllText (scope.ConfigPath);
+ Assert.Contains ("\"EditorSettings.IndentSize\": 8", text);
+ Assert.DoesNotContain ("\"EditorSettings.IndentSize\": 2", text);
+ }
+
+ [Fact]
+ public void SaveViewSettings_Persists_WordWrap_Changes ()
+ {
+ using ConfigPathScope scope = new ();
+ TedApp app = new ();
+ app.Editor.WordWrap = true;
+
+ InvokeSaveViewSettings (app);
+ Assert.True (File.Exists (scope.ConfigPath));
+ Assert.Contains ("\"EditorSettings.WordWrap\": true", File.ReadAllText (scope.ConfigPath));
+ }
+
+ [Fact]
+ public void Load_TedApp_Applies_Persisted_Settings ()
+ {
+ using ConfigPathScope scope = new ();
+
+ // Write a config file with non-default values
+ string? dir = Path.GetDirectoryName (scope.ConfigPath);
+ Assert.NotNull (dir);
+ Directory.CreateDirectory (dir);
+ File.WriteAllText (
+ scope.ConfigPath,
+ """
+ {
+ "EditorSettings.WordWrap": true,
+ "EditorSettings.ShowTabs": true,
+ "EditorSettings.LineNumbers": false,
+ "EditorSettings.IndentSize": 2
+ }
+ """);
+
+ // Load settings and construct TedApp — simulates real app startup
+ EditorSettings.Load (scope.ConfigPath);
+ TedApp app = new ();
+
+ // Assert via TedApp.Editor instance properties (not statics)
+ Assert.True (app.Editor.WordWrap);
+ Assert.True (app.Editor.ShowTabs);
+ Assert.False (app.Editor.GutterOptions.HasFlag (Terminal.Gui.Editor.GutterOptions.LineNumbers));
+ Assert.Equal (2, app.Editor.IndentationSize);
+ }
+
+ [Fact]
+ public void Load_Skips_Commented_Out_Settings ()
+ {
+ using ConfigPathScope scope = new ();
+
+ string? dir = Path.GetDirectoryName (scope.ConfigPath);
+ Assert.NotNull (dir);
+ Directory.CreateDirectory (dir);
+ File.WriteAllText (
+ scope.ConfigPath,
+ """
+ {
+ // "EditorSettings.WordWrap": true,
+ "EditorSettings.WordWrap": false,
+ // "EditorSettings.IndentSize": 99,
+ "EditorSettings.IndentSize": 3
+ }
+ """);
+
+ EditorSettings.Load (scope.ConfigPath);
+ TedApp app = new ();
+
+ Assert.False (app.Editor.WordWrap, "Should use active value (false), not commented value (true)");
+ Assert.Equal (3, app.Editor.IndentationSize);
+ }
+
+ [Fact]
+ public async Task ViewMenu_WordWrap_Toggle_Creates_ConfigFile ()
+ {
+ using ConfigPathScope scope = new ();
+ await using AppFixture fx = new (() => new TedApp ());
+ InputInjectionOptions options = new () { Mode = InputInjectionMode.Direct };
+ DateTime ts = new (2025, 1, 1, 12, 0, 0);
+
+ string[] initialLines = fx.Driver.ToString ().Split ('\n');
+ int viewHeaderX = initialLines[0].IndexOf ("View", StringComparison.Ordinal);
+ Assert.True (viewHeaderX >= 0);
+ Point viewHeader = new (viewHeaderX, 0);
+ fx.Injector.InjectMouse (
+ new Mouse { ScreenPosition = viewHeader, Flags = MouseFlags.LeftButtonPressed, Timestamp = ts },
+ options);
+ fx.Injector.InjectMouse (
+ new Mouse
+ {
+ ScreenPosition = viewHeader,
+ Flags = MouseFlags.LeftButtonReleased,
+ Timestamp = ts.AddMilliseconds (25)
+ },
+ options);
+ fx.Render ();
+
+ string[] menuLines = fx.Driver.ToString ().Split ('\n');
+ int y = Array.FindIndex (menuLines, static line => line.Contains ("Word Wrap", StringComparison.Ordinal));
+ Assert.True (y >= 0);
+ int x = -1;
+ // Prefer label text as the click target; glyph fallbacks handle renderer differences.
+ string[] targets = ["Word Wrap", "☐", "☑"];
+ foreach (string target in targets)
+ {
+ x = menuLines[y].IndexOf (target, StringComparison.Ordinal);
+ if (x >= 0)
+ {
+ break;
+ }
+ }
+
+ Assert.True (x >= 0);
+ Point wordWrapItem = new (x, y);
+
+ fx.Injector.InjectMouse (
+ new Mouse
+ {
+ ScreenPosition = wordWrapItem,
+ Flags = MouseFlags.LeftButtonPressed,
+ Timestamp = ts.AddMilliseconds (50)
+ },
+ options);
+ fx.Injector.InjectMouse (
+ new Mouse
+ {
+ ScreenPosition = wordWrapItem,
+ Flags = MouseFlags.LeftButtonReleased,
+ Timestamp = ts.AddMilliseconds (75)
+ },
+ options);
+ fx.Render ();
+
+ Assert.True (File.Exists (scope.ConfigPath));
+ }
+
+ [Fact]
+ public async Task ViewMenu_WordWrap_MouseToggle_Creates_ConfigFile ()
+ {
+ using ConfigPathScope scope = new ();
+ await using AppFixture fx = new (() => new TedApp ());
+ InputInjectionOptions options = new () { Mode = InputInjectionMode.Direct };
+
+ fx.Injector.InjectKey (Key.V.WithAlt, options);
+ fx.Render ();
+
+ string[] lines = fx.Driver.ToString ().Split ('\n');
+ int y = Array.FindIndex (lines, static line => line.Contains ("Word Wrap", StringComparison.Ordinal));
+ Assert.True (y >= 0);
+ int x = lines[y].IndexOf ("Word Wrap", StringComparison.Ordinal);
+ Assert.True (x >= 0);
+
+ DateTime ts = new (2025, 1, 1, 12, 0, 0);
+ Point click = new (x, y);
+ fx.Injector.InjectMouse (new Mouse { ScreenPosition = click, Flags = MouseFlags.LeftButtonPressed, Timestamp = ts }, options);
+ fx.Injector.InjectMouse (
+ new Mouse
+ {
+ ScreenPosition = click,
+ Flags = MouseFlags.LeftButtonReleased,
+ Timestamp = ts.AddMilliseconds (25)
+ },
+ options);
+ fx.Render ();
+
+ Assert.True (File.Exists (scope.ConfigPath));
+ }
+
+ [Fact]
+ public async Task ViewMenu_WordWrap_Toggle_Persists_True ()
+ {
+ // Reproduces the user-reported bug: toggling Word Wrap via the View menu
+ // should create ted.config.json AND persist "EditorSettings.WordWrap": true.
+ // Before the fix, a conflicting ValueChanged handler caused a double-toggle
+ // that reverted WordWrap to false immediately.
+ using ConfigPathScope scope = new ();
+ await using AppFixture fx = new (() => new TedApp ());
+ InputInjectionOptions options = new () { Mode = InputInjectionMode.Direct };
+ DateTime ts = new (2025, 1, 1, 12, 0, 0);
+
+ // Precondition: word wrap is initially off
+ Assert.False (fx.Top.Editor.WordWrap);
+
+ // Open View menu via keyboard (Alt+V) - same as real user interaction
+ fx.Injector.InjectKey (Key.V.WithAlt, options);
+ fx.Render ();
+
+ // Find and click "Word Wrap"
+ string[] menuLines = fx.Driver.ToString ().Split ('\n');
+ int y = Array.FindIndex (menuLines, static line => line.Contains ("Word Wrap", StringComparison.Ordinal));
+ Assert.True (y >= 0, "Could not find 'Word Wrap' in menu");
+ int x = menuLines[y].IndexOf ("Word Wrap", StringComparison.Ordinal);
+ Assert.True (x >= 0);
+ Point wordWrapItem = new (x, y);
+
+ fx.Injector.InjectMouse (
+ new Mouse
+ {
+ ScreenPosition = wordWrapItem,
+ Flags = MouseFlags.LeftButtonPressed,
+ Timestamp = ts
+ },
+ options);
+ fx.Injector.InjectMouse (
+ new Mouse
+ {
+ ScreenPosition = wordWrapItem,
+ Flags = MouseFlags.LeftButtonReleased,
+ Timestamp = ts.AddMilliseconds (25)
+ },
+ options);
+ fx.Render ();
+
+ // Assert: Editor.WordWrap is now true (not toggled back by double-fire)
+ Assert.True (fx.Top.Editor.WordWrap, "Editor.WordWrap should be true after toggle");
+
+ // Assert: config file created
+ Assert.True (File.Exists (scope.ConfigPath), "Config file was not created");
+
+ // Assert: config file contains the correct persisted value
+ string configContent = File.ReadAllText (scope.ConfigPath);
+ Assert.Contains ("\"EditorSettings.WordWrap\": true", configContent);
+ }
+
+ [Fact]
+ public void QuitFile_DoesNotPersist_ViewSettings ()
+ {
+ using ConfigPathScope scope = new ();
+ TedApp app = new ();
+ app.Editor.WordWrap = true;
+
+ Assert.True (app.QuitFile ());
+
+ Assert.False (File.Exists (scope.ConfigPath));
+ }
+
+ [Fact]
+ public void SaveViewSettings_Inserts_Comma_Before_Trailing_Comment_When_Appending_Settings ()
+ {
+ using ConfigPathScope scope = new ();
+ string? configDirectory = Path.GetDirectoryName (scope.ConfigPath);
+ Assert.NotNull (configDirectory);
+ Directory.CreateDirectory (configDirectory);
+ File.WriteAllText (scope.ConfigPath, "{\n \"Unrelated\": 1 // note\n}\n");
+
+ TedApp app = new ();
+ app.Editor.WordWrap = true;
+
+ InvokeSaveViewSettings (app);
+
+ string text = File.ReadAllText (scope.ConfigPath);
+ Assert.Contains ("\"Unrelated\": 1, // note", text);
+ Assert.Contains ("\"EditorSettings.WordWrap\": true", text);
+ }
+
+ [Fact]
+ public void SettingsDialog_IndentSize_Rejects_Zero ()
+ {
+ TedApp app = new ();
+ EditorSettingsDialog dialog = new (app.Editor);
+
+ // Access _indentSize via reflection (it's private)
+ FieldInfo? indentSizeField = typeof (EditorSettingsDialog).GetField ("_indentSize", BindingFlags.Instance | BindingFlags.NonPublic);
+ Assert.NotNull (indentSizeField);
+ var indentControl = (NumericUpDown)indentSizeField.GetValue (dialog)!;
+
+ int valueBefore = indentControl.Value;
+ Assert.True (valueBefore >= 1);
+
+ // Attempt to set Value to 0 — ValueChanging should reject it
+ indentControl.Value = 0;
+ Assert.Equal (valueBefore, indentControl.Value);
+
+ dialog.ApplyTo (app.Editor);
+ Assert.True (app.Editor.IndentationSize >= 1);
+ }
+
+ [Fact]
+ public void Load_Ignores_Key_Embedded_In_String_Value ()
+ {
+ // Regression: a line like "note": "... \"EditorSettings.WordWrap\": true ..."
+ // must NOT fool ReadBool into thinking WordWrap is true.
+ using ConfigPathScope scope = new ();
+
+ string? dir = Path.GetDirectoryName (scope.ConfigPath);
+ Assert.NotNull (dir);
+ Directory.CreateDirectory (dir);
+ File.WriteAllText (
+ scope.ConfigPath,
+ "{\n"
+ + " \"note\": \"see \\\"EditorSettings.WordWrap\\\": true for docs\",\n"
+ + " \"EditorSettings.WordWrap\": false\n"
+ + "}\n");
+
+ EditorSettings.Load (scope.ConfigPath);
+ TedApp app = new ();
+
+ Assert.False (app.Editor.WordWrap, "Should use actual property value (false), not embedded string match");
+ }
+
+ [Fact]
+ public void Save_Appends_Before_Real_Brace_Not_Comment_Brace ()
+ {
+ // Regression: trailing JSONC comment containing } should not be used as insert point.
+ using ConfigPathScope scope = new ();
+
+ string? dir = Path.GetDirectoryName (scope.ConfigPath);
+ Assert.NotNull (dir);
+ Directory.CreateDirectory (dir);
+ File.WriteAllText (
+ scope.ConfigPath,
+ "{\n \"EditorSettings.LineNumbers\": true\n}\n// end of config }\n");
+
+ TedApp app = new ();
+ app.Editor.WordWrap = true;
+
+ InvokeSaveViewSettings (app);
+
+ string text = File.ReadAllText (scope.ConfigPath);
+ // The inserted key should be valid JSON — not inside the comment
+ Assert.Contains ("\"EditorSettings.WordWrap\": true", text);
+ // Config should still be parseable: the real closing brace should come after our insertion
+ int wordWrapPos = text.IndexOf ("\"EditorSettings.WordWrap\"", StringComparison.Ordinal);
+ int lastRealBrace = text.LastIndexOf ('}');
+ // The comment line's } may still exist, but our key must be before the real object close
+ Assert.True (wordWrapPos < lastRealBrace, "WordWrap key should appear before the root closing brace");
+ }
+
+ private static string GetTedConfigPath ()
+ {
+ string home =
+ Environment.GetEnvironmentVariable ("HOME")
+ ?? Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)
+ ?? Directory.GetCurrentDirectory ();
+
+ return Path.Combine (home, ".tui", "ted.config.json");
+ }
+
+ private static void InvokeSaveViewSettings (TedApp app)
+ {
+ MethodInfo? saveViewSettings = typeof (TedApp).GetMethod (
+ "SaveViewSettings",
+ BindingFlags.Instance | BindingFlags.NonPublic);
+ Assert.NotNull (saveViewSettings);
+ saveViewSettings.Invoke (app, null);
+ }
+
+ private sealed class ConfigPathScope : IDisposable
+ {
+ private readonly string _tempRoot;
+ private readonly string? _originalHome;
+ private readonly bool _hadExistingConfig;
+ private readonly string? _existingConfigContent;
+
+ internal ConfigPathScope ()
+ {
+ _tempRoot = Path.Combine (Path.GetTempPath (), $"ted-home-{Guid.NewGuid ():N}");
+ Directory.CreateDirectory (_tempRoot);
+
+ _originalHome = Environment.GetEnvironmentVariable ("HOME");
+ Environment.SetEnvironmentVariable ("HOME", _tempRoot);
+
+ ConfigPath = GetTedConfigPath ();
+ _hadExistingConfig = File.Exists (ConfigPath);
+ _existingConfigContent = _hadExistingConfig ? File.ReadAllText (ConfigPath) : null;
+
+ if (_hadExistingConfig)
+ {
+ File.Delete (ConfigPath);
+ }
+ }
+
+ internal string ConfigPath { get; }
+
+ public void Dispose ()
+ {
+ // Reset EditorSettings statics to defaults (tests may have mutated them via Load)
+ EditorSettings.LineNumbers = true;
+ EditorSettings.FoldIndicators = true;
+ EditorSettings.WordWrap = false;
+ EditorSettings.ShowTabs = false;
+ EditorSettings.UseThemeBackground = true;
+ EditorSettings.IndentSize = 4;
+ EditorSettings.ConvertTabsToSpaces = true;
+ EditorSettings.AutoIndent = true;
+
+ if (_hadExistingConfig)
+ {
+ string? configDirectory = Path.GetDirectoryName (ConfigPath);
+ Assert.NotNull (_existingConfigContent);
+
+ if (!string.IsNullOrWhiteSpace (configDirectory))
+ {
+ Directory.CreateDirectory (configDirectory);
+ }
+
+ File.WriteAllText (ConfigPath, _existingConfigContent);
+ }
+ else if (File.Exists (ConfigPath))
+ {
+ File.Delete (ConfigPath);
+ }
+
+ Environment.SetEnvironmentVariable ("HOME", _originalHome);
+
+ if (Directory.Exists (_tempRoot))
+ {
+ Directory.Delete (_tempRoot, true);
+ }
+ }
+ }
+}