diff --git a/Sharprompt.Tests/ConsoleKeyBindingTests.cs b/Sharprompt.Tests/ConsoleKeyBindingTests.cs new file mode 100644 index 0000000..0de2cf2 --- /dev/null +++ b/Sharprompt.Tests/ConsoleKeyBindingTests.cs @@ -0,0 +1,109 @@ +using System; + +using Sharprompt.Internal; + +using Xunit; + +namespace Sharprompt.Tests; + +public class ConsoleKeyBindingTests +{ + [Fact] + public void Constructor_DefaultModifiers_IsNone() + { + var binding = new ConsoleKeyBinding(ConsoleKey.Enter); + + Assert.Equal(ConsoleKey.Enter, binding.Key); + Assert.Equal(default(ConsoleModifiers), binding.Modifiers); + } + + [Fact] + public void Constructor_WithModifiers_StoresModifiers() + { + var binding = new ConsoleKeyBinding(ConsoleKey.LeftArrow, ConsoleModifiers.Control); + + Assert.Equal(ConsoleKey.LeftArrow, binding.Key); + Assert.Equal(ConsoleModifiers.Control, binding.Modifiers); + } + + [Fact] + public void Equals_SameKeyAndModifiers_ReturnsTrue() + { + var a = new ConsoleKeyBinding(ConsoleKey.A, ConsoleModifiers.Control); + var b = new ConsoleKeyBinding(ConsoleKey.A, ConsoleModifiers.Control); + + Assert.Equal(a, b); + Assert.True(a == b); + Assert.False(a != b); + } + + [Fact] + public void Equals_DifferentKey_ReturnsFalse() + { + var a = new ConsoleKeyBinding(ConsoleKey.A); + var b = new ConsoleKeyBinding(ConsoleKey.B); + + Assert.NotEqual(a, b); + Assert.False(a == b); + Assert.True(a != b); + } + + [Fact] + public void Equals_SameKeyDifferentModifiers_ReturnsFalse() + { + var plain = new ConsoleKeyBinding(ConsoleKey.LeftArrow); + var ctrl = new ConsoleKeyBinding(ConsoleKey.LeftArrow, ConsoleModifiers.Control); + + Assert.NotEqual(plain, ctrl); + Assert.False(plain == ctrl); + Assert.True(plain != ctrl); + } + + [Fact] + public void GetHashCode_SameKeyAndModifiers_SameHash() + { + var a = new ConsoleKeyBinding(ConsoleKey.Delete, ConsoleModifiers.Control); + var b = new ConsoleKeyBinding(ConsoleKey.Delete, ConsoleModifiers.Control); + + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentBindings_DifferentHash() + { + var a = new ConsoleKeyBinding(ConsoleKey.Delete); + var b = new ConsoleKeyBinding(ConsoleKey.Delete, ConsoleModifiers.Control); + + Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void CanBeUsedAsDictionaryKey() + { + var dict = new System.Collections.Generic.Dictionary + { + [new ConsoleKeyBinding(ConsoleKey.LeftArrow)] = "plain left", + [new ConsoleKeyBinding(ConsoleKey.LeftArrow, ConsoleModifiers.Control)] = "ctrl left" + }; + + Assert.Equal("plain left", dict[new ConsoleKeyBinding(ConsoleKey.LeftArrow)]); + Assert.Equal("ctrl left", dict[new ConsoleKeyBinding(ConsoleKey.LeftArrow, ConsoleModifiers.Control)]); + } + + [Fact] + public void Equals_WithObject_ReturnsTrueForSameBinding() + { + var a = new ConsoleKeyBinding(ConsoleKey.Enter); + object b = new ConsoleKeyBinding(ConsoleKey.Enter); + + Assert.True(a.Equals(b)); + } + + [Fact] + public void Equals_WithDifferentType_ReturnsFalse() + { + var a = new ConsoleKeyBinding(ConsoleKey.Enter); + + Assert.False(a.Equals("Enter")); + } +} diff --git a/Sharprompt/Forms/FormBase.cs b/Sharprompt/Forms/FormBase.cs index 98f242b..f47892c 100644 --- a/Sharprompt/Forms/FormBase.cs +++ b/Sharprompt/Forms/FormBase.cs @@ -19,6 +19,11 @@ protected FormBase(PromptConfiguration configuration) _consoleDriver.CancellationCallback = CancellationHandler; _formRenderer = new FormRenderer(_consoleDriver, configuration); + + KeyHandlerMaps = new() + { + [new ConsoleKeyBinding(ConsoleKey.Escape)] = HandleEscape + }; } private readonly IConsoleDriver _consoleDriver; @@ -29,7 +34,7 @@ protected FormBase(PromptConfiguration configuration) protected TextInputBuffer InputBuffer { get; } = new(); - protected Dictionary> KeyHandlerMaps { get; set; } = new(); + protected Dictionary> KeyHandlerMaps { get; set; } protected int Width => _consoleDriver.WindowWidth; @@ -103,7 +108,9 @@ private bool TryGetResult([NotNullWhen(true)] out T? result) return HandleEnter(out result); } - if (KeyHandlerMaps.TryGetValue(keyInfo.Key, out var keyHandler) && keyHandler(keyInfo)) + var binding = new ConsoleKeyBinding(keyInfo.Key, keyInfo.Modifiers); + + if (KeyHandlerMaps.TryGetValue(binding, out var keyHandler) && keyHandler()) { continue; } @@ -135,4 +142,11 @@ private void CancellationHandler() Environment.Exit(1); } + + private bool HandleEscape() + { + CancellationHandler(); + + return true; + } } diff --git a/Sharprompt/Forms/ListForm.cs b/Sharprompt/Forms/ListForm.cs index 167518b..d588566 100644 --- a/Sharprompt/Forms/ListForm.cs +++ b/Sharprompt/Forms/ListForm.cs @@ -89,15 +89,15 @@ protected override bool HandleEnter([NotNullWhen(true)] out IEnumerable? resu return false; } - protected override bool HandleDelete(ConsoleKeyInfo keyInfo) + protected override bool HandleCtrlDelete() { - if (keyInfo.Modifiers == ConsoleModifiers.Control && _inputItems.Count > 0) + if (_inputItems.Count > 0) { _inputItems.RemoveAt(_inputItems.Count - 1); return true; } - return base.HandleDelete(keyInfo); + return base.HandleCtrlDelete(); } } diff --git a/Sharprompt/Forms/MultiSelectForm.cs b/Sharprompt/Forms/MultiSelectForm.cs index 0e6d9f8..ab6d816 100644 --- a/Sharprompt/Forms/MultiSelectForm.cs +++ b/Sharprompt/Forms/MultiSelectForm.cs @@ -23,9 +23,9 @@ public MultiSelectForm(MultiSelectOptions options, PromptConfiguration config _selectedItems.Add(defaultValue); } - KeyHandlerMaps[ConsoleKey.Spacebar] = HandleSpacebar; - KeyHandlerMaps[ConsoleKey.A] = HandleAWithControl; - KeyHandlerMaps[ConsoleKey.I] = HandleIWithControl; + KeyHandlerMaps[new ConsoleKeyBinding(ConsoleKey.Spacebar)] = HandleSpacebar; + KeyHandlerMaps[new ConsoleKeyBinding(ConsoleKey.A, ConsoleModifiers.Control)] = HandleCtrlA; + KeyHandlerMaps[new ConsoleKeyBinding(ConsoleKey.I, ConsoleModifiers.Control)] = HandleCtrlI; } private readonly MultiSelectOptions _options; @@ -100,7 +100,7 @@ protected override bool HandleEnter([NotNullWhen(true)] out IEnumerable? resu return false; } - private bool HandleSpacebar(ConsoleKeyInfo keyInfo) + private bool HandleSpacebar() { if (!Paginator.TryGetSelectedItem(out var currentItem)) { @@ -122,13 +122,8 @@ private bool HandleSpacebar(ConsoleKeyInfo keyInfo) return true; } - private bool HandleAWithControl(ConsoleKeyInfo keyInfo) + private bool HandleCtrlA() { - if (keyInfo.Modifiers != ConsoleModifiers.Control) - { - return false; - } - if (_selectedItems.Count == Paginator.TotalCount) { _selectedItems.Clear(); @@ -144,13 +139,8 @@ private bool HandleAWithControl(ConsoleKeyInfo keyInfo) return true; } - private bool HandleIWithControl(ConsoleKeyInfo keyInfo) + private bool HandleCtrlI() { - if (keyInfo.Modifiers != ConsoleModifiers.Control) - { - return false; - } - var invertedItems = Paginator.Except(_selectedItems).ToArray(); _selectedItems.Clear(); diff --git a/Sharprompt/Forms/PasswordForm.cs b/Sharprompt/Forms/PasswordForm.cs index eaaea86..a5692a0 100644 --- a/Sharprompt/Forms/PasswordForm.cs +++ b/Sharprompt/Forms/PasswordForm.cs @@ -14,10 +14,7 @@ public PasswordForm(PasswordOptions options, PromptConfiguration configuration) _options = options; - KeyHandlerMaps = new() - { - [ConsoleKey.Backspace] = HandleBackspace - }; + KeyHandlerMaps[new ConsoleKeyBinding(ConsoleKey.Backspace)] = HandleBackspace; } private readonly PasswordOptions _options; @@ -63,7 +60,7 @@ protected override bool HandleEnter([NotNullWhen(true)] out string? result) return true; } - private bool HandleBackspace(ConsoleKeyInfo keyInfo) + private bool HandleBackspace() { if (InputBuffer.IsStart) { diff --git a/Sharprompt/Forms/SelectFormBase.cs b/Sharprompt/Forms/SelectFormBase.cs index 08d632b..88c4350 100644 --- a/Sharprompt/Forms/SelectFormBase.cs +++ b/Sharprompt/Forms/SelectFormBase.cs @@ -20,11 +20,11 @@ protected void InitializePaginator(IEnumerable items, int pageSize, Optio LoopingSelection = loopingSelection }; - KeyHandlerMaps[ConsoleKey.UpArrow] = HandleUpArrow; - KeyHandlerMaps[ConsoleKey.DownArrow] = HandleDownArrow; - KeyHandlerMaps[ConsoleKey.LeftArrow] = HandleLeftArrow; - KeyHandlerMaps[ConsoleKey.RightArrow] = HandleRightArrow; - KeyHandlerMaps[ConsoleKey.Backspace] = HandleBackspace; + KeyHandlerMaps[new ConsoleKeyBinding(ConsoleKey.UpArrow)] = HandleUpArrow; + KeyHandlerMaps[new ConsoleKeyBinding(ConsoleKey.DownArrow)] = HandleDownArrow; + KeyHandlerMaps[new ConsoleKeyBinding(ConsoleKey.LeftArrow)] = HandleLeftArrow; + KeyHandlerMaps[new ConsoleKeyBinding(ConsoleKey.RightArrow)] = HandleRightArrow; + KeyHandlerMaps[new ConsoleKeyBinding(ConsoleKey.Backspace)] = HandleBackspace; } protected override bool HandleTextInput(ConsoleKeyInfo keyInfo) @@ -45,35 +45,35 @@ protected void RenderPagination(OffscreenBuffer offscreenBuffer, Func : FormBase { protected TextFormBase(PromptConfiguration configuration) : base(configuration) { - KeyHandlerMaps = new() + KeyHandlerMaps = new(KeyHandlerMaps) { - [ConsoleKey.LeftArrow] = HandleLeftArrow, - [ConsoleKey.RightArrow] = HandleRightArrow, - [ConsoleKey.Home] = HandleHome, - [ConsoleKey.End] = HandleEnd, - [ConsoleKey.Backspace] = HandleBackspace, - [ConsoleKey.Delete] = HandleDelete + [new ConsoleKeyBinding(ConsoleKey.LeftArrow)] = HandleLeftArrow, + [new ConsoleKeyBinding(ConsoleKey.LeftArrow, ConsoleModifiers.Control)] = HandleCtrlLeftArrow, + [new ConsoleKeyBinding(ConsoleKey.RightArrow)] = HandleRightArrow, + [new ConsoleKeyBinding(ConsoleKey.RightArrow, ConsoleModifiers.Control)] = HandleCtrlRightArrow, + [new ConsoleKeyBinding(ConsoleKey.Home)] = HandleHome, + [new ConsoleKeyBinding(ConsoleKey.End)] = HandleEnd, + [new ConsoleKeyBinding(ConsoleKey.Backspace)] = HandleBackspace, + [new ConsoleKeyBinding(ConsoleKey.Backspace, ConsoleModifiers.Control)] = HandleCtrlBackspace, + [new ConsoleKeyBinding(ConsoleKey.Delete)] = HandleDelete, + [new ConsoleKeyBinding(ConsoleKey.Delete, ConsoleModifiers.Control)] = HandleCtrlDelete }; } - protected virtual bool HandleLeftArrow(ConsoleKeyInfo keyInfo) + protected virtual bool HandleLeftArrow() { if (InputBuffer.IsStart) { return false; } - if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) - { - InputBuffer.MoveToPreviousWord(); - } - else + InputBuffer.MoveBackward(); + + return true; + } + + protected virtual bool HandleCtrlLeftArrow() + { + if (InputBuffer.IsStart) { - InputBuffer.MoveBackward(); + return false; } + InputBuffer.MoveToPreviousWord(); + return true; } - protected virtual bool HandleRightArrow(ConsoleKeyInfo keyInfo) + protected virtual bool HandleRightArrow() { if (InputBuffer.IsEnd) { return false; } - if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) - { - InputBuffer.MoveToNextWord(); - } - else + InputBuffer.MoveForward(); + + return true; + } + + protected virtual bool HandleCtrlRightArrow() + { + if (InputBuffer.IsEnd) { - InputBuffer.MoveForward(); + return false; } + InputBuffer.MoveToNextWord(); + return true; } - protected virtual bool HandleHome(ConsoleKeyInfo keyInfo) + protected virtual bool HandleHome() { if (InputBuffer.IsStart) { @@ -67,7 +83,7 @@ protected virtual bool HandleHome(ConsoleKeyInfo keyInfo) return true; } - protected virtual bool HandleEnd(ConsoleKeyInfo keyInfo) + protected virtual bool HandleEnd() { if (InputBuffer.IsEnd) { @@ -79,41 +95,51 @@ protected virtual bool HandleEnd(ConsoleKeyInfo keyInfo) return true; } - protected virtual bool HandleBackspace(ConsoleKeyInfo keyInfo) + protected virtual bool HandleBackspace() { if (InputBuffer.IsStart) { return false; } - if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) - { - InputBuffer.BackspaceWord(); - } - else + InputBuffer.Backspace(); + + return true; + } + + protected virtual bool HandleCtrlBackspace() + { + if (InputBuffer.IsStart) { - InputBuffer.Backspace(); + return false; } + InputBuffer.BackspaceWord(); + return true; } - protected virtual bool HandleDelete(ConsoleKeyInfo keyInfo) + protected virtual bool HandleDelete() { if (InputBuffer.IsEnd) { return false; } - if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) - { - InputBuffer.DeleteWord(); - } - else + InputBuffer.Delete(); + + return true; + } + + protected virtual bool HandleCtrlDelete() + { + if (InputBuffer.IsEnd) { - InputBuffer.Delete(); + return false; } + InputBuffer.DeleteWord(); + return true; } } diff --git a/Sharprompt/Internal/ConsoleKeyBinding.cs b/Sharprompt/Internal/ConsoleKeyBinding.cs new file mode 100644 index 0000000..0f1141c --- /dev/null +++ b/Sharprompt/Internal/ConsoleKeyBinding.cs @@ -0,0 +1,20 @@ +using System; + +namespace Sharprompt.Internal; + +internal readonly struct ConsoleKeyBinding(ConsoleKey key, ConsoleModifiers modifiers = default) : IEquatable +{ + public ConsoleKey Key { get; } = key; + + public ConsoleModifiers Modifiers { get; } = modifiers; + + public bool Equals(ConsoleKeyBinding other) => Key == other.Key && Modifiers == other.Modifiers; + + public override bool Equals(object? obj) => obj is ConsoleKeyBinding other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(Key, Modifiers); + + public static bool operator ==(ConsoleKeyBinding left, ConsoleKeyBinding right) => left.Equals(right); + + public static bool operator !=(ConsoleKeyBinding left, ConsoleKeyBinding right) => !left.Equals(right); +}