diff --git a/src/Fallout.Build/Utilities/ConsoleUtility.cs b/src/Fallout.Build/Utilities/ConsoleUtility.cs index 7d166d76..bcaced67 100644 --- a/src/Fallout.Build/Utilities/ConsoleUtility.cs +++ b/src/Fallout.Build/Utilities/ConsoleUtility.cs @@ -2,27 +2,40 @@ using System.Drawing; using System.Linq; using System.Text; +using Fallout.Build.Utilities; namespace Fallout.Common.Utilities; public class ConsoleUtility { - private static int BufferWidth => EnvironmentInfo.IsWin ? Console.BufferWidth - 1 : Console.BufferWidth; + public static IConsole ConsoleWrapper { get; set; } = new SystemConsole(); + + private static int BufferWidth => ConsoleWrapper.BufferWidth; private static string Default => "[default: {0}]"; + private static string Confirmed => "¬"; + private static string Selected => "»"; + private static string Unselected => " "; private const ConsoleKey ConfirmationKey = ConsoleKey.Enter; private const ConsoleKey InterruptKey = ConsoleKey.F8; - private static bool s_interrupted; + internal static bool IsInterrupted; + private static readonly char[] AllowedSpecialCharacters = ['.', '/', '\\', '_', '-']; + + internal static bool IsValidInputKey(ConsoleKeyInfo key) => + key.Key is >= ConsoleKey.A and <= ConsoleKey.Z + || key.Key is >= ConsoleKey.D0 and <= ConsoleKey.D9 + || AllowedSpecialCharacters.Any(x => x == key.KeyChar) + || char.IsLetterOrDigit(key.KeyChar); // ReSharper disable once CognitiveComplexity public static string PromptForInput(string question, string defaultValue) { - if (s_interrupted) + if (IsInterrupted) return defaultValue; Host.Information(question); @@ -31,40 +44,40 @@ public static string PromptForInput(string question, string defaultValue) var input = new StringBuilder(); do { - Console.CursorLeft = 0; - Console.WriteLine(Selected.PadRight(BufferWidth), Color.DeepSkyBlue); - Console.CursorTop--; - Console.CursorLeft = 3; + ConsoleWrapper.CursorLeft = 0; + ConsoleWrapper.WriteLine(Selected.PadRight(BufferWidth), Color.DeepSkyBlue); + ConsoleWrapper.CursorTop--; + ConsoleWrapper.CursorLeft = 3; if (input.Length > 0) { - Console.Write(input.ToString()); + ConsoleWrapper.Write(input.ToString()); } else if (defaultValue != null) { - Console.Write($" {string.Format(Default, defaultValue)}", Color.DarkGray); - Console.CursorLeft = 3; + ConsoleWrapper.Write($" {string.Format(Default, defaultValue)}", Color.DarkGray); + ConsoleWrapper.CursorLeft = 3; } - key = Console.ReadKey(intercept: true); - if (key.IsValidInputKey()) + key = ConsoleWrapper.ReadKey(intercept: true); + if (IsValidInputKey(key)) input.Append(key.KeyChar); else if (key.Key == ConsoleKey.Backspace && input.Length > 0) input.Remove(input.Length - 1, length: 1); else if (key.Key == InterruptKey) - s_interrupted = true; + IsInterrupted = true; } while (key.Key is not (ConfirmationKey or InterruptKey)); var result = input.Length > 0 ? input.ToString() : defaultValue; - Console.CursorLeft = 0; - Console.WriteLine($"{Confirmed} {result ?? ""}".PadRight(BufferWidth), Color.Lime); + ConsoleWrapper.CursorLeft = 0; + ConsoleWrapper.WriteLine($"{Confirmed} {result ?? ""}".PadRight(BufferWidth), Color.Lime); return result; } // ReSharper disable once CognitiveComplexity public static T PromptForChoice(string question, params (T Value, string Description)[] options) { - if (s_interrupted) + if (IsInterrupted) return options.First().Value; var selection = 0; @@ -79,25 +92,28 @@ public static T PromptForChoice(string question, params (T Value, string Desc var selected = i == selection; var prefix = selected ? Selected : Unselected; var color = selected ? Color.DeepSkyBlue : Color.DarkGray; - Console.WriteLine($"{prefix} {option.Description}", color); + ConsoleWrapper.WriteLine($"{prefix} {option.Description}", color); } - key = Console.ReadKey(intercept: true).Key; + key = ConsoleWrapper.ReadKey(intercept: true).Key; if (key == ConsoleKey.UpArrow) selection--; else if (key == ConsoleKey.DownArrow) selection++; else if (key == InterruptKey) - s_interrupted = true; + IsInterrupted = true; + selection = Math.Max(val1: 0, Math.Min(options.Length - 1, selection)); - Console.CursorTop -= options.Length; - foreach (var option in options) - Console.WriteLine(' '.Repeat(BufferWidth)); - Console.CursorTop -= options.Length; - } while (!(key == ConfirmationKey || key == InterruptKey)); + ConsoleWrapper.CursorTop -= options.Length; + foreach (var unused in options) + ConsoleWrapper.WriteLine(' '.Repeat(BufferWidth)); - Console.WriteLine($"{Confirmed} {options[selection].Description}", Color.Lime); + ConsoleWrapper.CursorTop -= options.Length; + } + while (key is not (ConfirmationKey or InterruptKey)); + + ConsoleWrapper.WriteLine($"{Confirmed} {options[selection].Description}", Color.Lime); return options[selection].Value; } @@ -109,7 +125,7 @@ public static string ReadSecret() do { - var key = Console.ReadKey(intercept: true); + var key = ConsoleWrapper.ReadKey(intercept: true); if (key.Key == ConsoleKey.Backspace) { if (secret.Length > 0) @@ -119,22 +135,24 @@ public static string ReadSecret() (key.Modifiers & ConsoleModifiers.Alt) != 0 && EnvironmentInfo.IsOsx ? secret.Length : 1; + var length = secret.Length - charsToRemove; secret = secret[..length]; - Console.Write(string.Concat(Enumerable.Repeat("\b \b", charsToRemove))); + ConsoleWrapper.Write(string.Concat(Enumerable.Repeat("\b \b", charsToRemove))); } } else if (key.Key == ConsoleKey.Enter) { - Console.WriteLine(); + ConsoleWrapper.WriteLine(); break; } else if (!char.IsControl(key.KeyChar)) { secret += key.KeyChar; - Console.Write("*"); + ConsoleWrapper.Write("*"); } - } while (true); + } + while (true); return secret; } diff --git a/src/Fallout.Build/Utilities/ConsoleUtilityExtensions.cs b/src/Fallout.Build/Utilities/ConsoleUtilityExtensions.cs deleted file mode 100644 index b080d8a9..00000000 --- a/src/Fallout.Build/Utilities/ConsoleUtilityExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Linq; - -namespace Fallout.Common.Utilities; - -public static class ConsoleUtilityExtensions -{ - private static readonly char[] AllowedSpecialCharacters = ['.', '/', '\\', '_', '-']; - - public static bool IsValidInputKey(this ConsoleKeyInfo key) - { - return key.Key is >= ConsoleKey.A and <= ConsoleKey.Z - || key.Key is >= ConsoleKey.D0 and <= ConsoleKey.D9 - || AllowedSpecialCharacters.Any(x => x == key.KeyChar) - || char.IsLetterOrDigit(key.KeyChar); - } -} diff --git a/src/Fallout.Build/Utilities/IConsole.cs b/src/Fallout.Build/Utilities/IConsole.cs new file mode 100644 index 00000000..52cfc69b --- /dev/null +++ b/src/Fallout.Build/Utilities/IConsole.cs @@ -0,0 +1,15 @@ +using System; +using System.Drawing; + +namespace Fallout.Build.Utilities; + +public interface IConsole +{ + int BufferWidth { get; } + int CursorLeft { get; set; } + int CursorTop { get; set; } + void Write(string value, Color? color = null); + void WriteLine(); + void WriteLine(string value, Color? color = null); + ConsoleKeyInfo ReadKey(bool intercept); +} diff --git a/src/Fallout.Build/Utilities/SystemConsole.cs b/src/Fallout.Build/Utilities/SystemConsole.cs new file mode 100644 index 00000000..f8809fb7 --- /dev/null +++ b/src/Fallout.Build/Utilities/SystemConsole.cs @@ -0,0 +1,40 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using Fallout.Common; + +namespace Fallout.Build.Utilities; + +public class SystemConsole : IConsole +{ + public int BufferWidth => EnvironmentInfo.IsWin ? Console.BufferWidth - 1 : Console.BufferWidth; + + public int CursorLeft + { + get => Console.CursorLeft; + set => Console.CursorLeft = value; + } + + public int CursorTop + { + get => Console.CursorTop; + set => Console.CursorTop = value; + } + + public void Write(string value, Color? color = null) + { + Console.Write(value); + } + + public void WriteLine() + { + Console.WriteLine(); + } + + public void WriteLine(string value, Color? color = null) + { + Console.WriteLine(value); + } + + public ConsoleKeyInfo ReadKey(bool intercept) => Console.ReadKey(intercept); +} diff --git a/tests/Fallout.Build.Specs/Utilities/ConsoleUtilitySpecs.cs b/tests/Fallout.Build.Specs/Utilities/ConsoleUtilitySpecs.cs new file mode 100644 index 00000000..fec23bac --- /dev/null +++ b/tests/Fallout.Build.Specs/Utilities/ConsoleUtilitySpecs.cs @@ -0,0 +1,51 @@ +using Fallout.Common.Utilities; +using Xunit; +using System; +using System.Collections.Generic; +using System.Drawing; +using Fallout.Build.Utilities; +using FluentAssertions; + +namespace Fallout.Build.Tests.Utilities; + +public class ConsoleUtilitySpecs +{ + private class MockConsole : IConsole + { + public int BufferWidth { get; set; } = 80; + public int CursorLeft { get; set; } + public int CursorTop { get; set; } + public Queue Keys { get; } = new(); + public void Write(string value, Color? color = null) { } + public void WriteLine() { } + public void WriteLine(string value, Color? color = null) { } + public ConsoleKeyInfo ReadKey(bool intercept) => Keys.Count > 0 + ? Keys.Dequeue() + : new ConsoleKeyInfo('a', ConsoleKey.A, false, false, false); + } + + [Theory] + [ClassData(typeof(ValidInputData))] + public void Valid_characters_are_accepted_as_input(ConsoleKey key, char keyChar, bool expected) + { + var keyInfo = new ConsoleKeyInfo(keyChar, key, false, false, false); + var result = ConsoleUtility.IsValidInputKey(keyInfo); + result.Should().Be(expected); + } + + [Fact] + public void Prompt_returns_default_value_when_interrupted() + { + // Arrange + var mockConsole = new MockConsole(); + mockConsole.Keys.Enqueue(new ConsoleKeyInfo('\0', ConsoleKey.F8, false, false, false)); + ConsoleUtility.ConsoleWrapper = mockConsole; + ConsoleUtility.IsInterrupted = false; + + // Act + var result = ConsoleUtility.PromptForInput("Question", "Default"); + + // Assert + result.Should().Be("Default"); + } +} diff --git a/tests/Fallout.Build.Specs/Utilities/ValidInputData.cs b/tests/Fallout.Build.Specs/Utilities/ValidInputData.cs new file mode 100644 index 00000000..6116e79b --- /dev/null +++ b/tests/Fallout.Build.Specs/Utilities/ValidInputData.cs @@ -0,0 +1,31 @@ +using System.Collections; +using System.Collections.Generic; +using System; +using Xunit; + +namespace Fallout.Build.Tests.Utilities; + +public class ValidInputData : IEnumerable +{ + public IEnumerator GetEnumerator() + { + yield return [ConsoleKey.A, 'a', true]; + yield return [ConsoleKey.Z, 'z', true ]; + yield return [ConsoleKey.D0, '0', true ]; + yield return [ConsoleKey.D9, '9', true ]; + yield return [ConsoleKey.Enter, '\r', false ]; + yield return [ConsoleKey.Backspace, '\b', false ]; + yield return [ConsoleKey.F8, '\0', false ]; + yield return [ConsoleKey.Spacebar, ' ', false ]; + yield return [ConsoleKey.OemPeriod, '.', true ]; + yield return [ConsoleKey.Divide, '/', true ]; + yield return [ConsoleKey.OemMinus, '-', true ]; + yield return [ConsoleKey.NoName, 'ä', true ]; + yield return [ConsoleKey.NoName, 'ö', true ]; + yield return [ConsoleKey.NoName, '中', true ]; + yield return [ConsoleKey.NoName, 'д', true ]; + yield return [ConsoleKey.NoName, 'α', true ]; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/tests/Fallout.Build.Tests/Utilities/ConsoleUtilityExtensionsSpecs.cs b/tests/Fallout.Build.Tests/Utilities/ConsoleUtilityExtensionsSpecs.cs deleted file mode 100644 index 8c19b3db..00000000 --- a/tests/Fallout.Build.Tests/Utilities/ConsoleUtilityExtensionsSpecs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Fallout.Common.Utilities; -using Xunit; -using System; -using FluentAssertions; - -namespace Fallout.Build.Tests.Utilities; - -public class ConsoleUtilityExtensionsSpecs -{ - [Theory] - [InlineData(ConsoleKey.A, 'a', true)] - [InlineData(ConsoleKey.Z, 'z', true)] - [InlineData(ConsoleKey.D0, '0', true)] - [InlineData(ConsoleKey.D9, '9', true)] - [InlineData(ConsoleKey.Enter, '\r', false)] - [InlineData(ConsoleKey.Backspace, '\b', false)] - [InlineData(ConsoleKey.F8, '\0', false)] - [InlineData(ConsoleKey.Spacebar, ' ', false)] - [InlineData(ConsoleKey.OemPeriod, '.', true)] - [InlineData(ConsoleKey.Divide, '/', true)] - [InlineData(ConsoleKey.OemMinus, '-', true)] - [InlineData(ConsoleKey.NoName, 'ä', true)] - [InlineData(ConsoleKey.NoName, 'ö', true)] - [InlineData(ConsoleKey.NoName, '中', true)] - [InlineData(ConsoleKey.NoName, 'д', true)] - [InlineData(ConsoleKey.NoName, 'α', true)] - public void Valid_characters_are_accepted_as_input(ConsoleKey key, char keyChar, bool expected) - { - var keyInfo = new ConsoleKeyInfo(keyChar, key, false, false, false); - var result = keyInfo.IsValidInputKey(); - result.Should().Be(expected); - } -}