WindowsOutput.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. #nullable enable
  2. using System.ComponentModel;
  3. using System.Runtime.InteropServices;
  4. using Microsoft.Extensions.Logging;
  5. using static Terminal.Gui.WindowsConsole;
  6. namespace Terminal.Gui;
  7. internal class WindowsOutput : IConsoleOutput
  8. {
  9. [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
  10. private static extern bool WriteConsole (
  11. nint hConsoleOutput,
  12. string lpbufer,
  13. uint numberOfCharsToWriten,
  14. out uint lpNumberOfCharsWritten,
  15. nint lpReserved
  16. );
  17. [DllImport ("kernel32.dll", SetLastError = true)]
  18. private static extern bool CloseHandle (nint handle);
  19. [DllImport ("kernel32.dll", SetLastError = true)]
  20. private static extern nint CreateConsoleScreenBuffer (
  21. DesiredAccess dwDesiredAccess,
  22. ShareMode dwShareMode,
  23. nint secutiryAttributes,
  24. uint flags,
  25. nint screenBufferData
  26. );
  27. [DllImport ("kernel32.dll", SetLastError = true)]
  28. private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi);
  29. [Flags]
  30. private enum ShareMode : uint
  31. {
  32. FileShareRead = 1,
  33. FileShareWrite = 2
  34. }
  35. [Flags]
  36. private enum DesiredAccess : uint
  37. {
  38. GenericRead = 2147483648,
  39. GenericWrite = 1073741824
  40. }
  41. internal static nint INVALID_HANDLE_VALUE = new (-1);
  42. [DllImport ("kernel32.dll", SetLastError = true)]
  43. private static extern bool SetConsoleActiveScreenBuffer (nint handle);
  44. [DllImport ("kernel32.dll")]
  45. private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition);
  46. private readonly nint _screenBuffer;
  47. public WindowsOutput ()
  48. {
  49. Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}");
  50. _screenBuffer = CreateConsoleScreenBuffer (
  51. DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
  52. ShareMode.FileShareRead | ShareMode.FileShareWrite,
  53. nint.Zero,
  54. 1,
  55. nint.Zero
  56. );
  57. if (_screenBuffer == INVALID_HANDLE_VALUE)
  58. {
  59. int err = Marshal.GetLastWin32Error ();
  60. if (err != 0)
  61. {
  62. throw new Win32Exception (err);
  63. }
  64. }
  65. if (!SetConsoleActiveScreenBuffer (_screenBuffer))
  66. {
  67. throw new Win32Exception (Marshal.GetLastWin32Error ());
  68. }
  69. }
  70. public void Write (string str)
  71. {
  72. if (!WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero))
  73. {
  74. throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to write to console screen buffer.");
  75. }
  76. }
  77. public void Write (IOutputBuffer buffer)
  78. {
  79. ExtendedCharInfo [] outputBuffer = new ExtendedCharInfo [buffer.Rows * buffer.Cols];
  80. // TODO: probably do need this right?
  81. /*
  82. if (!windowSize.IsEmpty && (windowSize.Width != buffer.Cols || windowSize.Height != buffer.Rows))
  83. {
  84. return;
  85. }*/
  86. var bufferCoords = new Coord
  87. {
  88. X = (short)buffer.Cols, //Clip.Width,
  89. Y = (short)buffer.Rows //Clip.Height
  90. };
  91. for (var row = 0; row < buffer.Rows; row++)
  92. {
  93. if (!buffer.DirtyLines [row])
  94. {
  95. continue;
  96. }
  97. buffer.DirtyLines [row] = false;
  98. for (var col = 0; col < buffer.Cols; col++)
  99. {
  100. int position = row * buffer.Cols + col;
  101. outputBuffer [position].Attribute = buffer.Contents [row, col].Attribute.GetValueOrDefault ();
  102. if (buffer.Contents [row, col].IsDirty == false)
  103. {
  104. outputBuffer [position].Empty = true;
  105. outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
  106. continue;
  107. }
  108. outputBuffer [position].Empty = false;
  109. if (buffer.Contents [row, col].Rune.IsBmp)
  110. {
  111. outputBuffer [position].Char = (char)buffer.Contents [row, col].Rune.Value;
  112. }
  113. else
  114. {
  115. //outputBuffer [position].Empty = true;
  116. outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
  117. if (buffer.Contents [row, col].Rune.GetColumns () > 1 && col + 1 < buffer.Cols)
  118. {
  119. // TODO: This is a hack to deal with non-BMP and wide characters.
  120. col++;
  121. position = row * buffer.Cols + col;
  122. outputBuffer [position].Empty = false;
  123. outputBuffer [position].Char = ' ';
  124. }
  125. }
  126. }
  127. }
  128. var damageRegion = new SmallRect
  129. {
  130. Top = 0,
  131. Left = 0,
  132. Bottom = (short)buffer.Rows,
  133. Right = (short)buffer.Cols
  134. };
  135. //size, ExtendedCharInfo [] charInfoBuffer, Coord , SmallRect window,
  136. if (!WriteToConsole (
  137. new (buffer.Cols, buffer.Rows),
  138. outputBuffer,
  139. bufferCoords,
  140. damageRegion,
  141. false))
  142. {
  143. int err = Marshal.GetLastWin32Error ();
  144. if (err != 0)
  145. {
  146. throw new Win32Exception (err);
  147. }
  148. }
  149. SmallRect.MakeEmpty (ref damageRegion);
  150. }
  151. public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
  152. {
  153. var stringBuilder = new StringBuilder ();
  154. //Debug.WriteLine ("WriteToConsole");
  155. //if (_screenBuffer == nint.Zero)
  156. //{
  157. // ReadFromConsoleOutput (size, bufferSize, ref window);
  158. //}
  159. var result = false;
  160. if (force16Colors)
  161. {
  162. var i = 0;
  163. CharInfo [] ci = new CharInfo [charInfoBuffer.Length];
  164. foreach (ExtendedCharInfo info in charInfoBuffer)
  165. {
  166. ci [i++] = new ()
  167. {
  168. Char = new () { UnicodeChar = info.Char },
  169. Attributes =
  170. (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4))
  171. };
  172. }
  173. result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new () { X = window.Left, Y = window.Top }, ref window);
  174. }
  175. else
  176. {
  177. stringBuilder.Clear ();
  178. stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
  179. stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
  180. Attribute? prev = null;
  181. foreach (ExtendedCharInfo info in charInfoBuffer)
  182. {
  183. Attribute attr = info.Attribute;
  184. if (attr != prev)
  185. {
  186. prev = attr;
  187. stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
  188. stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
  189. }
  190. if (info.Char != '\x1b')
  191. {
  192. if (!info.Empty)
  193. {
  194. stringBuilder.Append (info.Char);
  195. }
  196. }
  197. else
  198. {
  199. stringBuilder.Append (' ');
  200. }
  201. }
  202. stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition);
  203. stringBuilder.Append (EscSeqUtils.CSI_HideCursor);
  204. var s = stringBuilder.ToString ();
  205. // TODO: requires extensive testing if we go down this route
  206. // If console output has changed
  207. //if (s != _lastWrite)
  208. //{
  209. // supply console with the new content
  210. result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
  211. foreach (SixelToRender sixel in Application.Sixel)
  212. {
  213. SetCursorPosition ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y);
  214. WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero);
  215. }
  216. }
  217. if (!result)
  218. {
  219. int err = Marshal.GetLastWin32Error ();
  220. if (err != 0)
  221. {
  222. throw new Win32Exception (err);
  223. }
  224. }
  225. return result;
  226. }
  227. public Size GetWindowSize ()
  228. {
  229. var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
  230. csbi.cbSize = (uint)Marshal.SizeOf (csbi);
  231. if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
  232. {
  233. //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
  234. return Size.Empty;
  235. }
  236. Size sz = new (
  237. csbi.srWindow.Right - csbi.srWindow.Left + 1,
  238. csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
  239. return sz;
  240. }
  241. /// <inheritdoc/>
  242. public void SetCursorVisibility (CursorVisibility visibility)
  243. {
  244. var sb = new StringBuilder ();
  245. sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
  246. Write (sb.ToString ());
  247. }
  248. private Point _lastCursorPosition;
  249. /// <inheritdoc/>
  250. public void SetCursorPosition (int col, int row)
  251. {
  252. if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row)
  253. {
  254. return;
  255. }
  256. _lastCursorPosition = new (col, row);
  257. SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row));
  258. }
  259. private bool _isDisposed;
  260. /// <inheritdoc/>
  261. public void Dispose ()
  262. {
  263. if (_isDisposed)
  264. {
  265. return;
  266. }
  267. if (_screenBuffer != nint.Zero)
  268. {
  269. try
  270. {
  271. CloseHandle (_screenBuffer);
  272. }
  273. catch (Exception e)
  274. {
  275. Logging.Logger.LogError (e, "Error trying to close screen buffer handle in WindowsOutput via interop method");
  276. }
  277. }
  278. _isDisposed = true;
  279. }
  280. }