diff --git a/Consoul.Test/Program.cs b/Consoul.Test/Program.cs index 78fa1e8..dc070c0 100644 --- a/Consoul.Test/Program.cs +++ b/Consoul.Test/Program.cs @@ -62,8 +62,8 @@ static void Main(string[] args) Routines.InitializeRoutine(args); //Routines.UseDelays = true; // Showcases the usecase of reusing input delays to simulate user response - var tableTest = new Test.Views.TableView(); - tableTest.Run(); + var cancelReadTest = new Test.Views.CancellabelReadView(); + cancelReadTest.Run(); var view1 = new Welcome(); diff --git a/Consoul.Test/Views/CancellabelReadView.cs b/Consoul.Test/Views/CancellabelReadView.cs new file mode 100644 index 0000000..f8bf5ea --- /dev/null +++ b/Consoul.Test/Views/CancellabelReadView.cs @@ -0,0 +1,43 @@ +using ConsoulLibrary.Views; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoulLibrary.Test.Views +{ + public class CancellabelReadView : StaticView + { + public CancellabelReadView() + { + Title = (new BannerEntry("Testing the Read(CancellationToken) method")).Message; + } + + [ViewOption("Test Read()")] + public void Test() + { + Consoul.Write("Input some text:", ConsoleColor.Yellow); + string input = Consoul.Read(); + Consoul.Write("Read the following input:"); + Consoul.Write(input, ConsoleColor.Gray); + + Consoul.Wait(); + } + + [ViewOption("Test Read(CancellationToken)")] + public void TestCancellable() + { + const int TIMEOUT_SECONDS = 5; + string input = string.Empty; + Consoul.Write("Input some text:", ConsoleColor.Yellow); + using (var cancelSource = new CancellationTokenSource(TimeSpan.FromSeconds(TIMEOUT_SECONDS))) + { + cancelSource.Token.Register(() => Consoul.Write("Consoul.Read(CancellationToken) Timed Out!", ConsoleColor.Red)); + input = Consoul.Read(cancelSource.Token); + } + Consoul.Write("Read the following input:"); + Consoul.Write(input, ConsoleColor.Gray); + + Consoul.Wait(); + } + } +} diff --git a/Consoul/Consoul.cs b/Consoul/Consoul.cs index fc5a2f2..ec786be 100644 --- a/Consoul/Consoul.cs +++ b/Consoul/Consoul.cs @@ -1,8 +1,7 @@ using System; using System.IO; using System.Linq; -using System.Reflection; -using System.Text; +using System.Threading; namespace ConsoulLibrary { public static class Consoul @@ -13,11 +12,10 @@ public static class Consoul /// Waits for the user to press "Enter". Performs Console.ReadLine() /// Flags whether or not to show continue message. /// - public static void Wait(bool silent = false) + public static void Wait(bool silent = false, CancellationToken cancellationToken = default) { - if (!silent) - Consoul._write(RenderOptions.ContinueMessage, RenderOptions.SubnoteColor); - Read(); + if (!silent) Consoul._write(RenderOptions.ContinueMessage, RenderOptions.SubnoteColor); + Read(cancellationToken); } /// @@ -27,14 +25,14 @@ public static void Wait(bool silent = false) /// Override the Prompt Message color /// Specifies whether the user can provide an empty response. Can result in string.Empty /// User response (string) - public static string Input(string message, ConsoleColor? color = null, bool allowEmpty = false) + public static string Input(string message, ConsoleColor? color = null, bool allowEmpty = false, CancellationToken cancellationToken = default) { string output = string.Empty; bool valid = false; do { Consoul._write(message, RenderOptions.GetColor(color)); - output = Read(); + output = Read(cancellationToken); if (allowEmpty) { valid = true; @@ -88,12 +86,33 @@ public static void Write(string message, ConsoleColor? color = null, bool writeL break; } } - + + /// + /// Reads user input from the console. + /// + /// Response from the user. + public static string Read() => Read("\r\n"); + /// /// Waits for user input and reads the user response. /// + /// Reference to the string that indicates the end of stream. /// Value from the user - public static string Read() + public static string Read(string exitCode = "\r\n") + { + using (var cancelSource = new CancellationTokenSource()) + { + return Read(cancelSource.Token, exitCode); + } + } + + /// + /// Asynchronously reads any input from the user and allows the operation to be cancelled at any time. + /// + /// Reference to the cancellation token to stop the read operation. + /// Reference to the string that indicates the end of stream. + /// Response from the user. + public static string Read(CancellationToken cancellationToken = default, string exitCode = "\r\n") { bool keyControl = false, keyAlt = false, keyShift = false; RoutineInput input = new RoutineInput(); @@ -115,7 +134,37 @@ public static string Read() else { string userInput = string.Empty; - input.Value = Console.ReadLine(); + using (var stream = Console.OpenStandardInput()) + { + byte[] data = new byte[1]; + while (!cancellationToken.IsCancellationRequested) + { + using (var readCanceller = new CancellationTokenSource(TimeSpan.FromMilliseconds(500))) + { + try + { + stream.ReadAsync(data, 0, data.Length, readCanceller.Token).Wait(cancellationToken); + } + catch (OperationCanceledException cancelled) + { + break; + } + + if (data.Length > 0 && data[0] >= 0) + { + userInput += Console.InputEncoding.GetString(data); + } + + if (userInput.EndsWith(exitCode)) + { + userInput = userInput.Substring(0, userInput.Length - exitCode.Length); + break; + } + } + } + stream.Close(); + } + input.Value = userInput; } if (Routines.PromptRegistry.Any()) @@ -145,7 +194,6 @@ public static void Center(string message, int maxWidth, ConsoleColor? color = nu { string text = message.Length > maxWidth ? message.Substring(0, maxWidth - 3) + "..." : message; - int remainder = maxWidth - text.Length - 1; int left, right; right = remainder / 2; @@ -167,7 +215,7 @@ public static void Center(string message, int maxWidth, ConsoleColor? color = nu /// Specifies whether the user can provide an empty response. Default is typically the 'No', but can be overriden /// Specifies whether the default entry should be 'No'. This only applies if 'allowEmpty' is true. /// Boolean of users response relative to 'Yes' or 'No' - public static bool Ask(string message, bool clear = false, bool allowEmpty = false, bool defaultIsNo = true) + public static bool Ask(string message, bool clear = false, bool allowEmpty = false, bool defaultIsNo = true, CancellationToken cancellationToken = default) { string input = ""; string orEmpty = $" or Press Enter"; @@ -185,7 +233,7 @@ public static bool Ask(string message, bool clear = false, bool allowEmpty = fal } Consoul._write(message, RenderOptions.PromptColor); Consoul._write(optionMessage, RenderOptions.SubnoteColor); - input = Read();// Console.ReadLine(); + input = Read(cancellationToken); if (input.ToLower() != "y" && input.ToLower() != "n" && !string.IsNullOrEmpty(input)) { Consoul._write("Invalid input!", RenderOptions.InvalidColor); @@ -202,10 +250,22 @@ public static bool Ask(string message, bool clear = false, bool allowEmpty = fal /// /// Indicates whether or not to clear the console window. /// Simple list of options. - /// + /// Index of the option that was chosen. Returns -1 if selection was invalid. public static int Prompt(string message, bool clear = false, params string[] options) { - return (new Prompt(message, clear, options)).Run(); + return Prompt(message, clear, CancellationToken.None, options); + } + + /// + /// Prompts the user with a simple list of choices. + /// + /// + /// Indicates whether or not to clear the console window. + /// Simple list of options. + /// Index of the option that was chosen. Returns -1 if selection was invalid. + public static int Prompt(string message, bool clear = false, CancellationToken cancellationToken = default, params string[] options) + { + return (new Prompt(message, clear, options)).Run(cancellationToken); } /// @@ -227,12 +287,12 @@ public static int Prompt(string message, PromptOption[] options, bool clear = fa /// Indicates whether to check the file exists before allowing the user exit the loop. /// /// - public static string PromptForFilepath(string message, bool checkExists, ConsoleColor? color = null) { + public static string PromptForFilepath(string message, bool checkExists, ConsoleColor? color = null, CancellationToken cancellationToken = default) { string path; do { - Consoul.Write(message, color); - path = Consoul.Read(); + Write(message, color); + path = Read(cancellationToken); } while (string.IsNullOrEmpty(path) && (checkExists ? !File.Exists(path) : true)); if (path.StartsWith("\"") && path.EndsWith("\"")) path = path.Substring(1, path.Length - 2); return path; @@ -246,10 +306,10 @@ public static string PromptForFilepath(string message, bool checkExists, Console /// /// /// - public static string PromptForFilepath(string defaultPath, string message, bool checkExists, ConsoleColor? color = null) { + public static string PromptForFilepath(string defaultPath, string message, bool checkExists, ConsoleColor? color = null, CancellationToken cancellationToken = default) { string path = defaultPath; - if (!File.Exists(path) || !Consoul.Ask($"Would you like to use the default path:\r\n{path}", defaultIsNo: false)) { - path = PromptForFilepath(message, checkExists, color); + if (!File.Exists(path) || !Ask($"Would you like to use the default path:\r\n{path}", defaultIsNo: false)) { + path = PromptForFilepath(message, checkExists, color, cancellationToken); } return path; } @@ -270,8 +330,8 @@ public static void Ding() /// Specifies whether to use Console.WriteLine() or Console.Write() public static void Alert(string message, ConsoleColor? color = null, bool writeLine = true) { - Consoul.Write(message, color, writeLine); - Consoul.Ding(); + Write(message, color, writeLine); + Ding(); } } } diff --git a/Consoul/Consoul.csproj b/Consoul/Consoul.csproj index 69126fc..870a119 100644 --- a/Consoul/Consoul.csproj +++ b/Consoul/Consoul.csproj @@ -9,7 +9,7 @@ Console tbm0115 - 1.6.1 + 1.6.2 Added Ding and Alert methods 1.5.14.0 1.5.14.0 diff --git a/Consoul/Prompt/Prompt.cs b/Consoul/Prompt/Prompt.cs index 866bb67..58d5520 100644 --- a/Consoul/Prompt/Prompt.cs +++ b/Consoul/Prompt/Prompt.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Options = ConsoulLibrary.RenderOptions; namespace ConsoulLibrary { public delegate void PromptChoiceCallback(TTarget choice); @@ -67,7 +68,7 @@ public void Clear() /// Displays the options for this prompt. Loops until the user "selects" the appropriate option. /// /// Zero-based index of the selected option. - public int Run() + public int Run(CancellationToken cancellationToken = default) { string[] escapePhrases = new string[] { @@ -98,7 +99,7 @@ public int Run() i++; } Console.ForegroundColor = RenderOptions.DefaultColor; - input = Consoul.Read();// Console.ReadLine(); + input = Consoul.Read(cancellationToken); if (string.IsNullOrEmpty(input) && defaultOption != null) { selection = defaultOption.Index; diff --git a/Consoul/Table/TableView.cs b/Consoul/Table/TableView.cs index 3471367..ca27628 100644 --- a/Consoul/Table/TableView.cs +++ b/Consoul/Table/TableView.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; +using System.Threading; namespace ConsoulLibrary.Table { @@ -110,7 +110,7 @@ public void Write(bool clearConsole = true){ } } - public int Prompt(string message = "", ConsoleColor? color = null, bool allowEmpty = false, bool clearConsole = true) + public int Prompt(string message = "", ConsoleColor? color = null, bool allowEmpty = false, bool clearConsole = true, CancellationToken cancellationToken = default) { if (Contents?.Any() == false) return -1; @@ -132,7 +132,7 @@ public int Prompt(string message = "", ConsoleColor? color = null, bool allowEmp Consoul.Write("Press Enter to continue", ConsoulLibrary.RenderOptions.SubnoteColor); } - string input = Consoul.Read(); + string input = Consoul.Read(cancellationToken); if (string.IsNullOrEmpty(input) && allowEmpty) { selection = Contents.Count + 1;