using Unosquare.Swan.Abstractions; using System; using System.Collections.Concurrent; using System.Text; using System.Threading; namespace Unosquare.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 Int32 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 Boolean? _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 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; } } } /// /// Gets or sets the cursor top position. /// /// /// The cursor top. /// 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 /// /// Gets a value indicating whether the Console is present. /// /// /// true if this instance is console present; otherwise, false. /// 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; } } /// /// Gets a value indicating whether a debugger is attached. /// /// /// true if this instance is debugger attached; otherwise, false. /// public static Boolean IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached; /// /// Gets the available output writers in a bitwise mask. /// /// /// The available writers. /// public static TerminalWriters AvailableWriters { get { TerminalWriters writers = TerminalWriters.None; if(IsConsolePresent) { writers = TerminalWriters.StandardError | TerminalWriters.StandardOutput; } if(IsDebuggerAttached) { 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; } 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; } } } /// /// Sets the cursor position. /// /// The left. /// The top. 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)); } } /// /// 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) { 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); } } /// /// 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 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 /// /// Represents an asynchronous output context. /// private sealed class OutputContext { /// /// Initializes a new instance of the class. /// 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 } }