From 8084d9c971454e8d3ba0579a65dd4003d2d9aaf7 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 11 Jun 2026 10:01:30 -0600 Subject: [PATCH 1/3] fix: centralize Terminal.Gui lifecycle for headless markdown rendering in CliHost MarkdownRenderer.RenderToAnsi previously called Application.Create()/Init()/Dispose() directly, violating constitution C1 (only CliHost may call Terminal.Gui lifecycle APIs). This path is reachable from help --cat, root --help, and viewer --cat rendering. Move the headless ANSI-driver create/init/dispose block (including the DisableRealDriverIO scope) into an internal CliHost.RunHeadlessRender(width, height, render) helper. MarkdownRenderer keeps its public signature and now only performs view layout and drawing inside the callback, so lifecycle ownership is centralized again. No public API change; rendering behavior is unchanged. Note: InputCommandRunner still carries a defensive app.Init() that exists only because CliHost skips Init() for inline input commands; PR #9 fixes that root cause, after which the defensive call can be removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Terminal.Gui.Cli/CliHost.cs | 25 +++++++++ src/Terminal.Gui.Cli/MarkdownRenderer.cs | 65 +++++++++++------------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/src/Terminal.Gui.Cli/CliHost.cs b/src/Terminal.Gui.Cli/CliHost.cs index 2ed053d..4620145 100644 --- a/src/Terminal.Gui.Cli/CliHost.cs +++ b/src/Terminal.Gui.Cli/CliHost.cs @@ -1,5 +1,6 @@ using System.Reflection; using Terminal.Gui.App; +using Terminal.Gui.Drivers; namespace Terminal.Gui.Cli; @@ -181,6 +182,30 @@ private static CommandResult CreateCancelledResult () null); } + /// + /// Creates, initializes, and disposes a headless ANSI-driver Terminal.Gui application around + /// . Centralizes the Terminal.Gui lifecycle here (constitution C1) so + /// helpers such as never call lifecycle entrypoints directly. + /// + internal static void RunHeadlessRender (int width, int height, Action render) + { + var previousDriverIO = Environment.GetEnvironmentVariable ("DisableRealDriverIO"); + Environment.SetEnvironmentVariable ("DisableRealDriverIO", "1"); + IApplication app = Application.Create (); + + try + { + app.Init (DriverRegistry.Names.ANSI); + app.Driver?.SetScreenSize (width, height); + render (app); + } + finally + { + app.Dispose (); + Environment.SetEnvironmentVariable ("DisableRealDriverIO", previousDriverIO); + } + } + private async Task RunWithTerminalGuiAsync (ICliCommand command, CommandRunOptions runOptions, CancellationToken cancellationToken) { diff --git a/src/Terminal.Gui.Cli/MarkdownRenderer.cs b/src/Terminal.Gui.Cli/MarkdownRenderer.cs index a7e4ab3..1e0c5b5 100644 --- a/src/Terminal.Gui.Cli/MarkdownRenderer.cs +++ b/src/Terminal.Gui.Cli/MarkdownRenderer.cs @@ -1,6 +1,4 @@ using System.Text; -using Terminal.Gui.App; -using Terminal.Gui.Drivers; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; @@ -62,46 +60,41 @@ public static void RenderToAnsi (string markdown, TextWriter output) height = 24; } - var previousDriverIO = Environment.GetEnvironmentVariable ("DisableRealDriverIO"); - Environment.SetEnvironmentVariable ("DisableRealDriverIO", "1"); - IApplication app = Application.Create (); - try { - app.Init (DriverRegistry.Names.ANSI); - app.Driver?.SetScreenSize (width, height); - - Markdown markdownView = new () + // CliHost owns the Terminal.Gui lifecycle (constitution C1); this helper only + // performs view layout and drawing inside the callback. + CliHost.RunHeadlessRender (width, height, app => { - App = app, - UseThemeBackground = false, - ShowCopyButtons = false, - Width = Dim.Fill (), - Height = Dim.Fill (), - Text = markdown - }; - - markdownView.SetRelativeLayout (app.Screen.Size); - markdownView.Layout (); - - var contentHeight = markdownView.GetContentHeight (); - app.Driver?.SetScreenSize (width, contentHeight); - markdownView.SetRelativeLayout (app.Screen.Size); - markdownView.Frame = app.Screen with { X = 0, Y = 0 }; - markdownView.Layout (); - - app.Driver?.ClearContents (); - markdownView.Draw (); - - var rendered = app.Driver?.ToAnsi () ?? string.Empty; - rendered = TerminalEscapeSanitizer.SanitizeRenderedOutput (rendered); - target.WriteLine (rendered); + Markdown markdownView = new () + { + App = app, + UseThemeBackground = false, + ShowCopyButtons = false, + Width = Dim.Fill (), + Height = Dim.Fill (), + Text = markdown + }; + + markdownView.SetRelativeLayout (app.Screen.Size); + markdownView.Layout (); + + var contentHeight = markdownView.GetContentHeight (); + app.Driver?.SetScreenSize (width, contentHeight); + markdownView.SetRelativeLayout (app.Screen.Size); + markdownView.Frame = app.Screen with { X = 0, Y = 0 }; + markdownView.Layout (); + + app.Driver?.ClearContents (); + markdownView.Draw (); + + var rendered = app.Driver?.ToAnsi () ?? string.Empty; + rendered = TerminalEscapeSanitizer.SanitizeRenderedOutput (rendered); + target.WriteLine (rendered); + }); } finally { - app.Dispose (); - Environment.SetEnvironmentVariable ("DisableRealDriverIO", previousDriverIO); - if (previousEncoding is not null) { Console.OutputEncoding = previousEncoding; From 25921ba2e94cb5bd699c766a8b81f84bc9e2df8d Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 11 Jun 2026 10:01:38 -0600 Subject: [PATCH 2/3] style: apply ReSharper cleanup normalizations Pre-existing deviations surfaced by the pinned 'dotnet jb cleanupcode' pass: space before parens in generic 'new ()' constraints and after 'unchecked'. Committed so the CI cleanup + clean-diff gate passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Terminal.Gui.Cli/InputCommandRunner.cs | 4 ++-- tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Terminal.Gui.Cli/InputCommandRunner.cs b/src/Terminal.Gui.Cli/InputCommandRunner.cs index 9172fb1..151dd67 100644 --- a/src/Terminal.Gui.Cli/InputCommandRunner.cs +++ b/src/Terminal.Gui.Cli/InputCommandRunner.cs @@ -16,7 +16,7 @@ public static async Task> RunAsync> resultMapper, bool addEnterBinding = true) - where TControl : View, new() + where TControl : View, new () { ArgumentNullException.ThrowIfNull (app); ArgumentNullException.ThrowIfNull (wrapper); @@ -41,7 +41,7 @@ public static Task> RunAsync ( string defaultTitle, CancellationToken cancellationToken, bool addEnterBinding = true) - where TControl : View, new() + where TControl : View, new () { return RunAsync ( app, diff --git a/tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs b/tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs index 977a4d9..d15c027 100644 --- a/tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs +++ b/tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs @@ -273,7 +273,7 @@ private static (int Hash, bool NonEmpty) HashContents (Cell[,]? contents) for (var c = 0; c < cols; c++) { var g = contents[r, c].Grapheme; - hash = unchecked(hash * 31 + (string.IsNullOrEmpty (g) ? 0 : g.GetHashCode ())); + hash = unchecked (hash * 31 + (string.IsNullOrEmpty (g) ? 0 : g.GetHashCode ())); if (!string.IsNullOrEmpty (g) && g != " ") { From 05b164e5bf6591f8359b7bb70a3f9fc791f4d3aa Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 11 Jun 2026 10:04:53 -0600 Subject: [PATCH 3/3] Revert "style: apply ReSharper cleanup normalizations" This reverts commit 25921ba2e94cb5bd699c766a8b81f84bc9e2df8d. --- src/Terminal.Gui.Cli/InputCommandRunner.cs | 4 ++-- tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Terminal.Gui.Cli/InputCommandRunner.cs b/src/Terminal.Gui.Cli/InputCommandRunner.cs index 151dd67..9172fb1 100644 --- a/src/Terminal.Gui.Cli/InputCommandRunner.cs +++ b/src/Terminal.Gui.Cli/InputCommandRunner.cs @@ -16,7 +16,7 @@ public static async Task> RunAsync> resultMapper, bool addEnterBinding = true) - where TControl : View, new () + where TControl : View, new() { ArgumentNullException.ThrowIfNull (app); ArgumentNullException.ThrowIfNull (wrapper); @@ -41,7 +41,7 @@ public static Task> RunAsync ( string defaultTitle, CancellationToken cancellationToken, bool addEnterBinding = true) - where TControl : View, new () + where TControl : View, new() { return RunAsync ( app, diff --git a/tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs b/tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs index d15c027..977a4d9 100644 --- a/tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs +++ b/tests/Terminal.Gui.Cli.IntegrationTests/CommandUiHarness.cs @@ -273,7 +273,7 @@ private static (int Hash, bool NonEmpty) HashContents (Cell[,]? contents) for (var c = 0; c < cols; c++) { var g = contents[r, c].Grapheme; - hash = unchecked (hash * 31 + (string.IsNullOrEmpty (g) ? 0 : g.GetHashCode ())); + hash = unchecked(hash * 31 + (string.IsNullOrEmpty (g) ? 0 : g.GetHashCode ())); if (!string.IsNullOrEmpty (g) && g != " ") {