Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Spectre.Console.Testing/TestConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public sealed class TestConsole : IAnsiConsole, IDisposable
/// <inheritdoc/>
IAnsiConsoleInput IAnsiConsole.Input => Input;

/// <inheritdoc/>
public List<string> InputHistory => _console.InputHistory;

/// <summary>
/// Gets the console output.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2144,6 +2144,7 @@
Spectre.Console.IAnsiConsoleCursor Cursor { get; }
Spectre.Console.IExclusivityMode ExclusivityMode { get; }
Spectre.Console.IAnsiConsoleInput Input { get; }
System.Collections.Generic.List<string> InputHistory { get; }
Spectre.Console.Rendering.RenderPipeline Pipeline { get; }
Spectre.Console.Profile Profile { get; }
void Clear(bool home);
Expand Down Expand Up @@ -2658,6 +2659,7 @@
public Spectre.Console.IAnsiConsoleCursor Cursor { get; }
public Spectre.Console.IExclusivityMode ExclusivityMode { get; }
public Spectre.Console.IAnsiConsoleInput Input { get; }
public System.Collections.Generic.List<string> InputHistory { get; }
public Spectre.Console.Rendering.RenderPipeline Pipeline { get; }
public Spectre.Console.Profile Profile { get; }
public void Clear(bool home) { }
Expand Down
90 changes: 90 additions & 0 deletions src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,96 @@ public async Task Uses_case_insensitive_comparison_when_no_comparer_is_passed(st
result.ShouldBe("Yes");
}

[Fact]
public void Should_Add_Entered_Text_To_Input_History()
{
// Given
var console = new TestConsole();
console.Input.PushTextWithEnter("Hello World");

// When
var result = console.Prompt(new TextPrompt<string>("Enter text:"));

// Then
console.InputHistory.Count.ShouldBe(1);
console.InputHistory[0].ShouldBe("Hello World");
}

[Fact]
public void Should_Return_Correct_Input_After_Pressing_Pressing_Up_Arrow()
{
// Given
var console = new TestConsole { EmitAnsiSequences = true, };
console.Input.PushTextWithEnter("First input");

var firstPrompt = new TextPrompt<string>("First prompt");
console.Prompt(firstPrompt);

console.Input.PushKey(ConsoleKey.UpArrow);
console.Input.PushKey(ConsoleKey.Enter);

var secondPrompt = new TextPrompt<string>("Second prompt");

// When
var result = console.Prompt(secondPrompt);

// Then
result.ShouldBe("First input");
}

[Fact]
public void Should_Return_Correct_Input_After_Pressing_Pressing_Down_Arrow()
{
// Given
var console = new TestConsole { EmitAnsiSequences = true, };
console.Input.PushTextWithEnter("First input");

var firstPrompt = new TextPrompt<string>("First prompt");
console.Prompt(firstPrompt);

console.Input.PushTextWithEnter("Second input");

var secondPrompt = new TextPrompt<string>("Second prompt");
console.Prompt(secondPrompt);

console.Input.PushKey(ConsoleKey.UpArrow);
console.Input.PushKey(ConsoleKey.UpArrow);
console.Input.PushKey(ConsoleKey.DownArrow);
console.Input.PushKey(ConsoleKey.Enter);

var thirdPrompt = new TextPrompt<string>("Third prompt");

// When
var result = console.Prompt(thirdPrompt);

// Then
result.ShouldBe("Second input");
}

[Fact]
public void User_Input_Should_Persist_After_Pressing_Down_Arrow()
{
// Given
var console = new TestConsole { EmitAnsiSequences = true, };
console.Input.PushTextWithEnter("First input");

var firstPrompt = new TextPrompt<string>("First prompt");
console.Prompt(firstPrompt);

console.Input.PushText("User input");
console.Input.PushKey(ConsoleKey.UpArrow);
console.Input.PushKey(ConsoleKey.DownArrow);
console.Input.PushKey(ConsoleKey.Enter);

var secondPrompt = new TextPrompt<string>("Second prompt");

// When
var result = console.Prompt(secondPrompt);

// Then
result.ShouldBe("User input");
}

[Fact]
public void Validate_BoolOverload_ShortCircuits()
{
Expand Down
37 changes: 37 additions & 0 deletions src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace Spectre.Console;
/// </summary>
public static partial class AnsiConsoleExtensions
{
private static int? _inputHistoryIndex;
private static string _inputReplacedByHistory = string.Empty;

internal static async Task<string> ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask,
IEnumerable<string>? items = null, string? initialInput = null, CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -48,6 +51,7 @@ internal static async Task<string> ReadLine(this IAnsiConsole console, Style? st
var key = rawKey.Value;
if (key.Key == ConsoleKey.Enter)
{
console.InputHistory.Add(text);
return text;
}

Expand Down Expand Up @@ -90,6 +94,39 @@ internal static async Task<string> ReadLine(this IAnsiConsole console, Style? st
continue;
}

if (key.Key == ConsoleKey.UpArrow && console.InputHistory.Count > 0)
{
if (_inputHistoryIndex.HasValue)
{
_inputHistoryIndex = _inputHistoryIndex.Value != 0 ? _inputHistoryIndex - 1 : 0;
}
else
{
_inputHistoryIndex = console.InputHistory.Count - 1;
_inputReplacedByHistory = text;
}

var replace = console.InputHistory[_inputHistoryIndex.Value];
console.Write("\b \b".Repeat(text.Length), style);
console.Write(replace);
text = replace;
continue;
}

if (key.Key == ConsoleKey.DownArrow && console.InputHistory.Count > 0)
{
if (_inputHistoryIndex.HasValue)
{
_inputHistoryIndex = _inputHistoryIndex.Value != console.InputHistory.Count - 1 ? _inputHistoryIndex + 1 : null;

var replace = _inputHistoryIndex.HasValue ? console.InputHistory[_inputHistoryIndex.Value] : _inputReplacedByHistory;
console.Write("\b \b".Repeat(text.Length), style);
console.Write(replace);
text = replace;
continue;
}
}

if (!char.IsControl(key.KeyChar))
{
text += key.KeyChar.ToString();
Expand Down
5 changes: 5 additions & 0 deletions src/Spectre.Console/IAnsiConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public interface IAnsiConsole
/// </summary>
RenderPipeline Pipeline { get; }

/// <summary>
/// Gets the input history
/// </summary>
List<string> InputHistory { get; }

/// <summary>
/// Clears the console.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ internal sealed class AnsiConsoleFacade : IAnsiConsole
public IAnsiConsoleInput Input { get; }
public IExclusivityMode ExclusivityMode { get; }
public RenderPipeline Pipeline { get; }
public List<string> InputHistory { get; }

public AnsiConsoleFacade(Profile profile, IExclusivityMode exclusivityMode)
{
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
Input = new DefaultInput(Profile);
ExclusivityMode = exclusivityMode ?? throw new ArgumentNullException(nameof(exclusivityMode));
Pipeline = new RenderPipeline();
InputHistory = [];

_renderLock = new object();
_ansiBackend = new AnsiConsoleBackend(this);
Expand Down
3 changes: 3 additions & 0 deletions src/Spectre.Console/Recorder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class Recorder : IAnsiConsole, IDisposable
/// <inheritdoc/>
public RenderPipeline Pipeline => _console.Pipeline;

/// <inheritdoc/>
public List<string> InputHistory => _console.InputHistory;

/// <summary>
/// Initializes a new instance of the <see cref="Recorder"/> class.
/// </summary>
Expand Down