diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..cf0e982a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + groups: + dependencies: + patterns: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + dependencies: + patterns: + - "*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c862ed3e..4ed3abb1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,16 +7,16 @@ on: branches: [ master ] env: - DOTNET_VERSION: 6.0.x + DOTNET_VERSION: 8.0.x jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Use .NET ${{ env.DOTNET_VERSION }} - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3a7a6122..fc108806 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -7,7 +7,7 @@ on: branches: [ "master" ] env: - DOTNET_VERSION: 6.0.x + DOTNET_VERSION: 8.0.x jobs: analyze: @@ -25,21 +25,21 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Use .NET ${{ env.DOTNET_VERSION }} - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6a031724..a02d7a30 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,25 +5,33 @@ on: tags: [ v* ] env: - DOTNET_VERSION: 6.0.x + DOTNET_VERSION: 8.0.x jobs: publish: + permissions: + id-token: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Use .NET ${{ env.DOTNET_VERSION }} - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: NuGet login (OIDC → temp API key) + uses: NuGet/login@v1 + id: login + with: + user: shibayan + - name: Setup Version id: setup_version - run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/} + run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT - name: Pack NuGet Package run: dotnet pack Sharprompt/Sharprompt.csproj -c Release -o ./dist -p:Version=${{ steps.setup_version.outputs.VERSION }} - name: Publish - run: dotnet nuget push dist/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json + run: dotnet nuget push dist/*.nupkg -k ${{ steps.login.outputs.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json diff --git a/Sharprompt.Example/Models/MyFormModel.cs b/Sharprompt.Example/Models/MyFormModel.cs index 7d53d0c5..8ac407fc 100644 --- a/Sharprompt.Example/Models/MyFormModel.cs +++ b/Sharprompt.Example/Models/MyFormModel.cs @@ -9,26 +9,26 @@ public class MyFormModel [BindIgnore] public int Id { get; set; } - public string ReadOnly { get; } + public string ReadOnly { get; } = null!; [Display(Name = "What's your name?", Prompt = "Required", Order = 1)] [Required] - public string Name { get; set; } + public string Name { get; set; } = null!; [Display(Name = "Type new password", Order = 2)] [DataType(DataType.Password)] [Required] [MinLength(8)] - public string Password { get; set; } + public string Password { get; set; } = null!; [Display(Name = "Select enum value", Order = 3)] public MyEnum? MyEnum { get; set; } [Display(Name = "Select enum values", Order = 4)] - public IEnumerable MyEnums { get; set; } + public IEnumerable MyEnums { get; set; } = null!; [Display(Name = "Please add item(s)", Order = 5)] - public IEnumerable Lists { get; set; } + public IEnumerable Lists { get; set; } = null!; [Display(Name = "Are you ready?", Order = 10)] public bool? Ready { get; set; } diff --git a/Sharprompt.Example/Program.cs b/Sharprompt.Example/Program.cs index b1bc8390..f2b30e62 100644 --- a/Sharprompt.Example/Program.cs +++ b/Sharprompt.Example/Program.cs @@ -54,7 +54,7 @@ static void Main(string[] args) private static void RunInputSample() { - var name = Prompt.Input("What's your name?", defaultValue: "John Smith", placeholder: "At least 3 characters", validators: new[] { Validators.Required(), Validators.MinLength(3) }); + var name = Prompt.Input("What's your name?", defaultValue: "John Smith", placeholder: "At least 3 characters", validators: [Validators.Required(), Validators.MinLength(3)]); Console.WriteLine($"Hello, {name}!"); } @@ -66,19 +66,19 @@ private static void RunConfirmSample() private static void RunPasswordSample() { - var secret = Prompt.Password("Type new password", placeholder: "At least 8 characters", validators: new[] { Validators.Required(), Validators.MinLength(8) }); + var secret = Prompt.Password("Type new password", placeholder: "At least 8 characters", validators: [Validators.Required(), Validators.MinLength(8)]); Console.WriteLine($"Password OK, {secret}"); } private static void RunSelectSample() { - var city = Prompt.Select("Select your city", new[] { "Seattle", "London", "Tokyo", "New York", "Singapore", "Shanghai" }, pageSize: 3); + var city = Prompt.Select("Select your city", ["Seattle", "London", "Tokyo", "New York", "Singapore", "Shanghai"], pageSize: 3); Console.WriteLine($"Hello, {city}!"); } private static void RunMultiSelectSample() { - var options = Prompt.MultiSelect("Which cities would you like to visit?", new[] { "Seattle", "London", "Tokyo", "New York", "Singapore", "Shanghai" }, pageSize: 3, defaultValues: new[] { "Tokyo" }); + var options = Prompt.MultiSelect("Which cities would you like to visit?", ["Seattle", "London", "Tokyo", "New York", "Singapore", "Shanghai"], pageSize: 3, defaultValues: ["Tokyo"]); Console.WriteLine($"You picked {string.Join(", ", options)}"); } @@ -90,7 +90,7 @@ private static void RunSelectEnumSample() private static void RunMultiSelectEnumSample() { - var value = Prompt.MultiSelect("Select enum value", defaultValues: new[] { MyEnum.Bar }); + var value = Prompt.MultiSelect("Select enum value", defaultValues: [MyEnum.Bar]); Console.WriteLine($"You picked {string.Join(", ", value)}"); } diff --git a/Sharprompt.Example/Sharprompt.Example.csproj b/Sharprompt.Example/Sharprompt.Example.csproj index 37880b56..711bfdab 100644 --- a/Sharprompt.Example/Sharprompt.Example.csproj +++ b/Sharprompt.Example/Sharprompt.Example.csproj @@ -1,8 +1,9 @@ - + Exe - net6.0 + net8.0 + enable diff --git a/Sharprompt.Tests/PaginatorTests.cs b/Sharprompt.Tests/PaginatorTests.cs index 60cce8c3..3d9b8c93 100644 --- a/Sharprompt.Tests/PaginatorTests.cs +++ b/Sharprompt.Tests/PaginatorTests.cs @@ -13,17 +13,17 @@ public void Basic() { var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString()); - var subset1 = paginator.ToSubset(); + var currentItems1 = paginator.CurrentItems; - Assert.Equal(5, subset1.Count); - Assert.Equal(new[] { 0, 1, 2, 3, 4 }, subset1); + Assert.Equal(5, currentItems1.Length); + Assert.Equal(new[] { 0, 1, 2, 3, 4 }, currentItems1.ToArray()); paginator.NextPage(); - var subset2 = paginator.ToSubset(); + var currentItems2 = paginator.CurrentItems; - Assert.Equal(5, subset2.Count); - Assert.Equal(new[] { 5, 6, 7, 8, 9 }, subset2); + Assert.Equal(5, currentItems2.Length); + Assert.Equal(new[] { 5, 6, 7, 8, 9 }, currentItems2.ToArray()); } [Fact] @@ -33,10 +33,10 @@ public void Filter_NotEmpty() paginator.UpdateFilter("0"); - var subset = paginator.ToSubset(); + var currentItems = paginator.CurrentItems; - Assert.Equal(2, subset.Count); - Assert.Equal(new[] { 0, 10 }, subset); + Assert.Equal(2, currentItems.Length); + Assert.Equal(new[] { 0, 10 }, currentItems.ToArray()); } [Fact] @@ -46,9 +46,9 @@ public void Filter_Empty() paginator.UpdateFilter("x"); - var subset = paginator.ToSubset(); + var subset = paginator.CurrentItems; - Assert.Empty(subset); + Assert.True(subset.IsEmpty); } [Fact] diff --git a/Sharprompt.Tests/PropertyMetadataTests.cs b/Sharprompt.Tests/PropertyMetadataTests.cs index ad071120..f04bea2c 100644 --- a/Sharprompt.Tests/PropertyMetadataTests.cs +++ b/Sharprompt.Tests/PropertyMetadataTests.cs @@ -17,7 +17,7 @@ public void Basic() var metadata = PropertyMetadataFactory.Create(new BasicModel()); Assert.NotNull(metadata); - Assert.Equal(1, metadata.Count); + Assert.Single(metadata); Assert.Equal(typeof(string), metadata[0].Type); Assert.Equal(FormType.Input, metadata[0].DetermineFormType()); @@ -27,7 +27,7 @@ public void Basic() Assert.False(metadata[0].IsCollection); Assert.Null(metadata[0].DefaultValue); Assert.Null(metadata[0].Order); - Assert.Equal(1, metadata[0].Validators.Count); + Assert.Single(metadata[0].Validators); } [Fact] @@ -36,7 +36,7 @@ public void Basic_DefaultValue() var metadata = PropertyMetadataFactory.Create(new BasicModel { Value = "sample" }); Assert.NotNull(metadata); - Assert.Equal(1, metadata.Count); + Assert.Single(metadata); Assert.Equal(typeof(string), metadata[0].Type); Assert.Equal("sample", metadata[0].DefaultValue); @@ -182,7 +182,7 @@ public void BindIgnore() var metadata = PropertyMetadataFactory.Create(new BindIgnoreModel()); Assert.NotNull(metadata); - Assert.Equal(1, metadata.Count); + Assert.Single(metadata); } [Fact] @@ -191,7 +191,7 @@ public void ReadOnly() var metadata = PropertyMetadataFactory.Create(new ReadOnlyModel()); Assert.NotNull(metadata); - Assert.Equal(1, metadata.Count); + Assert.Single(metadata); } [Fact] @@ -213,12 +213,12 @@ public class BasicModel { [Display(Name = "Input Value", Prompt = "Required Value")] [Required] - public string Value { get; set; } + public string Value { get; set; } = null!; } public class ComplexModel { - public string Value1 { get; set; } + public string Value1 { get; set; } = null!; public int Value2 { get; set; } public bool Value3 { get; set; } } @@ -226,7 +226,7 @@ public class ComplexModel public class ComplexWithOrderModel { [Display(Order = 3)] - public string Value1 { get; set; } + public string Value1 { get; set; } = null!; [Display(Order = 1)] public int Value2 { get; set; } @@ -237,9 +237,9 @@ public class ComplexWithOrderModel public class CollectionModel { - public IEnumerable StrArray { get; set; } + public IEnumerable StrArray { get; set; } = null!; - public IReadOnlyList IntArray { get; set; } + public IReadOnlyList IntArray { get; set; } = null!; } public class NullableModel @@ -253,7 +253,7 @@ public class EnumModel { public EnumValue Enum1 { get; set; } public EnumValue? Enum2 { get; set; } - public IEnumerable Enum3 { get; set; } + public IEnumerable Enum3 { get; set; } = null!; } public enum EnumValue @@ -269,7 +269,7 @@ public class InlineItemsModel public int IntValue { get; set; } [InlineItems(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] - public IEnumerable IntArray { get; set; } + public IEnumerable IntArray { get; set; } = null!; } public class BindIgnoreModel @@ -277,14 +277,14 @@ public class BindIgnoreModel [BindIgnore] public int IntValue { get; set; } - public string StringValue { get; set; } + public string StringValue { get; set; } = null!; } public class ReadOnlyModel { public int IntValue { get; } - public string StringValue { get; set; } + public string StringValue { get; set; } = null!; } public class MemberItemsModel @@ -297,9 +297,9 @@ public class MemberItemsModel public static IEnumerable GetSelectItems() { - return new[] { 1, 2, 3, 4, 5 }; + return [1, 2, 3, 4, 5]; } - public static IEnumerable SelectItems => new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + public static IEnumerable SelectItems => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; } } diff --git a/Sharprompt.Tests/Sharprompt.Tests.csproj b/Sharprompt.Tests/Sharprompt.Tests.csproj index 781bb2aa..ccc3a167 100644 --- a/Sharprompt.Tests/Sharprompt.Tests.csproj +++ b/Sharprompt.Tests/Sharprompt.Tests.csproj @@ -1,15 +1,15 @@ - net6.0 - + net8.0 + enable false - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Sharprompt/BindIgnoreAttribute.cs b/Sharprompt/BindIgnoreAttribute.cs index 7ae45f6a..f7eea913 100644 --- a/Sharprompt/BindIgnoreAttribute.cs +++ b/Sharprompt/BindIgnoreAttribute.cs @@ -3,6 +3,4 @@ namespace Sharprompt; [AttributeUsage(AttributeTargets.Property)] -public sealed class BindIgnoreAttribute : Attribute -{ -} +public sealed class BindIgnoreAttribute : Attribute; diff --git a/Sharprompt/ConfirmOptions.cs b/Sharprompt/ConfirmOptions.cs index a6b2ee2d..f084e8db 100644 --- a/Sharprompt/ConfirmOptions.cs +++ b/Sharprompt/ConfirmOptions.cs @@ -4,12 +4,12 @@ namespace Sharprompt; public class ConfirmOptions { - public string Message { get; set; } + public string Message { get; set; } = null!; public bool? DefaultValue { get; set; } internal void EnsureOptions() { - _ = Message ?? throw new ArgumentNullException(nameof(Message)); + ArgumentNullException.ThrowIfNull(Message); } } diff --git a/Sharprompt/Drivers/DefaultConsoleDriver.cs b/Sharprompt/Drivers/DefaultConsoleDriver.cs index 2d136daa..3da5577d 100644 --- a/Sharprompt/Drivers/DefaultConsoleDriver.cs +++ b/Sharprompt/Drivers/DefaultConsoleDriver.cs @@ -76,9 +76,9 @@ public ConsoleKeyInfo ReadKey() { var keyInfo = Console.ReadKey(true); - if (keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers == ConsoleModifiers.Control) + if (keyInfo is { Key: ConsoleKey.C, Modifiers: ConsoleModifiers.Control }) { - CancellationCallback?.Invoke(); + CancellationCallback.Invoke(); } return keyInfo; @@ -99,7 +99,6 @@ public void Write(string value, ConsoleColor color) public bool CursorVisible { - get => Console.CursorVisible; set => Console.CursorVisible = value; } @@ -115,7 +114,7 @@ public bool CursorVisible public int WindowHeight => Console.WindowHeight; - public Action CancellationCallback { get; set; } + public Action CancellationCallback { get; set; } = () => { }; #endregion } diff --git a/Sharprompt/Drivers/IConsoleDriver.cs b/Sharprompt/Drivers/IConsoleDriver.cs index 198eabac..695a192f 100644 --- a/Sharprompt/Drivers/IConsoleDriver.cs +++ b/Sharprompt/Drivers/IConsoleDriver.cs @@ -12,7 +12,7 @@ internal interface IConsoleDriver : IDisposable void WriteLine(); void SetCursorPosition(int left, int top); bool KeyAvailable { get; } - bool CursorVisible { get; set; } + bool CursorVisible { set; } int CursorLeft { get; } int CursorTop { get; } int BufferWidth { get; } diff --git a/Sharprompt/Fluent/InputOptionsExtensions.cs b/Sharprompt/Fluent/InputOptionsExtensions.cs index 26896c79..1598c060 100644 --- a/Sharprompt/Fluent/InputOptionsExtensions.cs +++ b/Sharprompt/Fluent/InputOptionsExtensions.cs @@ -26,7 +26,7 @@ public static InputOptions WithDefaultValue(this InputOptions options, return options; } - public static InputOptions AddValidators(this InputOptions options, params Func[] validators) + public static InputOptions AddValidators(this InputOptions options, params Func[] validators) { foreach (var validator in validators) { diff --git a/Sharprompt/Fluent/ListOptionsExtensions.cs b/Sharprompt/Fluent/ListOptionsExtensions.cs index ccd68702..2ea54b10 100644 --- a/Sharprompt/Fluent/ListOptionsExtensions.cs +++ b/Sharprompt/Fluent/ListOptionsExtensions.cs @@ -6,35 +6,35 @@ namespace Sharprompt.Fluent; public static class ListOptionsExtensions { - public static ListOptions WithMessage(this ListOptions options, string message) + public static ListOptions WithMessage(this ListOptions options, string message) where T : notnull { options.Message = message; return options; } - public static ListOptions WithDefaultValues(this ListOptions options, IEnumerable defaultValues) + public static ListOptions WithDefaultValues(this ListOptions options, IEnumerable defaultValues) where T : notnull { options.DefaultValues = defaultValues; return options; } - public static ListOptions WithMinimum(this ListOptions options, int minimum) + public static ListOptions WithMinimum(this ListOptions options, int minimum) where T : notnull { options.Minimum = minimum; return options; } - public static ListOptions WithMaximum(this ListOptions options, int maximum) + public static ListOptions WithMaximum(this ListOptions options, int maximum) where T : notnull { options.Maximum = maximum; return options; } - public static ListOptions AddValidators(this ListOptions options, params Func[] validators) + public static ListOptions AddValidators(this ListOptions options, params Func[] validators) where T : notnull { foreach (var validator in validators) { diff --git a/Sharprompt/Fluent/MultiSelectOptionsExtensions.cs b/Sharprompt/Fluent/MultiSelectOptionsExtensions.cs index 22ecd620..1cc14487 100644 --- a/Sharprompt/Fluent/MultiSelectOptionsExtensions.cs +++ b/Sharprompt/Fluent/MultiSelectOptionsExtensions.cs @@ -5,56 +5,56 @@ namespace Sharprompt.Fluent; public static class MultiSelectOptionsExtensions { - public static MultiSelectOptions WithMessage(this MultiSelectOptions options, string message) + public static MultiSelectOptions WithMessage(this MultiSelectOptions options, string message) where T : notnull { options.Message = message; return options; } - public static MultiSelectOptions WithItems(this MultiSelectOptions options, IEnumerable items) + public static MultiSelectOptions WithItems(this MultiSelectOptions options, IEnumerable items) where T : notnull { options.Items = items; return options; } - public static MultiSelectOptions WithDefaultValues(this MultiSelectOptions options, IEnumerable defaultValues) + public static MultiSelectOptions WithDefaultValues(this MultiSelectOptions options, IEnumerable defaultValues) where T : notnull { options.DefaultValues = defaultValues; return options; } - public static MultiSelectOptions WithPageSize(this MultiSelectOptions options, int pageSize) + public static MultiSelectOptions WithPageSize(this MultiSelectOptions options, int pageSize) where T : notnull { options.PageSize = pageSize; return options; } - public static MultiSelectOptions WithMinimum(this MultiSelectOptions options, int minimum) + public static MultiSelectOptions WithMinimum(this MultiSelectOptions options, int minimum) where T : notnull { options.Minimum = minimum; return options; } - public static MultiSelectOptions WithMaximum(this MultiSelectOptions options, int maximum) + public static MultiSelectOptions WithMaximum(this MultiSelectOptions options, int maximum) where T : notnull { options.Maximum = maximum; return options; } - public static MultiSelectOptions WithTextSelector(this MultiSelectOptions options, Func textSelector) + public static MultiSelectOptions WithTextSelector(this MultiSelectOptions options, Func textSelector) where T : notnull { options.TextSelector = textSelector; return options; } - public static MultiSelectOptions WithPagination(this MultiSelectOptions options, Func pagination) + public static MultiSelectOptions WithPagination(this MultiSelectOptions options, Func pagination) where T : notnull { options.Pagination = pagination; diff --git a/Sharprompt/Fluent/PasswordOptionsExtensions.cs b/Sharprompt/Fluent/PasswordOptionsExtensions.cs index 86fcb3b0..f3f6d9f7 100644 --- a/Sharprompt/Fluent/PasswordOptionsExtensions.cs +++ b/Sharprompt/Fluent/PasswordOptionsExtensions.cs @@ -26,7 +26,7 @@ public static PasswordOptions WithPasswordChar(this PasswordOptions options, str return options; } - public static PasswordOptions AddValidators(this PasswordOptions options, params Func[] validators) + public static PasswordOptions AddValidators(this PasswordOptions options, params Func[] validators) { foreach (var validator in validators) { diff --git a/Sharprompt/Fluent/SelectOptionsExtensions.cs b/Sharprompt/Fluent/SelectOptionsExtensions.cs index 2481ad42..e7f37d74 100644 --- a/Sharprompt/Fluent/SelectOptionsExtensions.cs +++ b/Sharprompt/Fluent/SelectOptionsExtensions.cs @@ -5,42 +5,42 @@ namespace Sharprompt.Fluent; public static class SelectOptionsExtensions { - public static SelectOptions WithMessage(this SelectOptions options, string message) + public static SelectOptions WithMessage(this SelectOptions options, string message) where T : notnull { options.Message = message; return options; } - public static SelectOptions WithItems(this SelectOptions options, IEnumerable items) + public static SelectOptions WithItems(this SelectOptions options, IEnumerable items) where T : notnull { options.Items = items; return options; } - public static SelectOptions WithDefaultValue(this SelectOptions options, T defaultValue) + public static SelectOptions WithDefaultValue(this SelectOptions options, T defaultValue) where T : notnull { options.DefaultValue = defaultValue; return options; } - public static SelectOptions WithPageSize(this SelectOptions options, int pageSize) + public static SelectOptions WithPageSize(this SelectOptions options, int pageSize) where T : notnull { options.PageSize = pageSize; return options; } - public static SelectOptions WithTextSelector(this SelectOptions options, Func textSelector) + public static SelectOptions WithTextSelector(this SelectOptions options, Func textSelector) where T : notnull { options.TextSelector = textSelector; return options; } - public static SelectOptions WithPagination(this SelectOptions options, Func pagination) + public static SelectOptions WithPagination(this SelectOptions options, Func pagination) where T : notnull { options.Pagination = pagination; diff --git a/Sharprompt/Forms/ConfirmForm.cs b/Sharprompt/Forms/ConfirmForm.cs index a2456e4d..499fe7e9 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)) { @@ -108,7 +63,7 @@ private bool HandleEnter(out bool result) else { if (input.Equals(Resource.ConfirmForm_Answer_Yes, StringComparison.OrdinalIgnoreCase) || - input.Equals(Resource.ConfirmForm_Answer_Yes.Remove(1), StringComparison.OrdinalIgnoreCase)) + input.Equals(Resource.ConfirmForm_Answer_Yes[..1], StringComparison.OrdinalIgnoreCase)) { result = true; @@ -116,13 +71,15 @@ private bool HandleEnter(out bool result) } if (input.Equals(Resource.ConfirmForm_Answer_No, StringComparison.OrdinalIgnoreCase) || - input.Equals(Resource.ConfirmForm_Answer_No.Remove(1), StringComparison.OrdinalIgnoreCase)) + input.Equals(Resource.ConfirmForm_Answer_No[..1], StringComparison.OrdinalIgnoreCase)) { result = false; return true; } + InputBuffer.Clear(); + SetError(Resource.Validation_Invalid); } diff --git a/Sharprompt/Forms/FormBase.cs b/Sharprompt/Forms/FormBase.cs index 413bace3..b9090ded 100644 --- a/Sharprompt/Forms/FormBase.cs +++ b/Sharprompt/Forms/FormBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Sharprompt.Drivers; @@ -13,17 +14,24 @@ internal abstract class FormBase : IDisposable { protected FormBase() { - ConsoleDriver = new DefaultConsoleDriver + _consoleDriver = new DefaultConsoleDriver { CancellationCallback = CancellationHandler }; - _formRenderer = new FormRenderer(ConsoleDriver); + _formRenderer = new FormRenderer(_consoleDriver); } + private readonly IConsoleDriver _consoleDriver; private readonly FormRenderer _formRenderer; - protected IConsoleDriver ConsoleDriver { get; } + protected TextInputBuffer InputBuffer { get; } = new(); + + protected Dictionary> KeyHandlerMaps { get; set; } = new(); + + protected int Width => _consoleDriver.WindowWidth; + + protected int Height => _consoleDriver.WindowHeight; public void Dispose() => _formRenderer.Dispose(); @@ -44,19 +52,26 @@ public T Start() } } - protected abstract bool TryGetResult(out T result); - 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); - protected void SetError(ValidationResult validationResult) => SetError(validationResult.ErrorMessage); + protected void SetError(ValidationResult validationResult) => SetError(validationResult.ErrorMessage!); - protected bool TryValidate(object input, IList> validators) + protected bool TryValidate([NotNullWhen(true)] object? input, IList> validators) { var result = validators.Select(x => x(input)) .FirstOrDefault(x => x != ValidationResult.Success); @@ -71,6 +86,38 @@ protected bool TryValidate(object input, IList> v return true; } + private 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; + } + private void CancellationHandler() { _formRenderer.Cancel(); diff --git a/Sharprompt/Forms/FormRenderer.cs b/Sharprompt/Forms/FormRenderer.cs index 26452bf6..afe2d8e1 100644 --- a/Sharprompt/Forms/FormRenderer.cs +++ b/Sharprompt/Forms/FormRenderer.cs @@ -5,16 +5,11 @@ namespace Sharprompt.Forms; -internal class FormRenderer : IDisposable +internal class FormRenderer(IConsoleDriver consoleDriver) : IDisposable { - public FormRenderer(IConsoleDriver consoleDriver) - { - _offscreenBuffer = new OffscreenBuffer(consoleDriver); - } + private readonly OffscreenBuffer _offscreenBuffer = new(consoleDriver); - private readonly OffscreenBuffer _offscreenBuffer; - - public string ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } public void Dispose() => _offscreenBuffer.Dispose(); @@ -26,17 +21,19 @@ public void Render(Action template) { template(_offscreenBuffer); - if (!string.IsNullOrEmpty(ErrorMessage)) + if (string.IsNullOrEmpty(ErrorMessage)) { - _offscreenBuffer.WriteLine(); - _offscreenBuffer.WriteError(ErrorMessage); - - ErrorMessage = null; + return; } + + _offscreenBuffer.WriteLine(); + _offscreenBuffer.WriteError(ErrorMessage); + + ErrorMessage = null; } } - 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 aae7b3fa..7798362d 100644 --- a/Sharprompt/Forms/InputForm.cs +++ b/Sharprompt/Forms/InputForm.cs @@ -1,11 +1,12 @@ using System; +using System.Diagnostics.CodeAnalysis; using Sharprompt.Internal; using Sharprompt.Strings; namespace Sharprompt.Forms; -internal class InputForm : FormBase +internal class InputForm : TextFormBase { public InputForm(InputOptions options) { @@ -19,72 +20,6 @@ public InputForm(InputOptions options) private readonly InputOptions _options; private readonly Optional _defaultValue; - private readonly TextInputBuffer _textInputBuffer = new(); - - protected override bool TryGetResult(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); @@ -94,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) @@ -109,19 +44,19 @@ protected override void FinishTemplate(OffscreenBuffer offscreenBuffer, T result if (result is not null) { - offscreenBuffer.WriteAnswer(result.ToString()); + offscreenBuffer.WriteAnswer(result.ToString()!); } } - private bool HandleEnter(out T result) + protected override bool HandleEnter([NotNullWhen(true)] out T? result) { - var input = _textInputBuffer.ToString(); + var input = InputBuffer.ToString(); try { if (string.IsNullOrEmpty(input)) { - if (TypeHelper.IsValueType && !_defaultValue.HasValue) + if (!TypeHelper.IsNullable && !_defaultValue.HasValue) { SetError(Resource.Validation_Required); @@ -137,11 +72,7 @@ private bool HandleEnter(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 56be498b..11aafadd 100644 --- a/Sharprompt/Forms/ListForm.cs +++ b/Sharprompt/Forms/ListForm.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Sharprompt.Internal; @@ -7,7 +8,7 @@ namespace Sharprompt.Forms; -internal class ListForm : FormBase> +internal class ListForm : TextFormBase> where T : notnull { public ListForm(ListOptions options) { @@ -15,68 +16,18 @@ public ListForm(ListOptions options) _options = options; - _inputItems.AddRange(options.DefaultValues ?? Enumerable.Empty()); + _inputItems.AddRange(options.DefaultValues); } private readonly ListOptions _options; - private readonly List _inputItems = new(); - private readonly TextInputBuffer _textInputBuffer = new(); - - protected override bool TryGetResult(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; - } + private readonly List _inputItems = []; protected override void InputTemplate(OffscreenBuffer offscreenBuffer) { offscreenBuffer.WritePrompt(_options.Message); - offscreenBuffer.WriteInput(_textInputBuffer); + offscreenBuffer.WriteInput(InputBuffer); foreach (var inputItem in _inputItems) { @@ -91,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 { @@ -125,7 +76,7 @@ private bool HandleEnter(out IEnumerable result) return false; } - _textInputBuffer.Clear(); + InputBuffer.Clear(); _inputItems.Add(inputValue); } @@ -138,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 06e4691c..3e2adbd4 100644 --- a/Sharprompt/Forms/MultiSelectForm.cs +++ b/Sharprompt/Forms/MultiSelectForm.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Sharprompt.Internal; @@ -7,145 +8,56 @@ namespace Sharprompt.Forms; -internal class MultiSelectForm : FormBase> +internal class MultiSelectForm : FormBase> where T : notnull { public MultiSelectForm(MultiSelectOptions options) { options.EnsureOptions(); _options = options; + _paginator = new Paginator(options.Items, Math.Min(options.PageSize, Height - 2), Optional.Empty, options.TextSelector) + { + LoopingSelection = options.LoopingSelection + }; - var maxPageSize = ConsoleDriver.WindowHeight - 2; - var pageSize = Math.Min(options.PageSize ?? maxPageSize, maxPageSize); - - _paginator = new Paginator(options.Items, pageSize, Optional.Empty, options.TextSelector); - - if (options.DefaultValues is not null) + foreach (var defaultValue in options.DefaultValues) { - foreach (var defaultValue in options.DefaultValues) - { - _selectedItems.Add(defaultValue); - } + _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(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; - } + private readonly HashSet _selectedItems = []; protected override void InputTemplate(OffscreenBuffer offscreenBuffer) { + _paginator.UpdatePageSize(Math.Min(_options.PageSize, Height - 2)); + offscreenBuffer.WritePrompt(_options.Message); - offscreenBuffer.Write(_paginator.FilterTerm); + offscreenBuffer.Write(_paginator.FilterKeyword); offscreenBuffer.PushCursor(); - if (string.IsNullOrEmpty(_paginator.FilterTerm)) + if (string.IsNullOrEmpty(_paginator.FilterKeyword)) { offscreenBuffer.WriteHint(Resource.MultiSelectForm_Message_Hint); } - var subset = _paginator.ToSubset(); - - foreach (var item in subset) + foreach (var item in _paginator.CurrentItems) { var value = _options.TextSelector(item); @@ -168,10 +80,10 @@ protected override void InputTemplate(OffscreenBuffer offscreenBuffer) } } - if (_paginator.PageCount > 1 && _options.Pagination is not null) + if (_paginator.PageCount > 1) { offscreenBuffer.WriteLine(); - offscreenBuffer.WriteHint(_options.Pagination(_paginator.TotalCount, _paginator.SelectedPage + 1, _paginator.PageCount)); + offscreenBuffer.WriteHint(_options.Pagination(_paginator.TotalCount, _paginator.CurrentPage + 1, _paginator.PageCount)); } } @@ -180,4 +92,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) + { + _selectedItems.Add(item); + } + } + + return true; + } + + private bool HandleIWithControl(ConsoleKeyInfo keyInfo) + { + if (keyInfo.Modifiers != ConsoleModifiers.Control) + { + return false; + } + + var invertedItems = _paginator.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 986317f0..d063d48b 100644 --- a/Sharprompt/Forms/PasswordForm.cs +++ b/Sharprompt/Forms/PasswordForm.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Sharprompt.Internal; @@ -12,54 +13,20 @@ public PasswordForm(PasswordOptions options) options.EnsureOptions(); _options = options; - } - - private readonly PasswordOptions _options; - private readonly TextInputBuffer _textInputBuffer = new(); - - protected override bool TryGetResult(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); @@ -67,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(); } } @@ -78,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 500402d4..241db252 100644 --- a/Sharprompt/Forms/SelectForm.cs +++ b/Sharprompt/Forms/SelectForm.cs @@ -1,90 +1,47 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Sharprompt.Internal; using Sharprompt.Strings; namespace Sharprompt.Forms; -internal class SelectForm : FormBase +internal class SelectForm : FormBase where T : notnull { 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) + { + LoopingSelection = options.LoopingSelection + }; - var maxPageSize = ConsoleDriver.WindowHeight - 2; - var pageSize = Math.Min(options.PageSize ?? maxPageSize, 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(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) { + _paginator.UpdatePageSize(Math.Min(_options.PageSize, Height - 2)); + offscreenBuffer.WritePrompt(_options.Message); - offscreenBuffer.Write(_paginator.FilterTerm); + offscreenBuffer.Write(_paginator.FilterKeyword); offscreenBuffer.PushCursor(); - var subset = _paginator.ToSubset(); - - foreach (var item in subset) + foreach (var item in _paginator.CurrentItems) { var value = _options.TextSelector(item); @@ -100,10 +57,10 @@ protected override void InputTemplate(OffscreenBuffer offscreenBuffer) } } - if (_paginator.PageCount > 1 && _options.Pagination is not null) + if (_paginator.PageCount > 1) { offscreenBuffer.WriteLine(); - offscreenBuffer.WriteHint(_options.Pagination(_paginator.TotalCount, _paginator.SelectedPage + 1, _paginator.PageCount)); + offscreenBuffer.WriteHint(_options.Pagination(_paginator.TotalCount, _paginator.CurrentPage + 1, _paginator.PageCount)); } } @@ -112,4 +69,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 00000000..bd12429f --- /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 39d8f686..5a0a84c6 100644 --- a/Sharprompt/InlineItemsAttribute.cs +++ b/Sharprompt/InlineItemsAttribute.cs @@ -12,10 +12,12 @@ public sealed class InlineItemsAttribute : Attribute, IItemsProvider { public InlineItemsAttribute(params object[] items) { + ArgumentNullException.ThrowIfNull(items); + _items = 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/InputOptions.cs b/Sharprompt/InputOptions.cs index 970911b6..0c45379f 100644 --- a/Sharprompt/InputOptions.cs +++ b/Sharprompt/InputOptions.cs @@ -6,16 +6,16 @@ namespace Sharprompt; public class InputOptions { - public string Message { get; set; } + public string Message { get; set; } = null!; - public string Placeholder { get; set; } + public string? Placeholder { get; set; } - public object DefaultValue { get; set; } + public object? DefaultValue { get; set; } - public IList> Validators { get; } = new List>(); + public IList> Validators { get; } = new List>(); internal void EnsureOptions() { - _ = Message ?? throw new ArgumentNullException(nameof(Message)); + ArgumentNullException.ThrowIfNull(Message); } } diff --git a/Sharprompt/Internal/EastAsianWidth.cs b/Sharprompt/Internal/EastAsianWidth.cs index 22b89ae7..3339ba1d 100644 --- a/Sharprompt/Internal/EastAsianWidth.cs +++ b/Sharprompt/Internal/EastAsianWidth.cs @@ -61,7 +61,7 @@ private static bool IsFullWidth(uint codePoint) } private static readonly EastAsianWidthRange[] s_eastAsianWidthRanges = - { + [ new(161, 0, true), new(164, 0, true), new(167, 1, true), @@ -267,8 +267,7 @@ private static bool IsFullWidth(uint codePoint) new(11931, 88, false), new(12032, 213, false), new(12272, 11, false), - new(12288, 0, false), - new(12289, 61, false), + new(12288, 62, false), new(12353, 85, false), new(12441, 102, false), new(12549, 42, false), @@ -301,7 +300,9 @@ private static bool IsFullWidth(uint codePoint) new(110581, 6, false), new(110589, 1, false), new(110592, 290, false), + new(110898, 0, false), new(110928, 2, false), + new(110933, 0, false), new(110948, 3, false), new(110960, 395, false), new(126980, 0, false), @@ -341,7 +342,7 @@ private static bool IsFullWidth(uint codePoint) new(128716, 0, false), new(128720, 2, false), new(128725, 2, false), - new(128733, 2, false), + new(128732, 3, false), new(128747, 1, false), new(128756, 8, false), new(128992, 11, false), @@ -349,33 +350,19 @@ private static bool IsFullWidth(uint codePoint) new(129292, 46, false), new(129340, 9, false), new(129351, 184, false), - new(129648, 4, false), - new(129656, 4, false), - new(129664, 6, false), - new(129680, 28, false), - new(129712, 10, false), - new(129728, 5, false), - new(129744, 9, false), - new(129760, 7, false), - new(129776, 6, false), + new(129648, 12, false), + new(129664, 8, false), + new(129680, 45, false), + new(129727, 6, false), + new(129742, 13, false), + new(129760, 8, false), + new(129776, 8, false), new(131072, 65533, false), new(196608, 65533, false), new(917760, 239, true), new(983040, 65533, true), new(1048576, 65533, true) - }; - - private readonly struct EastAsianWidthRange - { - public EastAsianWidthRange(uint start, ushort count, bool ambiguous) - { - Start = start; - Count = count; - Ambiguous = ambiguous; - } + ]; - public uint Start { get; } - public ushort Count { get; } - public bool Ambiguous { get; } - } + public readonly record struct EastAsianWidthRange(uint Start, ushort Count, bool Ambiguous); } diff --git a/Sharprompt/Internal/EnumHelper.cs b/Sharprompt/Internal/EnumHelper.cs index 163935cc..ad2833dc 100644 --- a/Sharprompt/Internal/EnumHelper.cs +++ b/Sharprompt/Internal/EnumHelper.cs @@ -6,11 +6,11 @@ namespace Sharprompt.Internal; -internal static class EnumHelper +internal static class EnumHelper where TEnum : notnull { static EnumHelper() { - var values = (T[])Enum.GetValues(typeof(T)); + var values = (TEnum[])Enum.GetValuesAsUnderlyingType(typeof(TEnum)); foreach (var value in values) { @@ -18,28 +18,20 @@ static EnumHelper() } } - private static readonly Dictionary s_metadataCache = new(); + private static readonly Dictionary s_metadataCache = new(); - private static EnumMetadata GetEnumMetadata(T value) + private static EnumMetadata GetEnumMetadata(TEnum value) { - var displayAttribute = typeof(T).GetField(value.ToString())?.GetCustomAttribute(); + var displayAttribute = typeof(TEnum).GetField(value.ToString()!)?.GetCustomAttribute(); - return new EnumMetadata - { - DisplayName = displayAttribute?.GetName(), - Order = displayAttribute?.GetOrder() - }; + return new EnumMetadata(displayAttribute?.GetName(), displayAttribute?.GetOrder()); } - public static string GetDisplayName(T value) => s_metadataCache[value]?.DisplayName ?? value.ToString(); + public static string GetDisplayName(TEnum value) => s_metadataCache[value].DisplayName ?? value.ToString()!; - public static IEnumerable GetValues() => s_metadataCache.OrderBy(x => x.Value.Order) - .Select(x => x.Key) - .ToArray(); + public static IEnumerable GetValues() => s_metadataCache.OrderBy(x => x.Value.Order) + .Select(x => x.Key) + .ToArray(); - private class EnumMetadata - { - public string DisplayName { get; set; } - public int? Order { get; set; } - } + private record EnumMetadata(string? DisplayName, int? Order); } diff --git a/Sharprompt/Internal/EnumItemsProvider.cs b/Sharprompt/Internal/EnumItemsProvider.cs new file mode 100644 index 00000000..96a90cec --- /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 879a00ed..e67c5c7a 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 new file mode 100644 index 00000000..72961600 --- /dev/null +++ b/Sharprompt/Internal/NullItemsProvider.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace Sharprompt.Internal; + +internal class NullItemsProvider : IItemsProvider +{ + public IEnumerable GetItems(PropertyInfo targetPropertyInfo) where T : notnull => []; + + public static IItemsProvider Instance { get; } = new NullItemsProvider(); +} diff --git a/Sharprompt/Internal/OffscreenBuffer.cs b/Sharprompt/Internal/OffscreenBuffer.cs index c8523335..da07b172 100644 --- a/Sharprompt/Internal/OffscreenBuffer.cs +++ b/Sharprompt/Internal/OffscreenBuffer.cs @@ -16,10 +16,10 @@ public OffscreenBuffer(IConsoleDriver consoleDriver) } private readonly IConsoleDriver _consoleDriver; - private readonly List> _outputBuffer = new() { new List() }; + private readonly List> _outputBuffer = [new()]; private int _cursorBottom; - private Cursor _pushedCursor; + private Cursor? _pushedCursor; private int WrittenLineCount => _outputBuffer.Sum(x => (x.Sum(xs => xs.Width) - 1) / _consoleDriver.BufferWidth + 1) - 1; @@ -37,7 +37,7 @@ public void Write(string text, ConsoleColor color) _outputBuffer.Last().Add(new TextInfo(text, color)); } - public void WriteLine() => _outputBuffer.Add(new List()); + public void WriteLine() => _outputBuffer.Add([]); public void PushCursor() { @@ -46,11 +46,7 @@ public void PushCursor() return; } - _pushedCursor = new Cursor - { - Left = _outputBuffer.Last().Sum(x => x.Width), - Top = _outputBuffer.Count - 1 - }; + _pushedCursor = new Cursor(_outputBuffer.Last().Sum(x => x.Width), _outputBuffer.Count - 1); } public IDisposable BeginRender() => new RenderScope(this, _consoleDriver, _cursorBottom, WrittenLineCount); @@ -74,24 +70,28 @@ public void RenderToConsole() _cursorBottom = _consoleDriver.CursorTop; - if (_pushedCursor is not null) + if (_pushedCursor is null) + { + return; + } + + var physicalLeft = _pushedCursor.Left % _consoleDriver.BufferWidth; + var physicalTop = _pushedCursor.Top + (_pushedCursor.Left / _consoleDriver.BufferWidth); + + var consoleTop = _cursorBottom - WrittenLineCount + physicalTop; + + if (_pushedCursor.Left > 0 && physicalLeft == 0) { - var physicalLeft = _pushedCursor.Left % _consoleDriver.BufferWidth; - var physicalTop = _pushedCursor.Top + (_pushedCursor.Left / _consoleDriver.BufferWidth); + _consoleDriver.WriteLine(); - var consoleTop = _cursorBottom - WrittenLineCount + physicalTop; - if (_pushedCursor.Left > 0 && physicalLeft == 0) + if (consoleTop == _consoleDriver.BufferHeight) { - _consoleDriver.WriteLine(); - if (consoleTop == _consoleDriver.BufferHeight) - { - _cursorBottom--; - consoleTop--; - } + _cursorBottom--; + consoleTop--; } - - _consoleDriver.SetCursorPosition(physicalLeft, consoleTop); } + + _consoleDriver.SetCursorPosition(physicalLeft, consoleTop); } public void ClearConsole(int cursorBottom, int writtenLineCount) @@ -105,7 +105,7 @@ public void ClearConsole(int cursorBottom, int writtenLineCount) public void ClearBuffer() { _outputBuffer.Clear(); - _outputBuffer.Add(new List()); + _outputBuffer.Add([]); _pushedCursor = null; } @@ -118,17 +118,15 @@ public void Cancel() _consoleDriver.WriteLine(); } - private class Cursor - { - public int Left { get; set; } - public int Top { get; set; } - } + private record Cursor(int Left, int Top); private readonly struct TextInfo { public TextInfo(string text, ConsoleColor color) { - Text = text ?? throw new ArgumentNullException(nameof(text)); + ArgumentNullException.ThrowIfNull(text); + + Text = text; Color = color; Width = text.GetWidth(); } diff --git a/Sharprompt/Internal/Optional.cs b/Sharprompt/Internal/Optional.cs index b8f3f303..15367c8d 100644 --- a/Sharprompt/Internal/Optional.cs +++ b/Sharprompt/Internal/Optional.cs @@ -8,15 +8,15 @@ public Optional(T value) Value = value; } - public bool HasValue { get; } + public bool HasValue { get; } = false; - public T Value { get; } + public T Value { get; } = default!; public static readonly Optional Empty = new(); public static implicit operator T(Optional optional) => optional.Value; - public static Optional Create(T value) => value is null ? Empty : new Optional(value); + public static Optional Create(T? value) => value is null ? Empty : new Optional(value); - public static Optional Create(object value) => value is null ? Empty : new Optional((T)value); + public static Optional Create(object? value) => value is null ? Empty : new Optional((T)value); } diff --git a/Sharprompt/Internal/Paginator.cs b/Sharprompt/Internal/Paginator.cs index a3fa61a9..70ae8ebf 100644 --- a/Sharprompt/Internal/Paginator.cs +++ b/Sharprompt/Internal/Paginator.cs @@ -1,60 +1,96 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Sharprompt.Internal; -internal class Paginator +internal class Paginator : IEnumerable where T : notnull { public Paginator(IEnumerable items, int pageSize, Optional defaultValue, Func textSelector) { _items = items.ToArray(); - _pageSize = Math.Min(pageSize, _items.Length); + _pageSize = pageSize <= 0 ? _items.Length : Math.Min(pageSize, _items.Length); _textSelector = textSelector; InitializeDefaults(defaultValue); } private readonly T[] _items; - private readonly int _pageSize; private readonly Func _textSelector; + private int _pageSize; + private T[] _filteredItems = []; private int _selectedIndex = -1; - public T[] FilteredItems { get; private set; } + public ReadOnlySpan CurrentItems => new(_filteredItems, _pageSize * CurrentPage, Count); public int PageCount { get; private set; } - public int SelectedPage { get; private set; } + public int CurrentPage { get; private set; } - public int Count => Math.Min(FilteredItems.Length - (_pageSize * SelectedPage), _pageSize); + public int Count => Math.Min(_filteredItems.Length - (_pageSize * CurrentPage), _pageSize); - public int TotalCount => FilteredItems.Length; + public int TotalCount => _filteredItems.Length; - public string FilterTerm { get; private set; } = ""; + public string FilterKeyword { get; private set; } = ""; - public bool TryGetSelectedItem(out T selectedItem) + public bool LoopingSelection { get; set; } + + public bool TryGetSelectedItem([NotNullWhen(true)] out T? selectedItem) { - if (_selectedIndex == -1 || FilteredItems.Length == 0) + if (_filteredItems.Length == 1) + { + selectedItem = _filteredItems[0]; + + return true; + } + + if (_selectedIndex == -1 || _filteredItems.Length == 0) { selectedItem = default; return false; } - selectedItem = FilteredItems[(_pageSize * SelectedPage) + _selectedIndex]; + selectedItem = _filteredItems[(_pageSize * CurrentPage) + _selectedIndex]; return true; } public void NextItem() { - _selectedIndex = _selectedIndex >= Count - 1 ? 0 : _selectedIndex + 1; + if (_selectedIndex >= Count - 1) + { + if (!LoopingSelection) + { + NextPage(); + } + + _selectedIndex = 0; + } + else + { + _selectedIndex += 1; + } } public void PreviousItem() { - _selectedIndex = _selectedIndex <= 0 ? Count - 1 : _selectedIndex - 1; + if (_selectedIndex <= 0) + { + if (!LoopingSelection) + { + PreviousPage(); + } + + _selectedIndex = Count - 1; + } + else + { + _selectedIndex -= 1; + } } public void NextPage() @@ -64,8 +100,8 @@ public void NextPage() return; } - SelectedPage = SelectedPage >= PageCount - 1 ? 0 : SelectedPage + 1; _selectedIndex = -1; + CurrentPage = CurrentPage >= PageCount - 1 ? 0 : CurrentPage + 1; } public void PreviousPage() @@ -75,45 +111,65 @@ public void PreviousPage() return; } - SelectedPage = SelectedPage <= 0 ? PageCount - 1 : SelectedPage - 1; _selectedIndex = -1; + CurrentPage = CurrentPage <= 0 ? PageCount - 1 : CurrentPage - 1; } - public void UpdateFilter(string term) + public void UpdateFilter(string keyword) { - FilterTerm = term; + FilterKeyword = keyword; _selectedIndex = -1; - SelectedPage = 0; - UpdateItems(); + UpdateFilteredItems(); + } + + public void UpdatePageSize(int newPageSize) + { + if (_pageSize == newPageSize) + { + return; + } + + TryGetSelectedItem(out var selectedItem); + + _pageSize = newPageSize <= 0 ? _items.Length : Math.Min(newPageSize, _items.Length); + + InitializeDefaults(Optional.Create(selectedItem)); } - public ArraySegment ToSubset() => new(FilteredItems, _pageSize * SelectedPage, Count); + public IEnumerator GetEnumerator() => ((IEnumerable)_filteredItems).GetEnumerator(); - private void UpdateItems() + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private void UpdateFilteredItems() { - FilteredItems = _items.Where(x => _textSelector(x).IndexOf(FilterTerm, StringComparison.OrdinalIgnoreCase) != -1) - .ToArray(); + _filteredItems = _items.Where(x => _textSelector(x).IndexOf(FilterKeyword, StringComparison.OrdinalIgnoreCase) != -1) + .ToArray(); + + PageCount = (_filteredItems.Length - 1) / _pageSize + 1; - PageCount = (FilteredItems.Length - 1) / _pageSize + 1; + if (CurrentPage >= PageCount) + { + CurrentPage = 0; + } } private void InitializeDefaults(Optional defaultValue) { - UpdateItems(); + UpdateFilteredItems(); if (!defaultValue.HasValue) { return; } - for (var i = 0; i < FilteredItems.Length; i++) + for (var i = 0; i < _filteredItems.Length; i++) { - if (EqualityComparer.Default.Equals(FilteredItems[i], defaultValue)) + if (EqualityComparer.Default.Equals(_filteredItems[i], defaultValue)) { _selectedIndex = i % _pageSize; - SelectedPage = i / _pageSize; + CurrentPage = i / _pageSize; break; } diff --git a/Sharprompt/Internal/PropertyMetadata.cs b/Sharprompt/Internal/PropertyMetadata.cs index 3997d4d9..c78cd41d 100644 --- a/Sharprompt/Internal/PropertyMetadata.cs +++ b/Sharprompt/Internal/PropertyMetadata.cs @@ -15,38 +15,35 @@ 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; - Message = displayAttribute?.GetName() ?? displayAttribute?.GetDescription(); + DataType = propertyInfo.GetCustomAttribute()?.DataType; + Message = displayAttribute?.GetName() ?? displayAttribute?.GetDescription() ?? propertyInfo.Name; Placeholder = displayAttribute?.GetPrompt(); Order = displayAttribute?.GetOrder(); DefaultValue = propertyInfo.GetValue(model); Validators = propertyInfo.GetCustomAttributes(true) .Select(x => new ValidationAttributeAdapter(x).GetValidator(propertyInfo.Name, model)) .ToArray(); - ItemsProvider = (IItemsProvider)propertyInfo.GetCustomAttribute(true) ?? propertyInfo.GetCustomAttribute(true); - BindIgnore = propertyInfo.GetCustomAttribute() is not null; + ItemsProvider = GetItemsProvider(propertyInfo); } public PropertyInfo PropertyInfo { get; } public Type Type { get; } - public Type ElementType { get; set; } + public Type? ElementType { get; set; } public bool IsNullable { get; set; } public bool IsCollection { get; } public AnnotationsDataType? DataType { get; } public string Message { get; } - public string Placeholder { get; set; } + public string? Placeholder { get; set; } public int? Order { get; } - public object DefaultValue { get; } - public IReadOnlyList> Validators { get; } - public IItemsProvider ItemsProvider { get; set; } - public bool BindIgnore { get; set; } + public object? DefaultValue { get; } + public IReadOnlyList> Validators { get; } + public IItemsProvider ItemsProvider { get; } public FormType DetermineFormType() { @@ -60,22 +57,30 @@ public FormType DetermineFormType() return FormType.Confirm; } - if (!IsCollection && (Type.IsEnum || ItemsProvider is not null)) + if (ItemsProvider is not NullItemsProvider) { - return FormType.Select; + return IsCollection ? FormType.MultiSelect : FormType.Select; } - if (IsCollection && (ElementType.IsEnum || ItemsProvider is not null)) + return IsCollection ? FormType.List : FormType.Input; + } + + private IItemsProvider GetItemsProvider(PropertyInfo propertyInfo) + { + var itemsProvider = (IItemsProvider?)propertyInfo.GetCustomAttribute(true) ?? + propertyInfo.GetCustomAttribute(true); + + if (itemsProvider is not null) { - return FormType.MultiSelect; + return itemsProvider; } - if (IsCollection && ItemsProvider is null) + if (Type.IsEnum || (ElementType is not null && ElementType.IsEnum)) { - return FormType.List; + return new EnumItemsProvider(); } - return FormType.Input; + return NullItemsProvider.Instance; } private class ValidationAttributeAdapter @@ -87,7 +92,7 @@ public ValidationAttributeAdapter(ValidationAttribute validationAttribute) private readonly ValidationAttribute _validationAttribute; - public Func GetValidator(string propertyName, object model) + public Func GetValidator(string propertyName, object model) { var validationContext = new ValidationContext(model) { diff --git a/Sharprompt/Internal/PropertyMetadataFactory.cs b/Sharprompt/Internal/PropertyMetadataFactory.cs index d479ba1a..207d5db0 100644 --- a/Sharprompt/Internal/PropertyMetadataFactory.cs +++ b/Sharprompt/Internal/PropertyMetadataFactory.cs @@ -6,12 +6,11 @@ namespace Sharprompt.Internal; internal static class PropertyMetadataFactory { - public static IReadOnlyList Create(T model) + public static IReadOnlyList Create(T model) where T : notnull { return typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(x => x.CanWrite) + .Where(x => x.CanWrite && x.GetCustomAttribute() is null) .Select(x => new PropertyMetadata(model, x)) - .Where(x => !x.BindIgnore) .OrderBy(x => x.Order) .ToArray(); } diff --git a/Sharprompt/Internal/RenderScope.cs b/Sharprompt/Internal/RenderScope.cs index a12e1d59..1992073f 100644 --- a/Sharprompt/Internal/RenderScope.cs +++ b/Sharprompt/Internal/RenderScope.cs @@ -10,8 +10,8 @@ public RenderScope(OffscreenBuffer offscreenBuffer, IConsoleDriver consoleDriver { _offscreenBuffer = offscreenBuffer; _consoleDriver = consoleDriver; - _cursorBottom = cursorBottom; - _writtenLineCount = writtenLineCount; + _cursorBottom = Math.Min(cursorBottom, _consoleDriver.WindowHeight - 1); + _writtenLineCount = Math.Min(writtenLineCount, _consoleDriver.WindowHeight - 1); _offscreenBuffer.ClearBuffer(); } diff --git a/Sharprompt/Internal/TypeHelper.cs b/Sharprompt/Internal/TypeHelper.cs index 156a1413..b9c250ec 100644 --- a/Sharprompt/Internal/TypeHelper.cs +++ b/Sharprompt/Internal/TypeHelper.cs @@ -8,15 +8,15 @@ internal static class TypeHelper { public static bool IsNullable(Type type) => !type.IsValueType || Nullable.GetUnderlyingType(type) is not null; - public static bool IsCollection(Type type) => typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string); + public static bool IsCollection(Type type) => type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type); } internal static class TypeHelper { private static readonly Type s_targetType = typeof(T); - private static readonly Type s_underlyingType = Nullable.GetUnderlyingType(typeof(T)); + private static readonly Type? s_underlyingType = Nullable.GetUnderlyingType(typeof(T)); - public static bool IsValueType => s_targetType.IsValueType && s_underlyingType is null; + public static bool IsNullable => !s_targetType.IsValueType || s_underlyingType is not null; - public static T ConvertTo(string value) => (T)TypeDescriptor.GetConverter(s_underlyingType ?? s_targetType).ConvertFromInvariantString(value); + public static T? ConvertTo(string value) => (T?)TypeDescriptor.GetConverter(s_underlyingType ?? s_targetType).ConvertFromInvariantString(value); } diff --git a/Sharprompt/Internal/ValidatorsExtensions.cs b/Sharprompt/Internal/ValidatorsExtensions.cs index 78cf0aa8..5de9b581 100644 --- a/Sharprompt/Internal/ValidatorsExtensions.cs +++ b/Sharprompt/Internal/ValidatorsExtensions.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; namespace Sharprompt.Internal; internal static class ValidatorsExtensions { - public static void Merge(this IList> source, IEnumerable> validators) + public static void Merge(this IList> source, IEnumerable>? validators) { - foreach (var validator in validators ?? Enumerable.Empty>()) + foreach (var validator in validators ?? []) { source.Add(validator); } diff --git a/Sharprompt/ListOptions.cs b/Sharprompt/ListOptions.cs index 921401dd..9a00c549 100644 --- a/Sharprompt/ListOptions.cs +++ b/Sharprompt/ListOptions.cs @@ -6,21 +6,21 @@ namespace Sharprompt; -public class ListOptions +public class ListOptions where T : notnull { - public string Message { get; set; } + public string Message { get; set; } = null!; - public IEnumerable DefaultValues { get; set; } + public IEnumerable DefaultValues { get; set; } = []; public int Minimum { get; set; } = 1; public int Maximum { get; set; } = int.MaxValue; - public IList> Validators { get; } = new List>(); + public IList> Validators { get; } = new List>(); internal void EnsureOptions() { - _ = Message ?? throw new ArgumentNullException(nameof(Message)); + ArgumentNullException.ThrowIfNull(Message); if (Minimum < 0) { diff --git a/Sharprompt/MemberItemsAttribute.cs b/Sharprompt/MemberItemsAttribute.cs index 5d1aa504..9d3d9fea 100644 --- a/Sharprompt/MemberItemsAttribute.cs +++ b/Sharprompt/MemberItemsAttribute.cs @@ -13,23 +13,31 @@ public sealed class MemberItemsAttribute : Attribute, IItemsProvider { public MemberItemsAttribute(string memberName) { + ArgumentNullException.ThrowIfNull(memberName); + _memberName = memberName; - _memberType = null; } public MemberItemsAttribute(string memberName, Type memberType) + : this(memberName) { - _memberName = memberName; + ArgumentNullException.ThrowIfNull(memberType); + _memberType = memberType; } private readonly string _memberName; - private readonly Type _memberType; + private readonly Type? _memberType; - public IEnumerable GetItems(PropertyInfo targetPropertyInfo) + public IEnumerable GetItems(PropertyInfo targetPropertyInfo) where T : notnull { var targetType = _memberType ?? targetPropertyInfo.DeclaringType; + if (targetType is null) + { + throw new ArgumentException(string.Format(Resource.Validation_Type_MemberNotFound, _memberType, _memberName)); + } + var memberInfo = targetType.GetMember(_memberName, BindingFlags.Public | BindingFlags.Static) .FirstOrDefault(); @@ -40,7 +48,7 @@ public IEnumerable GetItems(PropertyInfo targetPropertyInfo) throw new ArgumentException(string.Format(Resource.Validation_Type_Incompatible, propertyInfo.PropertyType, typeof(IEnumerable))); } - return (IEnumerable)propertyInfo.GetValue(null); + return (IEnumerable)propertyInfo.GetValue(null)!; } if (memberInfo is MethodInfo methodInfo) @@ -55,7 +63,7 @@ public IEnumerable GetItems(PropertyInfo targetPropertyInfo) throw new ArgumentException(string.Format(Resource.Validation_Type_Incompatible, methodInfo.ReturnType, typeof(IEnumerable))); } - return (IEnumerable)methodInfo.Invoke(null, null); + return (IEnumerable)methodInfo.Invoke(null, null)!; } throw new ArgumentException(string.Format(Resource.Validation_Type_MemberNotFound, _memberType, _memberName)); diff --git a/Sharprompt/MultiSelectOptions.cs b/Sharprompt/MultiSelectOptions.cs index 3fd867e3..c7dbdad1 100644 --- a/Sharprompt/MultiSelectOptions.cs +++ b/Sharprompt/MultiSelectOptions.cs @@ -6,36 +6,42 @@ namespace Sharprompt; -public class MultiSelectOptions +public class MultiSelectOptions where T : notnull { - public string Message { get; set; } + public MultiSelectOptions() + { + if (typeof(T).IsAssignableTo(typeof(Enum))) + { + Items = EnumHelper.GetValues(); + TextSelector = EnumHelper.GetDisplayName; + } + } + + public string Message { get; set; } = null!; - public IEnumerable Items { get; set; } + public IEnumerable Items { get; set; } = null!; - public IEnumerable DefaultValues { get; set; } + public IEnumerable DefaultValues { get; set; } = []; - public int? PageSize { get; set; } + public int PageSize { get; set; } = int.MaxValue; public int Minimum { get; set; } = 1; public int Maximum { get; set; } = int.MaxValue; - public Func TextSelector { get; set; } + public Func TextSelector { get; set; } = x => x.ToString()!; - public Func Pagination { get; set; } + public Func Pagination { get; set; } = (count, current, total) => string.Format(Resource.Message_Pagination, count, current, total); + + public bool LoopingSelection { get; set; } = true; internal void EnsureOptions() { - if (Items is null && typeof(T).IsEnum) - { - Items = EnumHelper.GetValues(); - } - - TextSelector ??= typeof(T).IsEnum ? EnumHelper.GetDisplayName : x => x.ToString(); - Pagination ??= (count, current, total) => string.Format(Resource.Message_Pagination, count, current, total); - - _ = Message ?? throw new ArgumentNullException(nameof(Message)); - _ = Items ?? throw new ArgumentNullException(nameof(Items)); + ArgumentNullException.ThrowIfNull(Message); + ArgumentNullException.ThrowIfNull(Items); + ArgumentNullException.ThrowIfNull(DefaultValues); + ArgumentNullException.ThrowIfNull(TextSelector); + ArgumentNullException.ThrowIfNull(Pagination); if (Minimum < 0) { diff --git a/Sharprompt/PasswordOptions.cs b/Sharprompt/PasswordOptions.cs index 51c4455e..8f643e3e 100644 --- a/Sharprompt/PasswordOptions.cs +++ b/Sharprompt/PasswordOptions.cs @@ -6,17 +6,17 @@ namespace Sharprompt; public class PasswordOptions { - public string Message { get; set; } + public string Message { get; set; } = null!; - public string Placeholder { get; set; } + public string? Placeholder { get; set; } public string PasswordChar { get; set; } = "*"; - public IList> Validators { get; } = new List>(); + public IList> Validators { get; } = new List>(); internal void EnsureOptions() { - _ = Message ?? throw new ArgumentNullException(nameof(Message)); - _ = PasswordChar ?? throw new ArgumentNullException(nameof(PasswordChar)); + ArgumentNullException.ThrowIfNull(Message); + ArgumentNullException.ThrowIfNull(PasswordChar); } } diff --git a/Sharprompt/Prompt.Basic.cs b/Sharprompt/Prompt.Basic.cs index 95d3fc4d..2a126203 100644 --- a/Sharprompt/Prompt.Basic.cs +++ b/Sharprompt/Prompt.Basic.cs @@ -25,7 +25,7 @@ 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, string? placeholder = default, IList>? validators = default) { return Input(options => { @@ -53,7 +53,7 @@ public static string Password(Action configure) return Password(options); } - public static string Password(string message, string passwordChar = "*", string placeholder = default, IList> validators = default) + public static string Password(string message, string passwordChar = "*", string? placeholder = default, IList>? validators = default) { return Password(options => { @@ -90,14 +90,14 @@ public static bool Confirm(string message, bool? defaultValue = default) }); } - public static T Select(SelectOptions options) + public static T Select(SelectOptions options) where T : notnull { using var form = new SelectForm(options); return form.Start(); } - public static T Select(Action> configure) + public static T Select(Action> configure) where T : notnull { var options = new SelectOptions(); @@ -106,26 +106,35 @@ public static T Select(Action> configure) return Select(options); } - public static T Select(string message, IEnumerable items = default, int? pageSize = default, object defaultValue = default, Func textSelector = default) + 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 => { options.Message = message; - options.Items = items; + + if (items is not null) + { + options.Items = items; + } + options.DefaultValue = defaultValue; options.PageSize = pageSize; - options.TextSelector = textSelector; + + if (textSelector is not null) + { + options.TextSelector = textSelector; + } }); } - public static IEnumerable MultiSelect(MultiSelectOptions options) + public static IEnumerable MultiSelect(MultiSelectOptions options) where T : notnull { using var form = new MultiSelectForm(options); return form.Start(); } - public static IEnumerable MultiSelect(Action> configure) + public static IEnumerable MultiSelect(Action> configure) where T : notnull { var options = new MultiSelectOptions(); @@ -134,28 +143,41 @@ public static IEnumerable MultiSelect(Action> config return MultiSelect(options); } - public static IEnumerable MultiSelect(string message, IEnumerable items = null, int? pageSize = default, int minimum = 1, int maximum = int.MaxValue, IEnumerable defaultValues = default, Func textSelector = default) + 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 { return MultiSelect(options => { options.Message = message; - options.Items = items; - options.DefaultValues = defaultValues; + + if (items is not null) + { + options.Items = items; + } + + if (defaultValues is not null) + { + options.DefaultValues = defaultValues; + } + options.PageSize = pageSize; options.Minimum = minimum; options.Maximum = maximum; - options.TextSelector = textSelector; + + if (textSelector is not null) + { + options.TextSelector = textSelector; + } }); } - public static IEnumerable List(ListOptions options) + public static IEnumerable List(ListOptions options) where T : notnull { using var form = new ListForm(options); return form.Start(); } - public static IEnumerable List(Action> configure) + public static IEnumerable List(Action> configure) where T : notnull { var options = new ListOptions(); @@ -164,7 +186,7 @@ public static IEnumerable List(Action> configure) return List(options); } - public static IEnumerable List(string message, int minimum = 1, int maximum = int.MaxValue, IList> validators = default) + public static IEnumerable List(string message, int minimum = 1, int maximum = int.MaxValue, IList>? validators = default) where T : notnull { return List(options => { diff --git a/Sharprompt/Prompt.Bind.cs b/Sharprompt/Prompt.Bind.cs index 6f4b9d82..f23c6bcf 100644 --- a/Sharprompt/Prompt.Bind.cs +++ b/Sharprompt/Prompt.Bind.cs @@ -9,21 +9,21 @@ namespace Sharprompt; public static partial class Prompt { - public static T Bind() where T : new() + public static T Bind() where T : notnull, new() { var model = new T(); return Bind(model); } - public static T Bind(T model) + public static T Bind(T model) where T : notnull { StartBind(model); return model; } - private static void StartBind(T model) + private static void StartBind(T model) where T : notnull { var propertyMetadatas = PropertyMetadataFactory.Create(model); @@ -39,7 +39,7 @@ private static void StartBind(T model) FormType.MultiSelect => MakeMultiSelect(propertyMetadata), FormType.Password => MakePassword(propertyMetadata), FormType.Select => MakeSelect(propertyMetadata), - _ => null + _ => throw new ArgumentOutOfRangeException() }; propertyMetadata.PropertyInfo.SetValue(model, result); @@ -71,12 +71,12 @@ private static T MakeInputCore(PropertyMetadata propertyMetadata) private static object MakeList(PropertyMetadata propertyMetadata) => InvokeMethod(nameof(MakeListCore), propertyMetadata, propertyMetadata.ElementType); - private static IEnumerable MakeListCore(PropertyMetadata propertyMetadata) + private static IEnumerable MakeListCore(PropertyMetadata propertyMetadata) where T : notnull { return List(options => { options.Message = propertyMetadata.Message; - options.DefaultValues = (IEnumerable)propertyMetadata.DefaultValue; + options.DefaultValues = (IEnumerable?)propertyMetadata.DefaultValue ?? []; options.Validators.Merge(propertyMetadata.Validators); }); @@ -84,13 +84,13 @@ private static IEnumerable MakeListCore(PropertyMetadata propertyMetadata) private static object MakeMultiSelect(PropertyMetadata propertyMetadata) => InvokeMethod(nameof(MakeMultiSelectCore), propertyMetadata, propertyMetadata.ElementType); - private static IEnumerable MakeMultiSelectCore(PropertyMetadata propertyMetadata) + private static IEnumerable MakeMultiSelectCore(PropertyMetadata propertyMetadata) where T : notnull { return MultiSelect(options => { options.Message = propertyMetadata.Message; - options.Items = propertyMetadata.ItemsProvider?.GetItems(propertyMetadata.PropertyInfo); - options.DefaultValues = (IEnumerable)propertyMetadata.DefaultValue; + options.Items = propertyMetadata.ItemsProvider.GetItems(propertyMetadata.PropertyInfo); + options.DefaultValues = (IEnumerable?)propertyMetadata.DefaultValue ?? []; }); } @@ -107,21 +107,21 @@ private static string MakePassword(PropertyMetadata propertyMetadata) private static object MakeSelect(PropertyMetadata propertyMetadata) => InvokeMethod(nameof(MakeSelectCore), propertyMetadata); - private static T MakeSelectCore(PropertyMetadata propertyMetadata) + private static T MakeSelectCore(PropertyMetadata propertyMetadata) where T : notnull { return Select(options => { options.Message = propertyMetadata.Message; - options.Items = propertyMetadata.ItemsProvider?.GetItems(propertyMetadata.PropertyInfo); + options.Items = propertyMetadata.ItemsProvider.GetItems(propertyMetadata.PropertyInfo); options.DefaultValue = propertyMetadata.DefaultValue; }); } - private static object InvokeMethod(string name, PropertyMetadata propertyMetadata, Type genericType = default) + private static object InvokeMethod(string name, PropertyMetadata propertyMetadata, Type? genericType = default) { - var method = typeof(Prompt).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static) + var method = typeof(Prompt).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static)! .MakeGenericMethod(genericType ?? propertyMetadata.Type); - return method.Invoke(null, new object[] { propertyMetadata }); + return method.Invoke(null, [propertyMetadata])!; } } diff --git a/Sharprompt/PromptCanceledException.cs b/Sharprompt/PromptCanceledException.cs index db1a2c49..7bc02c3b 100644 --- a/Sharprompt/PromptCanceledException.cs +++ b/Sharprompt/PromptCanceledException.cs @@ -1,9 +1,7 @@ using System; -using System.Runtime.Serialization; namespace Sharprompt; -[Serializable] public class PromptCanceledException : Exception { public PromptCanceledException() @@ -26,10 +24,5 @@ public PromptCanceledException(string message, string promptType) PromptType = promptType; } - protected PromptCanceledException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - - public string PromptType { get; } + public string? PromptType { get; } } diff --git a/Sharprompt/SelectOptions.cs b/Sharprompt/SelectOptions.cs index eb3d87d5..e3c2e15a 100644 --- a/Sharprompt/SelectOptions.cs +++ b/Sharprompt/SelectOptions.cs @@ -6,31 +6,36 @@ namespace Sharprompt; -public class SelectOptions +public class SelectOptions where T : notnull { - public string Message { get; set; } + public SelectOptions() + { + if (typeof(T).IsAssignableTo(typeof(Enum))) + { + Items = EnumHelper.GetValues(); + TextSelector = EnumHelper.GetDisplayName; + } + } - public IEnumerable Items { get; set; } + public string Message { get; set; } = null!; - public object DefaultValue { get; set; } + public IEnumerable Items { get; set; } = null!; - public int? PageSize { get; set; } + public object? DefaultValue { get; set; } - public Func TextSelector { get; set; } + public int PageSize { get; set; } = int.MaxValue; - public Func Pagination { get; set; } + public Func TextSelector { get; set; } = x => x.ToString()!; - internal void EnsureOptions() - { - if (Items is null && typeof(T).IsEnum) - { - Items = EnumHelper.GetValues(); - } + public Func Pagination { get; set; } = (count, current, total) => string.Format(Resource.Message_Pagination, count, current, total); - TextSelector ??= typeof(T).IsEnum ? EnumHelper.GetDisplayName : x => x.ToString(); - Pagination ??= (count, current, total) => string.Format(Resource.Message_Pagination, count, current, total); + public bool LoopingSelection { get; set; } = true; - _ = Message ?? throw new ArgumentNullException(nameof(Message)); - _ = Items ?? throw new ArgumentNullException(nameof(Items)); + internal void EnsureOptions() + { + ArgumentNullException.ThrowIfNull(Message); + ArgumentNullException.ThrowIfNull(Items); + ArgumentNullException.ThrowIfNull(TextSelector); + ArgumentNullException.ThrowIfNull(Pagination); } } diff --git a/Sharprompt/Sharprompt.csproj b/Sharprompt/Sharprompt.csproj index 5107a703..cdbc4639 100644 --- a/Sharprompt/Sharprompt.csproj +++ b/Sharprompt/Sharprompt.csproj @@ -1,8 +1,8 @@  - netstandard2.0 - latest + net8.0 + enable @@ -11,17 +11,13 @@ Sharprompt icon.png README.md - https://github.com/shibayan/Sharprompt/releases + https://github.com/shibayan/Sharprompt/releases/tag/v$(Version) MIT cli;command-line;console;interactive;prompt;terminal https://github.com/shibayan/Sharprompt git - - - - diff --git a/Sharprompt/Symbol.cs b/Sharprompt/Symbol.cs index 01ea13b2..ff1591b2 100644 --- a/Sharprompt/Symbol.cs +++ b/Sharprompt/Symbol.cs @@ -3,18 +3,9 @@ namespace Sharprompt; -public class Symbol +public class Symbol(string value, string fallbackValue) { - public Symbol(string value, string fallbackValue) - { - _value = value; - _fallbackValue = fallbackValue; - } - - private readonly string _value; - private readonly string _fallbackValue; - - public override string ToString() => IsUnicodeSupported ? _value : _fallbackValue; + public override string ToString() => IsUnicodeSupported ? value : fallbackValue; public static implicit operator string(Symbol symbol) => symbol.ToString(); diff --git a/Sharprompt/Validators.cs b/Sharprompt/Validators.cs index 631c6aa2..e61c976c 100644 --- a/Sharprompt/Validators.cs +++ b/Sharprompt/Validators.cs @@ -8,7 +8,7 @@ namespace Sharprompt; public static class Validators { - public static Func Required(string errorMessage = default) + public static Func Required(string? errorMessage = default) { return input => { @@ -26,7 +26,7 @@ public static Func Required(string errorMessage = defa }; } - public static Func MinLength(int length, string errorMessage = default) + public static Func MinLength(int length, string? errorMessage = default) { return input => { @@ -44,7 +44,7 @@ public static Func MinLength(int length, string errorM }; } - public static Func MaxLength(int length, string errorMessage = default) + public static Func MaxLength(int length, string? errorMessage = default) { return input => { @@ -62,7 +62,7 @@ public static Func MaxLength(int length, string errorM }; } - public static Func RegularExpression(string pattern, string errorMessage = default) + public static Func RegularExpression(string pattern, string? errorMessage = default) { return input => {