using System.ComponentModel; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; internal partial class WindowsOutput : OutputBase, IOutput { [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [return: MarshalAs (UnmanagedType.Bool)] private static partial bool WriteConsole ( nint hConsoleOutput, ReadOnlySpan lpBuffer, uint numberOfCharsToWrite, out uint lpNumberOfCharsWritten, nint lpReserved ); [LibraryImport ("kernel32.dll", SetLastError = true)] private static partial nint GetStdHandle (int nStdHandle); [LibraryImport ("kernel32.dll", SetLastError = true)] [return: MarshalAs (UnmanagedType.Bool)] private static partial bool CloseHandle (nint handle); [LibraryImport ("kernel32.dll", SetLastError = true)] private static partial nint CreateConsoleScreenBuffer ( DesiredAccess dwDesiredAccess, ShareMode dwShareMode, nint securityAttributes, uint flags, nint screenBufferData ); [DllImport ("kernel32.dll", SetLastError = true)] [return: MarshalAs (UnmanagedType.Bool)] private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX csbi); [Flags] private enum ShareMode : uint { FileShareRead = 1, FileShareWrite = 2 } [Flags] private enum DesiredAccess : uint { GenericRead = 2147483648, GenericWrite = 1073741824 } internal static nint INVALID_HANDLE_VALUE = new (-1); [LibraryImport ("kernel32.dll", SetLastError = true)] [return: MarshalAs (UnmanagedType.Bool)] private static partial bool SetConsoleActiveScreenBuffer (nint handle); [LibraryImport ("kernel32.dll")] [return: MarshalAs (UnmanagedType.Bool)] private static partial bool SetConsoleCursorPosition (nint hConsoleOutput, WindowsConsole.Coord dwCursorPosition); [DllImport ("kernel32.dll", SetLastError = true)] [return: MarshalAs (UnmanagedType.Bool)] private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref WindowsConsole.ConsoleCursorInfo lpConsoleCursorInfo); [LibraryImport ("kernel32.dll", SetLastError = true)] [return: MarshalAs (UnmanagedType.Bool)] public static partial bool SetConsoleTextAttribute (nint hConsoleOutput, ushort wAttributes); [LibraryImport ("kernel32.dll")] [return: MarshalAs (UnmanagedType.Bool)] private static partial bool GetConsoleMode (nint hConsoleHandle, out uint lpMode); [LibraryImport ("kernel32.dll")] [return: MarshalAs (UnmanagedType.Bool)] private static partial bool SetConsoleMode (nint hConsoleHandle, uint dwMode); [LibraryImport ("kernel32.dll", SetLastError = true)] private static partial WindowsConsole.Coord GetLargestConsoleWindowSize ( nint hConsoleOutput ); [DllImport ("kernel32.dll", SetLastError = true)] [return: MarshalAs (UnmanagedType.Bool)] private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX consoleScreenBufferInfo); [DllImport ("kernel32.dll", SetLastError = true)] [return: MarshalAs (UnmanagedType.Bool)] private static extern bool SetConsoleWindowInfo ( nint hConsoleOutput, bool bAbsolute, [In] ref WindowsConsole.SmallRect lpConsoleWindow ); private const int STD_OUTPUT_HANDLE = -11; private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; private readonly nint _outputHandle; private nint _screenBuffer; private readonly bool _isVirtualTerminal; private readonly ConsoleColor _foreground; private readonly ConsoleColor _background; public WindowsOutput () { Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}"); if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { return; } // Get the standard output handle which is the current screen buffer. _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); GetConsoleMode (_outputHandle, out uint mode); _isVirtualTerminal = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; if (_isVirtualTerminal) { if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null) { //Enable alternative screen buffer. Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); } else { _foreground = Console.ForegroundColor; _background = Console.BackgroundColor; } } else { CreateScreenBuffer (); if (!GetConsoleMode (_screenBuffer, out mode)) { throw new ApplicationException ($"Failed to get screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}."); } const uint ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002; mode &= ~ENABLE_WRAP_AT_EOL_OUTPUT; // Disable wrap if (!SetConsoleMode (_screenBuffer, mode)) { throw new ApplicationException ($"Failed to set screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}."); } // Force 16 colors if not in virtual terminal mode. // BUGBUG: This is bad. It does not work if the app was crated without // BUGBUG: Apis. //ApplicationImpl.Instance.Force16Colors = true; } GetSize (); } private void CreateScreenBuffer () { _screenBuffer = CreateConsoleScreenBuffer ( DesiredAccess.GenericRead | DesiredAccess.GenericWrite, ShareMode.FileShareRead | ShareMode.FileShareWrite, nint.Zero, 1, nint.Zero ); if (_screenBuffer == INVALID_HANDLE_VALUE) { int err = Marshal.GetLastWin32Error (); if (err != 0) { throw new Win32Exception (err); } } if (!SetConsoleActiveScreenBuffer (_screenBuffer)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } } public void Write (ReadOnlySpan str) { if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { return; } if (!WriteConsole (_isVirtualTerminal ? _outputHandle : _screenBuffer, str, (uint)str.Length, out uint _, nint.Zero)) { throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to write to console screen buffer."); } } public Size ResizeBuffer (Size size) { Size newSize = size; try { newSize = SetConsoleWindow ((short)Math.Max (size.Width, 0), (short)Math.Max (size.Height, 0)); } catch { // Do nothing; unit tests } return newSize; } internal Size SetConsoleWindow (short cols, short rows) { if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { return new (cols, rows); } var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); csbi.cbSize = (uint)Marshal.SizeOf (csbi); if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } WindowsConsole.Coord maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); short newCols = Math.Min (cols, maxWinSize.X); short newRows = Math.Min (rows, maxWinSize.Y); csbi.dwSize = new (newCols, Math.Max (newRows, (short)1)); csbi.srWindow = new (0, 0, newCols, newRows); csbi.dwMaximumWindowSize = new (newCols, newRows); if (!SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } var winRect = new WindowsConsole.SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0)); if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect)) { //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); return new (cols, rows); } SetConsoleOutputWindow (csbi); return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1); } private void SetConsoleOutputWindow (WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX csbi) { if ((_isVirtualTerminal ? _outputHandle : _screenBuffer) != nint.Zero && !SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } } public override void Write (IOutputBuffer outputBuffer) { // BUGBUG: This is bad. It does not work if the app was crated without // BUGBUG: Apis. //_force16Colors = ApplicationImpl.Instance.Driver!.Force16Colors; _force16Colors = false; _everythingStringBuilder.Clear (); // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. _consoleBuffer = 0; if (_force16Colors) { if (_isVirtualTerminal) { _consoleBuffer = _outputHandle; } else { _consoleBuffer = _screenBuffer; } } else { _consoleBuffer = _outputHandle; } base.Write (outputBuffer); try { if (_force16Colors && !_isVirtualTerminal) { SetConsoleActiveScreenBuffer (_consoleBuffer); } else { ReadOnlySpan span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); if (!result) { int err = Marshal.GetLastWin32Error (); if (err == 1) { Logging.Logger.LogError ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}"); return; } if (err != 0) { throw new Win32Exception (err); } } } } catch (Exception e) { Logging.Logger.LogError ($"Error: {e.Message} in {nameof (WindowsOutput)}"); if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { throw; } } } /// protected override void Write (StringBuilder output) { if (output.Length == 0) { return; } var str = output.ToString (); if (_force16Colors && !_isVirtualTerminal) { char [] a = str.ToCharArray (); WriteConsole (_screenBuffer, a, (uint)a.Length, out _, nint.Zero); } else { _everythingStringBuilder.Append (str); } } /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { // BUGBUG: This is bad. It does not work if the app was crated without // BUGBUG: Apis. // bool force16Colors = ApplicationImpl.Instance.Force16Colors; bool force16Colors = false; if (force16Colors) { if (_isVirtualTerminal) { output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); } else { var as16ColorInt = (ushort)((int)attr.Foreground.GetClosestNamedColor16 () | ((int)attr.Background.GetClosestNamedColor16 () << 4)); SetConsoleTextAttribute (_screenBuffer, as16ColorInt); } } else { EscSeqUtils.CSI_AppendForegroundColorRGB (output, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B); EscSeqUtils.CSI_AppendBackgroundColorRGB (output, attr.Background.R, attr.Background.G, attr.Background.B); EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); } } private Size? _lastSize; private Size? _lastWindowSizeBeforeMaximized; private bool _lockResize; public Size GetSize () { if (_lockResize) { return _lastSize!.Value; } Size newSize = GetWindowSize (out _); Size largestWindowSize = GetLargestConsoleWindowSize (); if (_lastWindowSizeBeforeMaximized is null && newSize == largestWindowSize) { _lastWindowSizeBeforeMaximized = _lastSize; } else if (_lastWindowSizeBeforeMaximized is { } && newSize != largestWindowSize) { if (newSize != _lastWindowSizeBeforeMaximized) { newSize = _lastWindowSizeBeforeMaximized.Value; } _lastWindowSizeBeforeMaximized = null; } if (_lastSize == null || _lastSize != newSize) { // User is resizing the screen, they can only ever resize the active // buffer since. We now however have issue because background offscreen // buffer will be wrong size, recreate it to ensure it doesn't result in // differing active and back buffer sizes (which causes flickering of window size) Size? bufSize = null; while (bufSize != newSize) { _lockResize = true; bufSize = ResizeBuffer (newSize); } _lockResize = false; _lastSize = newSize; } return newSize; } public Size GetWindowSize (out WindowsConsole.Coord cursorPosition) { try { var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); csbi.cbSize = (uint)Marshal.SizeOf (csbi); if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); cursorPosition = default (WindowsConsole.Coord); return Size.Empty; } Size sz = new ( csbi.srWindow.Right - csbi.srWindow.Left + 1, csbi.srWindow.Bottom - csbi.srWindow.Top + 1); cursorPosition = csbi.dwCursorPosition; return sz; } catch { cursorPosition = default (WindowsConsole.Coord); } return new (80, 25); } private Size GetLargestConsoleWindowSize () { WindowsConsole.Coord maxWinSize; try { maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); } catch { maxWinSize = new (80, 25); } return new (maxWinSize.X, maxWinSize.Y); } /// protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY) { if (_force16Colors && !_isVirtualTerminal) { SetConsoleCursorPosition (_screenBuffer, new ((short)screenPositionX, (short)screenPositionY)); } else { // CSI codes are 1 indexed _everythingStringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); EscSeqUtils.CSI_AppendCursorPosition (_everythingStringBuilder, screenPositionY + 1, screenPositionX + 1); } _lastCursorPosition = new (screenPositionX, screenPositionY); return true; } /// public override void SetCursorVisibility (CursorVisibility visibility) { if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { return; } if (!_isVirtualTerminal) { var info = new WindowsConsole.ConsoleCursorInfo { dwSize = (uint)visibility & 0x00FF, bVisible = ((uint)visibility & 0xFF00) != 0 }; SetConsoleCursorInfo (_screenBuffer, ref info); } else { string cursorVisibilitySequence = visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor; Write (cursorVisibilitySequence); } } /// public Point GetCursorPosition () { return _lastCursorPosition ?? Point.Empty; } private Point? _lastCursorPosition; /// public void SetCursorPosition (int col, int row) { if (_lastCursorPosition is { } && _lastCursorPosition.Value.X == col && _lastCursorPosition.Value.Y == row) { return; } _lastCursorPosition = new (col, row); if (_isVirtualTerminal) { var sb = new StringBuilder (); EscSeqUtils.CSI_AppendCursorPosition (sb, row + 1, col + 1); Write (sb.ToString ()); } else { SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row)); } } /// public void SetSize (int width, int height) { // Do Nothing. } private bool _isDisposed; private bool _force16Colors; private nint _consoleBuffer; private readonly StringBuilder _everythingStringBuilder = new (); /// public void Dispose () { if (_isDisposed) { return; } if (_isVirtualTerminal) { if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null) { //Disable alternative screen buffer. Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); } else { // Simulate restoring the color and clearing the screen. Console.ForegroundColor = _foreground; Console.BackgroundColor = _background; Console.Clear (); } } else { if (_screenBuffer != nint.Zero) { CloseHandle (_screenBuffer); } _screenBuffer = nint.Zero; } _isDisposed = true; } }