Skip to content

Commit c14b0b3

Browse files
committed
Add PTY-backed terminal control and wire F7 in demo app
1 parent f7a2173 commit c14b0b3

7 files changed

Lines changed: 994 additions & 241 deletions

File tree

Examples/DemoApp/Program.cs

Lines changed: 5 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// Demonstrates all features with modern patterns adapted from the original examples
44
// -----------------------------------------------------------------------
55

6-
using System.Diagnostics;
76
using System.IO;
87
using SharpConsoleUI;
98
using SharpConsoleUI.Builders;
@@ -40,6 +39,8 @@ internal class Program
4039
/// <returns>Exit code</returns>
4140
static async Task<int> Main(string[] args)
4241
{
42+
if (SharpConsoleUI.PtyShim.RunIfShim(args)) return 127;
43+
4344
try
4445
{
4546
// Initialize console window system with modern patterns
@@ -224,8 +225,8 @@ private static void CreateMainMenuWindow()
224225
"[yellow]F6[/] - [bold]Interactive Demo[/]",
225226
" Shows modern control interactions",
226227
"",
227-
"[cyan]F7[/] - [bold]Command Window[/]",
228-
" Interactive command prompt with async I/O",
228+
"[cyan]F7[/] - [bold]Terminal (bash)[/]",
229+
" PTY-backed embedded terminal",
229230
"",
230231
"[white]F8[/] - [bold]Dropdown Demo[/]",
231232
" Country selection with styled dropdowns",
@@ -285,7 +286,7 @@ private static void SetupMainWindowKeyHandlers()
285286
e.Handled = true;
286287
break;
287288
case ConsoleKey.F7:
288-
_ = CreateCommandWindow();
289+
Controls.Terminal().Open(_windowSystem!);
289290
e.Handled = true;
290291
break;
291292
case ConsoleKey.F8:
@@ -620,243 +621,6 @@ private static List<string> GetSystemInformation()
620621
return info;
621622
}
622623

623-
/// <summary>
624-
/// Create command window with interactive command prompt (adapted from CommandWindow.cs)
625-
/// </summary>
626-
private static Task CreateCommandWindow()
627-
{
628-
if (_windowSystem == null)
629-
return Task.CompletedTask;
630-
631-
var commandWindow = new WindowBuilder(_windowSystem)
632-
.WithTitle("Interactive Command Window")
633-
.WithSize(80, 25)
634-
.AtPosition(2, 2)
635-
.WithColors(SpectreColor.Grey93, SpectreColor.Grey15)
636-
.Build();
637-
638-
// Create prompt control for command input
639-
var promptControl = new PromptControl
640-
{
641-
Prompt = "CMD> ",
642-
UnfocusOnEnter = false,
643-
StickyPosition = StickyPosition.Top,
644-
HorizontalAlignment = SharpConsoleUI.Layout.HorizontalAlignment.Stretch
645-
};
646-
647-
// Create multiline output control
648-
var outputControl = new MultilineEditControl
649-
{
650-
ViewportHeight = commandWindow.Height - 4, // Leave space for prompt and borders
651-
WrapMode = WrapMode.Wrap,
652-
ReadOnly = true,
653-
};
654-
655-
// Add controls to window
656-
commandWindow.AddControl(promptControl);
657-
commandWindow.AddControl(new RuleControl { StickyPosition = StickyPosition.Top });
658-
commandWindow.AddControl(outputControl);
659-
660-
// Setup window resize handler
661-
commandWindow.OnResize += (sender, args) =>
662-
{
663-
outputControl.ViewportHeight = commandWindow.Height - 4;
664-
};
665-
666-
// Add initial welcome message
667-
outputControl.AppendContent(
668-
"Interactive command prompt started. Modern async implementation.\n"
669-
);
670-
outputControl.AppendContent("Type 'help' for available commands, 'exit' to close.\n");
671-
672-
// Setup command execution with modern async patterns
673-
promptControl.Entered += async (sender, command) =>
674-
{
675-
try
676-
{
677-
// Display the command in output
678-
outputControl.AppendContent($"\n> {command}\n");
679-
680-
// Handle built-in commands first
681-
if (await HandleBuiltInCommand(command.Trim(), outputControl, commandWindow))
682-
{
683-
promptControl.SetInput(string.Empty);
684-
return;
685-
}
686-
687-
// Execute external command with proper async handling
688-
await ExecuteExternalCommand(command, outputControl);
689-
690-
promptControl.SetInput(string.Empty);
691-
}
692-
catch (Exception ex)
693-
{
694-
outputControl.AppendContent($"Error: {ex.Message}\n");
695-
_windowSystem?.LogService.LogError($"Error executing command: {command}", ex);
696-
}
697-
finally
698-
{
699-
outputControl.GoToEnd();
700-
}
701-
};
702-
703-
// Setup ESC key handler
704-
commandWindow.KeyPressed += (sender, e) =>
705-
{
706-
if (e.KeyInfo.Key == ConsoleKey.Escape)
707-
{
708-
_windowSystem?.CloseWindow(commandWindow);
709-
e.Handled = true;
710-
}
711-
};
712-
713-
_windowSystem.AddWindow(commandWindow);
714-
_windowSystem?.LogService.LogInfo("Command window created with modern async patterns");
715-
716-
return Task.CompletedTask;
717-
}
718-
719-
/// <summary>
720-
/// Handle built-in commands that don't need external process execution
721-
/// </summary>
722-
private static async Task<bool> HandleBuiltInCommand(
723-
string command,
724-
MultilineEditControl output,
725-
Window window
726-
)
727-
{
728-
await Task.Delay(10); // Simulate async operation
729-
730-
switch (command.ToLowerInvariant())
731-
{
732-
case "help":
733-
output.AppendContent("Built-in commands:\n");
734-
output.AppendContent(" help - Show this help\n");
735-
output.AppendContent(" clear - Clear the output\n");
736-
output.AppendContent(" date - Show current date and time\n");
737-
output.AppendContent(" version - Show application version\n");
738-
output.AppendContent(" exit - Close this window\n");
739-
output.AppendContent(" sysinfo - Show system information\n");
740-
output.AppendContent(
741-
"\nAll other commands will be executed as external processes.\n"
742-
);
743-
return true;
744-
745-
case "clear":
746-
output.SetContent("");
747-
output.AppendContent(
748-
"Interactive command prompt started. Modern async implementation.\n"
749-
);
750-
return true;
751-
752-
case "date":
753-
output.AppendContent($"{DateTime.Now:F}\n");
754-
return true;
755-
756-
case "version":
757-
output.AppendContent("Modern SharpConsoleUI Demo v1.0\n");
758-
output.AppendContent($".NET Version: {Environment.Version}\n");
759-
return true;
760-
761-
case "exit":
762-
_windowSystem?.CloseWindow(window);
763-
return true;
764-
765-
case "sysinfo":
766-
output.AppendContent($"OS: {Environment.OSVersion}\n");
767-
output.AppendContent($"Machine: {Environment.MachineName}\n");
768-
output.AppendContent($"User: {Environment.UserName}\n");
769-
output.AppendContent($"Processors: {Environment.ProcessorCount}\n");
770-
output.AppendContent(
771-
$"Working Set: {Environment.WorkingSet / (1024 * 1024):N0} MB\n"
772-
);
773-
return true;
774-
775-
default:
776-
return false; // Not a built-in command
777-
}
778-
}
779-
780-
/// <summary>
781-
/// Execute external command using modern async patterns
782-
/// </summary>
783-
private static async Task ExecuteExternalCommand(string command, MultilineEditControl output)
784-
{
785-
try
786-
{
787-
using var process = new System.Diagnostics.Process();
788-
789-
// Setup process for cross-platform compatibility
790-
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
791-
{
792-
process.StartInfo.FileName = "cmd.exe";
793-
process.StartInfo.Arguments = $"/c {command}";
794-
}
795-
else
796-
{
797-
process.StartInfo.FileName = "/bin/bash";
798-
process.StartInfo.Arguments = $"-c \"{command}\"";
799-
}
800-
801-
process.StartInfo.RedirectStandardOutput = true;
802-
process.StartInfo.RedirectStandardError = true;
803-
process.StartInfo.UseShellExecute = false;
804-
process.StartInfo.CreateNoWindow = true;
805-
806-
// Start process and read output asynchronously
807-
process.Start();
808-
809-
// Read both output and error streams concurrently
810-
var outputTask = process.StandardOutput.ReadToEndAsync();
811-
var errorTask = process.StandardError.ReadToEndAsync();
812-
813-
// Wait for process to complete with timeout
814-
var processTask = process.WaitForExitAsync();
815-
var timeoutTask = Task.Delay(30000); // 30 second timeout
816-
817-
var completedTask = await Task.WhenAny(processTask, timeoutTask);
818-
819-
if (completedTask == timeoutTask)
820-
{
821-
process.Kill();
822-
output.AppendContent("Command timed out after 30 seconds.\n");
823-
return;
824-
}
825-
826-
// Get the results
827-
var outputText = await outputTask;
828-
var errorText = await errorTask;
829-
830-
// Display results
831-
if (!string.IsNullOrEmpty(outputText))
832-
{
833-
output.AppendContent(outputText);
834-
if (!outputText.EndsWith('\n'))
835-
output.AppendContent("\n");
836-
}
837-
838-
if (!string.IsNullOrEmpty(errorText))
839-
{
840-
output.AppendContent($"Error: {errorText}");
841-
if (!errorText.EndsWith('\n'))
842-
output.AppendContent("\n");
843-
}
844-
845-
if (string.IsNullOrEmpty(outputText) && string.IsNullOrEmpty(errorText))
846-
{
847-
output.AppendContent($"Command completed with exit code: {process.ExitCode}\n");
848-
}
849-
}
850-
catch (Exception ex)
851-
{
852-
output.AppendContent($"Failed to execute command: {ex.Message}\n");
853-
_windowSystem?.LogService.LogError(
854-
$"Failed to execute external command: {command}",
855-
ex
856-
);
857-
}
858-
}
859-
860624
/// <summary>
861625
/// Create dropdown demo window (adapted from DropDownWindow.cs)
862626
/// </summary>

SharpConsoleUI/Builders/Controls.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,8 @@ public static FigleControlBuilder Figlet(string? text = null)
270270
/// <param name="content">The text content (supports Spectre markup)</param>
271271
/// <returns>A configured panel control</returns>
272272
public static PanelControl Panel(string content) => new PanelControl(content);
273+
274+
/// <summary>Creates a terminal builder for a PTY-backed terminal control.</summary>
275+
public static TerminalBuilder Terminal(string exe = "/bin/bash")
276+
=> new TerminalBuilder().WithExe(exe);
273277
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using SharpConsoleUI.Controls.Terminal;
2+
3+
namespace SharpConsoleUI.Builders;
4+
5+
public class TerminalBuilder
6+
{
7+
private string _exe = "/bin/bash";
8+
private string[]? _args;
9+
10+
public TerminalBuilder WithExe(string exe) { _exe = exe; return this; }
11+
public TerminalBuilder WithArgs(params string[] a) { _args = a; return this; }
12+
13+
/// <summary>Returns a self-contained TerminalControl (PTY open, shim running, read loop active).</summary>
14+
public TerminalControl Build() => new TerminalControl(_exe, _args);
15+
16+
/// <summary>
17+
/// Convenience: builds a TerminalControl and opens a default centered window.
18+
/// </summary>
19+
public void Open(ConsoleWindowSystem ws)
20+
{
21+
var t = Build();
22+
int rows = Math.Max(ws.DesktopDimensions.Height - 6, 20);
23+
int cols = Math.Max(ws.DesktopDimensions.Width - 6, 60);
24+
var window = new WindowBuilder(ws)
25+
.WithTitle(t.Title)
26+
.WithSize(cols + 2, rows + 2)
27+
.Centered()
28+
.Closable(true)
29+
.AddControl(t)
30+
.Build();
31+
ws.AddWindow(window);
32+
}
33+
}

0 commit comments

Comments
 (0)