diff --git a/Sharprompt.Example/ExampleType.cs b/Sharprompt.Example/ExampleType.cs index 0c729ac..5ee3d68 100644 --- a/Sharprompt.Example/ExampleType.cs +++ b/Sharprompt.Example/ExampleType.cs @@ -3,6 +3,7 @@ public enum ExampleType { Input, + InputWithDefaultValueSelection, Confirm, Password, Select, diff --git a/Sharprompt.Example/Models/MyFormModel.cs b/Sharprompt.Example/Models/MyFormModel.cs index 8ac407f..66bc7f5 100644 --- a/Sharprompt.Example/Models/MyFormModel.cs +++ b/Sharprompt.Example/Models/MyFormModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Sharprompt.Example.Models; @@ -15,19 +16,27 @@ public class MyFormModel [Required] public string Name { get; set; } = null!; - [Display(Name = "Type new password", Order = 2)] + [Display(Name = "What's your favourite colour?", Order = 2)] + [DefaultValueMustBeSelected] + public string FavouriteColour { get; set; } = "blue"; + + [Display(Name = "Type new password", Order = 3)] [DataType(DataType.Password)] [Required] [MinLength(8)] public string Password { get; set; } = null!; - [Display(Name = "Select enum value", Order = 3)] + [Display(Name = "Select enum value", Order = 4)] public MyEnum? MyEnum { get; set; } - [Display(Name = "Select enum values", Order = 4)] + [Display(Name = "Select enum values", Order = 5)] public IEnumerable MyEnums { get; set; } = null!; - [Display(Name = "Please add item(s)", Order = 5)] + [Display(Name = "Select enum values without text selector", Order = 6)] + [DoNotUseTextSelector] + public IEnumerable MyEnumsWithoutTextSelector { get; set; } = null!; + + [Display(Name = "Please add item(s)", Order = 7)] public IEnumerable Lists { get; set; } = null!; [Display(Name = "Are you ready?", Order = 10)] diff --git a/Sharprompt.Example/Program.cs b/Sharprompt.Example/Program.cs index f2b30e6..9695b85 100644 --- a/Sharprompt.Example/Program.cs +++ b/Sharprompt.Example/Program.cs @@ -22,6 +22,9 @@ static void Main(string[] args) case ExampleType.Input: RunInputSample(); break; + case ExampleType.InputWithDefaultValueSelection: + RunInputSampleWithDefaultValueSelection(); + break; case ExampleType.Confirm: RunConfirmSample(); break; @@ -58,6 +61,12 @@ private static void RunInputSample() Console.WriteLine($"Hello, {name}!"); } + private static void RunInputSampleWithDefaultValueSelection() + { + var colour = Prompt.Input("What's your favourite colour?", defaultValue: "blue", defaultValueMustBeSelected: true); + Console.WriteLine($"Your answer is: {colour}!"); + } + private static void RunConfirmSample() { var answer = Prompt.Confirm("Are you ready?"); diff --git a/Sharprompt.Tests/PaginatorTests.cs b/Sharprompt.Tests/PaginatorTests.cs index 3d9b8c9..dbe5ee2 100644 --- a/Sharprompt.Tests/PaginatorTests.cs +++ b/Sharprompt.Tests/PaginatorTests.cs @@ -51,6 +51,32 @@ public void Filter_Empty() Assert.True(subset.IsEmpty); } + [Fact] + public void Filter_NotEmpty_UseTextSelectorFalse() + { + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString(), false); + + paginator.UpdateFilter("0"); + + var currentItems = paginator.CurrentItems; + + Assert.Equal(5, currentItems.Length); + Assert.Equal(new[] { 0, 1, 2, 3, 4 }, currentItems.ToArray()); + } + + [Fact] + public void Filter_Empty_UseTextSelectorFalse() + { + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString(), false); + + paginator.UpdateFilter("x"); + + var currentItems = paginator.CurrentItems; + + Assert.Equal(5, currentItems.Length); + Assert.Equal(new[] { 0, 1, 2, 3, 4 }, currentItems.ToArray()); + } + [Fact] public void SelectedItem() { diff --git a/Sharprompt.Tests/PropertyMetadataTests.cs b/Sharprompt.Tests/PropertyMetadataTests.cs index f04bea2..3528ab8 100644 --- a/Sharprompt.Tests/PropertyMetadataTests.cs +++ b/Sharprompt.Tests/PropertyMetadataTests.cs @@ -209,6 +209,31 @@ public void MemberItems() Assert.Equal(Enumerable.Range(1, 10), metadata[1].ItemsProvider.GetItems(metadata[1].PropertyInfo)); } + [Fact] + public void DefaultValueMustBeSelected() + { + var metadata = PropertyMetadataFactory.Create(new DefaultValueMustBeSelectedModel()); + + Assert.NotNull(metadata); + Assert.Equal(2, metadata.Count); + + Assert.True(metadata[0].DefaultValueMustBeSelected); + Assert.False(metadata[1].DefaultValueMustBeSelected); + } + + [Fact] + public void DoNotUseTextSelector() + { + var metadata = PropertyMetadataFactory.Create(new DoNotUseTextSelectorModel()); + + Assert.NotNull(metadata); + Assert.Equal(2, metadata.Count); + + Assert.True(metadata[0].UseTextSelector); + Assert.False(metadata[1].UseTextSelector); + } + + public class BasicModel { [Display(Name = "Input Value", Prompt = "Required Value")] @@ -302,4 +327,20 @@ public static IEnumerable GetSelectItems() public static IEnumerable SelectItems => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; } + + public class DefaultValueMustBeSelectedModel + { + [DefaultValueMustBeSelected] + public string MemberValue1 { get; set; } = "something"; + + public string MemberValue2 { get; set; } = "nothing"; + } + + public class DoNotUseTextSelectorModel + { + public IEnumerable StrArray { get; set; } = null!; + + [DoNotUseTextSelector] + public IReadOnlyList IntArray { get; set; } = null!; + } } diff --git a/Sharprompt/DefaultValueMustBeSelectedAttribute.cs b/Sharprompt/DefaultValueMustBeSelectedAttribute.cs new file mode 100644 index 0000000..1bab4a2 --- /dev/null +++ b/Sharprompt/DefaultValueMustBeSelectedAttribute.cs @@ -0,0 +1,6 @@ +using System; + +namespace Sharprompt; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class DefaultValueMustBeSelectedAttribute : Attribute; diff --git a/Sharprompt/DoNotUseTextSelectorAttribute.cs b/Sharprompt/DoNotUseTextSelectorAttribute.cs new file mode 100644 index 0000000..d7dbe0e --- /dev/null +++ b/Sharprompt/DoNotUseTextSelectorAttribute.cs @@ -0,0 +1,5 @@ +using System; + +namespace Sharprompt; + +public class DoNotUseTextSelectorAttribute : Attribute; diff --git a/Sharprompt/Fluent/MultiSelectOptionsExtensions.cs b/Sharprompt/Fluent/MultiSelectOptionsExtensions.cs index 1cc1448..ff2306a 100644 --- a/Sharprompt/Fluent/MultiSelectOptionsExtensions.cs +++ b/Sharprompt/Fluent/MultiSelectOptionsExtensions.cs @@ -54,6 +54,13 @@ public static MultiSelectOptions WithTextSelector(this MultiSelectOptions< return options; } + public static MultiSelectOptions WithoutTextSelector(this MultiSelectOptions options) where T : notnull + { + options.UseTextSelector = false; + + return options; + } + public static MultiSelectOptions WithPagination(this MultiSelectOptions options, Func pagination) where T : notnull { options.Pagination = pagination; diff --git a/Sharprompt/Fluent/SelectOptionsExtensions.cs b/Sharprompt/Fluent/SelectOptionsExtensions.cs index e7f37d7..c5b3284 100644 --- a/Sharprompt/Fluent/SelectOptionsExtensions.cs +++ b/Sharprompt/Fluent/SelectOptionsExtensions.cs @@ -40,6 +40,13 @@ public static SelectOptions WithTextSelector(this SelectOptions options return options; } + public static SelectOptions WithoutTextSelector(this SelectOptions options) where T : notnull + { + options.UseTextSelector = false; + + return options; + } + public static SelectOptions WithPagination(this SelectOptions options, Func pagination) where T : notnull { options.Pagination = pagination; diff --git a/Sharprompt/Forms/InputForm.cs b/Sharprompt/Forms/InputForm.cs index 7798362..e3851da 100644 --- a/Sharprompt/Forms/InputForm.cs +++ b/Sharprompt/Forms/InputForm.cs @@ -10,6 +10,8 @@ internal class InputForm : TextFormBase { public InputForm(InputOptions options) { + KeyHandlerMaps.Add(ConsoleKey.Tab, HandleTab); + options.EnsureOptions(); _options = options; @@ -26,7 +28,14 @@ protected override void InputTemplate(OffscreenBuffer offscreenBuffer) if (_defaultValue.HasValue) { - offscreenBuffer.WriteHint($"({_defaultValue.Value}) "); + if (_options.DefaultValueMustBeSelected) + { + offscreenBuffer.WriteHint($"({_defaultValue.Value} - Tab to select) "); + } + else + { + offscreenBuffer.WriteHint($"({_defaultValue.Value}) "); + } } if (InputBuffer.Length == 0 && !string.IsNullOrEmpty(_options.Placeholder)) @@ -65,7 +74,7 @@ protected override bool HandleEnter([NotNullWhen(true)] out T? result) return false; } - result = _defaultValue; + result = _options.DefaultValueMustBeSelected ? default : _defaultValue; } else { @@ -83,4 +92,18 @@ protected override bool HandleEnter([NotNullWhen(true)] out T? result) return false; } + + protected bool HandleTab(ConsoleKeyInfo keyInfo) + { + if (_options.DefaultValueMustBeSelected && _defaultValue.HasValue) + { + InputBuffer.Clear(); + foreach (var c in _defaultValue.Value.ToString()) + { + InputBuffer.Insert(c); + } + } + + return true; + } } diff --git a/Sharprompt/Forms/MultiSelectForm.cs b/Sharprompt/Forms/MultiSelectForm.cs index 3e2adbd..f92b9ac 100644 --- a/Sharprompt/Forms/MultiSelectForm.cs +++ b/Sharprompt/Forms/MultiSelectForm.cs @@ -15,7 +15,7 @@ public MultiSelectForm(MultiSelectOptions options) options.EnsureOptions(); _options = options; - _paginator = new Paginator(options.Items, Math.Min(options.PageSize, Height - 2), Optional.Empty, options.TextSelector) + _paginator = new Paginator(options.Items, Math.Min(options.PageSize, Height - 2), Optional.Empty, options.TextSelector, options.UseTextSelector) { LoopingSelection = options.LoopingSelection }; diff --git a/Sharprompt/Forms/SelectForm.cs b/Sharprompt/Forms/SelectForm.cs index 241db25..9641434 100644 --- a/Sharprompt/Forms/SelectForm.cs +++ b/Sharprompt/Forms/SelectForm.cs @@ -14,7 +14,7 @@ public SelectForm(SelectOptions options) options.EnsureOptions(); _options = options; - _paginator = new Paginator(options.Items, Math.Min(options.PageSize, Height - 2), Optional.Create(options.DefaultValue), options.TextSelector) + _paginator = new Paginator(options.Items, Math.Min(options.PageSize, Height - 2), Optional.Create(options.DefaultValue), options.TextSelector, options.UseTextSelector) { LoopingSelection = options.LoopingSelection }; diff --git a/Sharprompt/InputOptions.cs b/Sharprompt/InputOptions.cs index 0c45379..8569b44 100644 --- a/Sharprompt/InputOptions.cs +++ b/Sharprompt/InputOptions.cs @@ -12,6 +12,8 @@ public class InputOptions public object? DefaultValue { get; set; } + public bool DefaultValueMustBeSelected { get; set; } = false; + public IList> Validators { get; } = new List>(); internal void EnsureOptions() diff --git a/Sharprompt/Internal/Paginator.cs b/Sharprompt/Internal/Paginator.cs index 70ae8eb..cc689b2 100644 --- a/Sharprompt/Internal/Paginator.cs +++ b/Sharprompt/Internal/Paginator.cs @@ -8,17 +8,19 @@ namespace Sharprompt.Internal; internal class Paginator : IEnumerable where T : notnull { - public Paginator(IEnumerable items, int pageSize, Optional defaultValue, Func textSelector) + public Paginator(IEnumerable items, int pageSize, Optional defaultValue, Func textSelector, bool useTextSelector = true) { _items = items.ToArray(); _pageSize = pageSize <= 0 ? _items.Length : Math.Min(pageSize, _items.Length); _textSelector = textSelector; + _useTextSelector = useTextSelector; InitializeDefaults(defaultValue); } private readonly T[] _items; private readonly Func _textSelector; + private readonly bool _useTextSelector; private int _pageSize; private T[] _filteredItems = []; @@ -117,6 +119,11 @@ public void PreviousPage() public void UpdateFilter(string keyword) { + if (!_useTextSelector) + { + return; + } + FilterKeyword = keyword; _selectedIndex = -1; diff --git a/Sharprompt/Internal/PropertyMetadata.cs b/Sharprompt/Internal/PropertyMetadata.cs index c78cd41..545119f 100644 --- a/Sharprompt/Internal/PropertyMetadata.cs +++ b/Sharprompt/Internal/PropertyMetadata.cs @@ -18,7 +18,9 @@ public PropertyMetadata(object model, PropertyInfo propertyInfo) PropertyInfo = propertyInfo; Type = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType; - ElementType = TypeHelper.IsCollection(propertyInfo.PropertyType) ? propertyInfo.PropertyType.GetGenericArguments()[0] : null; + ElementType = TypeHelper.IsCollection(propertyInfo.PropertyType) + ? propertyInfo.PropertyType.GetGenericArguments()[0] + : null; IsNullable = TypeHelper.IsNullable(propertyInfo.PropertyType); IsCollection = TypeHelper.IsCollection(propertyInfo.PropertyType); DataType = propertyInfo.GetCustomAttribute()?.DataType; @@ -26,10 +28,12 @@ public PropertyMetadata(object model, PropertyInfo propertyInfo) Placeholder = displayAttribute?.GetPrompt(); Order = displayAttribute?.GetOrder(); DefaultValue = propertyInfo.GetValue(model); + DefaultValueMustBeSelected = propertyInfo.GetCustomAttribute() is not null; Validators = propertyInfo.GetCustomAttributes(true) - .Select(x => new ValidationAttributeAdapter(x).GetValidator(propertyInfo.Name, model)) - .ToArray(); + .Select(x => new ValidationAttributeAdapter(x).GetValidator(propertyInfo.Name, model)) + .ToArray(); ItemsProvider = GetItemsProvider(propertyInfo); + UseTextSelector = propertyInfo.GetCustomAttribute() is null; } public PropertyInfo PropertyInfo { get; } @@ -42,8 +46,10 @@ public PropertyMetadata(object model, PropertyInfo propertyInfo) public string? Placeholder { get; set; } public int? Order { get; } public object? DefaultValue { get; } + public bool DefaultValueMustBeSelected { get; } public IReadOnlyList> Validators { get; } public IItemsProvider ItemsProvider { get; } + public bool UseTextSelector { get; set; } public FormType DetermineFormType() { @@ -96,8 +102,7 @@ public ValidationAttributeAdapter(ValidationAttribute validationAttribute) { var validationContext = new ValidationContext(model) { - DisplayName = propertyName, - MemberName = propertyName + DisplayName = propertyName, MemberName = propertyName }; return input => _validationAttribute.GetValidationResult(input, validationContext); diff --git a/Sharprompt/MultiSelectOptions.cs b/Sharprompt/MultiSelectOptions.cs index c7dbdad..eb5d89d 100644 --- a/Sharprompt/MultiSelectOptions.cs +++ b/Sharprompt/MultiSelectOptions.cs @@ -31,6 +31,8 @@ public MultiSelectOptions() public Func TextSelector { get; set; } = x => x.ToString()!; + public bool UseTextSelector { get; set; } = true; + public Func Pagination { get; set; } = (count, current, total) => string.Format(Resource.Message_Pagination, count, current, total); public bool LoopingSelection { get; set; } = true; diff --git a/Sharprompt/Prompt.Basic.cs b/Sharprompt/Prompt.Basic.cs index 2a12620..897d69b 100644 --- a/Sharprompt/Prompt.Basic.cs +++ b/Sharprompt/Prompt.Basic.cs @@ -25,13 +25,14 @@ public static T Input(Action> configure) return Input(options); } - public static T Input(string message, object? defaultValue = default, string? placeholder = default, IList>? validators = default) + public static T Input(string message, object? defaultValue = default, bool defaultValueMustBeSelected = false, string? placeholder = default, IList>? validators = default) { return Input(options => { options.Message = message; options.Placeholder = placeholder; options.DefaultValue = defaultValue; + options.DefaultValueMustBeSelected = defaultValueMustBeSelected; options.Validators.Merge(validators); }); @@ -106,7 +107,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, object? 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, bool useTextSelector = true) where T : notnull { return Select(options => { @@ -124,6 +125,8 @@ public static T Select(string message, IEnumerable? items = default, int p { options.TextSelector = textSelector; } + + options.UseTextSelector = useTextSelector; }); } @@ -143,7 +146,7 @@ public static IEnumerable MultiSelect(Action> config return MultiSelect(options); } - public static IEnumerable MultiSelect(string message, IEnumerable? items = null, int pageSize = int.MaxValue, int minimum = 1, int maximum = int.MaxValue, IEnumerable? defaultValues = default, Func? textSelector = default) where T : notnull + public static IEnumerable MultiSelect(string message, IEnumerable? items = null, int pageSize = int.MaxValue, int minimum = 1, int maximum = int.MaxValue, IEnumerable? defaultValues = default, Func? textSelector = default, bool useTextSelector = true) where T : notnull { return MultiSelect(options => { @@ -167,6 +170,8 @@ public static IEnumerable MultiSelect(string message, IEnumerable? item { options.TextSelector = textSelector; } + + options.UseTextSelector = useTextSelector; }); } diff --git a/Sharprompt/Prompt.Bind.cs b/Sharprompt/Prompt.Bind.cs index f23c6bc..76a5294 100644 --- a/Sharprompt/Prompt.Bind.cs +++ b/Sharprompt/Prompt.Bind.cs @@ -63,6 +63,7 @@ private static T MakeInputCore(PropertyMetadata propertyMetadata) { options.Message = propertyMetadata.Message; options.DefaultValue = propertyMetadata.DefaultValue; + options.DefaultValueMustBeSelected = propertyMetadata.DefaultValueMustBeSelected; options.Placeholder = propertyMetadata.Placeholder; options.Validators.Merge(propertyMetadata.Validators); @@ -91,6 +92,7 @@ private static IEnumerable MakeMultiSelectCore(PropertyMetadata propertyMe options.Message = propertyMetadata.Message; options.Items = propertyMetadata.ItemsProvider.GetItems(propertyMetadata.PropertyInfo); options.DefaultValues = (IEnumerable?)propertyMetadata.DefaultValue ?? []; + options.UseTextSelector = propertyMetadata.UseTextSelector; }); } @@ -114,6 +116,7 @@ private static T MakeSelectCore(PropertyMetadata propertyMetadata) where T : options.Message = propertyMetadata.Message; options.Items = propertyMetadata.ItemsProvider.GetItems(propertyMetadata.PropertyInfo); options.DefaultValue = propertyMetadata.DefaultValue; + options.UseTextSelector = propertyMetadata.UseTextSelector; }); } diff --git a/Sharprompt/SelectOptions.cs b/Sharprompt/SelectOptions.cs index e3c2e15..4a002b3 100644 --- a/Sharprompt/SelectOptions.cs +++ b/Sharprompt/SelectOptions.cs @@ -27,6 +27,8 @@ public SelectOptions() public Func TextSelector { get; set; } = x => x.ToString()!; + public bool UseTextSelector { get; set; } = true; + public Func Pagination { get; set; } = (count, current, total) => string.Format(Resource.Message_Pagination, count, current, total); public bool LoopingSelection { get; set; } = true;