Skip to content
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions src/ReadLine.Demo/TimerDemo.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
}
}
8 changes: 7 additions & 1 deletion src/ReadLine/KeyHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,19 @@ public string Text
}
}

public KeyHandler(IConsole console, List<string> history, Func<string, int, string[]> autoCompleteHandler)
public KeyHandler(IConsole console, string initialInput, List<string> history, Func<string, int, string[]> autoCompleteHandler)
{
Console2 = console;

_historyIndex = history.Count;
_history = history;
_text = new StringBuilder();

if (initialInput.Length > 0)
{
WriteNewString(initialInput);
}

_keyActions = new Dictionary<string, Action>();

_keyActions["LeftArrow"] = MoveCursorLeft;
Expand Down
118 changes: 103 additions & 15 deletions src/ReadLine/ReadLine.cs
Original file line number Diff line number Diff line change
@@ -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<string> _history;

static ReadLine()
public struct ReadLineResult
{
_history = new List<string>();
public string Result;
public bool Interrupted;
}

private static KeyHandler _keyHandler;
private static List<string> _history = new List<string>();
private static bool _active;

public static void AddHistory(params string[] text) => _history.AddRange(text);
public static List<string> GetHistory() => _history;
public static void ClearHistory() => _history = new List<string>();
public static Func<string, int, string[]> AutoCompletionHandler { private get; set; }
public static bool PasswordMode { private get; set; }
public static int InterruptInterval = 5;
public static Func<bool> 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
};
}
}
}