Skip to content

Commit

Permalink
Implemented CancellationToken on Read() (#35)
Browse files Browse the repository at this point in the history
- Added asynchronous read operation that can be cancelled using a CancellationToken.
  • Loading branch information
tbm0115 authored Feb 3, 2023
1 parent 4ed6181 commit 808bbb4
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 32 deletions.
4 changes: 2 additions & 2 deletions Consoul.Test/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
43 changes: 43 additions & 0 deletions Consoul.Test/Views/CancellabelReadView.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
108 changes: 84 additions & 24 deletions Consoul/Consoul.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,11 +12,10 @@ public static class Consoul
/// Waits for the user to press "Enter". Performs Console.ReadLine()
/// <paramref name="silent">Flags whether or not to show continue message.</paramref>
/// </summary>
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);
}

/// <summary>
Expand All @@ -27,14 +25,14 @@ public static void Wait(bool silent = false)
/// <param name="color">Override the Prompt Message color</param>
/// <param name="allowEmpty">Specifies whether the user can provide an empty response. Can result in string.Empty</param>
/// <returns>User response (string)</returns>
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;
Expand Down Expand Up @@ -88,12 +86,33 @@ public static void Write(string message, ConsoleColor? color = null, bool writeL
break;
}
}


/// <summary>
/// Reads user input from the console.
/// </summary>
/// <returns>Response from the user.</returns>
public static string Read() => Read("\r\n");

/// <summary>
/// Waits for user input and reads the user response.
/// </summary>
/// <param name="exitCode">Reference to the string that indicates the end of stream.</param>
/// <returns>Value from the user</returns>
public static string Read()
public static string Read(string exitCode = "\r\n")
{
using (var cancelSource = new CancellationTokenSource())
{
return Read(cancelSource.Token, exitCode);
}
}

/// <summary>
/// Asynchronously reads any input from the user and allows the operation to be cancelled at any time.
/// </summary>
/// <param name="cancellationToken">Reference to the cancellation token to stop the read operation.</param>
/// <param name="exitCode">Reference to the string that indicates the end of stream.</param>
/// <returns>Response from the user.</returns>
public static string Read(CancellationToken cancellationToken = default, string exitCode = "\r\n")
{
bool keyControl = false, keyAlt = false, keyShift = false;
RoutineInput input = new RoutineInput();
Expand All @@ -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())
Expand Down Expand Up @@ -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;
Expand All @@ -167,7 +215,7 @@ public static void Center(string message, int maxWidth, ConsoleColor? color = nu
/// <param name="allowEmpty">Specifies whether the user can provide an empty response. Default is typically the 'No', but can be overriden</param>
/// <param name="defaultIsNo">Specifies whether the default entry should be 'No'. This only applies if 'allowEmpty' is true.</param>
/// <returns>Boolean of users response relative to 'Yes' or 'No'</returns>
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";
Expand All @@ -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);
Expand All @@ -202,10 +250,22 @@ public static bool Ask(string message, bool clear = false, bool allowEmpty = fal
/// <param name="message"><inheritdoc cref="Write" path="/param[@name='message']"/></param>
/// <param name="clear">Indicates whether or not to clear the console window.</param>
/// <param name="options">Simple list of options.</param>
/// <returns></returns>
/// <returns>Index of the option that was chosen. Returns -1 if selection was invalid.</returns>
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);
}

/// <summary>
/// Prompts the user with a simple list of choices.
/// </summary>
/// <param name="message"><inheritdoc cref="Write" path="/param[@name='message']"/></param>
/// <param name="clear">Indicates whether or not to clear the console window.</param>
/// <param name="options">Simple list of options.</param>
/// <returns>Index of the option that was chosen. Returns -1 if selection was invalid.</returns>
public static int Prompt(string message, bool clear = false, CancellationToken cancellationToken = default, params string[] options)
{
return (new Prompt(message, clear, options)).Run(cancellationToken);
}

/// <summary>
Expand All @@ -227,12 +287,12 @@ public static int Prompt(string message, PromptOption[] options, bool clear = fa
/// <param name="checkExists">Indicates whether to check the file exists before allowing the user exit the loop.</param>
/// <param name="color"><inheritdoc cref="Write" path="/param[@name='color']"/></param>
/// <returns></returns>
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;
Expand All @@ -246,10 +306,10 @@ public static string PromptForFilepath(string message, bool checkExists, Console
/// <param name="checkExists"><inheritdoc cref="PromptForFilepath(string, bool, ConsoleColor?)" path="/param[@name='checkExists']"/></param>
/// <param name="color"><inheritdoc cref="Write" path="/param[@name='color']"/></param>
/// <returns></returns>
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;
}
Expand All @@ -270,8 +330,8 @@ public static void Ding()
/// <param name="writeLine">Specifies whether to use Console.WriteLine() or Console.Write()</param>
public static void Alert(string message, ConsoleColor? color = null, bool writeLine = true)
{
Consoul.Write(message, color, writeLine);
Consoul.Ding();
Write(message, color, writeLine);
Ding();
}
}
}
2 changes: 1 addition & 1 deletion Consoul/Consoul.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageTags>Console</PackageTags>
<Authors>tbm0115</Authors>
<Company />
<Version>1.6.1</Version>
<Version>1.6.2</Version>
<PackageReleaseNotes>Added Ding and Alert methods</PackageReleaseNotes>
<AssemblyVersion>1.5.14.0</AssemblyVersion>
<FileVersion>1.5.14.0</FileVersion>
Expand Down
5 changes: 3 additions & 2 deletions Consoul/Prompt/Prompt.cs
Original file line number Diff line number Diff line change
@@ -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>(TTarget choice);
Expand Down Expand Up @@ -67,7 +68,7 @@ public void Clear()
/// Displays the options for this prompt. Loops until the user "selects" the appropriate option.
/// </summary>
/// <returns>Zero-based index of the selected option.</returns>
public int Run()
public int Run(CancellationToken cancellationToken = default)
{
string[] escapePhrases = new string[]
{
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions Consoul/Table/TableView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;

namespace ConsoulLibrary.Table
{
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down

0 comments on commit 808bbb4

Please sign in to comment.