// // ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations. // using System.Text; using System; using System.Diagnostics; using System.Linq; using Terminal.Gui.ConsoleDrivers; 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 => _cols; internal set { _cols = value; ClearContents (); } } /// /// The number of rows visible in the terminal. /// public virtual int Rows { get => _rows; internal set { _rows = value; ClearContents (); } } /// /// 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. 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) => 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; bool validLocation = IsValidLocation (Col, Row); if (validLocation) { rune = rune.MakePrintable (); runeWidth = rune.GetColumns (); if (runeWidth == 0 && rune.IsCombiningMark ()) { // AtlasEngine does not support NON-NORMALIZED combining marks in a way // compatible with the driver architecture. Any CMs (except in the first col) // are correctly combined with the base char, but are ALSO treated as 1 column // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. // // Until this is addressed (see Issue #), we do our best by // a) Attempting to normalize any CM with the base char to it's left // b) Ignoring any CMs that don't normalize 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); // Ignore. 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]; // Ignore. 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); // Ignore. 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 (int 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 (int row = 0; row < Rows; row++) { for (int 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 => 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; int _cols; int _rows; /// /// 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. new ( -1, // only used by cursesdriver! foreground, background ); #endregion #region Mouse and Keyboard /// /// Event fired when a key is pressed down. This is a precursor to . /// public event EventHandler KeyDown; /// /// Called when a key is pressed down. Fires the event. This is a precursor to . /// /// public void OnKeyDown (Key a) => KeyDown?.Invoke (this, a); /// /// Event fired when a key is released. /// /// /// Drivers that do not support key release events will fire this event after processing is complete. /// public event EventHandler KeyUp; /// /// Called when a key is released. Fires the event. /// /// /// Drivers that do not support key release events will calls this method after processing is complete. /// /// public void OnKeyUp (Key a) => KeyUp?.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; } /// /// Gets the dimensions of the terminal. /// public Rect Bounds => new Rect (0, 0, Cols, Rows); /// /// 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 (int r = rect.Y; r < rect.Y + rect.Height; r++) { for (int 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 } /// /// The enumeration encodes key information from s and provides a consistent /// way for application code to specify keys and receive key events. /// /// The class provides a higher-level abstraction, with helper methods and properties for common /// operations. For example, and provide a convenient way /// to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed. /// /// /// /// /// Lowercase alpha keys are encoded as values between 65 and 90 corresponding to the un-shifted A to Z keys on a keyboard. Enum values /// are provided for these (e.g. , , etc.). Even though the values are the same as the ASCII /// values for uppercase characters, these enum values represent *lowercase*, un-shifted characters. /// /// /// Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. , , etc.). /// /// /// The shift modifiers (, , and ) can be combined (with logical or) /// with the other key codes to represent shifted keys. For example, the enum value represents the un-shifted 'a' key, while /// | represents the 'A' key (shifted 'a' key). Likewise, | /// represents the 'Alt+A' key combination. /// /// /// All other keys that produce a printable character are encoded as the Unicode value of the character. For example, the /// for the '!' character is 33, which is the Unicode value for '!'. Likewise, `â` is 226, `Â` is 194, etc. /// /// /// If the is set, then the value is that of the special mask, /// otherwise, the value is the one of the lower bits (as extracted by ). /// /// [Flags] public enum KeyCode : uint { /// /// Mask that indicates that the key is a unicode codepoint. Values outside this range /// indicate the key has shift modifiers or is a special key like function keys, arrows keys and so on. /// CharMask = 0x_f_ffff, /// /// If the is set, then the value is that of the special mask, /// otherwise, the value is in the the lower bits (as extracted by ). /// SpecialMask = 0x_fff0_0000, /// /// When this value is set, the Key encodes the sequence Shift-KeyValue. /// The actual value must be extracted by removing the ShiftMask. /// ShiftMask = 0x_1000_0000, /// /// When this value is set, the Key encodes the sequence Alt-KeyValue. /// The actual value must be extracted by removing the AltMask. /// AltMask = 0x_8000_0000, /// /// When this value is set, the Key encodes the sequence Ctrl-KeyValue. /// The actual value must be extracted by removing the CtrlMask. /// CtrlMask = 0x_4000_0000, /// /// The key code representing an invalid or empty key. /// Null = 0, /// /// Backspace key. /// Backspace = 8, /// /// The key code for the tab key (forwards tab key). /// Tab = 9, /// /// The key code for the return key. /// Enter = ConsoleKey.Enter, /// /// The key code for the clear key. /// Clear = 12, /// /// The key code for the escape key. /// Esc = 27, /// /// The key code for the space bar key. /// Space = 32, /// /// Digit 0. /// D0 = 48, /// /// Digit 1. /// D1, /// /// Digit 2. /// D2, /// /// Digit 3. /// D3, /// /// Digit 4. /// D4, /// /// Digit 5. /// D5, /// /// Digit 6. /// D6, /// /// Digit 7. /// D7, /// /// Digit 8. /// D8, /// /// Digit 9. /// D9, /// /// The key code for the A key /// A = 65, /// /// The key code for the B key /// B, /// /// The key code for the C key /// C, /// /// The key code for the D key /// D, /// /// The key code for the E key /// E, /// /// The key code for the F key /// F, /// /// The key code for the G key /// G, /// /// The key code for the H key /// H, /// /// The key code for the I key /// I, /// /// The key code for the J key /// J, /// /// The key code for the K key /// K, /// /// The key code for the L key /// L, /// /// The key code for the M key /// M, /// /// The key code for the N key /// N, /// /// The key code for the O key /// O, /// /// The key code for the P key /// P, /// /// The key code for the Q key /// Q, /// /// The key code for the R key /// R, /// /// The key code for the S key /// S, /// /// The key code for the T key /// T, /// /// The key code for the U key /// U, /// /// The key code for the V key /// V, /// /// The key code for the W key /// W, /// /// The key code for the X key /// X, /// /// The key code for the Y key /// Y, /// /// The key code for the Z key /// Z, ///// ///// The key code for the Delete key. ///// //Delete = 127, // --- Special keys --- // The values below are common non-alphanum keys. Their values are // based on the .NET ConsoleKey values, which, in-turn are based on the // VK_ values from the Windows API. // We add MaxCodePoint to avoid conflicts with the Unicode values. /// /// The maximum Unicode codepoint value. Used to encode the non-alphanumeric control /// keys. /// MaxCodePoint = 0x10FFFF, /// /// Cursor up key /// CursorUp = MaxCodePoint + ConsoleKey.UpArrow, /// /// Cursor down key. /// CursorDown = MaxCodePoint + ConsoleKey.DownArrow, /// /// Cursor left key. /// CursorLeft = MaxCodePoint + ConsoleKey.LeftArrow, /// /// Cursor right key. /// CursorRight = MaxCodePoint + ConsoleKey.RightArrow, /// /// Page Up key. /// PageUp = MaxCodePoint + ConsoleKey.PageUp, /// /// Page Down key. /// PageDown = MaxCodePoint + ConsoleKey.PageDown, /// /// Home key. /// Home = MaxCodePoint + ConsoleKey.Home, /// /// End key. /// End = MaxCodePoint + ConsoleKey.End, /// /// Insert (INS) key. /// Insert = MaxCodePoint + ConsoleKey.Insert, /// /// Delete (DEL) key. /// Delete = MaxCodePoint + ConsoleKey.Delete, /// /// Print screen character key. /// PrintScreen = MaxCodePoint + ConsoleKey.PrintScreen, /// /// F1 key. /// F1 = MaxCodePoint + ConsoleKey.F1, /// /// F2 key. /// F2 = MaxCodePoint + ConsoleKey.F2, /// /// F3 key. /// F3 = MaxCodePoint + ConsoleKey.F3, /// /// F4 key. /// F4 = MaxCodePoint + ConsoleKey.F4, /// /// F5 key. /// F5 = MaxCodePoint + ConsoleKey.F5, /// /// F6 key. /// F6 = MaxCodePoint + ConsoleKey.F6, /// /// F7 key. /// F7 = MaxCodePoint + ConsoleKey.F7, /// /// F8 key. /// F8 = MaxCodePoint + ConsoleKey.F8, /// /// F9 key. /// F9 = MaxCodePoint + ConsoleKey.F9, /// /// F10 key. /// F10 = MaxCodePoint + ConsoleKey.F10, /// /// F11 key. /// F11 = MaxCodePoint + ConsoleKey.F11, /// /// F12 key. /// F12 = MaxCodePoint + ConsoleKey.F12, /// /// F13 key. /// F13 = MaxCodePoint + ConsoleKey.F13, /// /// F14 key. /// F14 = MaxCodePoint + ConsoleKey.F14, /// /// F15 key. /// F15 = MaxCodePoint + ConsoleKey.F15, /// /// F16 key. /// F16 = MaxCodePoint + ConsoleKey.F16, /// /// F17 key. /// F17 = MaxCodePoint + ConsoleKey.F17, /// /// F18 key. /// F18 = MaxCodePoint + ConsoleKey.F18, /// /// F19 key. /// F19 = MaxCodePoint + ConsoleKey.F19, /// /// F20 key. /// F20 = MaxCodePoint + ConsoleKey.F20, /// /// F21 key. /// F21 = MaxCodePoint + ConsoleKey.F21, /// /// F22 key. /// F22 = MaxCodePoint + ConsoleKey.F22, /// /// F23 key. /// F23 = MaxCodePoint + ConsoleKey.F23, /// /// F24 key. /// F24 = MaxCodePoint + ConsoleKey.F24, }