WindowsConsole.cs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using System.ComponentModel;
  4. using System.Runtime.InteropServices;
  5. #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
  6. #pragma warning disable IDE1006// Naming rule violation: Prefix '_' is not expected
  7. namespace Terminal.Gui.Drivers;
  8. public partial class WindowsConsole
  9. {
  10. private CancellationTokenSource? _inputReadyCancellationTokenSource;
  11. private readonly BlockingCollection<InputRecord> _inputQueue = new (new ConcurrentQueue<InputRecord> ());
  12. internal WindowsMainLoop? _mainLoop;
  13. public const int STD_OUTPUT_HANDLE = -11;
  14. public const int STD_INPUT_HANDLE = -10;
  15. private readonly nint _inputHandle;
  16. private nint _outputHandle;
  17. private nint _screenBuffer;
  18. private readonly uint _originalConsoleMode;
  19. private CursorVisibility? _initialCursorVisibility;
  20. private CursorVisibility? _currentCursorVisibility;
  21. private CursorVisibility? _pendingCursorVisibility;
  22. private readonly StringBuilder _stringBuilder = new (256 * 1024);
  23. private string _lastWrite = string.Empty;
  24. public WindowsConsole ()
  25. {
  26. _inputHandle = GetStdHandle (STD_INPUT_HANDLE);
  27. _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
  28. _originalConsoleMode = ConsoleMode;
  29. uint newConsoleMode = _originalConsoleMode;
  30. newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags);
  31. newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode;
  32. newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput;
  33. ConsoleMode = newConsoleMode;
  34. IsVirtualTerminal = GetConsoleMode (_outputHandle, out uint mode) && (mode & (uint)ConsoleModes.EnableVirtualTerminalProcessing) != 0;
  35. if (!IsVirtualTerminal)
  36. {
  37. CreateConsoleScreenBuffer ();
  38. Size bufferSize = GetConsoleBufferWindow (out _);
  39. SmallRect window = new ()
  40. {
  41. Top = 0,
  42. Left = 0,
  43. Bottom = (short)bufferSize.Height,
  44. Right = (short)bufferSize.Width
  45. };
  46. ReadFromConsoleOutput (bufferSize, new ((short)bufferSize.Width, (short)bufferSize.Height), ref window);
  47. if (!GetConsoleMode (_screenBuffer, out mode))
  48. {
  49. throw new ApplicationException ($"Failed to get screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}.");
  50. }
  51. const uint ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002;
  52. mode &= ~ENABLE_WRAP_AT_EOL_OUTPUT; // Disable wrap
  53. if (!SetConsoleMode (_screenBuffer, mode))
  54. {
  55. throw new ApplicationException ($"Failed to set screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}.");
  56. }
  57. }
  58. SetInitialCursorVisibility ();
  59. _inputReadyCancellationTokenSource = new ();
  60. Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
  61. }
  62. private void CreateConsoleScreenBuffer ()
  63. {
  64. _screenBuffer = CreateConsoleScreenBuffer (
  65. DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
  66. ShareMode.FileShareRead | ShareMode.FileShareWrite,
  67. nint.Zero,
  68. 1,
  69. nint.Zero
  70. );
  71. if (_screenBuffer == INVALID_HANDLE_VALUE)
  72. {
  73. int err = Marshal.GetLastWin32Error ();
  74. if (err != 0)
  75. {
  76. throw new Win32Exception (err);
  77. }
  78. }
  79. if (!SetConsoleActiveScreenBuffer (_screenBuffer))
  80. {
  81. throw new Win32Exception (Marshal.GetLastWin32Error ());
  82. }
  83. }
  84. public InputRecord? DequeueInput ()
  85. {
  86. while (_inputReadyCancellationTokenSource is { })
  87. {
  88. try
  89. {
  90. return _inputQueue.Take (_inputReadyCancellationTokenSource.Token);
  91. }
  92. catch (OperationCanceledException)
  93. {
  94. return null;
  95. }
  96. }
  97. return null;
  98. }
  99. public InputRecord? ReadConsoleInput ()
  100. {
  101. const int BUFFER_SIZE = 1;
  102. InputRecord inputRecord = default;
  103. uint numberEventsRead = 0;
  104. while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
  105. {
  106. try
  107. {
  108. // Peek to check if there is any input available
  109. if (PeekConsoleInput (_inputHandle, out _, BUFFER_SIZE, out uint eventsRead) && eventsRead > 0)
  110. {
  111. // Read the input since it is available
  112. ReadConsoleInput (
  113. _inputHandle,
  114. out inputRecord,
  115. BUFFER_SIZE,
  116. out numberEventsRead);
  117. }
  118. if (numberEventsRead > 0)
  119. {
  120. return inputRecord;
  121. }
  122. try
  123. {
  124. Task.Delay (100, _inputReadyCancellationTokenSource.Token).Wait (_inputReadyCancellationTokenSource.Token);
  125. }
  126. catch (OperationCanceledException)
  127. {
  128. return null;
  129. }
  130. }
  131. catch (Exception ex)
  132. {
  133. if (ex is OperationCanceledException or ObjectDisposedException)
  134. {
  135. return null;
  136. }
  137. throw;
  138. }
  139. }
  140. return null;
  141. }
  142. private void ProcessInputQueue ()
  143. {
  144. while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
  145. {
  146. try
  147. {
  148. if (_inputQueue.Count == 0)
  149. {
  150. while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
  151. {
  152. try
  153. {
  154. InputRecord? inpRec = ReadConsoleInput ();
  155. if (inpRec is { })
  156. {
  157. _inputQueue.Add (inpRec.Value);
  158. break;
  159. }
  160. }
  161. catch (OperationCanceledException)
  162. {
  163. return;
  164. }
  165. }
  166. }
  167. }
  168. catch (OperationCanceledException)
  169. {
  170. return;
  171. }
  172. }
  173. }
  174. // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
  175. private TextStyle _redrawTextStyle = TextStyle.None;
  176. private CharInfo []? _originalStdOutChars;
  177. private struct Run
  178. {
  179. public ushort attr;
  180. public string text;
  181. public Run (ushort attr, string text)
  182. {
  183. this.attr = attr;
  184. this.text = text;
  185. }
  186. }
  187. public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
  188. {
  189. //Debug.WriteLine ("WriteToConsole");
  190. Attribute? prev = null;
  191. var result = false;
  192. if (force16Colors)
  193. {
  194. _stringBuilder.Clear ();
  195. var i = 0;
  196. List<Run> runs = [];
  197. Run? current = null;
  198. SetCursorPosition (new Coord (0, 0));
  199. foreach (ExtendedCharInfo info in charInfoBuffer)
  200. {
  201. if (IsVirtualTerminal)
  202. {
  203. Attribute attr = info.Attribute;
  204. AnsiColorCode fgColor = info.Attribute.Foreground.GetAnsiColorCode ();
  205. AnsiColorCode bgColor = info.Attribute.Background.GetAnsiColorCode ();
  206. if (attr != prev)
  207. {
  208. prev = attr;
  209. _stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColor (fgColor));
  210. _stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColor (bgColor));
  211. EscSeqUtils.CSI_AppendTextStyleChange (_stringBuilder, _redrawTextStyle, attr.Style);
  212. _redrawTextStyle = attr.Style;
  213. }
  214. if (info.Char [0] != '\x1b')
  215. {
  216. if (!info.Empty)
  217. {
  218. _stringBuilder.Append (info.Char);
  219. }
  220. }
  221. else
  222. {
  223. _stringBuilder.Append (' ');
  224. }
  225. }
  226. else
  227. {
  228. if (info.Empty)
  229. {
  230. i++;
  231. continue;
  232. }
  233. if (!info.Empty)
  234. {
  235. var attr = (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 ()
  236. | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4));
  237. // Start new run if needed
  238. if (current == null || attr != current.Value.attr)
  239. {
  240. if (current != null)
  241. {
  242. runs.Add (new (current.Value.attr, _stringBuilder.ToString ()));
  243. }
  244. _stringBuilder.Clear ();
  245. current = new Run (attr, "");
  246. }
  247. _stringBuilder!.Append (info.Char);
  248. }
  249. i++;
  250. if (i > 0 && i <= charInfoBuffer.Length && i % bufferSize.X == 0)
  251. {
  252. if (i < charInfoBuffer.Length)
  253. {
  254. _stringBuilder.AppendLine ();
  255. }
  256. runs.Add (new (current!.Value.attr, _stringBuilder.ToString ()));
  257. _stringBuilder.Clear ();
  258. }
  259. }
  260. }
  261. if (IsVirtualTerminal)
  262. {
  263. _stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition);
  264. _stringBuilder.Append (EscSeqUtils.CSI_HideCursor);
  265. var s = _stringBuilder.ToString ();
  266. // TODO: requires extensive testing if we go down this route
  267. // If console output has changed
  268. if (s != _lastWrite)
  269. {
  270. // supply console with the new content
  271. result = WriteConsole (_outputHandle, s, (uint)s.Length, out uint _, nint.Zero);
  272. }
  273. _lastWrite = s;
  274. foreach (var sixel in Application.Sixel)
  275. {
  276. SetCursorPosition (new Coord ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y));
  277. WriteConsole (IsVirtualTerminal ? _outputHandle : _screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero);
  278. }
  279. }
  280. else
  281. {
  282. foreach (var run in runs)
  283. {
  284. SetConsoleTextAttribute (IsVirtualTerminal ? _outputHandle : _screenBuffer, run.attr);
  285. result = WriteConsole (IsVirtualTerminal ? _outputHandle : _screenBuffer, run.text, (uint)run.text.Length, out _, nint.Zero);
  286. }
  287. }
  288. }
  289. else
  290. {
  291. _stringBuilder.Clear ();
  292. _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
  293. EscSeqUtils.CSI_AppendCursorPosition (_stringBuilder, 0, 0);
  294. foreach (ExtendedCharInfo info in charInfoBuffer)
  295. {
  296. Attribute attr = info.Attribute;
  297. if (attr != prev)
  298. {
  299. prev = attr;
  300. EscSeqUtils.CSI_AppendForegroundColorRGB (_stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B);
  301. EscSeqUtils.CSI_AppendBackgroundColorRGB (_stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B);
  302. EscSeqUtils.CSI_AppendTextStyleChange (_stringBuilder, _redrawTextStyle, attr.Style);
  303. _redrawTextStyle = attr.Style;
  304. }
  305. if (info.Char [0] != '\x1b')
  306. {
  307. if (!info.Empty)
  308. {
  309. _stringBuilder.Append (info.Char);
  310. }
  311. }
  312. else
  313. {
  314. _stringBuilder.Append (' ');
  315. }
  316. }
  317. _stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition);
  318. _stringBuilder.Append (EscSeqUtils.CSI_HideCursor);
  319. var s = _stringBuilder.ToString ();
  320. // TODO: requires extensive testing if we go down this route
  321. // If console output has changed
  322. if (s != _lastWrite)
  323. {
  324. // supply console with the new content
  325. result = WriteConsole (_outputHandle, s, (uint)s.Length, out uint _, nint.Zero);
  326. }
  327. _lastWrite = s;
  328. foreach (var sixel in Application.Sixel)
  329. {
  330. SetCursorPosition (new Coord ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y));
  331. WriteConsole (IsVirtualTerminal ? _outputHandle : _screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero);
  332. }
  333. }
  334. if (!result)
  335. {
  336. int err = Marshal.GetLastWin32Error ();
  337. if (err != 0)
  338. {
  339. throw new Win32Exception (err);
  340. }
  341. }
  342. return result;
  343. }
  344. internal bool WriteANSI (string ansi)
  345. {
  346. if (WriteConsole (_outputHandle, ansi, (uint)ansi.Length, out uint _, nint.Zero))
  347. {
  348. // Flush the output to make sure it's sent immediately
  349. return FlushFileBuffers (_outputHandle);
  350. }
  351. return false;
  352. }
  353. public void ReadFromConsoleOutput (Size size, Coord coords, ref SmallRect window)
  354. {
  355. _originalStdOutChars = new CharInfo [size.Height * size.Width];
  356. if (!ReadConsoleOutput (_screenBuffer, _originalStdOutChars, coords, new Coord { X = 0, Y = 0 }, ref window))
  357. {
  358. throw new Win32Exception (Marshal.GetLastWin32Error ());
  359. }
  360. }
  361. public bool SetCursorPosition (Coord position)
  362. {
  363. return SetConsoleCursorPosition (IsVirtualTerminal ? _outputHandle : _screenBuffer, position);
  364. }
  365. public void SetInitialCursorVisibility ()
  366. {
  367. if (_initialCursorVisibility.HasValue == false && GetCursorVisibility (out CursorVisibility visibility))
  368. {
  369. _initialCursorVisibility = visibility;
  370. }
  371. }
  372. public bool GetCursorVisibility (out CursorVisibility visibility)
  373. {
  374. if ((IsVirtualTerminal ? _outputHandle : _screenBuffer) == nint.Zero)
  375. {
  376. visibility = CursorVisibility.Invisible;
  377. return false;
  378. }
  379. if (!GetConsoleCursorInfo (IsVirtualTerminal ? _outputHandle : _screenBuffer, out ConsoleCursorInfo info))
  380. {
  381. int err = Marshal.GetLastWin32Error ();
  382. if (err != 0)
  383. {
  384. throw new Win32Exception (err);
  385. }
  386. visibility = CursorVisibility.Default;
  387. return false;
  388. }
  389. if (!info.bVisible)
  390. {
  391. visibility = CursorVisibility.Invisible;
  392. }
  393. else if (info.dwSize > 50)
  394. {
  395. visibility = CursorVisibility.Default;
  396. }
  397. else
  398. {
  399. visibility = CursorVisibility.Default;
  400. }
  401. return visibility != CursorVisibility.Invisible;
  402. }
  403. public bool EnsureCursorVisibility ()
  404. {
  405. if (_initialCursorVisibility.HasValue && _pendingCursorVisibility.HasValue && SetCursorVisibility (_pendingCursorVisibility.Value))
  406. {
  407. _pendingCursorVisibility = null;
  408. return true;
  409. }
  410. return false;
  411. }
  412. public void ForceRefreshCursorVisibility ()
  413. {
  414. if (_currentCursorVisibility.HasValue)
  415. {
  416. _pendingCursorVisibility = _currentCursorVisibility;
  417. _currentCursorVisibility = null;
  418. }
  419. }
  420. public bool SetCursorVisibility (CursorVisibility visibility)
  421. {
  422. if (_initialCursorVisibility.HasValue == false)
  423. {
  424. _pendingCursorVisibility = visibility;
  425. return false;
  426. }
  427. if (_currentCursorVisibility.HasValue == false || _currentCursorVisibility.Value != visibility)
  428. {
  429. var info = new ConsoleCursorInfo
  430. {
  431. dwSize = (uint)visibility & 0x00FF,
  432. bVisible = ((uint)visibility & 0xFF00) != 0
  433. };
  434. if (!SetConsoleCursorInfo (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref info))
  435. {
  436. return false;
  437. }
  438. _currentCursorVisibility = visibility;
  439. }
  440. return true;
  441. }
  442. public void Cleanup ()
  443. {
  444. if (_initialCursorVisibility.HasValue)
  445. {
  446. SetCursorVisibility (_initialCursorVisibility.Value);
  447. }
  448. //SetConsoleOutputWindow (out _);
  449. ConsoleMode = _originalConsoleMode;
  450. _outputHandle = CreateConsoleScreenBuffer (
  451. DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
  452. ShareMode.FileShareRead | ShareMode.FileShareWrite,
  453. nint.Zero,
  454. 1,
  455. nint.Zero
  456. );
  457. if (!SetConsoleActiveScreenBuffer (_outputHandle))
  458. {
  459. int err = Marshal.GetLastWin32Error ();
  460. Console.WriteLine ("Error: {0}", err);
  461. }
  462. if (_screenBuffer != nint.Zero)
  463. {
  464. CloseHandle (_screenBuffer);
  465. }
  466. _screenBuffer = nint.Zero;
  467. _inputReadyCancellationTokenSource?.Cancel ();
  468. _inputReadyCancellationTokenSource?.Dispose ();
  469. _inputReadyCancellationTokenSource = null;
  470. }
  471. internal Size GetConsoleBufferWindow (out Point position)
  472. {
  473. if ((IsVirtualTerminal ? _outputHandle : _screenBuffer) == nint.Zero)
  474. {
  475. position = Point.Empty;
  476. return Size.Empty;
  477. }
  478. var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
  479. csbi.cbSize = (uint)Marshal.SizeOf (csbi);
  480. if (!GetConsoleScreenBufferInfoEx (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi))
  481. {
  482. //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
  483. position = Point.Empty;
  484. return Size.Empty;
  485. }
  486. Size sz = new (
  487. csbi.srWindow.Right - csbi.srWindow.Left + 1,
  488. csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
  489. position = new (csbi.srWindow.Left, csbi.srWindow.Top);
  490. return sz;
  491. }
  492. internal Size GetConsoleOutputWindow (out Point position)
  493. {
  494. var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
  495. csbi.cbSize = (uint)Marshal.SizeOf (csbi);
  496. if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
  497. {
  498. throw new Win32Exception (Marshal.GetLastWin32Error ());
  499. }
  500. Size sz = new (
  501. csbi.srWindow.Right - csbi.srWindow.Left + 1,
  502. csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
  503. position = new (csbi.srWindow.Left, csbi.srWindow.Top);
  504. return sz;
  505. }
  506. internal Size SetConsoleWindow (short cols, short rows)
  507. {
  508. var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
  509. csbi.cbSize = (uint)Marshal.SizeOf (csbi);
  510. if (!GetConsoleScreenBufferInfoEx (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi))
  511. {
  512. throw new Win32Exception (Marshal.GetLastWin32Error ());
  513. }
  514. Coord maxWinSize = GetLargestConsoleWindowSize (IsVirtualTerminal ? _outputHandle : _screenBuffer);
  515. short newCols = Math.Min (cols, maxWinSize.X);
  516. short newRows = Math.Min (rows, maxWinSize.Y);
  517. csbi.dwSize = new Coord (newCols, Math.Max (newRows, (short)1));
  518. csbi.srWindow = new SmallRect (0, 0, newCols, newRows);
  519. csbi.dwMaximumWindowSize = new Coord (newCols, newRows);
  520. if (!SetConsoleScreenBufferInfoEx (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi))
  521. {
  522. throw new Win32Exception (Marshal.GetLastWin32Error ());
  523. }
  524. var winRect = new SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0));
  525. if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
  526. {
  527. //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
  528. return new (cols, rows);
  529. }
  530. SetConsoleOutputWindow (csbi);
  531. return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1);
  532. }
  533. internal Size GetLargestConsoleWindowSize ()
  534. {
  535. Coord maxWinSize = GetLargestConsoleWindowSize (IsVirtualTerminal ? _outputHandle : _screenBuffer);
  536. return new (maxWinSize.X, maxWinSize.Y);
  537. }
  538. private void SetConsoleOutputWindow (CONSOLE_SCREEN_BUFFER_INFOEX csbi)
  539. {
  540. if ((IsVirtualTerminal
  541. ? _outputHandle
  542. : _screenBuffer) != nint.Zero && !SetConsoleScreenBufferInfoEx (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi))
  543. {
  544. throw new Win32Exception (Marshal.GetLastWin32Error ());
  545. }
  546. }
  547. internal Size SetConsoleOutputWindow (out Point position)
  548. {
  549. if ((IsVirtualTerminal ? _outputHandle : _screenBuffer) == nint.Zero)
  550. {
  551. position = Point.Empty;
  552. return Size.Empty;
  553. }
  554. var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
  555. csbi.cbSize = (uint)Marshal.SizeOf (csbi);
  556. if (!GetConsoleScreenBufferInfoEx (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi))
  557. {
  558. throw new Win32Exception (Marshal.GetLastWin32Error ());
  559. }
  560. Size sz = new (
  561. csbi.srWindow.Right - csbi.srWindow.Left + 1,
  562. Math.Max (csbi.srWindow.Bottom - csbi.srWindow.Top + 1, 0));
  563. position = new (csbi.srWindow.Left, csbi.srWindow.Top);
  564. SetConsoleOutputWindow (csbi);
  565. var winRect = new SmallRect (0, 0, (short)(sz.Width - 1), (short)Math.Max (sz.Height - 1, 0));
  566. if (!SetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
  567. {
  568. throw new Win32Exception (Marshal.GetLastWin32Error ());
  569. }
  570. if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
  571. {
  572. throw new Win32Exception (Marshal.GetLastWin32Error ());
  573. }
  574. return sz;
  575. }
  576. internal bool IsVirtualTerminal { get; init; }
  577. private uint ConsoleMode
  578. {
  579. get
  580. {
  581. GetConsoleMode (_inputHandle, out uint v);
  582. return v;
  583. }
  584. set => SetConsoleMode (_inputHandle, value);
  585. }
  586. [Flags]
  587. public enum ConsoleModes : uint
  588. {
  589. EnableProcessedInput = 1,
  590. EnableVirtualTerminalProcessing = 4,
  591. EnableMouseInput = 16,
  592. EnableQuickEditMode = 64,
  593. EnableExtendedFlags = 128
  594. }
  595. [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
  596. public struct KeyEventRecord
  597. {
  598. [FieldOffset (0)]
  599. [MarshalAs (UnmanagedType.Bool)]
  600. public bool bKeyDown;
  601. [FieldOffset (4)]
  602. [MarshalAs (UnmanagedType.U2)]
  603. public ushort wRepeatCount;
  604. [FieldOffset (6)]
  605. [MarshalAs (UnmanagedType.U2)]
  606. public ConsoleKeyMapping.VK wVirtualKeyCode;
  607. [FieldOffset (8)]
  608. [MarshalAs (UnmanagedType.U2)]
  609. public ushort wVirtualScanCode;
  610. [FieldOffset (10)]
  611. public char UnicodeChar;
  612. [FieldOffset (12)]
  613. [MarshalAs (UnmanagedType.U4)]
  614. public ControlKeyState dwControlKeyState;
  615. public readonly override string ToString ()
  616. {
  617. return
  618. $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]";
  619. }
  620. }
  621. [Flags]
  622. public enum ButtonState
  623. {
  624. NoButtonPressed = 0,
  625. Button1Pressed = 1,
  626. Button2Pressed = 4,
  627. Button3Pressed = 8,
  628. Button4Pressed = 16,
  629. RightmostButtonPressed = 2
  630. }
  631. [Flags]
  632. public enum ControlKeyState
  633. {
  634. NoControlKeyPressed = 0,
  635. RightAltPressed = 1,
  636. LeftAltPressed = 2,
  637. RightControlPressed = 4,
  638. LeftControlPressed = 8,
  639. ShiftPressed = 16,
  640. NumlockOn = 32,
  641. ScrolllockOn = 64,
  642. CapslockOn = 128,
  643. EnhancedKey = 256
  644. }
  645. [Flags]
  646. public enum EventFlags
  647. {
  648. NoEvent = 0,
  649. MouseMoved = 1,
  650. DoubleClick = 2,
  651. MouseWheeled = 4,
  652. MouseHorizontalWheeled = 8
  653. }
  654. [StructLayout (LayoutKind.Explicit)]
  655. public struct MouseEventRecord
  656. {
  657. [FieldOffset (0)]
  658. public Coord MousePosition;
  659. [FieldOffset (4)]
  660. public ButtonState ButtonState;
  661. [FieldOffset (8)]
  662. public ControlKeyState ControlKeyState;
  663. [FieldOffset (12)]
  664. public EventFlags EventFlags;
  665. public readonly override string ToString () { return $"[Mouse{MousePosition},{ButtonState},{ControlKeyState},{EventFlags}]"; }
  666. }
  667. public struct WindowBufferSizeRecord
  668. {
  669. public Coord _size;
  670. public WindowBufferSizeRecord (short x, short y) { _size = new Coord (x, y); }
  671. public readonly override string ToString () { return $"[WindowBufferSize{_size}"; }
  672. }
  673. [StructLayout (LayoutKind.Sequential)]
  674. public struct MenuEventRecord
  675. {
  676. public uint dwCommandId;
  677. }
  678. [StructLayout (LayoutKind.Sequential)]
  679. public struct FocusEventRecord
  680. {
  681. public uint bSetFocus;
  682. }
  683. public enum EventType : ushort
  684. {
  685. Focus = 0x10,
  686. Key = 0x1,
  687. Menu = 0x8,
  688. Mouse = 2,
  689. WindowBufferSize = 4
  690. }
  691. [StructLayout (LayoutKind.Explicit)]
  692. public struct InputRecord
  693. {
  694. [FieldOffset (0)]
  695. public EventType EventType;
  696. [FieldOffset (4)]
  697. public KeyEventRecord KeyEvent;
  698. [FieldOffset (4)]
  699. public MouseEventRecord MouseEvent;
  700. [FieldOffset (4)]
  701. public WindowBufferSizeRecord WindowBufferSizeEvent;
  702. [FieldOffset (4)]
  703. public MenuEventRecord MenuEvent;
  704. [FieldOffset (4)]
  705. public FocusEventRecord FocusEvent;
  706. public readonly override string ToString ()
  707. {
  708. return (EventType switch
  709. {
  710. EventType.Focus => FocusEvent.ToString (),
  711. EventType.Key => KeyEvent.ToString (),
  712. EventType.Menu => MenuEvent.ToString (),
  713. EventType.Mouse => MouseEvent.ToString (),
  714. EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (),
  715. _ => "Unknown event type: " + EventType
  716. })!;
  717. }
  718. }
  719. [Flags]
  720. private enum ShareMode : uint
  721. {
  722. FileShareRead = 1,
  723. FileShareWrite = 2
  724. }
  725. [Flags]
  726. private enum DesiredAccess : uint
  727. {
  728. GenericRead = 2147483648,
  729. GenericWrite = 1073741824
  730. }
  731. [StructLayout (LayoutKind.Sequential)]
  732. public struct ConsoleScreenBufferInfo
  733. {
  734. public Coord dwSize;
  735. public Coord dwCursorPosition;
  736. public ushort wAttributes;
  737. public SmallRect srWindow;
  738. public Coord dwMaximumWindowSize;
  739. }
  740. [StructLayout (LayoutKind.Sequential)]
  741. public struct Coord
  742. {
  743. public short X;
  744. public short Y;
  745. public Coord (short x, short y)
  746. {
  747. X = x;
  748. Y = y;
  749. }
  750. public readonly override string ToString () { return $"({X},{Y})"; }
  751. }
  752. [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
  753. public struct CharUnion
  754. {
  755. [FieldOffset (0)]
  756. public char UnicodeChar;
  757. [FieldOffset (0)]
  758. public byte AsciiChar;
  759. }
  760. [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
  761. public struct CharInfo
  762. {
  763. [FieldOffset (0)]
  764. public CharUnion Char;
  765. [FieldOffset (2)]
  766. public ushort Attributes;
  767. }
  768. public struct ExtendedCharInfo
  769. {
  770. public char [] Char { get; set; }
  771. public Attribute Attribute { get; set; }
  772. public bool Empty { get; set; } // TODO: Temp hack until virtual terminal sequences
  773. public ExtendedCharInfo (char [] character, Attribute attribute)
  774. {
  775. Char = character;
  776. Attribute = attribute;
  777. Empty = false;
  778. }
  779. }
  780. [StructLayout (LayoutKind.Sequential)]
  781. public struct SmallRect
  782. {
  783. public short Left;
  784. public short Top;
  785. public short Right;
  786. public short Bottom;
  787. public SmallRect (short left, short top, short right, short bottom)
  788. {
  789. Left = left;
  790. Top = top;
  791. Right = right;
  792. Bottom = bottom;
  793. }
  794. public static void MakeEmpty (ref SmallRect rect) { rect.Left = -1; }
  795. public static void Update (ref SmallRect rect, short col, short row)
  796. {
  797. if (rect.Left == -1)
  798. {
  799. rect.Left = rect.Right = col;
  800. rect.Bottom = rect.Top = row;
  801. return;
  802. }
  803. if (col >= rect.Left && col <= rect.Right && row >= rect.Top && row <= rect.Bottom)
  804. {
  805. return;
  806. }
  807. if (col < rect.Left)
  808. {
  809. rect.Left = col;
  810. }
  811. if (col > rect.Right)
  812. {
  813. rect.Right = col;
  814. }
  815. if (row < rect.Top)
  816. {
  817. rect.Top = row;
  818. }
  819. if (row > rect.Bottom)
  820. {
  821. rect.Bottom = row;
  822. }
  823. }
  824. public readonly override string ToString () { return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; }
  825. }
  826. [StructLayout (LayoutKind.Sequential)]
  827. public struct ConsoleKeyInfoEx
  828. {
  829. public ConsoleKeyInfo ConsoleKeyInfo;
  830. public bool CapsLock;
  831. public bool NumLock;
  832. public bool ScrollLock;
  833. public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock)
  834. {
  835. ConsoleKeyInfo = consoleKeyInfo;
  836. CapsLock = capslock;
  837. NumLock = numlock;
  838. ScrollLock = scrolllock;
  839. }
  840. /// <summary>
  841. /// Prints a ConsoleKeyInfoEx structure
  842. /// </summary>
  843. /// <param name="ex"></param>
  844. /// <returns></returns>
  845. public readonly string ToString (ConsoleKeyInfoEx ex)
  846. {
  847. var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar);
  848. var sb = new StringBuilder ();
  849. sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})");
  850. sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
  851. sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
  852. sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
  853. sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) ");
  854. sb.Append (ex.CapsLock ? "caps," : string.Empty);
  855. sb.Append (ex.NumLock ? "num," : string.Empty);
  856. sb.Append (ex.ScrollLock ? "scroll," : string.Empty);
  857. string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
  858. return $"[ConsoleKeyInfoEx({s})]";
  859. }
  860. }
  861. [DllImport ("kernel32.dll", SetLastError = true)]
  862. private static extern nint GetStdHandle (int nStdHandle);
  863. [DllImport ("kernel32.dll", SetLastError = true)]
  864. private static extern bool CloseHandle (nint handle);
  865. [DllImport ("kernel32.dll", SetLastError = true)]
  866. public static extern bool PeekConsoleInput (nint hConsoleInput, out InputRecord lpBuffer, uint nLength, out uint lpNumberOfEventsRead);
  867. [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)]
  868. public static extern bool ReadConsoleInput (
  869. nint hConsoleInput,
  870. out InputRecord lpBuffer,
  871. uint nLength,
  872. out uint lpNumberOfEventsRead
  873. );
  874. [DllImport ("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
  875. private static extern bool ReadConsoleOutput (
  876. nint hConsoleOutput,
  877. [Out] CharInfo [] lpBuffer,
  878. Coord dwBufferSize,
  879. Coord dwBufferCoord,
  880. ref SmallRect lpReadRegion
  881. );
  882. // TODO: This API is obsolete. See https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput
  883. [DllImport ("kernel32.dll", EntryPoint = "WriteConsoleOutputW", SetLastError = true, CharSet = CharSet.Unicode)]
  884. public static extern bool WriteConsoleOutput (
  885. nint hConsoleOutput,
  886. CharInfo [] lpBuffer,
  887. Coord dwBufferSize,
  888. Coord dwBufferCoord,
  889. ref SmallRect lpWriteRegion
  890. );
  891. [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
  892. [return: MarshalAs (UnmanagedType.Bool)]
  893. private static partial bool WriteConsole (
  894. nint hConsoleOutput,
  895. ReadOnlySpan<char> lpbufer,
  896. uint NumberOfCharsToWriten,
  897. out uint lpNumberOfCharsWritten,
  898. nint lpReserved
  899. );
  900. [DllImport ("kernel32.dll", SetLastError = true)]
  901. private static extern bool SetConsoleTextAttribute (
  902. nint hConsoleOutput,
  903. ushort wAttributes
  904. );
  905. [DllImport ("kernel32.dll", SetLastError = true)]
  906. private static extern bool FlushFileBuffers (nint hFile);
  907. [DllImport ("kernel32.dll")]
  908. private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition);
  909. [StructLayout (LayoutKind.Sequential)]
  910. public struct ConsoleCursorInfo
  911. {
  912. /// <summary>
  913. /// The percentage of the character cell that is filled by the cursor.This value is between 1 and 100.
  914. /// The cursor appearance varies, ranging from completely filling the cell to showing up as a horizontal
  915. /// line at the bottom of the cell.
  916. /// </summary>
  917. public uint dwSize;
  918. public bool bVisible;
  919. }
  920. [DllImport ("kernel32.dll", SetLastError = true)]
  921. private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref ConsoleCursorInfo lpConsoleCursorInfo);
  922. [DllImport ("kernel32.dll", SetLastError = true)]
  923. private static extern bool GetConsoleCursorInfo (nint hConsoleOutput, out ConsoleCursorInfo lpConsoleCursorInfo);
  924. [DllImport ("kernel32.dll")]
  925. private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);
  926. [DllImport ("kernel32.dll")]
  927. private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
  928. [DllImport ("kernel32.dll", SetLastError = true)]
  929. private static extern nint CreateConsoleScreenBuffer (
  930. DesiredAccess dwDesiredAccess,
  931. ShareMode dwShareMode,
  932. nint secutiryAttributes,
  933. uint flags,
  934. nint screenBufferData
  935. );
  936. internal static nint INVALID_HANDLE_VALUE = new (-1);
  937. [DllImport ("kernel32.dll", SetLastError = true)]
  938. private static extern bool SetConsoleActiveScreenBuffer (nint handle);
  939. [DllImport ("kernel32.dll", SetLastError = true)]
  940. private static extern bool GetNumberOfConsoleInputEvents (nint handle, out uint lpcNumberOfEvents);
  941. internal uint GetNumberOfConsoleInputEvents ()
  942. {
  943. if (!GetNumberOfConsoleInputEvents (_inputHandle, out uint numOfEvents))
  944. {
  945. Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
  946. return 0;
  947. }
  948. return numOfEvents;
  949. }
  950. [DllImport ("kernel32.dll", SetLastError = true)]
  951. private static extern bool FlushConsoleInputBuffer (nint handle);
  952. internal void FlushConsoleInputBuffer ()
  953. {
  954. if (!FlushConsoleInputBuffer (_inputHandle))
  955. {
  956. Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
  957. }
  958. }
  959. #if false // Not needed on the constructor. Perhaps could be used on resizing. To study.
  960. [DllImport ("kernel32.dll", ExactSpelling = true)]
  961. static extern IntPtr GetConsoleWindow ();
  962. [DllImport ("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  963. static extern bool ShowWindow (IntPtr hWnd, int nCmdShow);
  964. public const int HIDE = 0;
  965. public const int MAXIMIZE = 3;
  966. public const int MINIMIZE = 6;
  967. public const int RESTORE = 9;
  968. internal void ShowWindow (int state)
  969. {
  970. IntPtr thisConsole = GetConsoleWindow ();
  971. ShowWindow (thisConsole, state);
  972. }
  973. #endif
  974. // See: https://github.com/gui-cs/Terminal.Gui/issues/357
  975. [StructLayout (LayoutKind.Sequential)]
  976. public struct CONSOLE_SCREEN_BUFFER_INFOEX
  977. {
  978. public uint cbSize;
  979. public Coord dwSize;
  980. public Coord dwCursorPosition;
  981. public ushort wAttributes;
  982. public SmallRect srWindow;
  983. public Coord dwMaximumWindowSize;
  984. public ushort wPopupAttributes;
  985. public bool bFullscreenSupported;
  986. [MarshalAs (UnmanagedType.ByValArray, SizeConst = 16)]
  987. public COLORREF [] ColorTable;
  988. }
  989. [StructLayout (LayoutKind.Explicit, Size = 4)]
  990. public struct COLORREF
  991. {
  992. public COLORREF (byte r, byte g, byte b)
  993. {
  994. Value = 0;
  995. R = r;
  996. G = g;
  997. B = b;
  998. }
  999. public COLORREF (uint value)
  1000. {
  1001. R = 0;
  1002. G = 0;
  1003. B = 0;
  1004. Value = value & 0x00FFFFFF;
  1005. }
  1006. [FieldOffset (0)]
  1007. public byte R;
  1008. [FieldOffset (1)]
  1009. public byte G;
  1010. [FieldOffset (2)]
  1011. public byte B;
  1012. [FieldOffset (0)]
  1013. public uint Value;
  1014. }
  1015. [DllImport ("kernel32.dll", SetLastError = true)]
  1016. private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi);
  1017. [DllImport ("kernel32.dll", SetLastError = true)]
  1018. private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX consoleScreenBufferInfo);
  1019. [DllImport ("kernel32.dll", SetLastError = true)]
  1020. private static extern bool SetConsoleWindowInfo (
  1021. nint hConsoleOutput,
  1022. bool bAbsolute,
  1023. [In] ref SmallRect lpConsoleWindow
  1024. );
  1025. [DllImport ("kernel32.dll", SetLastError = true)]
  1026. private static extern Coord GetLargestConsoleWindowSize (
  1027. nint hConsoleOutput
  1028. );
  1029. }