diff --git a/src/Spectre.Console.Testing/TestConsole.cs b/src/Spectre.Console.Testing/TestConsole.cs
index a83228734..96bcccb8a 100644
--- a/src/Spectre.Console.Testing/TestConsole.cs
+++ b/src/Spectre.Console.Testing/TestConsole.cs
@@ -29,6 +29,9 @@ public sealed class TestConsole : IAnsiConsole, IDisposable
///
IAnsiConsoleInput IAnsiConsole.Input => Input;
+ ///
+ public List InputHistory => _console.InputHistory;
+
///
/// Gets the console output.
///
diff --git a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt
index 98944f91e..1fa2c6455 100644
--- a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt
+++ b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt
@@ -2144,6 +2144,7 @@
Spectre.Console.IAnsiConsoleCursor Cursor { get; }
Spectre.Console.IExclusivityMode ExclusivityMode { get; }
Spectre.Console.IAnsiConsoleInput Input { get; }
+ System.Collections.Generic.List InputHistory { get; }
Spectre.Console.Rendering.RenderPipeline Pipeline { get; }
Spectre.Console.Profile Profile { get; }
void Clear(bool home);
@@ -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 InputHistory { get; }
public Spectre.Console.Rendering.RenderPipeline Pipeline { get; }
public Spectre.Console.Profile Profile { get; }
public void Clear(bool home) { }
diff --git a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs
index d2646a8db..109d6f265 100644
--- a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs
+++ b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs
@@ -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("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("First prompt");
+ console.Prompt(firstPrompt);
+
+ console.Input.PushKey(ConsoleKey.UpArrow);
+ console.Input.PushKey(ConsoleKey.Enter);
+
+ var secondPrompt = new TextPrompt("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("First prompt");
+ console.Prompt(firstPrompt);
+
+ console.Input.PushTextWithEnter("Second input");
+
+ var secondPrompt = new TextPrompt("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("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("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("Second prompt");
+
+ // When
+ var result = console.Prompt(secondPrompt);
+
+ // Then
+ result.ShouldBe("User input");
+ }
+
[Fact]
public void Validate_BoolOverload_ShortCircuits()
{
diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs
index 758804cfe..66741f9be 100644
--- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs
+++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs
@@ -5,6 +5,9 @@ namespace Spectre.Console;
///
public static partial class AnsiConsoleExtensions
{
+ private static int? _inputHistoryIndex;
+ private static string _inputReplacedByHistory = string.Empty;
+
internal static async Task ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask,
IEnumerable? items = null, string? initialInput = null, CancellationToken cancellationToken = default)
{
@@ -48,6 +51,7 @@ internal static async Task ReadLine(this IAnsiConsole console, Style? st
var key = rawKey.Value;
if (key.Key == ConsoleKey.Enter)
{
+ console.InputHistory.Add(text);
return text;
}
@@ -90,6 +94,39 @@ internal static async Task 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();
diff --git a/src/Spectre.Console/IAnsiConsole.cs b/src/Spectre.Console/IAnsiConsole.cs
index d3d753658..7949911bd 100644
--- a/src/Spectre.Console/IAnsiConsole.cs
+++ b/src/Spectre.Console/IAnsiConsole.cs
@@ -30,6 +30,11 @@ public interface IAnsiConsole
///
RenderPipeline Pipeline { get; }
+ ///
+ /// Gets the input history
+ ///
+ List InputHistory { get; }
+
///
/// Clears the console.
///
diff --git a/src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs b/src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs
index f567c527e..85694c48a 100644
--- a/src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs
+++ b/src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs
@@ -11,6 +11,7 @@ internal sealed class AnsiConsoleFacade : IAnsiConsole
public IAnsiConsoleInput Input { get; }
public IExclusivityMode ExclusivityMode { get; }
public RenderPipeline Pipeline { get; }
+ public List InputHistory { get; }
public AnsiConsoleFacade(Profile profile, IExclusivityMode exclusivityMode)
{
@@ -18,6 +19,7 @@ public AnsiConsoleFacade(Profile profile, IExclusivityMode exclusivityMode)
Input = new DefaultInput(Profile);
ExclusivityMode = exclusivityMode ?? throw new ArgumentNullException(nameof(exclusivityMode));
Pipeline = new RenderPipeline();
+ InputHistory = [];
_renderLock = new object();
_ansiBackend = new AnsiConsoleBackend(this);
diff --git a/src/Spectre.Console/Recorder.cs b/src/Spectre.Console/Recorder.cs
index a68c48ad9..5a028e079 100644
--- a/src/Spectre.Console/Recorder.cs
+++ b/src/Spectre.Console/Recorder.cs
@@ -23,6 +23,9 @@ public class Recorder : IAnsiConsole, IDisposable
///
public RenderPipeline Pipeline => _console.Pipeline;
+ ///
+ public List InputHistory => _console.InputHistory;
+
///
/// Initializes a new instance of the class.
///