From 0b4204dfc0d1cb93e2c4e45522da0212e7cdf405 Mon Sep 17 00:00:00 2001 From: Tatsuro Shibamura Date: Sun, 13 Nov 2022 19:08:12 +0900 Subject: [PATCH] Rearchitecture of each form implementation (#242) * Rearchitecture of each form implementation * Reduce nest of if statement --- Sharprompt/Forms/ConfirmForm.cs | 55 +---- Sharprompt/Forms/FormBase.cs | 45 ++++- Sharprompt/Forms/FormRenderer.cs | 2 +- Sharprompt/Forms/InputForm.cs | 82 +------- Sharprompt/Forms/ListForm.cs | 72 ++----- Sharprompt/Forms/MultiSelectForm.cs | 244 ++++++++++++++--------- Sharprompt/Forms/PasswordForm.cs | 76 ++++--- Sharprompt/Forms/SelectForm.cs | 123 +++++++----- Sharprompt/Forms/TextFormBase.cs | 119 +++++++++++ Sharprompt/InlineItemsAttribute.cs | 2 +- Sharprompt/Internal/EnumItemsProvider.cs | 9 + Sharprompt/Internal/IItemsProvider.cs | 2 +- Sharprompt/Internal/NullItemsProvider.cs | 2 +- Sharprompt/Internal/PropertyMetadata.cs | 31 ++- Sharprompt/MemberItemsAttribute.cs | 2 +- Sharprompt/Prompt.Basic.cs | 2 +- Sharprompt/Prompt.Bind.cs | 2 +- Sharprompt/Prompt.Configuration.cs | 2 + Sharprompt/SelectOptions.cs | 2 +- 19 files changed, 486 insertions(+), 388 deletions(-) create mode 100644 Sharprompt/Forms/TextFormBase.cs create mode 100644 Sharprompt/Internal/EnumItemsProvider.cs diff --git a/Sharprompt/Forms/ConfirmForm.cs b/Sharprompt/Forms/ConfirmForm.cs index 783d925..499fe7e 100644 --- a/Sharprompt/Forms/ConfirmForm.cs +++ b/Sharprompt/Forms/ConfirmForm.cs @@ -5,7 +5,7 @@ namespace Sharprompt.Forms; -internal class ConfirmForm : FormBase +internal class ConfirmForm : TextFormBase { public ConfirmForm(ConfirmOptions options) { @@ -16,51 +16,6 @@ public ConfirmForm(ConfirmOptions options) private readonly ConfirmOptions _options; - private readonly TextInputBuffer _textInputBuffer = new(); - - protected override bool TryGetResult(out bool result) - { - do - { - var keyInfo = ConsoleDriver.ReadKey(); - - switch (keyInfo.Key) - { - case ConsoleKey.Enter: - return HandleEnter(out result); - case ConsoleKey.LeftArrow when !_textInputBuffer.IsStart: - _textInputBuffer.MoveBackward(); - break; - case ConsoleKey.RightArrow when !_textInputBuffer.IsEnd: - _textInputBuffer.MoveForward(); - break; - case ConsoleKey.Backspace when !_textInputBuffer.IsStart: - _textInputBuffer.Backspace(); - break; - case ConsoleKey.Delete when !_textInputBuffer.IsEnd: - _textInputBuffer.Delete(); - break; - case ConsoleKey.LeftArrow: - case ConsoleKey.RightArrow: - case ConsoleKey.Backspace: - case ConsoleKey.Delete: - ConsoleDriver.Beep(); - break; - default: - if (!char.IsControl(keyInfo.KeyChar)) - { - _textInputBuffer.Insert(keyInfo.KeyChar); - } - break; - } - - } while (ConsoleDriver.KeyAvailable); - - result = default; - - return false; - } - protected override void InputTemplate(OffscreenBuffer offscreenBuffer) { offscreenBuffer.WritePrompt(_options.Message); @@ -81,7 +36,7 @@ protected override void InputTemplate(OffscreenBuffer offscreenBuffer) } offscreenBuffer.WriteHint($"({answerYes}/{answerNo}) "); - offscreenBuffer.WriteInput(_textInputBuffer); + offscreenBuffer.WriteInput(InputBuffer); } protected override void FinishTemplate(OffscreenBuffer offscreenBuffer, bool result) @@ -90,9 +45,9 @@ protected override void FinishTemplate(OffscreenBuffer offscreenBuffer, bool res offscreenBuffer.WriteAnswer(result ? Resource.ConfirmForm_Answer_Yes : Resource.ConfirmForm_Answer_No); } - private bool HandleEnter(out bool result) + protected override bool HandleEnter(out bool result) { - var input = _textInputBuffer.ToString(); + var input = InputBuffer.ToString(); if (string.IsNullOrEmpty(input)) { @@ -123,6 +78,8 @@ private bool HandleEnter(out bool result) return true; } + InputBuffer.Clear(); + SetError(Resource.Validation_Invalid); } diff --git a/Sharprompt/Forms/FormBase.cs b/Sharprompt/Forms/FormBase.cs index dd09643..55905da 100644 --- a/Sharprompt/Forms/FormBase.cs +++ b/Sharprompt/Forms/FormBase.cs @@ -26,6 +26,10 @@ protected FormBase() protected IConsoleDriver ConsoleDriver { get; } + protected TextInputBuffer InputBuffer { get; } = new(); + + protected Dictionary> KeyHandlerMaps { get; set; } = new(); + public void Dispose() => _formRenderer.Dispose(); public T Start() @@ -45,12 +49,51 @@ public T Start() } } - protected abstract bool TryGetResult([NotNullWhen(true)] out T? result); + protected bool TryGetResult([NotNullWhen(true)] out T? result) + { + do + { + var keyInfo = ConsoleDriver.ReadKey(); + + if (keyInfo.Key == ConsoleKey.Enter) + { + return HandleEnter(out result); + } + + if (KeyHandlerMaps.TryGetValue(keyInfo.Key, out var keyHandler) && keyHandler(keyInfo)) + { + continue; + } + + if (!char.IsControl(keyInfo.KeyChar)) + { + HandleTextInput(keyInfo); + } + else + { + ConsoleDriver.Beep(); + } + + } while (ConsoleDriver.KeyAvailable); + + result = default; + + return false; + } protected abstract void InputTemplate(OffscreenBuffer offscreenBuffer); protected abstract void FinishTemplate(OffscreenBuffer offscreenBuffer, T result); + protected abstract bool HandleEnter([NotNullWhen(true)] out T? result); + + protected virtual bool HandleTextInput(ConsoleKeyInfo keyInfo) + { + InputBuffer.Insert(keyInfo.KeyChar); + + return true; + } + protected void SetError(string errorMessage) => _formRenderer.ErrorMessage = errorMessage; protected void SetError(Exception exception) => SetError(exception.Message); diff --git a/Sharprompt/Forms/FormRenderer.cs b/Sharprompt/Forms/FormRenderer.cs index d82850d..6494dc3 100644 --- a/Sharprompt/Forms/FormRenderer.cs +++ b/Sharprompt/Forms/FormRenderer.cs @@ -36,7 +36,7 @@ public void Render(Action template) } } - public void Render(Action template, TModel result) + public void Render(Action template, T result) { using (_offscreenBuffer.BeginRender()) { diff --git a/Sharprompt/Forms/InputForm.cs b/Sharprompt/Forms/InputForm.cs index b3873e0..cfbac9c 100644 --- a/Sharprompt/Forms/InputForm.cs +++ b/Sharprompt/Forms/InputForm.cs @@ -6,7 +6,7 @@ namespace Sharprompt.Forms; -internal class InputForm : FormBase +internal class InputForm : TextFormBase { public InputForm(InputOptions options) { @@ -20,72 +20,6 @@ public InputForm(InputOptions options) private readonly InputOptions _options; private readonly Optional _defaultValue; - private readonly TextInputBuffer _textInputBuffer = new(); - - protected override bool TryGetResult([NotNullWhen(true)] out T? result) - { - do - { - var keyInfo = ConsoleDriver.ReadKey(); - var controlPressed = keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control); - - switch (keyInfo.Key) - { - case ConsoleKey.Enter: - return HandleEnter(out result); - case ConsoleKey.LeftArrow when controlPressed && !_textInputBuffer.IsStart: - _textInputBuffer.MoveToPreviousWord(); - break; - case ConsoleKey.RightArrow when controlPressed && !_textInputBuffer.IsEnd: - _textInputBuffer.MoveToNextWord(); - break; - case ConsoleKey.LeftArrow when !_textInputBuffer.IsStart: - _textInputBuffer.MoveBackward(); - break; - case ConsoleKey.RightArrow when !_textInputBuffer.IsEnd: - _textInputBuffer.MoveForward(); - break; - case ConsoleKey.Home when !_textInputBuffer.IsStart: - _textInputBuffer.MoveToStart(); - break; - case ConsoleKey.End when !_textInputBuffer.IsEnd: - _textInputBuffer.MoveToEnd(); - break; - case ConsoleKey.Backspace when controlPressed && !_textInputBuffer.IsStart: - _textInputBuffer.BackspaceWord(); - break; - case ConsoleKey.Delete when controlPressed && !_textInputBuffer.IsEnd: - _textInputBuffer.DeleteWord(); - break; - case ConsoleKey.Backspace when !_textInputBuffer.IsStart: - _textInputBuffer.Backspace(); - break; - case ConsoleKey.Delete when !_textInputBuffer.IsEnd: - _textInputBuffer.Delete(); - break; - case ConsoleKey.LeftArrow: - case ConsoleKey.RightArrow: - case ConsoleKey.Home: - case ConsoleKey.End: - case ConsoleKey.Backspace: - case ConsoleKey.Delete: - ConsoleDriver.Beep(); - break; - default: - if (!char.IsControl(keyInfo.KeyChar)) - { - _textInputBuffer.Insert(keyInfo.KeyChar); - } - break; - } - - } while (ConsoleDriver.KeyAvailable); - - result = default; - - return false; - } - protected override void InputTemplate(OffscreenBuffer offscreenBuffer) { offscreenBuffer.WritePrompt(_options.Message); @@ -95,13 +29,13 @@ protected override void InputTemplate(OffscreenBuffer offscreenBuffer) offscreenBuffer.WriteHint($"({_defaultValue.Value}) "); } - if (_textInputBuffer.Length == 0 && !string.IsNullOrEmpty(_options.Placeholder)) + if (InputBuffer.Length == 0 && !string.IsNullOrEmpty(_options.Placeholder)) { offscreenBuffer.PushCursor(); offscreenBuffer.WriteHint(_options.Placeholder); } - offscreenBuffer.WriteInput(_textInputBuffer); + offscreenBuffer.WriteInput(InputBuffer); } protected override void FinishTemplate(OffscreenBuffer offscreenBuffer, T result) @@ -114,9 +48,9 @@ protected override void FinishTemplate(OffscreenBuffer offscreenBuffer, T result } } - private bool HandleEnter([NotNullWhen(true)] out T? result) + protected override bool HandleEnter([NotNullWhen(true)] out T? result) { - var input = _textInputBuffer.ToString(); + var input = InputBuffer.ToString(); try { @@ -138,11 +72,7 @@ private bool HandleEnter([NotNullWhen(true)] out T? result) result = TypeHelper.ConvertTo(input); } - if (TryValidate(result, _options.Validators)) - { - return true; - } - + return TryValidate(result, _options.Validators); } catch (Exception ex) { diff --git a/Sharprompt/Forms/ListForm.cs b/Sharprompt/Forms/ListForm.cs index 9389200..ae2d00b 100644 --- a/Sharprompt/Forms/ListForm.cs +++ b/Sharprompt/Forms/ListForm.cs @@ -8,7 +8,7 @@ namespace Sharprompt.Forms; -internal class ListForm : FormBase> where T : notnull +internal class ListForm : TextFormBase> where T : notnull { public ListForm(ListOptions options) { @@ -22,62 +22,12 @@ public ListForm(ListOptions options) private readonly ListOptions _options; private readonly List _inputItems = new(); - private readonly TextInputBuffer _textInputBuffer = new(); - - protected override bool TryGetResult([NotNullWhen(true)] out IEnumerable? result) - { - do - { - var keyInfo = ConsoleDriver.ReadKey(); - - switch (keyInfo.Key) - { - case ConsoleKey.Enter: - return HandleEnter(out result); - case ConsoleKey.LeftArrow when !_textInputBuffer.IsStart: - _textInputBuffer.MoveBackward(); - break; - case ConsoleKey.RightArrow when !_textInputBuffer.IsEnd: - _textInputBuffer.MoveForward(); - break; - case ConsoleKey.Backspace when !_textInputBuffer.IsStart: - _textInputBuffer.Backspace(); - break; - case ConsoleKey.Delete when !_textInputBuffer.IsEnd: - _textInputBuffer.Delete(); - break; - case ConsoleKey.Delete when keyInfo.Modifiers == ConsoleModifiers.Control: - if (_inputItems.Any()) - { - _inputItems.RemoveAt(_inputItems.Count - 1); - } - break; - case ConsoleKey.LeftArrow: - case ConsoleKey.RightArrow: - case ConsoleKey.Backspace: - case ConsoleKey.Delete: - ConsoleDriver.Beep(); - break; - default: - if (!char.IsControl(keyInfo.KeyChar)) - { - _textInputBuffer.Insert(keyInfo.KeyChar); - } - break; - } - - } while (ConsoleDriver.KeyAvailable); - - result = default; - - return false; - } protected override void InputTemplate(OffscreenBuffer offscreenBuffer) { offscreenBuffer.WritePrompt(_options.Message); - offscreenBuffer.WriteInput(_textInputBuffer); + offscreenBuffer.WriteInput(InputBuffer); foreach (var inputItem in _inputItems) { @@ -92,9 +42,9 @@ protected override void FinishTemplate(OffscreenBuffer offscreenBuffer, IEnumera offscreenBuffer.WriteAnswer(string.Join(", ", result)); } - private bool HandleEnter(out IEnumerable? result) + protected override bool HandleEnter([NotNullWhen(true)] out IEnumerable? result) { - var input = _textInputBuffer.ToString(); + var input = InputBuffer.ToString(); try { @@ -126,7 +76,7 @@ private bool HandleEnter(out IEnumerable? result) return false; } - _textInputBuffer.Clear(); + InputBuffer.Clear(); _inputItems.Add(inputValue); } @@ -139,4 +89,16 @@ private bool HandleEnter(out IEnumerable? result) return false; } + + protected override bool HandleDelete(ConsoleKeyInfo keyInfo) + { + if (keyInfo.Modifiers == ConsoleModifiers.Control && _inputItems.Any()) + { + _inputItems.RemoveAt(_inputItems.Count - 1); + + return true; + } + + return base.HandleDelete(keyInfo); + } } diff --git a/Sharprompt/Forms/MultiSelectForm.cs b/Sharprompt/Forms/MultiSelectForm.cs index f2f77e0..c763a89 100644 --- a/Sharprompt/Forms/MultiSelectForm.cs +++ b/Sharprompt/Forms/MultiSelectForm.cs @@ -25,109 +25,24 @@ public MultiSelectForm(MultiSelectOptions options) { _selectedItems.Add(defaultValue); } + + KeyHandlerMaps = new() + { + [ConsoleKey.Spacebar] = HandleSpacebar, + [ConsoleKey.UpArrow] = HandleUpArrow, + [ConsoleKey.DownArrow] = HandleDownArrow, + [ConsoleKey.LeftArrow] = HandleLeftArrow, + [ConsoleKey.RightArrow] = HandleRightArrow, + [ConsoleKey.Backspace] = HandleBackspace, + [ConsoleKey.A] = HandleAWithControl, + [ConsoleKey.I] = HandleIWithControl, + }; } private readonly MultiSelectOptions _options; private readonly Paginator _paginator; private readonly HashSet _selectedItems = new(); - private readonly TextInputBuffer _filterBuffer = new(); - - protected override bool TryGetResult([NotNullWhen(true)] out IEnumerable? result) - { - do - { - var keyInfo = ConsoleDriver.ReadKey(); - - switch (keyInfo.Key) - { - case ConsoleKey.Enter when _selectedItems.Count >= _options.Minimum: - result = _options.Items - .Where(x => _selectedItems.Contains(x)) - .ToArray(); - return true; - case ConsoleKey.Enter: - SetError(string.Format(Resource.Validation_Minimum_SelectionRequired, _options.Minimum)); - break; - case ConsoleKey.Spacebar when _paginator.TryGetSelectedItem(out var currentItem): - if (_selectedItems.Contains(currentItem)) - { - _selectedItems.Remove(currentItem); - } - else - { - if (_selectedItems.Count >= _options.Maximum) - { - SetError(string.Format(Resource.Validation_Maximum_SelectionRequired, _options.Maximum)); - } - else - { - _selectedItems.Add(currentItem); - } - } - - break; - case ConsoleKey.UpArrow: - _paginator.PreviousItem(); - break; - case ConsoleKey.DownArrow: - _paginator.NextItem(); - break; - case ConsoleKey.LeftArrow: - _paginator.PreviousPage(); - break; - case ConsoleKey.RightArrow: - _paginator.NextPage(); - break; - case ConsoleKey.Backspace when !_filterBuffer.IsStart: - _filterBuffer.Backspace(); - - _paginator.UpdateFilter(_filterBuffer.ToString()); - break; - case ConsoleKey.Backspace: - ConsoleDriver.Beep(); - break; - case ConsoleKey.A when keyInfo.Modifiers == ConsoleModifiers.Control: - if (_selectedItems.Count == _paginator.TotalCount) - { - _selectedItems.Clear(); - } - else - { - foreach (var item in _paginator.FilteredItems) - { - _selectedItems.Add(item); - } - } - break; - case ConsoleKey.I when keyInfo.Modifiers == ConsoleModifiers.Control: - { - var invertedItems = _paginator.FilteredItems.Except(_selectedItems).ToArray(); - - _selectedItems.Clear(); - - foreach (var item in invertedItems) - { - _selectedItems.Add(item); - } - } - break; - default: - if (!char.IsControl(keyInfo.KeyChar)) - { - _filterBuffer.Insert(keyInfo.KeyChar); - - _paginator.UpdateFilter(_filterBuffer.ToString()); - } - break; - } - - } while (ConsoleDriver.KeyAvailable); - - result = default; - - return false; - } protected override void InputTemplate(OffscreenBuffer offscreenBuffer) { @@ -178,4 +93,139 @@ protected override void FinishTemplate(OffscreenBuffer offscreenBuffer, IEnumera offscreenBuffer.WriteDone(_options.Message); offscreenBuffer.WriteAnswer(string.Join(", ", result.Select(_options.TextSelector))); } + + protected override bool HandleEnter([NotNullWhen(true)] out IEnumerable? result) + { + if (_selectedItems.Count >= _options.Minimum) + { + result = _options.Items + .Where(x => _selectedItems.Contains(x)) + .ToArray(); + + return true; + } + + SetError(string.Format(Resource.Validation_Minimum_SelectionRequired, _options.Minimum)); + + result = default; + + return false; + } + + protected override bool HandleTextInput(ConsoleKeyInfo keyInfo) + { + base.HandleTextInput(keyInfo); + + _paginator.UpdateFilter(InputBuffer.ToString()); + + return true; + } + + private bool HandleSpacebar(ConsoleKeyInfo keyInfo) + { + if (!_paginator.TryGetSelectedItem(out var currentItem)) + { + return false; + } + + if (_selectedItems.Contains(currentItem)) + { + _selectedItems.Remove(currentItem); + } + else + { + if (_selectedItems.Count >= _options.Maximum) + { + SetError(string.Format(Resource.Validation_Maximum_SelectionRequired, _options.Maximum)); + } + else + { + _selectedItems.Add(currentItem); + } + } + + return true; + } + + private bool HandleUpArrow(ConsoleKeyInfo keyInfo) + { + _paginator.PreviousItem(); + + return true; + } + + private bool HandleDownArrow(ConsoleKeyInfo keyInfo) + { + _paginator.NextItem(); + + return true; + } + + private bool HandleLeftArrow(ConsoleKeyInfo keyInfo) + { + _paginator.PreviousPage(); + + return true; + } + + private bool HandleRightArrow(ConsoleKeyInfo keyInfo) + { + _paginator.NextPage(); + + return true; + } + + private bool HandleBackspace(ConsoleKeyInfo keyInfo) + { + if (InputBuffer.IsStart) + { + return false; + } + + InputBuffer.Backspace(); + _paginator.UpdateFilter(InputBuffer.ToString()); + + return true; + } + + private bool HandleAWithControl(ConsoleKeyInfo keyInfo) + { + if (keyInfo.Modifiers != ConsoleModifiers.Control) + { + return false; + } + + if (_selectedItems.Count == _paginator.TotalCount) + { + _selectedItems.Clear(); + } + else + { + foreach (var item in _paginator.FilteredItems) + { + _selectedItems.Add(item); + } + } + + return true; + } + + private bool HandleIWithControl(ConsoleKeyInfo keyInfo) + { + if (keyInfo.Modifiers != ConsoleModifiers.Control) + { + return false; + } + + var invertedItems = _paginator.FilteredItems.Except(_selectedItems).ToArray(); + + _selectedItems.Clear(); + + foreach (var item in invertedItems) + { + _selectedItems.Add(item); + } + + return true; + } } diff --git a/Sharprompt/Forms/PasswordForm.cs b/Sharprompt/Forms/PasswordForm.cs index 60f9583..d063d48 100644 --- a/Sharprompt/Forms/PasswordForm.cs +++ b/Sharprompt/Forms/PasswordForm.cs @@ -13,54 +13,20 @@ public PasswordForm(PasswordOptions options) options.EnsureOptions(); _options = options; - } - - private readonly PasswordOptions _options; - private readonly TextInputBuffer _textInputBuffer = new(); - - protected override bool TryGetResult([NotNullWhen(true)] out string? result) - { - do + KeyHandlerMaps = new() { - var keyInfo = ConsoleDriver.ReadKey(); - - switch (keyInfo.Key) - { - case ConsoleKey.Enter: - result = _textInputBuffer.ToString(); - - if (TryValidate(result, _options.Validators)) - { - return true; - } - break; - case ConsoleKey.Backspace when !_textInputBuffer.IsStart: - _textInputBuffer.Backspace(); - break; - case ConsoleKey.Backspace: - ConsoleDriver.Beep(); - break; - default: - if (!char.IsControl(keyInfo.KeyChar)) - { - _textInputBuffer.Insert(keyInfo.KeyChar); - } - break; - } - - } while (ConsoleDriver.KeyAvailable); - - result = default; - - return false; + [ConsoleKey.Backspace] = HandleBackspace + }; } + private readonly PasswordOptions _options; + protected override void InputTemplate(OffscreenBuffer offscreenBuffer) { offscreenBuffer.WritePrompt(_options.Message); - if (_textInputBuffer.Length == 0 && !string.IsNullOrEmpty(_options.Placeholder)) + if (InputBuffer.Length == 0 && !string.IsNullOrEmpty(_options.Placeholder)) { offscreenBuffer.PushCursor(); offscreenBuffer.WriteHint(_options.Placeholder); @@ -68,7 +34,7 @@ protected override void InputTemplate(OffscreenBuffer offscreenBuffer) if (!string.IsNullOrEmpty(_options.PasswordChar)) { - offscreenBuffer.Write(string.Concat(Enumerable.Repeat(_options.PasswordChar, _textInputBuffer.Length))); + offscreenBuffer.Write(string.Concat(Enumerable.Repeat(_options.PasswordChar, InputBuffer.Length))); offscreenBuffer.PushCursor(); } } @@ -79,7 +45,33 @@ protected override void FinishTemplate(OffscreenBuffer offscreenBuffer, string r if (!string.IsNullOrEmpty(_options.PasswordChar)) { - offscreenBuffer.WriteAnswer(string.Concat(Enumerable.Repeat(_options.PasswordChar, _textInputBuffer.Length))); + offscreenBuffer.WriteAnswer(string.Concat(Enumerable.Repeat(_options.PasswordChar, InputBuffer.Length))); + } + } + + protected override bool HandleEnter([NotNullWhen(true)] out string? result) + { + result = InputBuffer.ToString(); + + if (!TryValidate(result, _options.Validators)) + { + InputBuffer.Clear(); + + return false; } + + return true; + } + + private bool HandleBackspace(ConsoleKeyInfo keyInfo) + { + if (InputBuffer.IsStart) + { + return false; + } + + InputBuffer.Backspace(); + + return true; } } diff --git a/Sharprompt/Forms/SelectForm.cs b/Sharprompt/Forms/SelectForm.cs index 46288d9..42be193 100644 --- a/Sharprompt/Forms/SelectForm.cs +++ b/Sharprompt/Forms/SelectForm.cs @@ -19,63 +19,20 @@ public SelectForm(SelectOptions options) var pageSize = Math.Min(options.PageSize, maxPageSize); _paginator = new Paginator(options.Items, pageSize, Optional.Create(options.DefaultValue), options.TextSelector); + + KeyHandlerMaps = new() + { + [ConsoleKey.UpArrow] = HandleUpArrow, + [ConsoleKey.DownArrow] = HandleDownArrow, + [ConsoleKey.LeftArrow] = HandleLeftArrow, + [ConsoleKey.RightArrow] = HandleRightArrow, + [ConsoleKey.Backspace] = HandleBackspace + }; } private readonly SelectOptions _options; private readonly Paginator _paginator; - private readonly TextInputBuffer _filterBuffer = new(); - - protected override bool TryGetResult([NotNullWhen(true)] out T? result) - { - do - { - var keyInfo = ConsoleDriver.ReadKey(); - - switch (keyInfo.Key) - { - case ConsoleKey.Enter when _paginator.TryGetSelectedItem(out result): - return true; - case ConsoleKey.Enter: - SetError(Resource.Validation_Required); - break; - case ConsoleKey.UpArrow: - _paginator.PreviousItem(); - break; - case ConsoleKey.DownArrow: - _paginator.NextItem(); - break; - case ConsoleKey.LeftArrow: - _paginator.PreviousPage(); - break; - case ConsoleKey.RightArrow: - _paginator.NextPage(); - break; - case ConsoleKey.Backspace when !_filterBuffer.IsStart: - _filterBuffer.Backspace(); - - _paginator.UpdateFilter(_filterBuffer.ToString()); - break; - case ConsoleKey.Backspace: - ConsoleDriver.Beep(); - break; - default: - if (!char.IsControl(keyInfo.KeyChar)) - { - _filterBuffer.Insert(keyInfo.KeyChar); - - _paginator.UpdateFilter(_filterBuffer.ToString()); - } - break; - } - - } while (ConsoleDriver.KeyAvailable); - - result = default; - - return false; - } - protected override void InputTemplate(OffscreenBuffer offscreenBuffer) { offscreenBuffer.WritePrompt(_options.Message); @@ -113,4 +70,66 @@ protected override void FinishTemplate(OffscreenBuffer offscreenBuffer, T result offscreenBuffer.WriteDone(_options.Message); offscreenBuffer.WriteAnswer(_options.TextSelector(result)); } + + protected override bool HandleEnter([NotNullWhen(true)] out T? result) + { + if (_paginator.TryGetSelectedItem(out result)) + { + return true; + } + + SetError(Resource.Validation_Required); + + return false; + } + + protected override bool HandleTextInput(ConsoleKeyInfo keyInfo) + { + base.HandleTextInput(keyInfo); + + _paginator.UpdateFilter(InputBuffer.ToString()); + + return true; + } + + private bool HandleUpArrow(ConsoleKeyInfo keyInfo) + { + _paginator.PreviousItem(); + + return true; + } + + private bool HandleDownArrow(ConsoleKeyInfo keyInfo) + { + _paginator.NextItem(); + + return true; + } + + private bool HandleLeftArrow(ConsoleKeyInfo keyInfo) + { + _paginator.PreviousPage(); + + return true; + } + + private bool HandleRightArrow(ConsoleKeyInfo keyInfo) + { + _paginator.NextPage(); + + return true; + } + + private bool HandleBackspace(ConsoleKeyInfo keyInfo) + { + if (InputBuffer.IsStart) + { + return false; + } + + InputBuffer.Backspace(); + _paginator.UpdateFilter(InputBuffer.ToString()); + + return true; + } } diff --git a/Sharprompt/Forms/TextFormBase.cs b/Sharprompt/Forms/TextFormBase.cs new file mode 100644 index 0000000..bd12429 --- /dev/null +++ b/Sharprompt/Forms/TextFormBase.cs @@ -0,0 +1,119 @@ +using System; + +namespace Sharprompt.Forms; + +internal abstract class TextFormBase : FormBase +{ + protected TextFormBase() + { + KeyHandlerMaps = new() + { + [ConsoleKey.LeftArrow] = HandleLeftArrow, + [ConsoleKey.RightArrow] = HandleRightArrow, + [ConsoleKey.Home] = HandleHome, + [ConsoleKey.End] = HandleEnd, + [ConsoleKey.Backspace] = HandleBackspace, + [ConsoleKey.Delete] = HandleDelete + }; + } + + protected virtual bool HandleLeftArrow(ConsoleKeyInfo keyInfo) + { + if (InputBuffer.IsStart) + { + return false; + } + + if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + InputBuffer.MoveToPreviousWord(); + } + else + { + InputBuffer.MoveBackward(); + } + + return true; + } + + protected virtual bool HandleRightArrow(ConsoleKeyInfo keyInfo) + { + if (InputBuffer.IsEnd) + { + return false; + } + + if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + InputBuffer.MoveToNextWord(); + } + else + { + InputBuffer.MoveForward(); + } + + return true; + } + + protected virtual bool HandleHome(ConsoleKeyInfo keyInfo) + { + if (InputBuffer.IsStart) + { + return false; + } + + InputBuffer.MoveToStart(); + + return true; + } + + protected virtual bool HandleEnd(ConsoleKeyInfo keyInfo) + { + if (InputBuffer.IsEnd) + { + return false; + } + + InputBuffer.MoveToEnd(); + + return true; + } + + protected virtual bool HandleBackspace(ConsoleKeyInfo keyInfo) + { + if (InputBuffer.IsStart) + { + return false; + } + + if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + InputBuffer.BackspaceWord(); + } + else + { + InputBuffer.Backspace(); + } + + return true; + } + + protected virtual bool HandleDelete(ConsoleKeyInfo keyInfo) + { + if (InputBuffer.IsEnd) + { + return false; + } + + if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + InputBuffer.DeleteWord(); + } + else + { + InputBuffer.Delete(); + } + + return true; + } +} diff --git a/Sharprompt/InlineItemsAttribute.cs b/Sharprompt/InlineItemsAttribute.cs index 39d8f68..129624b 100644 --- a/Sharprompt/InlineItemsAttribute.cs +++ b/Sharprompt/InlineItemsAttribute.cs @@ -17,5 +17,5 @@ public InlineItemsAttribute(params object[] items) private readonly object[] _items; - public IEnumerable GetItems(PropertyInfo targetPropertyInfo) => _items.Cast(); + public IEnumerable GetItems(PropertyInfo targetPropertyInfo) where T : notnull => _items.Cast(); } diff --git a/Sharprompt/Internal/EnumItemsProvider.cs b/Sharprompt/Internal/EnumItemsProvider.cs new file mode 100644 index 0000000..96a90ce --- /dev/null +++ b/Sharprompt/Internal/EnumItemsProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace Sharprompt.Internal; + +internal class EnumItemsProvider : IItemsProvider +{ + public IEnumerable GetItems(PropertyInfo targetPropertyInfo) where T : notnull => EnumHelper.GetValues(); +} diff --git a/Sharprompt/Internal/IItemsProvider.cs b/Sharprompt/Internal/IItemsProvider.cs index 879a00e..e67c5c7 100644 --- a/Sharprompt/Internal/IItemsProvider.cs +++ b/Sharprompt/Internal/IItemsProvider.cs @@ -5,5 +5,5 @@ namespace Sharprompt.Internal; internal interface IItemsProvider { - IEnumerable GetItems(PropertyInfo targetPropertyInfo); + IEnumerable GetItems(PropertyInfo targetPropertyInfo) where T : notnull; } diff --git a/Sharprompt/Internal/NullItemsProvider.cs b/Sharprompt/Internal/NullItemsProvider.cs index a9c5dc3..7892674 100644 --- a/Sharprompt/Internal/NullItemsProvider.cs +++ b/Sharprompt/Internal/NullItemsProvider.cs @@ -6,7 +6,7 @@ namespace Sharprompt.Internal; internal class NullItemsProvider : IItemsProvider { - public IEnumerable GetItems(PropertyInfo targetPropertyInfo) => Enumerable.Empty(); + public IEnumerable GetItems(PropertyInfo targetPropertyInfo) where T : notnull => Enumerable.Empty(); public static IItemsProvider Instance { get; } = new NullItemsProvider(); } diff --git a/Sharprompt/Internal/PropertyMetadata.cs b/Sharprompt/Internal/PropertyMetadata.cs index 9424d90..bf7b122 100644 --- a/Sharprompt/Internal/PropertyMetadata.cs +++ b/Sharprompt/Internal/PropertyMetadata.cs @@ -15,14 +15,13 @@ internal class PropertyMetadata public PropertyMetadata(object model, PropertyInfo propertyInfo) { var displayAttribute = propertyInfo.GetCustomAttribute(); - var dataTypeAttribute = propertyInfo.GetCustomAttribute(); PropertyInfo = propertyInfo; Type = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType; ElementType = TypeHelper.IsCollection(propertyInfo.PropertyType) ? propertyInfo.PropertyType.GetGenericArguments()[0] : null; IsNullable = TypeHelper.IsNullable(propertyInfo.PropertyType); IsCollection = TypeHelper.IsCollection(propertyInfo.PropertyType); - DataType = dataTypeAttribute?.DataType; + DataType = propertyInfo.GetCustomAttribute()?.DataType; Message = displayAttribute?.GetName() ?? displayAttribute?.GetDescription() ?? propertyInfo.Name; Placeholder = displayAttribute?.GetPrompt(); Order = displayAttribute?.GetOrder(); @@ -30,9 +29,7 @@ public PropertyMetadata(object model, PropertyInfo propertyInfo) Validators = propertyInfo.GetCustomAttributes(true) .Select(x => new ValidationAttributeAdapter(x).GetValidator(propertyInfo.Name, model)) .ToArray(); - ItemsProvider = (IItemsProvider?)propertyInfo.GetCustomAttribute(true) ?? - propertyInfo.GetCustomAttribute(true) ?? - NullItemsProvider.Instance; + ItemsProvider = GetItemsProvider(propertyInfo); BindIgnore = propertyInfo.GetCustomAttribute() is not null; } @@ -47,7 +44,7 @@ public PropertyMetadata(object model, PropertyInfo propertyInfo) public int? Order { get; } public object? DefaultValue { get; } public IReadOnlyList> Validators { get; } - public IItemsProvider ItemsProvider { get; set; } + public IItemsProvider ItemsProvider { get; } public bool BindIgnore { get; set; } public FormType DetermineFormType() @@ -62,12 +59,12 @@ public FormType DetermineFormType() return FormType.Confirm; } - if (!IsCollection && (Type.IsEnum || ItemsProvider is not NullItemsProvider)) + if (!IsCollection && ItemsProvider is not NullItemsProvider) { return FormType.Select; } - if (IsCollection && (ElementType is not null && ElementType.IsEnum || ItemsProvider is not NullItemsProvider)) + if (IsCollection && ItemsProvider is not NullItemsProvider) { return FormType.MultiSelect; } @@ -80,6 +77,24 @@ public FormType DetermineFormType() return FormType.Input; } + private IItemsProvider GetItemsProvider(PropertyInfo propertyInfo) + { + var itemsProvider = (IItemsProvider?)propertyInfo.GetCustomAttribute(true) ?? + propertyInfo.GetCustomAttribute(true); + + if (itemsProvider is not null) + { + return itemsProvider; + } + + if (Type.IsEnum || (ElementType is not null && ElementType.IsEnum)) + { + return new EnumItemsProvider(); + } + + return NullItemsProvider.Instance; + } + private class ValidationAttributeAdapter { public ValidationAttributeAdapter(ValidationAttribute validationAttribute) diff --git a/Sharprompt/MemberItemsAttribute.cs b/Sharprompt/MemberItemsAttribute.cs index 4cdcd3a..dcdcba1 100644 --- a/Sharprompt/MemberItemsAttribute.cs +++ b/Sharprompt/MemberItemsAttribute.cs @@ -26,7 +26,7 @@ public MemberItemsAttribute(string memberName, Type memberType) private readonly string _memberName; private readonly Type? _memberType; - public IEnumerable GetItems(PropertyInfo targetPropertyInfo) + public IEnumerable GetItems(PropertyInfo targetPropertyInfo) where T : notnull { var targetType = _memberType ?? targetPropertyInfo.DeclaringType; diff --git a/Sharprompt/Prompt.Basic.cs b/Sharprompt/Prompt.Basic.cs index 1a67b18..2a12620 100644 --- a/Sharprompt/Prompt.Basic.cs +++ b/Sharprompt/Prompt.Basic.cs @@ -106,7 +106,7 @@ public static T Select(Action> configure) where T : notnull return Select(options); } - public static T Select(string message, IEnumerable? items = default, int pageSize = int.MaxValue, T? defaultValue = default, Func? textSelector = default) where T : notnull + public static T Select(string message, IEnumerable? items = default, int pageSize = int.MaxValue, object? defaultValue = default, Func? textSelector = default) where T : notnull { return Select(options => { diff --git a/Sharprompt/Prompt.Bind.cs b/Sharprompt/Prompt.Bind.cs index 820d15a..cc44976 100644 --- a/Sharprompt/Prompt.Bind.cs +++ b/Sharprompt/Prompt.Bind.cs @@ -114,7 +114,7 @@ private static T MakeSelectCore(PropertyMetadata propertyMetadata) where T : { options.Message = propertyMetadata.Message; options.Items = propertyMetadata.ItemsProvider.GetItems(propertyMetadata.PropertyInfo); - options.DefaultValue = (T?)propertyMetadata.DefaultValue; + options.DefaultValue = propertyMetadata.DefaultValue; }); } diff --git a/Sharprompt/Prompt.Configuration.cs b/Sharprompt/Prompt.Configuration.cs index 07e2aa1..b15df36 100644 --- a/Sharprompt/Prompt.Configuration.cs +++ b/Sharprompt/Prompt.Configuration.cs @@ -9,6 +9,8 @@ public static partial class Prompt { public static bool ThrowExceptionOnCancel { get; set; } = false; + public static bool ClearInputOnValidationError { get; set; } = false; + public static CultureInfo Culture { get => Resource.Culture; diff --git a/Sharprompt/SelectOptions.cs b/Sharprompt/SelectOptions.cs index 4fe08c1..d837cdc 100644 --- a/Sharprompt/SelectOptions.cs +++ b/Sharprompt/SelectOptions.cs @@ -21,7 +21,7 @@ public SelectOptions() public IEnumerable Items { get; set; } = null!; - public T? DefaultValue { get; set; } + public object? DefaultValue { get; set; } public int PageSize { get; set; } = int.MaxValue;