// Copyright (c) Microsoft. All rights reserved.
using Harness.ConsoleReactiveFramework;
namespace Harness.ConsoleReactiveComponents;
///
/// Props for .
///
public record TextInputProps : ConsoleReactiveProps
{
/// Gets the prompt string displayed on the left (e.g. "> " or "user > ").
public string Prompt { get; init; } = "> ";
/// Gets the text content to render to the right of the prompt.
public string Text { get; init; } = "";
/// Gets the placeholder text shown in dark grey when is empty.
public string Placeholder { get; init; } = "";
}
///
/// A component that renders a prompt with text input. Supports multi-line text
/// where continuation lines are indented to align with the text start position
/// (i.e. the column after the prompt).
///
public class TextInput : ConsoleReactiveComponent
{
///
/// Calculates the height (in rows) required to render the prompt and text
/// given the available width.
///
/// The text input props.
/// The total available width in columns.
/// The number of rows needed.
public static int CalculateHeight(TextInputProps props, int availableWidth)
{
int promptLength = props.Prompt.Length;
int textWidth = availableWidth - promptLength;
if (textWidth <= 0 || props.Text.Length == 0)
{
return 1;
}
int lines = 1;
int remaining = props.Text.Length - textWidth;
while (remaining > 0)
{
lines++;
remaining -= textWidth;
}
return lines;
}
///
public override void RenderCore(TextInputProps props, ConsoleReactiveState state)
{
int promptLength = props.Prompt.Length;
int textWidth = props.Width - promptLength;
string indent = new(' ', promptLength);
// First line: prompt + start of text
Console.Write(AnsiEscapes.MoveCursor(props.Y, props.X));
Console.Write(AnsiEscapes.EraseEntireLine);
Console.Write(props.Prompt);
if (textWidth <= 0 || props.Text.Length == 0)
{
// Show placeholder if text is empty
if (props.Text.Length == 0 && props.Placeholder.Length > 0 && textWidth > 0)
{
Console.Write(AnsiEscapes.SetForegroundColor(ConsoleColor.DarkGray));
Console.Write(" ");
Console.Write(props.Placeholder[..Math.Min(props.Placeholder.Length, textWidth - 1)]);
Console.Write(AnsiEscapes.ResetAttributes);
}
return;
}
int offset = 0;
int firstChunk = Math.Min(textWidth, props.Text.Length);
Console.Write(props.Text[offset..firstChunk]);
offset = firstChunk;
// Continuation lines: indented to align with text start
int row = 1;
while (offset < props.Text.Length)
{
int chunk = Math.Min(textWidth, props.Text.Length - offset);
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
Console.Write(AnsiEscapes.EraseEntireLine);
Console.Write(indent);
Console.Write(props.Text[offset..(offset + chunk)]);
offset += chunk;
row++;
}
}
}