namespace Unosquare.Swan { using Abstractions; using System; using System.Collections.Concurrent; using System.Text; using System.Threading; /// /// 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) { #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 /// /// 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 (Settings.OverrideIsConsolePresent) return true; if (_isConsolePresent == null) { _isConsolePresent = true; try { var windowHeight = Console.WindowHeight; _isConsolePresent = windowHeight >= 0; } catch { _isConsolePresent = false; } } return _isConsolePresent.Value; } } /// /// Gets a value indicating whether a debugger is attached. /// /// /// true if this instance is debugger attached; otherwise, false. /// public static bool IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached; /// /// Gets the available output writers in a bitwise mask. /// /// /// The available writers. /// public static TerminalWriters AvailableWriters { get { var writers = TerminalWriters.None; if (IsConsolePresent) writers = TerminalWriters.StandardError | TerminalWriters.StandardOutput; if (IsDebuggerAttached) writers = writers | TerminalWriters.Diagnostics; return writers; } } /// /// 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 == false) 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); /// /// 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) == 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 /// /// 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 : 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 } }