diff --git a/README.md b/README.md index 6b191bd..5878fdb 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,39 @@ ReadLine.AutoCompletionHandler = (t, s) => _Note: If no "AutoCompletionHandler" is set, tab autocompletion will be disabled_ +### Initial Buffer + +```csharp +string input = ReadLine.Read("(prompt)> ", "", "initial"); +``` + +The editing session will begin with "initial" in the readline buffer. + +### Background Processing + +```csharp +ReadLine.CheckInterrupt = () => +{ + bool interrupt = /* whether to interrupt the readline */ + return interrupt; +} +ReadLine.InterruptInterval = 1000; /* milliseconds */ +``` + +Every `InterruptInterval` milliseconds, the library will silently run the `CheckInterrupt()` function. If this returns true, the read operation will stop immediately and return what the user has typed so far. + +The `CheckInterrupt()` function should not print anything, as this will be mixed in with the editing line. Do your printing after the read operation stops. + +If you want to know whether the operation was interrupted: + +```csharp +ReadLine.ReadLineResult info = ReadLine.ReadExt("(prompt)> "); +``` + +`info.Result` is the string result. `info.Interrupted` is a bool indicating whether the operation was stopped by the interrupt routine or by the user hitting Enter. + +By combining background processing and the initial buffer option in a loop, you can achieve the effect of an interruption which prints something and then resumes editing. See the [TimerDemo.cs](src/ReadLine.Demo/TimerDemo.cs) demo. + ## Contributing Contributions are highly welcome. If you have found a bug or if you have a feature request, please report them at this repository issues section. diff --git a/src/ReadLine.Demo/TimerDemo.cs b/src/ReadLine.Demo/TimerDemo.cs new file mode 100644 index 0000000..b4d9345 --- /dev/null +++ b/src/ReadLine.Demo/TimerDemo.cs @@ -0,0 +1,37 @@ +using System; + +namespace ConsoleApplication +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("ReadLine Library Timer Demo"); + Console.WriteLine(); + + ReadLine.InterruptInterval = 1000; + ReadLine.CheckInterrupt = () => true; + + string initial = ""; + + while (true) + { + ReadLine.ReadLineResult info = ReadLine.ReadExt("(prompt)> ", "", initial); + if (info.Interrupted) + { + initial = info.Result; + Console.WriteLine("Tick..."); + } + else + { + initial = ""; + if (info.Result.Length != 0) + { + Console.WriteLine("You typed: \"" + info.Result + "\""); + break; + } + } + } + } + } +} diff --git a/src/ReadLine/KeyHandler.cs b/src/ReadLine/KeyHandler.cs index 539f810..4e8bd94 100644 --- a/src/ReadLine/KeyHandler.cs +++ b/src/ReadLine/KeyHandler.cs @@ -204,13 +204,19 @@ public string Text } } - public KeyHandler(IConsole console, List history, Func autoCompleteHandler) + public KeyHandler(IConsole console, string initialInput, List history, Func autoCompleteHandler) { Console2 = console; _historyIndex = history.Count; _history = history; _text = new StringBuilder(); + + if (initialInput.Length > 0) + { + WriteNewString(initialInput); + } + _keyActions = new Dictionary(); _keyActions["LeftArrow"] = MoveCursorLeft; diff --git a/src/ReadLine/ReadLine.cs b/src/ReadLine/ReadLine.cs index 9e518bf..9bd16d1 100755 --- a/src/ReadLine/ReadLine.cs +++ b/src/ReadLine/ReadLine.cs @@ -1,48 +1,136 @@ using Internal.ReadLine; using Internal.ReadLine.Abstractions; +using System.Diagnostics; +using System.Threading; using System.Collections.Generic; namespace System { public static class ReadLine { - private static KeyHandler _keyHandler; - private static List _history; - - static ReadLine() + public struct ReadLineResult { - _history = new List(); + public string Result; + public bool Interrupted; } + private static KeyHandler _keyHandler; + private static List _history = new List(); + private static bool _active; + public static void AddHistory(params string[] text) => _history.AddRange(text); public static List GetHistory() => _history; public static void ClearHistory() => _history = new List(); public static Func AutoCompletionHandler { private get; set; } public static bool PasswordMode { private get; set; } + public static int InterruptInterval = 5; + public static Func CheckInterrupt; + public static bool IsReading => _active; - public static string Read(string prompt = "", string defaultInput = "") + // Wrapper for users who don't care about ReadLineResult. + public static string Read(string prompt = "", string defaultInput = "", string initialInput = "") { + ReadLineResult res = ReadExt(prompt, defaultInput, initialInput); + return res.Result; + } + + public static ReadLineResult ReadExt(string prompt = "", string defaultInput = "", string initialInput = "") + { + _active = true; Console.Write(prompt); - _keyHandler = new KeyHandler(new Console2() { PasswordMode = PasswordMode }, _history, AutoCompletionHandler); - ConsoleKeyInfo keyInfo = Console.ReadKey(true); + _keyHandler = new KeyHandler(new Console2() { PasswordMode = PasswordMode }, initialInput, _history, AutoCompletionHandler); - while (keyInfo.Key != ConsoleKey.Enter) + bool done = false; + bool interrupted = false; + + /* We need to poll KeyAvailable very frequently so that typing doesn't feel laggy. So we cap the sleep interval at a maximum of 5 ms; if the InterruptInterval is longer than that, we set a Stopwatch to know when to call CheckInterrupt. + (We should be calling Console.ReadKey with a timeout, but C# doesn't support that, sigh.) + */ + Stopwatch stopwatch = null; + int sleeptime = InterruptInterval; + if (sleeptime > 5) { - _keyHandler.Handle(keyInfo); - keyInfo = Console.ReadKey(true); + sleeptime = 5; + if (CheckInterrupt != null) + { + stopwatch = new Stopwatch(); + stopwatch.Start(); + } + } + + while (!done) { + // Handle all keys that have come in. + while (!done && Console.KeyAvailable) + { + ConsoleKeyInfo keyInfo = Console.ReadKey(true); + if (keyInfo.Key == ConsoleKey.Enter) + { + done = true; + break; + } + _keyHandler.Handle(keyInfo); + Thread.Sleep(0); + } + + // Handle the timer, if there is one. + if (!done && CheckInterrupt != null) + { + if (stopwatch == null) + { + // Check every 5ms tick. + interrupted = CheckInterrupt(); + } + else + { + // Check if enough time has elapsed. + var elapsed = stopwatch.ElapsedMilliseconds; + if (elapsed >= InterruptInterval) + { + stopwatch.Restart(); + interrupted = CheckInterrupt(); + } + } + if (interrupted) + { + done = true; + } + } + + if (!done) + { + Thread.Sleep(sleeptime); + } } Console.WriteLine(); string text = _keyHandler.Text; - if (String.IsNullOrWhiteSpace(text) && !String.IsNullOrWhiteSpace(defaultInput)) - text = defaultInput; + + /* If the entry was blank, we substitute the default value (if there was one). If the entry was non-blank, we add it to history. */ + if (String.IsNullOrWhiteSpace(text)) + { + if ((!interrupted) && (!String.IsNullOrWhiteSpace(defaultInput))) + { + text = defaultInput; + } + } else - _history.Add(text); + { + /* We never add blank or default values to the history list. We also don't add value that duplicates the most recent value. */ + var len = _history.Count; + if (len == 0 || text != _history[len-1]) + { + _history.Add(text); + } + } - return text; + _active = false; + return new ReadLineResult() { + Result = text, + Interrupted = interrupted + }; } } }