// // ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations. // using System.Text; using System; using System.Collections.Generic; using System.Diagnostics; using static Terminal.Gui.ColorScheme; using System.Linq; using System.Data; namespace Terminal.Gui; /// /// Base class for Terminal.Gui ConsoleDriver implementations. /// /// /// There are currently four implementations: /// - (for Unix and Mac) /// - /// - that uses the .NET Console API /// - for unit testing. /// public abstract class ConsoleDriver { /// /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver. /// /// public ColorTests () /// { /// ConsoleDriver.RunningUnitTests = true; /// } /// /// internal static bool RunningUnitTests { get; set; } #region Setup & Teardown /// /// Initializes the driver /// /// Returns an instance of using the for the driver. internal abstract MainLoop Init (); /// /// Ends the execution of the console driver. /// internal abstract void End (); #endregion /// /// The event fired when the terminal is resized. /// public event EventHandler SizeChanged; /// /// Called when the terminal size changes. Fires the event. /// /// public void OnSizeChanged (SizeChangedEventArgs args) => SizeChanged?.Invoke (this, args); /// /// The number of columns visible in the terminal. /// public virtual int Cols { get; internal set; } /// /// The number of rows visible in the terminal. /// public virtual int Rows { get; internal set; } /// /// The leftmost column in the terminal. /// public virtual int Left { get; internal set; } = 0; /// /// The topmost row in the terminal. /// public virtual int Top { get; internal set; } = 0; /// /// Get the operating system clipboard. /// public IClipboard Clipboard { get; internal set; } /// /// The contents of the application output. The driver outputs this buffer to the terminal when /// is called. /// /// The format of the array is rows, columns, and 3 values on the last column: Rune, Attribute and Dirty Flag /// /// //public int [,,] Contents { get; internal set; } ///// ///// The contents of the application output. The driver outputs this buffer to the terminal when ///// is called. ///// ///// The format of the array is rows, columns. The first index is the row, the second index is the column. ///// ///// public Cell [,] Contents { get; internal set; } /// /// Gets the column last set by . and /// are used by and to determine where to add content. /// public int Col { get; internal set; } /// /// Gets the row last set by . and /// are used by and to determine where to add content. /// public int Row { get; internal set; } /// /// Updates and to the specified column and row in . /// Used by and to determine where to add content. /// /// /// /// This does not move the cursor on the screen, it only updates the internal state of the driver. /// /// /// If or are negative or beyond and , /// the method still sets those properties. /// /// /// Column to move to. /// Row to move to. public virtual void Move (int col, int row) { Col = col; Row = row; } /// /// Tests if the specified rune is supported by the driver. /// /// /// if the rune can be properly presented; if the driver /// does not support displaying this rune. public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); } /// /// Adds the specified rune to the display at the current cursor position. /// /// /// /// When the method returns, will be incremented by the number of columns required, /// even if the new column value is outside of the or screen dimensions defined by . /// /// /// If requires more than one column, and plus the number of columns needed /// exceeds the or screen dimensions, the default Unicode replacement character (U+FFFD) will be added instead. /// /// /// Rune to add. public void AddRune (Rune rune) { int runeWidth = -1; var validLocation = IsValidLocation (Col, Row); if (validLocation) { rune = rune.MakePrintable (); runeWidth = rune.GetColumns (); if (runeWidth == 0 && rune.IsCombiningMark ()) { if (Col > 0) { if (Contents [Row, Col - 1].CombiningMarks.Count > 0) { // Just add this mark to the list Contents [Row, Col - 1].CombiningMarks.Add (rune); // Don't move to next column (let the driver figure out what to do). } else { // Attempt to normalize the cell to our left combined with this mark string combined = Contents [Row, Col - 1].Rune + rune.ToString (); // Normalize to Form C (Canonical Composition) string normalized = combined.Normalize (NormalizationForm.FormC); if (normalized.Length == 1) { // It normalized! We can just set the Cell to the left with the // normalized codepoint Contents [Row, Col - 1].Rune = (Rune)normalized [0]; // Don't move to next column because we're already there } else { // It didn't normalize. Add it to the Cell to left's CM list Contents [Row, Col - 1].CombiningMarks.Add (rune); // Don't move to next column (let the driver figure out what to do). } } Contents [Row, Col - 1].Attribute = CurrentAttribute; Contents [Row, Col - 1].IsDirty = true; } else { // Most drivers will render a combining mark at col 0 as the mark Contents [Row, Col].Rune = rune; Contents [Row, Col].Attribute = CurrentAttribute; Contents [Row, Col].IsDirty = true; Col++; } } else { Contents [Row, Col].Attribute = CurrentAttribute; Contents [Row, Col].IsDirty = true; if (Col > 0) { // Check if cell to left has a wide glyph if (Contents [Row, Col - 1].Rune.GetColumns () > 1) { // Invalidate cell to left Contents [Row, Col - 1].Rune = Rune.ReplacementChar; Contents [Row, Col - 1].IsDirty = true; } } if (runeWidth < 1) { Contents [Row, Col].Rune = Rune.ReplacementChar; } else if (runeWidth == 1) { Contents [Row, Col].Rune = rune; if (Col < Clip.Right - 1) { Contents [Row, Col + 1].IsDirty = true; } } else if (runeWidth == 2) { if (Col == Clip.Right - 1) { // We're at the right edge of the clip, so we can't display a wide character. // TODO: Figure out if it is better to show a replacement character or ' ' Contents [Row, Col].Rune = Rune.ReplacementChar; } else { Contents [Row, Col].Rune = rune; if (Col < Clip.Right - 1) { // Invalidate cell to right so that it doesn't get drawn // TODO: Figure out if it is better to show a replacement character or ' ' Contents [Row, Col + 1].Rune = Rune.ReplacementChar; Contents [Row, Col + 1].IsDirty = true; } } } else { // This is a non-spacing character, so we don't need to do anything Contents [Row, Col].Rune = (Rune)' '; Contents [Row, Col].IsDirty = false; } _dirtyLines [Row] = true; } } if (runeWidth is < 0 or > 0) { Col++; } if (runeWidth > 1) { Debug.Assert (runeWidth <= 2); if (validLocation && Col < Clip.Right) { // This is a double-width character, and we are not at the end of the line. // Col now points to the second column of the character. Ensure it doesn't // Get rendered. Contents [Row, Col].IsDirty = false; Contents [Row, Col].Attribute = CurrentAttribute; // TODO: Determine if we should wipe this out (for now now) //Contents [Row, Col].Rune = (Rune)' '; } Col++; } } /// /// Adds the specified to the display at the current cursor position. This method /// is a convenience method that calls with the constructor. /// /// Character to add. public void AddRune (char c) => AddRune (new Rune (c)); /// /// Adds the to the display at the cursor position. /// /// /// /// When the method returns, will be incremented by the number of columns required, /// unless the new column value is outside of the or screen dimensions defined by . /// /// /// If requires more columns than are available, the output will be clipped. /// /// /// String. public void AddStr (string str) { var runes = str.EnumerateRunes ().ToList (); for (var i = 0; i < runes.Count; i++) { //if (runes [i].IsCombiningMark()) { // // Attempt to normalize // string combined = runes [i-1] + runes [i].ToString(); // // Normalize to Form C (Canonical Composition) // string normalized = combined.Normalize (NormalizationForm.FormC); // runes [i-] //} AddRune (runes [i]); } } Rect _clip; /// /// Tests whether the specified coordinate are valid for drawing. /// /// The column. /// The row. /// if the coordinate is outside of the /// screen bounds or outside of . otherwise. public bool IsValidLocation (int col, int row) => col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row); /// /// Gets or sets the clip rectangle that and are /// subject to. /// /// The rectangle describing the bounds of . public Rect Clip { get => _clip; set => _clip = value; } /// /// Updates the screen to reflect all the changes that have been done to the display buffer /// public abstract void Refresh (); /// /// Sets the position of the terminal cursor to and . /// public abstract void UpdateCursor (); /// /// Gets the terminal cursor visibility. /// /// The current /// upon success public abstract bool GetCursorVisibility (out CursorVisibility visibility); /// /// Sets the terminal cursor visibility. /// /// The wished /// upon success public abstract bool SetCursorVisibility (CursorVisibility visibility); /// /// Determines if the terminal cursor should be visible or not and sets it accordingly. /// /// upon success public abstract bool EnsureCursorVisibility (); // As performance is a concern, we keep track of the dirty lines and only refresh those. // This is in addition to the dirty flag on each cell. internal bool [] _dirtyLines; /// /// Clears the of the driver. /// public void ClearContents () { // TODO: This method is really "Clear Contents" now and should not be abstract (or virtual) Contents = new Cell [Rows, Cols]; Clip = new Rect (0, 0, Cols, Rows); _dirtyLines = new bool [Rows]; lock (Contents) { // Can raise an exception while is still resizing. try { for (var row = 0; row < Rows; row++) { for (var c = 0; c < Cols; c++) { Contents [row, c] = new Cell () { Rune = (Rune)' ', Attribute = new Attribute (Color.White, Color.Black), IsDirty = true }; _dirtyLines [row] = true; } } } catch (IndexOutOfRangeException) { } } } /// /// Redraws the physical screen with the contents that have been queued up via any of the printing commands. /// public abstract void UpdateScreen (); #region Color Handling /// /// Gets whether the supports TrueColor output. /// public virtual bool SupportsTrueColor { get => true; } /// /// Gets or sets whether the should use 16 colors instead of the default TrueColors. See /// to change this setting via . /// /// /// /// Will be forced to if is , indicating /// that the cannot support TrueColor. /// /// internal virtual bool Force16Colors { get => Application.Force16Colors || !SupportsTrueColor; set => Application.Force16Colors = (value || !SupportsTrueColor); } Attribute _currentAttribute; /// /// The that will be used for the next or call. /// public Attribute CurrentAttribute { get => _currentAttribute; set { if (Application.Driver != null) { _currentAttribute = new Attribute (value.Foreground, value.Background); return; } _currentAttribute = value; } } /// /// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString. /// /// /// Implementations should call base.SetAttribute(c). /// /// C. public Attribute SetAttribute (Attribute c) { var prevAttribute = CurrentAttribute; CurrentAttribute = c; return prevAttribute; } /// /// Gets the current . /// /// The current attribute. public Attribute GetAttribute () => CurrentAttribute; // TODO: This is only overridden by CursesDriver. Once CursesDriver supports 24-bit color, this virtual method can be // removed (and Attribute can lose the platformColor property). /// /// Makes an . /// /// The foreground color. /// The background color. /// The attribute for the foreground and background colors. public virtual Attribute MakeColor (Color foreground, Color background) { // Encode the colors into the int value. return new Attribute ( platformColor: 0, // only used by cursesdriver! foreground: foreground, background: background ); } #endregion #region Mouse and Keyboard /// /// Event fired after a key has been pressed and released. /// public event EventHandler KeyPressed; /// /// Called after a key has been pressed and released. Fires the event. /// /// public void OnKeyPressed (KeyEventEventArgs a) => KeyPressed?.Invoke (this, a); /// /// Event fired when a key is released. /// public event EventHandler KeyUp; /// /// Called when a key is released. Fires the event. /// /// public void OnKeyUp (KeyEventEventArgs a) => KeyUp?.Invoke (this, a); /// /// Event fired when a key is pressed. /// public event EventHandler KeyDown; /// /// Called when a key is pressed. Fires the event. /// /// public void OnKeyDown (KeyEventEventArgs a) => KeyDown?.Invoke (this, a); /// /// Event fired when a mouse event occurs. /// public event EventHandler MouseEvent; /// /// Called when a mouse event occurs. Fires the event. /// /// public void OnMouseEvent (MouseEventEventArgs a) => MouseEvent?.Invoke (this, a); /// /// Simulates a key press. /// /// The key character. /// The key. /// If simulates the Shift key being pressed. /// If simulates the Alt key being pressed. /// If simulates the Ctrl key being pressed. public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); #endregion /// /// Enables diagnostic functions /// [Flags] public enum DiagnosticFlags : uint { /// /// All diagnostics off /// Off = 0b_0000_0000, /// /// When enabled, will draw a /// ruler in the frame for any side with a padding value greater than 0. /// FrameRuler = 0b_0000_0001, /// /// When enabled, will draw a /// 'L', 'R', 'T', and 'B' when clearing 's instead of ' '. /// FramePadding = 0b_0000_0010, } /// /// Set flags to enable/disable diagnostics. /// public static DiagnosticFlags Diagnostics { get; set; } /// /// Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver. /// /// This is only implemented in . public abstract void Suspend (); // TODO: Move FillRect to ./Drawing /// /// Fills the specified rectangle with the specified rune. /// /// /// public void FillRect (Rect rect, Rune rune = default) { for (var r = rect.Y; r < rect.Y + rect.Height; r++) { for (var c = rect.X; c < rect.X + rect.Width; c++) { Application.Driver.Move (c, r); Application.Driver.AddRune (rune == default ? new Rune (' ') : rune); } } } /// /// Fills the specified rectangle with the specified . This method /// is a convenience method that calls . /// /// /// public void FillRect (Rect rect, char c) => FillRect (rect, new Rune (c)); /// /// Returns the name of the driver and relevant library version information. /// /// public virtual string GetVersionInfo () => GetType ().Name; } /// /// Terminal Cursor Visibility settings. /// /// /// Hex value are set as 0xAABBCCDD where : /// /// AA stand for the TERMINFO DECSUSR parameter value to be used under Linux and MacOS /// BB stand for the NCurses curs_set parameter value to be used under Linux and MacOS /// CC stand for the CONSOLE_CURSOR_INFO.bVisible parameter value to be used under Windows /// DD stand for the CONSOLE_CURSOR_INFO.dwSize parameter value to be used under Windows /// public enum CursorVisibility { /// /// Cursor caret has default /// /// Works under Xterm-like terminal otherwise this is equivalent to . This default directly depends of the XTerm user configuration settings so it could be Block, I-Beam, Underline with possible blinking. Default = 0x00010119, /// /// Cursor caret is hidden /// Invisible = 0x03000019, /// /// Cursor caret is normally shown as a blinking underline bar _ /// Underline = 0x03010119, /// /// Cursor caret is normally shown as a underline bar _ /// /// Under Windows, this is equivalent to UnderlineFix = 0x04010119, /// /// Cursor caret is displayed a blinking vertical bar | /// /// Works under Xterm-like terminal otherwise this is equivalent to Vertical = 0x05010119, /// /// Cursor caret is displayed a blinking vertical bar | /// /// Works under Xterm-like terminal otherwise this is equivalent to VerticalFix = 0x06010119, /// /// Cursor caret is displayed as a blinking block ▉ /// Box = 0x01020164, /// /// Cursor caret is displayed a block ▉ /// /// Works under Xterm-like terminal otherwise this is equivalent to BoxFix = 0x02020164, }