using InteractiveReadLine.Abstractions; using InteractiveReadLine.Formatting; // https://github.com/mattj23/InteractiveReadLine #nullable disable namespace InteractiveReadLine { /// /// Exposes a IReadLineProvider that's wrapping a IConsole object, which by default is a wrapper around the System.Console. /// This class is the standard implementation of a ReadLine provider, intended for use with console applications. /// public class ConsoleReadLine : IReadLineProvider { private readonly IConsole console; private FormattedText lastWrittenText; private int lastWrittenCursor; private int startingRow; private int startingCol; public ConsoleReadLine(IConsole console = null) { this.console = console ?? new SystemConsoleWrapper(); this.Start(); } /// /// Reads a console key from the underlying provider. This method blocks until a key is received. /// /// public ConsoleKeyInfo ReadKey() { return console.ReadKey(); } /// /// Sets the display state on the underlying console, consisting of a prefix, body, suffix, and cursor /// position. /// /// public void SetDisplay(LineDisplayState state) { Console.CursorVisible = false; // AAB var totalText = state.Prefix + state.LineBody + state.Suffix; var cursor = state.Prefix.Length + state.Cursor; this.SetText(totalText, cursor); Console.CursorVisible = true; // AAB } /// /// Sets the read line to contain the specified text and cursor position. /// /// /// private void SetText(FormattedText totalText, int cursorPos) { // The process of setting the input text requires us to find the difference between // the last written text and the new text, then perform the minimum amount of character // writes necessary to make the two identical // First, we should determine what the new line needs to look like. If the new line is // longer than the old line, we will write the new line exactly. If it's shorter, we'll // need to pad it out with empty characters var writeText = (totalText.Length >= lastWrittenText.Length) ? totalText : totalText + new string(' ', lastWrittenText.Length - totalText.Length); // Sweep through each character in the text to write and determine if an edit needs to be // made (or does the FormattedChar match what was last written at this position?) // Cluster the edits into contiguous FormattedText objects, each with a cursor start position. var edits = new List>(); Tuple edit = null; for (int i = 0; i < writeText.Length; i++) { if (i < lastWrittenText.Length && writeText[i].Equals(lastWrittenText[i])) { if (edit != null) { edits.Add(edit); edit = null; } continue; } if (edit == null) { edit = new Tuple(i, writeText[i]); } else { edit = new Tuple(edit.Item1, edit.Item2 + writeText[i]); } /* int left = this.ColOffset(i); int top = this.RowOffset(i) + _startingRow; if (left != _console.CursorLeft) _console.CursorLeft = left; if (top != _console.CursorTop) _console.CursorTop = top; _console.Write(writeText[i]); */ } if (edit != null) edits.Add(edit); foreach (var e in edits) { int left = this.ColOffset(e.Item1); int top = this.RowOffset(e.Item1) + startingRow; if (left != console.CursorLeft) console.CursorLeft = left; if (top != console.CursorTop) console.CursorTop = top; console.Write(e.Item2); } lastWrittenText = totalText; // Check if we shifted down the buffer. In certain cases, if we reach the end of the buffer // height and we skip a line, the System.Console shifts everything up, and our starting row // will effectively be less than where we started. It will never move down. var writtenRowOffset = this.RowOffset(lastWrittenText.Length); if (writtenRowOffset + startingRow >= console.BufferHeight) { startingRow = console.BufferHeight - writtenRowOffset - 1; } console.CursorTop = startingRow + this.RowOffset(cursorPos); console.CursorLeft = this.ColOffset(cursorPos); lastWrittenCursor = cursorPos; } /// /// Writes a message out to the console out in the spot where the current read line input is, then /// immediately re-displays the line input on the next row. /// /// The text to write to the console, a newline char will be added automatically public void InsertText(FormattedText text) { var currentText = lastWrittenText; var currentCursor = lastWrittenCursor; SetText(text, text.Length); console.WriteLine(""); startingRow = console.CursorTop; lastWrittenText = ""; lastWrittenCursor = 0; SetText(currentText, currentCursor); } public void Dispose() { this.Finish(); } private void Start() { // _console.WriteLine(string.Empty); startingRow = console.CursorTop; startingCol = 1 + console.CursorLeft; // AAB // _console.CursorLeft = 0; lastWrittenText = string.Empty; } private void Finish() { console.WriteLine(string.Empty); } private int ColOffset(int length) => startingCol + length % console.BufferWidth; private int RowOffset(int length) => (length - this.ColOffset(length)) / console.BufferWidth; /// /// Provides a convenient static method of calling the ReadLine method on the System.Console /// /// /// public static string ReadLine(ReadLineConfig config = null) { var provider = new ConsoleReadLine(); return provider.ReadLine(config ?? ReadLineConfig.Basic); } } }