From a116be5fa0454ac2f81712f97ebe3b097d5df833 Mon Sep 17 00:00:00 2001 From: antek Date: Sat, 2 May 2026 21:23:42 +0200 Subject: [PATCH 1/2] add prompt history on pressing up/down arrow --- src/Spectre.Console.Testing/TestConsole.cs | 3 + .../Public_API.Output.verified.txt | 2 + .../Unit/Prompts/TextPromptTests.cs | 90 +++++++++++++++++++ .../Extensions/AnsiConsoleExtensions.Input.cs | 37 ++++++++ src/Spectre.Console/IAnsiConsole.cs | 5 ++ src/Spectre.Console/Recorder.cs | 3 + 6 files changed, 140 insertions(+) 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 252b1cdad..30bf7daea 100644 --- a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt @@ -2129,6 +2129,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); @@ -2643,6 +2644,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 09d0eb983..36b8754dd 100644 --- a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs +++ b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs @@ -489,4 +489,94 @@ public async Task Uses_case_insensitive_comparison_when_no_comparer_is_passed(st // Then 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"); + } } \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs index 5f7575940..8200a7034 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, CancellationToken cancellationToken = default, string? initialInput = null) { ArgumentNullException.ThrowIfNull(console); @@ -47,6 +50,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; } @@ -89,6 +93,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/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. /// From 679c687f65ce475b78e0c9b74829ec42742e4895 Mon Sep 17 00:00:00 2001 From: antek Date: Sat, 2 May 2026 21:49:24 +0200 Subject: [PATCH 2/2] add file i forgot to commit --- src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs | 2 ++ 1 file changed, 2 insertions(+) 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);