356 lines
10 KiB
C#
356 lines
10 KiB
C#
using Unosquare.Swan.Abstractions;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace Unosquare.Swan {
|
|
/// <summary>
|
|
/// A console terminal helper to create nicer output and receive input from the user.
|
|
/// This class is thread-safe :).
|
|
/// </summary>
|
|
public static partial class Terminal {
|
|
#region Private Declarations
|
|
|
|
private const Int32 OutputFlushInterval = 15;
|
|
private static readonly ExclusiveTimer DequeueOutputTimer;
|
|
private static readonly Object SyncLock = new Object();
|
|
private static readonly ConcurrentQueue<OutputContext> OutputQueue = new ConcurrentQueue<OutputContext>();
|
|
|
|
private static readonly ManualResetEventSlim OutputDone = new ManualResetEventSlim(false);
|
|
private static readonly ManualResetEventSlim InputDone = new ManualResetEventSlim(true);
|
|
|
|
private static Boolean? _isConsolePresent;
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Initializes static members of the <see cref="Terminal"/> class.
|
|
/// </summary>
|
|
static Terminal() {
|
|
lock(SyncLock) {
|
|
if(DequeueOutputTimer != null) {
|
|
return;
|
|
}
|
|
|
|
if(IsConsolePresent) {
|
|
#if !NET452
|
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
|
#endif
|
|
Console.CursorVisible = false;
|
|
}
|
|
|
|
// Here we start the output task, fire-and-forget
|
|
DequeueOutputTimer = new ExclusiveTimer(DequeueOutputCycle);
|
|
DequeueOutputTimer.Resume(OutputFlushInterval);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Synchronized Cursor Movement
|
|
|
|
/// <summary>
|
|
/// Gets or sets the cursor left position.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The cursor left.
|
|
/// </value>
|
|
public static Int32 CursorLeft {
|
|
get {
|
|
if(IsConsolePresent == false) {
|
|
return -1;
|
|
}
|
|
|
|
lock(SyncLock) {
|
|
Flush();
|
|
return Console.CursorLeft;
|
|
}
|
|
}
|
|
set {
|
|
if(IsConsolePresent == false) {
|
|
return;
|
|
}
|
|
|
|
lock(SyncLock) {
|
|
Flush();
|
|
Console.CursorLeft = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the cursor top position.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The cursor top.
|
|
/// </value>
|
|
public static Int32 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
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the Console is present.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if this instance is console present; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public static Boolean IsConsolePresent {
|
|
get {
|
|
if(Settings.OverrideIsConsolePresent) {
|
|
return true;
|
|
}
|
|
|
|
if(_isConsolePresent == null) {
|
|
_isConsolePresent = true;
|
|
try {
|
|
Int32 windowHeight = Console.WindowHeight;
|
|
_isConsolePresent = windowHeight >= 0;
|
|
} catch {
|
|
_isConsolePresent = false;
|
|
}
|
|
}
|
|
|
|
return _isConsolePresent.Value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether a debugger is attached.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if this instance is debugger attached; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public static Boolean IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached;
|
|
|
|
/// <summary>
|
|
/// Gets the available output writers in a bitwise mask.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The available writers.
|
|
/// </value>
|
|
public static TerminalWriters AvailableWriters {
|
|
get {
|
|
TerminalWriters writers = TerminalWriters.None;
|
|
if(IsConsolePresent) {
|
|
writers = TerminalWriters.StandardError | TerminalWriters.StandardOutput;
|
|
}
|
|
|
|
if(IsDebuggerAttached) {
|
|
writers |= TerminalWriters.Diagnostics;
|
|
}
|
|
|
|
return writers;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the output encoding for the current console.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The output encoding.
|
|
/// </value>
|
|
public static Encoding OutputEncoding {
|
|
get => Console.OutputEncoding;
|
|
set => Console.OutputEncoding = value;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="timeout">The timeout. Set the amount of time to black before this method exits.</param>
|
|
public static void Flush(TimeSpan? timeout = null) {
|
|
if(timeout == null) {
|
|
timeout = TimeSpan.Zero;
|
|
}
|
|
|
|
DateTime 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the cursor position.
|
|
/// </summary>
|
|
/// <param name="left">The left.</param>
|
|
/// <param name="top">The top.</param>
|
|
public static void SetCursorPosition(Int32 left, Int32 top) {
|
|
if(IsConsolePresent == false) {
|
|
return;
|
|
}
|
|
|
|
lock(SyncLock) {
|
|
Flush();
|
|
Console.SetCursorPosition(left.Clamp(0, left), top.Clamp(0, top));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static void BacklineCursor() => SetCursorPosition(0, CursorTop - 1);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
private static void EnqueueOutput(OutputContext context) {
|
|
lock(SyncLock) {
|
|
TerminalWriters 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs a Terminal I/O cycle in the <see cref="ThreadPool"/> thread.
|
|
/// </summary>
|
|
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 OutputContext context) == false) {
|
|
continue;
|
|
}
|
|
|
|
// Process Console output and Skip over stuff we can't display so we don't stress the output too much.
|
|
if(IsConsolePresent && (Settings.OverrideIsConsolePresent || OutputQueue.Count <= Console.BufferHeight)) {
|
|
// Output to the standard output
|
|
if(context.OutputWriters.HasFlag(TerminalWriters.StandardOutput)) {
|
|
Console.ForegroundColor = context.OutputColor;
|
|
Console.Out.Write(context.OutputText);
|
|
Console.ResetColor();
|
|
Console.ForegroundColor = context.OriginalColor;
|
|
}
|
|
|
|
// output to the standard error
|
|
if(context.OutputWriters.HasFlag(TerminalWriters.StandardError)) {
|
|
Console.ForegroundColor = context.OutputColor;
|
|
Console.Error.Write(context.OutputText);
|
|
Console.ResetColor();
|
|
Console.ForegroundColor = context.OriginalColor;
|
|
}
|
|
}
|
|
|
|
// Process Debugger output
|
|
if(IsDebuggerAttached && context.OutputWriters.HasFlag(TerminalWriters.Diagnostics)) {
|
|
System.Diagnostics.Debug.Write(new String(context.OutputText));
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Output Context
|
|
|
|
/// <summary>
|
|
/// Represents an asynchronous output context.
|
|
/// </summary>
|
|
private sealed class OutputContext {
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="OutputContext"/> class.
|
|
/// </summary>
|
|
public OutputContext() {
|
|
this.OriginalColor = Settings.DefaultColor;
|
|
this.OutputWriters = IsConsolePresent
|
|
? TerminalWriters.StandardOutput
|
|
: IsDebuggerAttached
|
|
? TerminalWriters.Diagnostics
|
|
: TerminalWriters.None;
|
|
}
|
|
|
|
public ConsoleColor OriginalColor {
|
|
get;
|
|
}
|
|
public ConsoleColor OutputColor {
|
|
get; set;
|
|
}
|
|
public Char[] OutputText {
|
|
get; set;
|
|
}
|
|
public TerminalWriters OutputWriters {
|
|
get; set;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|