Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sharprompt.Example/ExampleType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public enum ExampleType
{
Input,
InputWithDefaultValueSelection,
Confirm,
Password,
Select,
Expand Down
17 changes: 13 additions & 4 deletions Sharprompt.Example/Models/MyFormModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Sharprompt.Example.Models;
Expand All @@ -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<MyEnum> 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<MyEnum> MyEnumsWithoutTextSelector { get; set; } = null!;

[Display(Name = "Please add item(s)", Order = 7)]
public IEnumerable<string> Lists { get; set; } = null!;

[Display(Name = "Are you ready?", Order = 10)]
Expand Down
9 changes: 9 additions & 0 deletions Sharprompt.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ static void Main(string[] args)
case ExampleType.Input:
RunInputSample();
break;
case ExampleType.InputWithDefaultValueSelection:
RunInputSampleWithDefaultValueSelection();
break;
case ExampleType.Confirm:
RunConfirmSample();
break;
Expand Down Expand Up @@ -58,6 +61,12 @@ private static void RunInputSample()
Console.WriteLine($"Hello, {name}!");
}

private static void RunInputSampleWithDefaultValueSelection()
{
var colour = Prompt.Input<string>("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?");
Expand Down
26 changes: 26 additions & 0 deletions Sharprompt.Tests/PaginatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ public void Filter_Empty()
Assert.True(subset.IsEmpty);
}

[Fact]
public void Filter_NotEmpty_UseTextSelectorFalse()
{
var paginator = new Paginator<int>(Enumerable.Range(0, 20), 5, Optional<int>.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<int>(Enumerable.Range(0, 20), 5, Optional<int>.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()
{
Expand Down
41 changes: 41 additions & 0 deletions Sharprompt.Tests/PropertyMetadataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,31 @@ public void MemberItems()
Assert.Equal(Enumerable.Range(1, 10), metadata[1].ItemsProvider.GetItems<int>(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")]
Expand Down Expand Up @@ -302,4 +327,20 @@ public static IEnumerable<int> GetSelectItems()

public static IEnumerable<int> 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<string> StrArray { get; set; } = null!;

[DoNotUseTextSelector]
public IReadOnlyList<int> IntArray { get; set; } = null!;
}
}
6 changes: 6 additions & 0 deletions Sharprompt/DefaultValueMustBeSelectedAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using System;

namespace Sharprompt;

[AttributeUsage(AttributeTargets.Property)]
public sealed class DefaultValueMustBeSelectedAttribute : Attribute;
5 changes: 5 additions & 0 deletions Sharprompt/DoNotUseTextSelectorAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System;

namespace Sharprompt;

public class DoNotUseTextSelectorAttribute : Attribute;
7 changes: 7 additions & 0 deletions Sharprompt/Fluent/MultiSelectOptionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ public static MultiSelectOptions<T> WithTextSelector<T>(this MultiSelectOptions<
return options;
}

public static MultiSelectOptions<T> WithoutTextSelector<T>(this MultiSelectOptions<T> options) where T : notnull
{
options.UseTextSelector = false;

return options;
}

public static MultiSelectOptions<T> WithPagination<T>(this MultiSelectOptions<T> options, Func<int, int, int, string> pagination) where T : notnull
{
options.Pagination = pagination;
Expand Down
7 changes: 7 additions & 0 deletions Sharprompt/Fluent/SelectOptionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ public static SelectOptions<T> WithTextSelector<T>(this SelectOptions<T> options
return options;
}

public static SelectOptions<T> WithoutTextSelector<T>(this SelectOptions<T> options) where T : notnull
{
options.UseTextSelector = false;

return options;
}

public static SelectOptions<T> WithPagination<T>(this SelectOptions<T> options, Func<int, int, int, string> pagination) where T : notnull
{
options.Pagination = pagination;
Expand Down
27 changes: 25 additions & 2 deletions Sharprompt/Forms/InputForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ internal class InputForm<T> : TextFormBase<T>
{
public InputForm(InputOptions<T> options)
{
KeyHandlerMaps.Add(ConsoleKey.Tab, HandleTab);

options.EnsureOptions();

_options = options;
Expand All @@ -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))
Expand Down Expand Up @@ -65,7 +74,7 @@ protected override bool HandleEnter([NotNullWhen(true)] out T? result)
return false;
}

result = _defaultValue;
result = _options.DefaultValueMustBeSelected ? default : _defaultValue;
}
else
{
Expand All @@ -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;
}
}
2 changes: 1 addition & 1 deletion Sharprompt/Forms/MultiSelectForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public MultiSelectForm(MultiSelectOptions<T> options)
options.EnsureOptions();

_options = options;
_paginator = new Paginator<T>(options.Items, Math.Min(options.PageSize, Height - 2), Optional<T>.Empty, options.TextSelector)
_paginator = new Paginator<T>(options.Items, Math.Min(options.PageSize, Height - 2), Optional<T>.Empty, options.TextSelector, options.UseTextSelector)
{
LoopingSelection = options.LoopingSelection
};
Expand Down
2 changes: 1 addition & 1 deletion Sharprompt/Forms/SelectForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public SelectForm(SelectOptions<T> options)
options.EnsureOptions();

_options = options;
_paginator = new Paginator<T>(options.Items, Math.Min(options.PageSize, Height - 2), Optional<T>.Create(options.DefaultValue), options.TextSelector)
_paginator = new Paginator<T>(options.Items, Math.Min(options.PageSize, Height - 2), Optional<T>.Create(options.DefaultValue), options.TextSelector, options.UseTextSelector)
{
LoopingSelection = options.LoopingSelection
};
Expand Down
2 changes: 2 additions & 0 deletions Sharprompt/InputOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public class InputOptions<T>

public object? DefaultValue { get; set; }

public bool DefaultValueMustBeSelected { get; set; } = false;

public IList<Func<object?, ValidationResult?>> Validators { get; } = new List<Func<object?, ValidationResult?>>();

internal void EnsureOptions()
Expand Down
9 changes: 8 additions & 1 deletion Sharprompt/Internal/Paginator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ namespace Sharprompt.Internal;

internal class Paginator<T> : IEnumerable<T> where T : notnull
{
public Paginator(IEnumerable<T> items, int pageSize, Optional<T> defaultValue, Func<T, string> textSelector)
public Paginator(IEnumerable<T> items, int pageSize, Optional<T> defaultValue, Func<T, string> 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<T, string> _textSelector;
private readonly bool _useTextSelector;

private int _pageSize;
private T[] _filteredItems = [];
Expand Down Expand Up @@ -117,6 +119,11 @@ public void PreviousPage()

public void UpdateFilter(string keyword)
{
if (!_useTextSelector)
{
return;
}

FilterKeyword = keyword;

_selectedIndex = -1;
Expand Down
15 changes: 10 additions & 5 deletions Sharprompt/Internal/PropertyMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,22 @@ 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<DataTypeAttribute>()?.DataType;
Message = displayAttribute?.GetName() ?? displayAttribute?.GetDescription() ?? propertyInfo.Name;
Placeholder = displayAttribute?.GetPrompt();
Order = displayAttribute?.GetOrder();
DefaultValue = propertyInfo.GetValue(model);
DefaultValueMustBeSelected = propertyInfo.GetCustomAttribute<DefaultValueMustBeSelectedAttribute>() is not null;
Validators = propertyInfo.GetCustomAttributes<ValidationAttribute>(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<DoNotUseTextSelectorAttribute>() is null;
}

public PropertyInfo PropertyInfo { get; }
Expand All @@ -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<Func<object?, ValidationResult?>> Validators { get; }
public IItemsProvider ItemsProvider { get; }
public bool UseTextSelector { get; set; }

public FormType DetermineFormType()
{
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions Sharprompt/MultiSelectOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public MultiSelectOptions()

public Func<T, string> TextSelector { get; set; } = x => x.ToString()!;

public bool UseTextSelector { get; set; } = true;

public Func<int, int, int, string> Pagination { get; set; } = (count, current, total) => string.Format(Resource.Message_Pagination, count, current, total);

public bool LoopingSelection { get; set; } = true;
Expand Down
Loading