diff --git a/src/Spectre.Console.Tests/Unit/Prompts/MultiSelectionPromptTests.cs b/src/Spectre.Console.Tests/Unit/Prompts/MultiSelectionPromptTests.cs index 2ab4f53bb..f4d81be81 100644 --- a/src/Spectre.Console.Tests/Unit/Prompts/MultiSelectionPromptTests.cs +++ b/src/Spectre.Console.Tests/Unit/Prompts/MultiSelectionPromptTests.cs @@ -1,7 +1,32 @@ +using System.Reflection; + namespace Spectre.Console.Tests.Unit; public sealed class MultiSelectionPromptTests { + [Fact] + public void Should_Show_Cursor_When_Hiding_It_Throws() + { + // Given + var console = new TestConsole(); + console.Profile.Capabilities.Interactive = true; + console.Profile.Capabilities.Ansi = true; + + var cursor = new ThrowingCursor(); + var setCursorMethod = typeof(TestConsole).GetMethod("SetCursor", BindingFlags.Instance | BindingFlags.NonPublic); + setCursorMethod!.Invoke(console, [cursor]); + + var prompt = new MultiSelectionPrompt(); + prompt.AddChoices(["A", "B", "C"]); + + // When + Action action = () => prompt.Show(console); + + // Then + action.ShouldThrow(); + cursor.Calls.ShouldContain("show"); + } + [Fact] public void Should_Not_Mark_Item_As_Selected_By_Default() { diff --git a/src/Spectre.Console.Tests/Unit/Prompts/ThrowingCursor.cs b/src/Spectre.Console.Tests/Unit/Prompts/ThrowingCursor.cs new file mode 100644 index 000000000..1c7f51a0d --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Prompts/ThrowingCursor.cs @@ -0,0 +1,26 @@ +namespace Spectre.Console.Tests.Unit; + +internal sealed class ThrowingCursor : IAnsiConsoleCursor +{ + public List Calls { get; } = []; + + public void Show(bool show) + { + Calls.Add(show ? "show" : "hide"); + + if (show == false) + { + throw new InvalidOperationException("boom"); + } + } + + public void SetPosition(int column, int line) + { + Calls.Add($"set:{column}:{line}"); + } + + public void Move(CursorDirection direction, int steps) + { + Calls.Add($"move:{direction}:{steps}"); + } +} diff --git a/src/Spectre.Console/Generated/Spectre.Console.SourceGenerator/Spectre.Console.SourceGenerator.Emojis.EmojiGenerator/Emoji.Generated.g.cs b/src/Spectre.Console/Generated/Spectre.Console.SourceGenerator/Spectre.Console.SourceGenerator.Emojis.EmojiGenerator/Emoji.Generated.g.cs index 4b61c5bda..88c144dda 100644 --- a/src/Spectre.Console/Generated/Spectre.Console.SourceGenerator/Spectre.Console.SourceGenerator.Emojis.EmojiGenerator/Emoji.Generated.g.cs +++ b/src/Spectre.Console/Generated/Spectre.Console.SourceGenerator/Spectre.Console.SourceGenerator.Emojis.EmojiGenerator/Emoji.Generated.g.cs @@ -965,13 +965,13 @@ private static readonly Dictionary _emojis { "pig_nose", Emoji.Known.PigNose }, { "pile_of_poo", Emoji.Known.PileOfPoo }, { "pill", Emoji.Known.Pill }, - { "piñata", Emoji.Known.Piñata }, { "pinched_fingers", Emoji.Known.PinchedFingers }, { "pinching_hand", Emoji.Known.PinchingHand }, { "pineapple", Emoji.Known.Pineapple }, { "pine_decoration", Emoji.Known.PineDecoration }, { "ping_pong", Emoji.Known.PingPong }, { "pink_heart", Emoji.Known.PinkHeart }, + { "piñata", Emoji.Known.Piñata }, { "pisces", Emoji.Known.Pisces }, { "pizza", Emoji.Known.Pizza }, { "placard", Emoji.Known.Placard }, @@ -9016,14 +9016,6 @@ public static class Known /// public const string Pill = "\U0001F48A"; - /// - /// Gets the "Piñata" emoji. 🪅 - /// - /// - /// Lookup: piñata - /// - public const string Piñata = "\U0001FA85"; - /// /// Gets the "Pinched fingers" emoji. 🤌 /// @@ -9072,6 +9064,14 @@ public static class Known /// public const string PinkHeart = "\U0001FA77"; + /// + /// Gets the "Piñata" emoji. 🪅 + /// + /// + /// Lookup: piñata + /// + public const string Piñata = "\U0001FA85"; + /// /// Gets the "Pisces" emoji. ♓️ /// diff --git a/src/Spectre.Console/Prompts/List/ListPrompt.cs b/src/Spectre.Console/Prompts/List/ListPrompt.cs index c951e26e2..01ad559a1 100644 --- a/src/Spectre.Console/Prompts/List/ListPrompt.cs +++ b/src/Spectre.Console/Prompts/List/ListPrompt.cs @@ -53,43 +53,58 @@ public async Task> Show( skipUnselectableItems, searchEnabled, _strategy.CalculateInitialIndex(nodes)); + var hook = new ListPromptRenderHook(_console, () => BuildRenderable(state)); - using (new RenderHookScope(_console, hook)) + try { - _console.Cursor.Hide(); - hook.Refresh(); - - while (true) + using (new RenderHookScope(_console, hook)) { - cancellationToken.ThrowIfCancellationRequested(); - var rawKey = await _console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false); - if (rawKey == null) + try { - continue; - } + _console.Cursor.Hide(); + hook.Refresh(); - var key = rawKey.Value; - var result = _strategy.HandleInput(key, state); - if (result == ListPromptInputResult.Submit) - { - break; - } - else if (result == ListPromptInputResult.Abort) - { - state.Cancel(); - break; + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + var rawKey = await _console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false); + if (rawKey == null) + { + continue; + } + + var key = rawKey.Value; + + var result = _strategy.HandleInput(key, state); + + if (result == ListPromptInputResult.Submit) + { + break; + } + else if (result == ListPromptInputResult.Abort) + { + state.Cancel(); + break; + } + var stateUpdated = state.Update(key); + + if (stateUpdated || result == ListPromptInputResult.Refresh) + { + hook.Refresh(); + } + } } - - if (state.Update(key) || result == ListPromptInputResult.Refresh) + finally { - hook.Refresh(); + _console.Cursor.Show(); } } } - - hook.Clear(); - _console.Cursor.Show(); + finally + { + hook.Clear(); + } return state; } @@ -127,7 +142,8 @@ private IRenderable BuildRenderable(ListPromptState state) // Build the renderable return _strategy.Render( _console, - scrollable, cursorIndex, + scrollable, + cursorIndex, state.Items.Skip(skip).Take(take) .Select((node, index) => (index, node)), state.SkipUnselectableItems,