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 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) { 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(_isConsolePresent == null) { _isConsolePresent = true; try { Int32 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; } 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) { 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) { 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)) { 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() { this.OriginalColor = Settings.DefaultColor; this.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 } }