// // ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations. // using System.Diagnostics; 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 { // 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; // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application? /// Gets the location and size of the terminal screen. internal Rectangle Screen => new (0, 0, Cols, Rows); private Rectangle _clip; /// /// Gets or sets the clip rectangle that and are subject /// to. /// /// The rectangle describing the of region. public Rectangle Clip { get => _clip; set { if (_clip == value) { return; } // Don't ever let Clip be bigger than Screen _clip = Rectangle.Intersect (Screen, value); } } /// Get the operating system clipboard. public IClipboard Clipboard { 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; } /// The number of columns visible in the terminal. public virtual int Cols { get => _cols; internal set { _cols = value; ClearContents (); } } /// /// 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; } /// The leftmost column in the terminal. public virtual int Left { get; internal set; } = 0; /// /// Gets the row last set by . and are used by /// and to determine where to add content. /// public int Row { get; internal set; } /// The number of rows visible in the terminal. public virtual int Rows { get => _rows; internal set { _rows = value; ClearContents (); } } /// The topmost row in the terminal. public virtual int Top { get; internal set; } = 0; /// /// 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; } /// 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) { List runes = str.EnumerateRunes ().ToList (); for (var i = 0; i < runes.Count; i++) { AddRune (runes [i]); } } /// Clears the of the driver. public void ClearContents () { Contents = new Cell [Rows, Cols]; //CONCURRENCY: Unsynchronized access to Clip isn't safe. // TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere. Clip = Screen; _dirtyLines = new bool [Rows]; lock (Contents) { 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; } } } /// /// Sets as dirty for situations where views /// don't need layout and redrawing, but just refresh the screen. /// public void SetContentsAsDirty () { lock (Contents) { for (var row = 0; row < Rows; row++) { for (var c = 0; c < Cols; c++) { Contents [row, c].IsDirty = true; } _dirtyLines [row] = true; } } } /// Determines if the terminal cursor should be visible or not and sets it accordingly. /// upon success public abstract bool EnsureCursorVisibility (); /// Fills the specified rectangle with the specified rune, using /// /// The value of is honored. Any parts of the rectangle not in the clip will not be drawn. /// /// The Screen-relative rectangle. /// The Rune used to fill the rectangle public void FillRect (Rectangle rect, Rune rune = default) { rect = Rectangle.Intersect (rect, Clip); lock (Contents) { for (int r = rect.Y; r < rect.Y + rect.Height; r++) { for (int c = rect.X; c < rect.X + rect.Width; c++) { Contents [r, c] = new Cell { Rune = (rune != default ? rune : (Rune)' '), Attribute = CurrentAttribute, IsDirty = true }; _dirtyLines [r] = true; } } } } /// /// Fills the specified rectangle with the specified . This method is a convenience method /// that calls . /// /// /// public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); } /// Gets the terminal cursor visibility. /// The current /// upon success public abstract bool GetCursorVisibility (out CursorVisibility visibility); /// Returns the name of the driver and relevant library version information. /// public virtual string GetVersionInfo () { return GetType ().Name; } /// 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); } /// Tests whether the specified coordinate are valid for drawing. /// The column. /// The row. /// /// if the coordinate is outside the screen bounds or outside of . /// otherwise. /// public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row); } /// /// 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) { //Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0)); Col = col; Row = row; } /// Called when the terminal size changes. Fires the event. /// public void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); } /// Updates the screen to reflect all the changes that have been done to the display buffer public abstract void Refresh (); /// Sets the terminal cursor visibility. /// The wished /// upon success public abstract bool SetCursorVisibility (CursorVisibility visibility); /// The event fired when the terminal is resized. public event EventHandler SizeChanged; /// 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 (); /// Sets the position of the terminal cursor to and . public abstract void UpdateCursor (); /// Redraws the physical screen with the contents that have been queued up via any of the printing commands. public abstract void UpdateScreen (); #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 #region Color Handling /// Gets whether the supports TrueColor output. public virtual bool SupportsTrueColor => true; // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. /// /// 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; } private Attribute _currentAttribute; private int _cols; private int _rows; /// /// The that will be used for the next or /// call. /// public Attribute CurrentAttribute { get => _currentAttribute; set { // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed. if (Application.Driver is { }) { _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) { Attribute prevAttribute = CurrentAttribute; CurrentAttribute = c; return prevAttribute; } /// Gets the current . /// The current attribute. public Attribute GetAttribute () { return 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 (in Color foreground, in Color background) { // Encode the colors into the int value. return new Attribute ( -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 (MouseEvent 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 } /// 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 }