Skip to content

Commit 31199e0

Browse files
tigCopilot
andauthored
fix: set IApplication.AppModel and defer Init() for inline input commands (#16)
RunWithTerminalGuiAsync previously always called Init() regardless of whether the command actually uses the TUI. For inline input commands, Init() modifies terminal state (cursor save/scroll), and if the command returns without running the TUI event loop (e.g. greet hello), Dispose() restores the cursor to the wrong position, causing output to appear at incorrect rows. Fix: - Set app.AppModel on the IApplication instance (not the process-wide static) - Only call Init() upfront for fullscreen commands (viewers, --fullscreen) - For inline input commands, defer Init() to InputCommandRunner where the TUI is actually started via app.RunAsync() - Commands that compute and return without TUI (like greet) never trigger Init(), so terminal state is never modified Fixes #15 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e8e61bc commit 31199e0

3 files changed

Lines changed: 137 additions & 1 deletion

File tree

‎src/Terminal.Gui.Cli/CliHost.cs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,16 @@ private static CommandResult CreateCancelledResult ()
184184
private async Task<CommandResult> RunWithTerminalGuiAsync (ICliCommand command, CommandRunOptions runOptions,
185185
CancellationToken cancellationToken)
186186
{
187-
using IApplication app = Application.Create ().Init ();
187+
var useInline = command.Kind == CommandKind.Input && !runOptions.Fullscreen;
188+
189+
using IApplication app = Application.Create ();
190+
app.AppModel = useInline ? AppModel.Inline : AppModel.FullScreen;
191+
192+
if (!useInline)
193+
{
194+
app.Init ();
195+
}
196+
188197
return await command.RunAsync (app, runOptions.Initial, runOptions, cancellationToken);
189198
}
190199

‎src/Terminal.Gui.Cli/InputCommandRunner.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public static async Task<CommandResult<TValue>> RunAsync<TControl, TRawResult, T
2323
ArgumentNullException.ThrowIfNull (options);
2424
ArgumentNullException.ThrowIfNull (resultMapper);
2525

26+
if (!app.Initialized)
27+
{
28+
app.Init ();
29+
}
30+
2631
wrapper.Title = options.Title ?? defaultTitle;
2732
await app.RunAsync (wrapper, cancellationToken);
2833
return resultMapper (wrapper.Result);
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using Terminal.Gui.App;
2+
using Xunit;
3+
4+
namespace Terminal.Gui.Cli.IntegrationTests;
5+
6+
/// <summary>
7+
/// Verifies that CliHost.RunWithTerminalGuiAsync sets IApplication.AppModel
8+
/// correctly based on CommandKind and the --fullscreen option.
9+
/// </summary>
10+
public sealed class AppModelDispatchTests
11+
{
12+
[Fact]
13+
public async Task InputCommand_WithoutFullscreen_SetsInlineAppModel ()
14+
{
15+
AppModel? captured = null;
16+
SpyInputCommand spy = new (app => captured = app.AppModel);
17+
18+
CliHost host = new ();
19+
host.Registry.Register (spy);
20+
using StringWriter stdout = new ();
21+
using StringWriter stderr = new ();
22+
23+
await host.RunAsync (["spy-input"], TestContext.Current.CancellationToken, stdout, stderr);
24+
25+
Assert.NotNull (captured);
26+
Assert.Equal (AppModel.Inline, captured);
27+
}
28+
29+
[Fact]
30+
public async Task InputCommand_WithFullscreen_SetsFullScreenAppModel ()
31+
{
32+
AppModel? captured = null;
33+
SpyInputCommand spy = new (app => captured = app.AppModel);
34+
35+
CliHost host = new ();
36+
host.Registry.Register (spy);
37+
using StringWriter stdout = new ();
38+
using StringWriter stderr = new ();
39+
40+
await host.RunAsync (["spy-input", "--fullscreen"], TestContext.Current.CancellationToken, stdout, stderr);
41+
42+
Assert.NotNull (captured);
43+
Assert.Equal (AppModel.FullScreen, captured);
44+
}
45+
46+
[Fact]
47+
public async Task ViewerCommand_SetsFullScreenAppModel ()
48+
{
49+
AppModel? captured = null;
50+
SpyViewerCommand spy = new (app => captured = app.AppModel);
51+
52+
CliHost host = new ();
53+
host.Registry.Register (spy);
54+
using StringWriter stdout = new ();
55+
using StringWriter stderr = new ();
56+
57+
await host.RunAsync (["spy-viewer"], TestContext.Current.CancellationToken, stdout, stderr);
58+
59+
Assert.NotNull (captured);
60+
Assert.Equal (AppModel.FullScreen, captured);
61+
}
62+
63+
/// <summary>Spy input command that captures IApplication.AppModel then stops immediately.</summary>
64+
private sealed class SpyInputCommand : ICliCommand
65+
{
66+
private readonly Action<IApplication> _onRun;
67+
68+
public SpyInputCommand (Action<IApplication> onRun)
69+
{
70+
_onRun = onRun;
71+
}
72+
73+
public string PrimaryAlias => "spy-input";
74+
public IReadOnlyList<string> Aliases { get; } = ["spy-input"];
75+
public string Description => "Spy input command for testing.";
76+
public CommandKind Kind => CommandKind.Input;
77+
public Type ResultType => typeof (string);
78+
public IReadOnlyList<CommandOptionDescriptor> Options { get; } = [];
79+
80+
public Task<CommandResult> RunAsync (IApplication app, string? initial, CommandRunOptions options,
81+
CancellationToken cancellationToken)
82+
{
83+
_onRun (app);
84+
app.RequestStop ();
85+
86+
return Task.FromResult (new CommandResult (CommandStatus.Ok, "test", null, null));
87+
}
88+
}
89+
90+
/// <summary>Spy viewer command that captures IApplication.AppModel then stops immediately.</summary>
91+
private sealed class SpyViewerCommand : IViewerCommand
92+
{
93+
private readonly Action<IApplication> _onRun;
94+
95+
public SpyViewerCommand (Action<IApplication> onRun)
96+
{
97+
_onRun = onRun;
98+
}
99+
100+
public string PrimaryAlias => "spy-viewer";
101+
public IReadOnlyList<string> Aliases { get; } = ["spy-viewer"];
102+
public string Description => "Spy viewer command for testing.";
103+
public CommandKind Kind => CommandKind.Viewer;
104+
public Type ResultType => typeof (void);
105+
public IReadOnlyList<CommandOptionDescriptor> Options { get; } = [];
106+
107+
public Task<CommandResult> RunAsync (IApplication app, string? initial, CommandRunOptions options,
108+
CancellationToken cancellationToken)
109+
{
110+
_onRun (app);
111+
app.RequestStop ();
112+
113+
return Task.FromResult (new CommandResult (CommandStatus.Ok, null, null, null));
114+
}
115+
116+
public Task<CommandResult?> RenderCatAsync (CommandRunOptions options, TextWriter stdout,
117+
CancellationToken cancellationToken)
118+
{
119+
return Task.FromResult<CommandResult?> (null);
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)