I've created an small lib to allow writing to console with multiple threads and reading from console at the same time avoiding output being written in the middle of input because console cursor is there. My library implements simple mechanism to make console write output above input.
TSConsole
is the main class for interacting with console.
TSCString
class used for creating colored strings.
TSConsole.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace ThreadSafeConsole
{
public static class TSConsole
{
/* Private fields */
private static readonly object _lockObject = new object();
private static readonly StringBuilder _buffer = new StringBuilder();
private static bool _isReading = false;
private static int _bufferCursorPos = 0;
private static string _prompt = "";
/* Public Properties */
public static string Prompt
{
get
{
lock (_lockObject)
{
return _prompt;
}
}
set
{
lock (_lockObject)
{
if (_isReading)
throw new InvalidOperationException("Can't set prompt while reading.");
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException(nameof(value));
_prompt = value;
}
}
}
public static bool IsReading
{
get
{
lock (_lockObject)
{
return _isReading;
}
}
}
/* Public Methods */
public static void WriteLine(string str)
{
if (string.IsNullOrEmpty(str))
throw new ArgumentNullException(nameof(str));
lock (_lockObject)
{
GoToBufferBeginning();
Console.Write(str);
RestoreBuffer();
}
}
public static void WriteLine(IEnumerable<TSCString> enumeration)
{
if (enumeration is null)
throw new ArgumentNullException(nameof(enumeration));
lock (_lockObject)
{
ConsoleColor saveForeground = Console.ForegroundColor;
ConsoleColor saveBackground = Console.BackgroundColor;
GoToBufferBeginning();
foreach (var elem in enumeration)
{
if (!string.IsNullOrEmpty(elem.Data))
{
Console.ForegroundColor = elem.Foreground ?? saveForeground;
Console.BackgroundColor = elem.Backgroung ?? saveBackground;
Console.Write(elem.Data);
}
}
Console.ForegroundColor = saveForeground;
Console.BackgroundColor = saveBackground;
RestoreBuffer();
}
}
public static void WriteLine(params TSCString[] enumeration)
{
WriteLine((IEnumerable<TSCString>)enumeration);
}
public static string ReadLine(int maxLength = 0)
{
lock (_lockObject)
{
if (_isReading)
throw new InvalidOperationException("Some thread is already reading.");
_isReading = true;
_bufferCursorPos = 0;
Console.Write(_prompt);
}
while (true)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
lock (_lockObject)
{
if (!char.IsControl(keyInfo.KeyChar))
{
if (maxLength == 0 || _buffer.Length < maxLength)
{
int i = _buffer.Length - _bufferCursorPos;
Console.Write(keyInfo.KeyChar + _buffer.ToString(_bufferCursorPos, i));
MoveCursor(i * -1);
_buffer.Insert(_bufferCursorPos, keyInfo.KeyChar.ToString(CultureInfo.InvariantCulture));
_bufferCursorPos++;
}
}
else
{
switch (keyInfo.Key)
{
// [Backspace] Remove symbol before cursor
case ConsoleKey.Backspace:
if (_bufferCursorPos != 0)
{
MoveCursor(-1);
int i = _buffer.Length - _bufferCursorPos;
Console.Write(_buffer.ToString(_bufferCursorPos, i) + ' ');
MoveCursor(i * -1 - 1);
_buffer.Remove(_bufferCursorPos - 1, 1);
_bufferCursorPos--;
}
continue;
// [Del] Remove symbol after cursor
case ConsoleKey.Delete:
if (_bufferCursorPos != _buffer.Length)
{
int i = _buffer.Length - _bufferCursorPos;
Console.Write(_buffer.ToString(_bufferCursorPos + 1, i - 1) + ' ');
MoveCursor(i * -1);
_buffer.Remove(_bufferCursorPos, 1);
}
continue;
// [Enter] Clear buffer and return string
case ConsoleKey.Enter:
if (_buffer.Length != 0)
{
GoToBufferBeginning();
int i = _prompt.Length + _buffer.Length;
Console.Write(new string(' ', i));
MoveCursor(i * -1);
string result = _buffer.ToString();
_isReading = false;
_buffer.Clear();
_bufferCursorPos = 0;
return result;
}
continue;
// [Left Arrow] Move cursor left
case ConsoleKey.LeftArrow:
if (_bufferCursorPos != 0)
{
MoveCursor(-1);
_bufferCursorPos--;
}
continue;
// [Right Arrow] Move cursor right
case ConsoleKey.RightArrow:
if (_bufferCursorPos != _buffer.Length)
{
MoveCursor(1);
_bufferCursorPos++;
}
continue;
}
}
}
}
}
/* Private Helpers */
private static void GoToBufferBeginning()
{
if (_isReading)
{
Console.SetCursorPosition(0, Console.CursorTop - (_prompt.Length + _bufferCursorPos) / Console.BufferWidth);
}
}
private static void RestoreBuffer()
{
if (Console.CursorLeft != 0)
Console.Write(new string(' ', Console.BufferWidth - Console.CursorLeft));
if (_isReading)
{
Console.Write(_prompt);
Console.Write(_buffer.ToString());
MoveCursor(_bufferCursorPos - _buffer.Length);
}
}
private static void MoveCursor(int move)
{
if (move == 0)
return;
move += Console.CursorLeft;
int left = move % Console.BufferWidth;
int top = Console.CursorTop + move / Console.BufferWidth;
if (left < 0)
{
left += Console.BufferWidth;
top -= 1;
}
Console.SetCursorPosition(left, top);
}
}
}
TSCString.cs
using System;
namespace ThreadSafeConsole
{
public class TSCString
{
/* Public Properties */
public string Data { get; set; }
public ConsoleColor? Foreground { get; set; }
public ConsoleColor? Backgroung { get; set; }
/* Constructors */
public TSCString() { }
public TSCString(string data) => Data = data;
public TSCString(string data, ConsoleColor fg) : this(data) => Foreground = fg;
public TSCString(string data, ConsoleColor fg, ConsoleColor bg) : this(data, fg) => Backgroung = bg;
}
}
Few things I'm not sure about:
- Should I make
TSCString
struct instead of class? - Is it okay to have to have constructors like in
TSCString
or should I better avoid one constructor calling another in this situation?
Looking forward to your feedback!