diff --git a/Sharprompt.Tests/PromptTests.cs b/Sharprompt.Tests/PromptTests.cs new file mode 100644 index 0000000..628fc6d --- /dev/null +++ b/Sharprompt.Tests/PromptTests.cs @@ -0,0 +1,72 @@ +using System.Threading; +using System.Threading.Tasks; +using Sharprompt.Drivers; +using Xunit; + +namespace Sharprompt.Tests +{ + public class PromptTests + { + private MockConsoleDriver ConsoleDriver { get; } + + public PromptTests() + { + ConsoleDriver = new MockConsoleDriver(); + ConsoleDriverFactory.Instance = new MockConsoleDriverFactory(() => ConsoleDriver); + } + + [Fact] + public async Task InputString() + { + var keyWait = new AutoResetEvent(false); + ConsoleDriver.AwaitingKeyPress += (sender, args) => keyWait.Set(); + + var task = Task.Run(() => Prompt.Input("Get")); + + keyWait.WaitOne(); + Assert.Equal("? Get:", ConsoleDriver.GetOutputBuffer()); + + ConsoleDriver.InputBuffer.WriteLine("PASS"); + var result = await task; + + Assert.Equal("PASS", result); + Assert.Equal("V Get: PASS", ConsoleDriver.GetOutputBuffer()); + } + + [Fact] + public async Task InputInteger() + { + var keyWait = new AutoResetEvent(false); + ConsoleDriver.AwaitingKeyPress += (sender, args) => keyWait.Set(); + + var task = Task.Run(() => Prompt.Input("Get")); + + keyWait.WaitOne(); + Assert.Equal("? Get:", ConsoleDriver.GetOutputBuffer()); + + ConsoleDriver.InputBuffer.WriteLine("0"); + var result = await task; + + Assert.Equal(0, result); + Assert.Equal("V Get: 0", ConsoleDriver.GetOutputBuffer()); + } + + [Fact] + public void InputInteger_ValidationFailed() + { + var keyWait = new AutoResetEvent(false); + + ConsoleDriver.AwaitingKeyPress += (sender, args) => keyWait.Set(); + + Task.Run(() => Prompt.Input("Get")); + + keyWait.WaitOne(); + Assert.Equal("? Get:", ConsoleDriver.GetOutputBuffer()); + + ConsoleDriver.InputBuffer.WriteLine("PASS"); + + keyWait.WaitOne(); + Assert.Equal("? Get: PASS\n>> PASS is not a valid value for Int32. (Parameter 'value')", ConsoleDriver.GetOutputBuffer()); + } + } +} diff --git a/Sharprompt.Tests/Tools/InputBuffer.cs b/Sharprompt.Tests/Tools/InputBuffer.cs new file mode 100644 index 0000000..f383998 --- /dev/null +++ b/Sharprompt.Tests/Tools/InputBuffer.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Sharprompt.Tests +{ + public class InputBuffer : ConcurrentQueue + { + private Dictionary _inputList = SetupKeyMapping(); + + public void Write(string input) + { + foreach (var keyChar in input) + { + if (_inputList.TryGetValue(keyChar, out var keyInfo)) + { + Enqueue(keyInfo); + } + else + { + throw new InvalidOperationException("Unknown character to key mapping"); + } + } + } + + public void WriteLine(string input) + { + Write(input); + Write("\n"); + } + + private static Dictionary SetupKeyMapping() + { + var keyMapping = new Dictionary() + { + {(char)3, new ConsoleKeyInfo((char)3, ConsoleKey.C, false, false, true)}, + {' ', new ConsoleKeyInfo(' ', ConsoleKey.Spacebar, false, false, false)}, + {'\n', new ConsoleKeyInfo('\n', ConsoleKey.Enter, false, false, false)}, + {'\b', new ConsoleKeyInfo('\b', ConsoleKey.Backspace, false, false, false)} + }; + + for (char c = 'a'; c < 'z'; c++) + { + keyMapping.Add(c, new ConsoleKeyInfo(c, (c - 'a') + ConsoleKey.A, false, false, false)); + } + + for (char c = 'A'; c < 'Z'; c++) + { + keyMapping.Add(c, new ConsoleKeyInfo(c, (c - 'Z') + ConsoleKey.A, true, false, false)); + } + + for (char c = '0'; c < '9'; c++) + { + keyMapping.Add(c, new ConsoleKeyInfo(c, (c - '0') + ConsoleKey.D0, false, false, false)); + } + + return keyMapping; + } + } +} diff --git a/Sharprompt.Tests/Tools/MockConsoleDriver.cs b/Sharprompt.Tests/Tools/MockConsoleDriver.cs new file mode 100644 index 0000000..57b9c44 --- /dev/null +++ b/Sharprompt.Tests/Tools/MockConsoleDriver.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using Sharprompt.Drivers; + +namespace Sharprompt.Tests +{ + public class MockConsoleDriver : IConsoleDriver + { + private char[,] _buffer; + public InputBuffer InputBuffer { get; } = new InputBuffer(); + + public event EventHandler AwaitingKeyPress; + + public MockConsoleDriver() + { + Reset(); + } + + public bool KeyAvailable => InputBuffer.Any(); + public bool CursorVisible { get; set; } = true; + public int CursorLeft { get; set; } = 0; + public int CursorTop { get; set; } = 0; + public int BufferWidth { get; } = 120; + public int BufferHeight { get; } = 30; + public Action CancellationCallback { get; set; } + public bool IsUnicodeSupported { get; } = false; + + public void Dispose() + { + } + + public void Beep() + { + } + + public void Reset() + { + _buffer = new char[BufferHeight, BufferWidth]; + CursorLeft = CursorTop = 0; + } + + public void ClearLine(int top) + { + for (var x = 0; x < BufferWidth; x++) + { + _buffer[top, x] = ' '; + } + + if (CursorTop == top) CursorLeft = 0; + } + + public ConsoleKeyInfo ReadKey() + { + if (!KeyAvailable) + { + AwaitingKeyPress?.Invoke(this, EventArgs.Empty); + } + + var consoleKey = new ConsoleKeyInfo(); + SpinWait.SpinUntil(() => KeyAvailable && InputBuffer.TryDequeue(out consoleKey), 1000); + return consoleKey; + } + + public void Write(string value, ConsoleColor color) + { + for (int x = 0; x < value.Length; x++, CursorLeft++) + { + _buffer[CursorTop, CursorLeft] = value[x]; + } + } + + public void WriteLine() + { + Write("\n", ConsoleColor.Black); + CursorLeft = 0; + CursorTop++; + } + + public void SetCursorPosition(int left, int top) + { + CursorLeft = left; + CursorTop = top; + } + + public string GetOutputBuffer() + { + List lines = new List(); + for (int y = 0; y < BufferHeight; y++) + { + var stringBuilder = new StringBuilder(); + for (int x = 0; x < BufferWidth && _buffer[y,x] != '\0'; x++) + { + stringBuilder.Append(_buffer[y,x]); + } + + lines.Add(stringBuilder.ToString().TrimEnd()); + } + + var idx = lines.FindLastIndex((l) => !string.IsNullOrEmpty(l)); + + return string.Join('\n', lines.Take(idx + 1)); + } + } +} diff --git a/Sharprompt.Tests/Tools/MockConsoleDriverFactory.cs b/Sharprompt.Tests/Tools/MockConsoleDriverFactory.cs new file mode 100644 index 0000000..9f02393 --- /dev/null +++ b/Sharprompt.Tests/Tools/MockConsoleDriverFactory.cs @@ -0,0 +1,20 @@ +using System; +using Sharprompt.Drivers; + +namespace Sharprompt.Tests +{ + public class MockConsoleDriverFactory : IConsoleDriverFactory + { + private readonly Func _consoleDriverFn; + + /// + /// Initializes a new instance of the class. + /// + public MockConsoleDriverFactory(Func consoleDriverFn) + { + _consoleDriverFn = consoleDriverFn; + } + + public IConsoleDriver Create() => _consoleDriverFn(); + } +} diff --git a/Sharprompt/Drivers/ConsoleDriverFactory.cs b/Sharprompt/Drivers/ConsoleDriverFactory.cs new file mode 100644 index 0000000..8b17272 --- /dev/null +++ b/Sharprompt/Drivers/ConsoleDriverFactory.cs @@ -0,0 +1,7 @@ +namespace Sharprompt.Drivers +{ + public static class ConsoleDriverFactory + { + public static IConsoleDriverFactory Instance { get; set; } = new DefaultConsoleDriverFactory(); + } +} diff --git a/Sharprompt/Drivers/DefaultConsoleDriver.cs b/Sharprompt/Drivers/DefaultConsoleDriver.cs index ba650ca..0f8721f 100644 --- a/Sharprompt/Drivers/DefaultConsoleDriver.cs +++ b/Sharprompt/Drivers/DefaultConsoleDriver.cs @@ -1,34 +1,10 @@ using System; using System.Runtime.InteropServices; -using Sharprompt.Internal; - namespace Sharprompt.Drivers { internal sealed class DefaultConsoleDriver : IConsoleDriver { - static DefaultConsoleDriver() - { - if (Console.IsInputRedirected || Console.IsOutputRedirected) - { - throw new InvalidOperationException("Sharprompt requires an interactive environment."); - } - - Console.TreatControlCAsInput = true; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var hConsole = NativeMethods.GetStdHandle(NativeMethods.STD_OUTPUT_HANDLE); - - if (!NativeMethods.GetConsoleMode(hConsole, out var mode)) - { - return; - } - - NativeMethods.SetConsoleMode(hConsole, mode | NativeMethods.ENABLE_VIRTUAL_TERMINAL_PROCESSING); - } - } - #region IDisposable public void Dispose() => Reset(); @@ -93,6 +69,10 @@ public bool CursorVisible public Action CancellationCallback { get; set; } + public bool IsUnicodeSupported + { + get => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || Console.OutputEncoding.CodePage is 1200 or 65001; + } #endregion } } diff --git a/Sharprompt/Drivers/DefaultConsoleDriverFactory.cs b/Sharprompt/Drivers/DefaultConsoleDriverFactory.cs new file mode 100644 index 0000000..71f7918 --- /dev/null +++ b/Sharprompt/Drivers/DefaultConsoleDriverFactory.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices; + +using Sharprompt.Internal; + +namespace Sharprompt.Drivers +{ + internal sealed class DefaultConsoleDriverFactory : IConsoleDriverFactory + { + private bool isInitialized = false; + + private void InitializeDefaultConsoleDriver() + { + if (isInitialized) return; + + if (Console.IsInputRedirected || Console.IsOutputRedirected) + { + throw new InvalidOperationException("Sharprompt requires an interactive environment."); + } + + Console.TreatControlCAsInput = true; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var hConsole = NativeMethods.GetStdHandle(NativeMethods.STD_OUTPUT_HANDLE); + + if (!NativeMethods.GetConsoleMode(hConsole, out var mode)) + { + return; + } + + NativeMethods.SetConsoleMode(hConsole, mode | NativeMethods.ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } + + isInitialized = true; + } + + public IConsoleDriver Create() + { + InitializeDefaultConsoleDriver(); + return new DefaultConsoleDriver(); + } + } +} diff --git a/Sharprompt/Drivers/IConsoleDriver.cs b/Sharprompt/Drivers/IConsoleDriver.cs index 7b0eac7..0a9c1f3 100644 --- a/Sharprompt/Drivers/IConsoleDriver.cs +++ b/Sharprompt/Drivers/IConsoleDriver.cs @@ -2,7 +2,7 @@ namespace Sharprompt.Drivers { - internal interface IConsoleDriver : IDisposable + public interface IConsoleDriver : IDisposable { void Beep(); void Reset(); @@ -18,5 +18,6 @@ internal interface IConsoleDriver : IDisposable int BufferWidth { get; } int BufferHeight { get; } Action CancellationCallback { get; set; } + bool IsUnicodeSupported { get; } } } diff --git a/Sharprompt/Drivers/IConsoleDriverFactory.cs b/Sharprompt/Drivers/IConsoleDriverFactory.cs new file mode 100644 index 0000000..169c934 --- /dev/null +++ b/Sharprompt/Drivers/IConsoleDriverFactory.cs @@ -0,0 +1,7 @@ +namespace Sharprompt.Drivers +{ + public interface IConsoleDriverFactory + { + IConsoleDriver Create(); + } +} diff --git a/Sharprompt/Forms/FormBase.cs b/Sharprompt/Forms/FormBase.cs index 55b93b8..cf2b94e 100644 --- a/Sharprompt/Forms/FormBase.cs +++ b/Sharprompt/Forms/FormBase.cs @@ -11,10 +11,7 @@ internal abstract class FormBase : IDisposable { protected FormBase() { - ConsoleDriver = new DefaultConsoleDriver - { - CancellationCallback = CancellationHandler - }; + ConsoleDriver = ConsoleDriverFactory.Instance.Create(); _formRenderer = new FormRenderer(ConsoleDriver); } diff --git a/Sharprompt/Symbol.cs b/Sharprompt/Symbol.cs index 66c716f..5976b68 100644 --- a/Sharprompt/Symbol.cs +++ b/Sharprompt/Symbol.cs @@ -1,6 +1,8 @@ using System; using System.Runtime.InteropServices; +using Sharprompt.Drivers; + namespace Sharprompt { public class Symbol @@ -18,6 +20,13 @@ public Symbol(string value, string fallbackValue) public static implicit operator string(Symbol symbol) => symbol.ToString(); - private static bool IsUnicodeSupported => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || Console.OutputEncoding.CodePage is 1200 or 65001; + private static bool IsUnicodeSupported + { + get + { + var consoleDriver = ConsoleDriverFactory.Instance.Create(); + return consoleDriver.IsUnicodeSupported; + } + } } }