using System;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using Swan.Threading;
namespace Swan
{
///
/// A console terminal helper to create nicer output and receive input from the user.
/// This class is thread-safe :).
///
public static partial class Terminal
{
#region Private Declarations
private const int OutputFlushInterval = 15;
private static readonly ExclusiveTimer DequeueOutputTimer;
private static readonly object SyncLock = new object();
private static readonly ConcurrentQueue OutputQueue = new ConcurrentQueue();
private static readonly ManualResetEventSlim OutputDone = new ManualResetEventSlim(false);
private static readonly ManualResetEventSlim InputDone = new ManualResetEventSlim(true);
private static bool? _isConsolePresent;
#endregion
#region Constructors
///
/// Initializes static members of the class.
///
static Terminal()
{
lock (SyncLock)
{
if (DequeueOutputTimer != null) return;
if (IsConsolePresent)
{
Console.CursorVisible = false;
}
// Here we start the output task, fire-and-forget
DequeueOutputTimer = new ExclusiveTimer(DequeueOutputCycle);
DequeueOutputTimer.Resume(OutputFlushInterval);
}
}
#endregion
#region Synchronized Cursor Movement
///
/// Gets or sets the cursor left position.
///
///
/// The cursor left.
///
public static int CursorLeft
{
get
{
if (IsConsolePresent == false) return -1;
lock (SyncLock)
{
Flush();
return Console.CursorLeft;
}
}
set
{
if (IsConsolePresent == false) return;
lock (SyncLock)
{
Flush();
Console.CursorLeft = value;
}
}
}
///
/// Gets or sets the cursor top position.
///
///
/// The cursor top.
///
public static int CursorTop
{
get
{
if (IsConsolePresent == false) return -1;
lock (SyncLock)
{
Flush();
return Console.CursorTop;
}
}
set
{
if (IsConsolePresent == false) return;
lock (SyncLock)
{
Flush();
Console.CursorTop = value;
}
}
}
#endregion
#region Properties
///
/// Gets a value indicating whether the Console is present.
///
///
/// true if this instance is console present; otherwise, false.
///
public static bool IsConsolePresent
{
get
{
if (_isConsolePresent == null)
{
_isConsolePresent = true;
try
{
var windowHeight = Console.WindowHeight;
_isConsolePresent = windowHeight >= 0;
}
catch
{
_isConsolePresent = false;
}
}
return _isConsolePresent.Value;
}
}
///
/// Gets the available output writers in a bitwise mask.
///
///
/// The available writers.
///
public static TerminalWriters AvailableWriters =>
IsConsolePresent
? TerminalWriters.StandardError | TerminalWriters.StandardOutput
: TerminalWriters.None;
///
/// Gets or sets the output encoding for the current console.
///
///
/// The output encoding.
///
public static Encoding OutputEncoding
{
get => Console.OutputEncoding;
set => Console.OutputEncoding = value;
}
#endregion
#region Methods
///
/// Waits for all of the queued output messages to be written out to the console.
/// Call this method if it is important to display console text before
/// quitting the application such as showing usage or help.
/// Set the timeout to null or TimeSpan.Zero to wait indefinitely.
///
/// The timeout. Set the amount of time to black before this method exits.
public static void Flush(TimeSpan? timeout = null)
{
if (timeout == null) timeout = TimeSpan.Zero;
var startTime = DateTime.UtcNow;
while (OutputQueue.Count > 0)
{
// Manually trigger a timer cycle to run immediately
DequeueOutputTimer.Change(0, OutputFlushInterval);
// Wait for the output to finish
if (OutputDone.Wait(OutputFlushInterval))
break;
// infinite timeout
if (timeout.Value == TimeSpan.Zero)
continue;
// break if we have reached a timeout condition
if (DateTime.UtcNow.Subtract(startTime) >= timeout.Value)
break;
}
}
///
/// Sets the cursor position.
///
/// The left.
/// The top.
public static void SetCursorPosition(int left, int top)
{
if (!IsConsolePresent) return;
lock (SyncLock)
{
Flush();
Console.SetCursorPosition(left.Clamp(0, left), top.Clamp(0, top));
}
}
///
/// Moves the output cursor one line up starting at left position 0
/// Please note that backlining the cursor does not clear the contents of the
/// previous line so you might need to clear it by writing an empty string the
/// length of the console width.
///
public static void BacklineCursor() => SetCursorPosition(0, CursorTop - 1);
///
/// Writes a standard banner to the standard output
/// containing the company name, product name, assembly version and trademark.
///
/// The color.
public static void WriteWelcomeBanner(ConsoleColor color = ConsoleColor.Gray)
{
WriteLine($"{SwanRuntime.CompanyName} {SwanRuntime.ProductName} [Version {SwanRuntime.EntryAssemblyVersion}]", color);
WriteLine($"{SwanRuntime.ProductTrademark}", color);
}
///
/// Enqueues the output to be written to the console
/// This is the only method that should enqueue to the output
/// Please note that if AvailableWriters is None, then no output will be enqueued.
///
/// The context.
private static void EnqueueOutput(OutputContext context)
{
lock (SyncLock)
{
var availableWriters = AvailableWriters;
if (availableWriters == TerminalWriters.None || context.OutputWriters == TerminalWriters.None)
{
OutputDone.Set();
return;
}
if ((context.OutputWriters & availableWriters) == TerminalWriters.None)
return;
OutputDone.Reset();
OutputQueue.Enqueue(context);
}
}
///
/// Runs a Terminal I/O cycle in the thread.
///
private static void DequeueOutputCycle()
{
if (AvailableWriters == TerminalWriters.None)
{
OutputDone.Set();
return;
}
InputDone.Wait();
if (OutputQueue.Count <= 0)
{
OutputDone.Set();
return;
}
OutputDone.Reset();
while (OutputQueue.Count > 0)
{
if (!OutputQueue.TryDequeue(out var context)) continue;
// Process Console output and Skip over stuff we can't display so we don't stress the output too much.
if (!IsConsolePresent) continue;
Console.ForegroundColor = context.OutputColor;
// Output to the standard output
if (context.OutputWriters.HasFlag(TerminalWriters.StandardOutput))
{
Console.Out.Write(context.OutputText);
}
// output to the standard error
if (context.OutputWriters.HasFlag(TerminalWriters.StandardError))
{
Console.Error.Write(context.OutputText);
}
Console.ResetColor();
Console.ForegroundColor = context.OriginalColor;
}
}
#endregion
#region Output Context
///
/// Represents an asynchronous output context.
///
private sealed class OutputContext
{
///
/// Initializes a new instance of the class.
///
public OutputContext()
{
OriginalColor = Settings.DefaultColor;
OutputWriters = IsConsolePresent
? TerminalWriters.StandardOutput
: TerminalWriters.None;
}
public ConsoleColor OriginalColor { get; }
public ConsoleColor OutputColor { get; set; }
public char[] OutputText { get; set; }
public TerminalWriters OutputWriters { get; set; }
}
#endregion
}
}