ConsoleDriver.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. #nullable enable
  2. using System.Diagnostics;
  3. namespace Terminal.Gui.Drivers;
  4. /// <summary>Base class for Terminal.Gui IConsoleDriver implementations.</summary>
  5. /// <remarks>
  6. /// There are currently four implementations:
  7. /// - DotNetDriver that uses the .NET Console API and works on all platforms
  8. /// - UnixDriver optimized for Unix and Mac.
  9. /// - WindowsDriver optimized for Windows.
  10. /// - FakeDriver for unit testing.
  11. /// </remarks>
  12. public abstract class ConsoleDriver : IConsoleDriver
  13. {
  14. /// <summary>
  15. /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
  16. /// <code>
  17. /// public ColorTests ()
  18. /// {
  19. /// ConsoleDriver.RunningUnitTests = true;
  20. /// }
  21. /// </code>
  22. /// </summary>
  23. internal static bool RunningUnitTests { get; set; }
  24. /// <summary>Get the operating system clipboard.</summary>
  25. public IClipboard? Clipboard { get; internal set; }
  26. /// <summary>Returns the name of the driver and relevant library version information.</summary>
  27. /// <returns></returns>
  28. public virtual string GetVersionInfo () { return GetType ().Name; }
  29. #region ANSI Esc Sequence Handling
  30. // QUESTION: This appears to be an API to help in debugging. It's only implemented in UnixDriver and WindowsDriver.
  31. // QUESTION: Can it be factored such that it does not contaminate the ConsoleDriver API?
  32. /// <summary>
  33. /// Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
  34. /// </summary>
  35. /// <param name="ansi"></param>
  36. public abstract void WriteRaw (string ansi);
  37. #endregion ANSI Esc Sequence Handling
  38. #region Screen and Contents
  39. /// <summary>
  40. /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
  41. /// </summary>
  42. public TimeSpan EscTimeout { get; } = TimeSpan.FromMilliseconds (50);
  43. // As performance is a concern, we keep track of the dirty lines and only refresh those.
  44. // This is in addition to the dirty flag on each cell.
  45. internal bool []? _dirtyLines;
  46. // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application?
  47. /// <summary>
  48. /// Gets the location and size of the terminal screen. This is the single source of truth for
  49. /// the available drawing area.
  50. /// </summary>
  51. /// <remarks>
  52. /// <para>
  53. /// The screen rectangle always has origin (0,0) and size determined by <see cref="Cols"/> and <see cref="Rows"/>.
  54. /// When the terminal is resized, <see cref="Cols"/> and <see cref="Rows"/> are updated, and the
  55. /// <see cref="SizeChanged"/> event is fired.
  56. /// </para>
  57. /// <para>
  58. /// In production drivers (WindowsDriver, UnixDriver, DotNetDriver), this reflects the actual terminal size.
  59. /// In <see cref="FakeDriver"/>, this can be controlled programmatically via <c>SetBufferSize</c> for testing.
  60. /// </para>
  61. /// </remarks>
  62. /// <seealso cref="Cols"/>
  63. /// <seealso cref="Rows"/>
  64. /// <seealso cref="SizeChanged"/>
  65. public Rectangle Screen => new (0, 0, Cols, Rows);
  66. private Region? _clip;
  67. /// <summary>
  68. /// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
  69. /// to.
  70. /// </summary>
  71. /// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
  72. public Region? Clip
  73. {
  74. get => _clip;
  75. set
  76. {
  77. if (_clip == value)
  78. {
  79. return;
  80. }
  81. _clip = value;
  82. // Don't ever let Clip be bigger than Screen
  83. if (_clip is { })
  84. {
  85. _clip.Intersect (Screen);
  86. }
  87. }
  88. }
  89. /// <summary>
  90. /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
  91. /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
  92. /// </summary>
  93. public int Col { get; private set; }
  94. /// <summary>
  95. /// Gets or sets the number of columns visible in the terminal. This property, along with <see cref="Rows"/>,
  96. /// defines the dimensions of the <see cref="Screen"/> rectangle.
  97. /// </summary>
  98. /// <remarks>
  99. /// <para>
  100. /// In production drivers, this value reflects the actual terminal width and is updated when
  101. /// the terminal is resized. In <see cref="FakeDriver"/>, this can be set programmatically
  102. /// via <c>SetBufferSize</c> or <c>SetWindowSize</c> for testing.
  103. /// </para>
  104. /// <para>
  105. /// <strong>Warning:</strong> Setting this property directly clears the contents buffer.
  106. /// Prefer using resize methods (<c>SetBufferSize</c> in FakeDriver) that properly handle
  107. /// the resize sequence including firing <see cref="SizeChanged"/> events.
  108. /// </para>
  109. /// </remarks>
  110. /// <seealso cref="Rows"/>
  111. /// <seealso cref="Screen"/>
  112. /// <seealso cref="SizeChanged"/>
  113. public virtual int Cols
  114. {
  115. get => _cols;
  116. set
  117. {
  118. _cols = value;
  119. ClearContents ();
  120. }
  121. }
  122. /// <summary>
  123. /// The contents of the application output. The driver outputs this buffer to the terminal when
  124. /// <see cref="UpdateScreen"/> is called.
  125. /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks>
  126. /// </summary>
  127. public Cell [,]? Contents { get; set; }
  128. /// <summary>The leftmost column in the terminal.</summary>
  129. public virtual int Left { get; set; } = 0;
  130. /// <summary>Tests if the specified rune is supported by the driver.</summary>
  131. /// <param name="rune"></param>
  132. /// <returns>
  133. /// <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not
  134. /// support displaying this rune.
  135. /// </returns>
  136. public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); }
  137. /// <summary>Tests whether the specified coordinate are valid for drawing.</summary>
  138. /// <param name="col">The column.</param>
  139. /// <param name="row">The row.</param>
  140. /// <returns>
  141. /// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>.
  142. /// <see langword="true"/> otherwise.
  143. /// </returns>
  144. public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); }
  145. /// <summary>
  146. /// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
  147. /// Used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
  148. /// </summary>
  149. /// <remarks>
  150. /// <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para>
  151. /// <para>
  152. /// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="Cols"/> and
  153. /// <see cref="Rows"/>, the method still sets those properties.
  154. /// </para>
  155. /// </remarks>
  156. /// <param name="col">Column to move to.</param>
  157. /// <param name="row">Row to move to.</param>
  158. public virtual void Move (int col, int row)
  159. {
  160. //Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0));
  161. Col = col;
  162. Row = row;
  163. }
  164. /// <summary>
  165. /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
  166. /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
  167. /// </summary>
  168. public int Row { get; private set; }
  169. /// <summary>
  170. /// Gets or sets the number of rows visible in the terminal. This property, along with <see cref="Cols"/>,
  171. /// defines the dimensions of the <see cref="Screen"/> rectangle.
  172. /// </summary>
  173. /// <remarks>
  174. /// <para>
  175. /// In production drivers, this value reflects the actual terminal height and is updated when
  176. /// the terminal is resized. In <see cref="FakeDriver"/>, this can be set programmatically
  177. /// via <c>SetBufferSize</c> or <c>SetWindowSize</c> for testing.
  178. /// </para>
  179. /// <para>
  180. /// <strong>Warning:</strong> Setting this property directly clears the contents buffer.
  181. /// Prefer using resize methods (<c>SetBufferSize</c> in FakeDriver) that properly handle
  182. /// the resize sequence including firing <see cref="SizeChanged"/> events.
  183. /// </para>
  184. /// </remarks>
  185. /// <seealso cref="Cols"/>
  186. /// <seealso cref="Screen"/>
  187. /// <seealso cref="SizeChanged"/>
  188. public virtual int Rows
  189. {
  190. get => _rows;
  191. set
  192. {
  193. _rows = value;
  194. ClearContents ();
  195. }
  196. }
  197. /// <summary>The topmost row in the terminal.</summary>
  198. public virtual int Top { get; set; } = 0;
  199. /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
  200. /// <remarks>
  201. /// <para>
  202. /// When the method returns, <see cref="Col"/> will be incremented by the number of columns
  203. /// <paramref name="rune"/> required, even if the new column value is outside of the <see cref="Clip"/> or screen
  204. /// dimensions defined by <see cref="Cols"/>.
  205. /// </para>
  206. /// <para>
  207. /// If <paramref name="rune"/> requires more than one column, and <see cref="Col"/> plus the number of columns
  208. /// needed exceeds the <see cref="Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD)
  209. /// will be added instead.
  210. /// </para>
  211. /// </remarks>
  212. /// <param name="rune">Rune to add.</param>
  213. public void AddRune (Rune rune)
  214. {
  215. int runeWidth = -1;
  216. bool validLocation = IsValidLocation (rune, Col, Row);
  217. if (Contents is null)
  218. {
  219. return;
  220. }
  221. Rectangle clipRect = Clip!.GetBounds ();
  222. if (validLocation)
  223. {
  224. rune = rune.MakePrintable ();
  225. runeWidth = rune.GetColumns ();
  226. lock (Contents)
  227. {
  228. if (runeWidth == 0 && rune.IsCombiningMark ())
  229. {
  230. // AtlasEngine does not support NON-NORMALIZED combining marks in a way
  231. // compatible with the driver architecture. Any CMs (except in the first col)
  232. // are correctly combined with the base char, but are ALSO treated as 1 column
  233. // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`.
  234. //
  235. // Until this is addressed (see Issue #), we do our best by
  236. // a) Attempting to normalize any CM with the base char to it's left
  237. // b) Ignoring any CMs that don't normalize
  238. if (Col > 0)
  239. {
  240. if (Contents [Row, Col - 1].CombiningMarks.Count > 0)
  241. {
  242. // Just add this mark to the list
  243. Contents [Row, Col - 1].AddCombiningMark (rune);
  244. // Ignore. Don't move to next column (let the driver figure out what to do).
  245. }
  246. else
  247. {
  248. // Attempt to normalize the cell to our left combined with this mark
  249. string combined = Contents [Row, Col - 1].Rune + rune.ToString ();
  250. // Normalize to Form C (Canonical Composition)
  251. string normalized = combined.Normalize (NormalizationForm.FormC);
  252. if (normalized.Length == 1)
  253. {
  254. // It normalized! We can just set the Cell to the left with the
  255. // normalized codepoint
  256. Contents [Row, Col - 1].Rune = (Rune)normalized [0];
  257. // Ignore. Don't move to next column because we're already there
  258. }
  259. else
  260. {
  261. // It didn't normalize. Add it to the Cell to left's CM list
  262. Contents [Row, Col - 1].AddCombiningMark (rune);
  263. // Ignore. Don't move to next column (let the driver figure out what to do).
  264. }
  265. }
  266. Contents [Row, Col - 1].Attribute = CurrentAttribute;
  267. Contents [Row, Col - 1].IsDirty = true;
  268. }
  269. else
  270. {
  271. // Most drivers will render a combining mark at col 0 as the mark
  272. Contents [Row, Col].Rune = rune;
  273. Contents [Row, Col].Attribute = CurrentAttribute;
  274. Contents [Row, Col].IsDirty = true;
  275. Col++;
  276. }
  277. }
  278. else
  279. {
  280. Contents [Row, Col].Attribute = CurrentAttribute;
  281. Contents [Row, Col].IsDirty = true;
  282. if (Col > 0)
  283. {
  284. // Check if cell to left has a wide glyph
  285. if (Contents [Row, Col - 1].Rune.GetColumns () > 1)
  286. {
  287. // Invalidate cell to left
  288. Contents [Row, Col - 1].Rune = (Rune)'\0';
  289. Contents [Row, Col - 1].IsDirty = true;
  290. }
  291. }
  292. if (runeWidth < 1)
  293. {
  294. Contents [Row, Col].Rune = Rune.ReplacementChar;
  295. }
  296. else if (runeWidth == 1)
  297. {
  298. Contents [Row, Col].Rune = rune;
  299. if (Col < clipRect.Right - 1)
  300. {
  301. Contents [Row, Col + 1].IsDirty = true;
  302. }
  303. }
  304. else if (runeWidth == 2)
  305. {
  306. if (!Clip.Contains (Col + 1, Row))
  307. {
  308. // We're at the right edge of the clip, so we can't display a wide character.
  309. // TODO: Figure out if it is better to show a replacement character or ' '
  310. Contents [Row, Col].Rune = Rune.ReplacementChar;
  311. }
  312. else if (!Clip.Contains (Col, Row))
  313. {
  314. // Our 1st column is outside the clip, so we can't display a wide character.
  315. Contents [Row, Col + 1].Rune = Rune.ReplacementChar;
  316. }
  317. else
  318. {
  319. Contents [Row, Col].Rune = rune;
  320. if (Col < clipRect.Right - 1)
  321. {
  322. // Invalidate cell to right so that it doesn't get drawn
  323. // TODO: Figure out if it is better to show a replacement character or ' '
  324. Contents [Row, Col + 1].Rune = (Rune)'\0';
  325. Contents [Row, Col + 1].IsDirty = true;
  326. }
  327. }
  328. }
  329. else
  330. {
  331. // This is a non-spacing character, so we don't need to do anything
  332. Contents [Row, Col].Rune = (Rune)' ';
  333. Contents [Row, Col].IsDirty = false;
  334. }
  335. _dirtyLines! [Row] = true;
  336. }
  337. }
  338. }
  339. if (runeWidth is < 0 or > 0)
  340. {
  341. Col++;
  342. }
  343. if (runeWidth > 1)
  344. {
  345. Debug.Assert (runeWidth <= 2);
  346. if (validLocation && Col < clipRect.Right)
  347. {
  348. lock (Contents!)
  349. {
  350. // This is a double-width character, and we are not at the end of the line.
  351. // Col now points to the second column of the character. Ensure it doesn't
  352. // Get rendered.
  353. Contents [Row, Col].IsDirty = false;
  354. Contents [Row, Col].Attribute = CurrentAttribute;
  355. // TODO: Determine if we should wipe this out (for now now)
  356. //Contents [Row, Col].Rune = (Rune)' ';
  357. }
  358. }
  359. Col++;
  360. }
  361. }
  362. /// <summary>
  363. /// Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
  364. /// convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor.
  365. /// </summary>
  366. /// <param name="c">Character to add.</param>
  367. public void AddRune (char c) { AddRune (new Rune (c)); }
  368. /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
  369. /// <remarks>
  370. /// <para>
  371. /// When the method returns, <see cref="Col"/> will be incremented by the number of columns
  372. /// <paramref name="str"/> required, unless the new column value is outside of the <see cref="Clip"/> or screen
  373. /// dimensions defined by <see cref="Cols"/>.
  374. /// </para>
  375. /// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
  376. /// </remarks>
  377. /// <param name="str">String.</param>
  378. public void AddStr (string str)
  379. {
  380. List<Rune> runes = str.EnumerateRunes ().ToList ();
  381. for (var i = 0; i < runes.Count; i++)
  382. {
  383. AddRune (runes [i]);
  384. }
  385. }
  386. /// <summary>Fills the specified rectangle with the specified rune, using <see cref="CurrentAttribute"/></summary>
  387. /// <remarks>
  388. /// The value of <see cref="Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn.
  389. /// </remarks>
  390. /// <param name="rect">The Screen-relative rectangle.</param>
  391. /// <param name="rune">The Rune used to fill the rectangle</param>
  392. public void FillRect (Rectangle rect, Rune rune = default)
  393. {
  394. // BUGBUG: This should be a method on Region
  395. rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
  396. lock (Contents!)
  397. {
  398. for (int r = rect.Y; r < rect.Y + rect.Height; r++)
  399. {
  400. for (int c = rect.X; c < rect.X + rect.Width; c++)
  401. {
  402. if (!IsValidLocation (rune, c, r))
  403. {
  404. continue;
  405. }
  406. Contents [r, c] = new Cell
  407. {
  408. Rune = rune != default ? rune : (Rune)' ',
  409. Attribute = CurrentAttribute, IsDirty = true
  410. };
  411. _dirtyLines! [r] = true;
  412. }
  413. }
  414. }
  415. }
  416. /// <summary>Clears the <see cref="Contents"/> of the driver.</summary>
  417. public void ClearContents ()
  418. {
  419. Contents = new Cell [Rows, Cols];
  420. //CONCURRENCY: Unsynchronized access to Clip isn't safe.
  421. // TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
  422. Clip = new (Screen);
  423. _dirtyLines = new bool [Rows];
  424. lock (Contents)
  425. {
  426. for (var row = 0; row < Rows; row++)
  427. {
  428. for (var c = 0; c < Cols; c++)
  429. {
  430. Contents [row, c] = new ()
  431. {
  432. Rune = (Rune)' ',
  433. Attribute = new Attribute (Color.White, Color.Black),
  434. IsDirty = true
  435. };
  436. }
  437. _dirtyLines [row] = true;
  438. }
  439. }
  440. ClearedContents?.Invoke (this, EventArgs.Empty);
  441. }
  442. /// <summary>
  443. /// Raised each time <see cref="ClearContents"/> is called. For benchmarking.
  444. /// </summary>
  445. public event EventHandler<EventArgs>? ClearedContents;
  446. /// <summary>
  447. /// Sets <see cref="Contents"/> as dirty for situations where views
  448. /// don't need layout and redrawing, but just refresh the screen.
  449. /// </summary>
  450. protected void SetContentsAsDirty ()
  451. {
  452. lock (Contents!)
  453. {
  454. for (var row = 0; row < Rows; row++)
  455. {
  456. for (var c = 0; c < Cols; c++)
  457. {
  458. Contents [row, c].IsDirty = true;
  459. }
  460. _dirtyLines! [row] = true;
  461. }
  462. }
  463. }
  464. /// <summary>
  465. /// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
  466. /// that calls <see cref="FillRect(Rectangle, Rune)"/>.
  467. /// </summary>
  468. /// <param name="rect"></param>
  469. /// <param name="c"></param>
  470. public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); }
  471. #endregion Screen and Contents
  472. #region Cursor Handling
  473. /// <summary>Gets the terminal cursor visibility.</summary>
  474. /// <param name="visibility">The current <see cref="CursorVisibility"/></param>
  475. /// <returns><see langword="true"/> upon success</returns>
  476. public abstract bool GetCursorVisibility (out CursorVisibility visibility);
  477. /// <summary>Tests whether the specified coordinate are valid for drawing the specified Rune.</summary>
  478. /// <param name="rune">Used to determine if one or two columns are required.</param>
  479. /// <param name="col">The column.</param>
  480. /// <param name="row">The row.</param>
  481. /// <returns>
  482. /// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>.
  483. /// <see langword="true"/> otherwise.
  484. /// </returns>
  485. public bool IsValidLocation (Rune rune, int col, int row)
  486. {
  487. if (rune.GetColumns () < 2)
  488. {
  489. return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row);
  490. }
  491. else
  492. {
  493. return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row);
  494. }
  495. }
  496. /// <summary>
  497. /// Called when the terminal screen size changes. This method fires the <see cref="SizeChanged"/> event,
  498. /// notifying subscribers that <see cref="Screen"/>, <see cref="Cols"/>, and <see cref="Rows"/> have changed.
  499. /// </summary>
  500. /// <remarks>
  501. /// <para>
  502. /// This method is typically called internally by the driver when it detects a terminal resize.
  503. /// For <see cref="FakeDriver"/>, it is called by <c>SetBufferSize</c> and <c>SetWindowSize</c> methods.
  504. /// </para>
  505. /// <para>
  506. /// <strong>For driver implementations:</strong> Call this method after updating <see cref="Cols"/> and
  507. /// <see cref="Rows"/> to notify the application that the screen dimensions have changed.
  508. /// </para>
  509. /// <para>
  510. /// <strong>For application code:</strong> Subscribe to the <see cref="SizeChanged"/> event to respond
  511. /// to screen size changes rather than calling this method directly.
  512. /// </para>
  513. /// </remarks>
  514. /// <param name="args">Event arguments containing the new screen size.</param>
  515. /// <seealso cref="SizeChanged"/>
  516. /// <seealso cref="Screen"/>
  517. public void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); }
  518. /// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary>
  519. public void Refresh ()
  520. {
  521. bool updated = UpdateScreen ();
  522. UpdateCursor ();
  523. Refreshed?.Invoke (this, new EventArgs<bool> (in updated));
  524. }
  525. /// <summary>
  526. /// Raised each time <see cref="Refresh"/> is called. For benchmarking.
  527. /// </summary>
  528. public event EventHandler<EventArgs<bool>>? Refreshed;
  529. /// <summary>Sets the terminal cursor visibility.</summary>
  530. /// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
  531. /// <returns><see langword="true"/> upon success</returns>
  532. public abstract bool SetCursorVisibility (CursorVisibility visibility);
  533. /// <summary>
  534. /// Event fired when the terminal screen is resized. Provides the new screen dimensions via
  535. /// <see cref="SizeChangedEventArgs"/>.
  536. /// </summary>
  537. /// <remarks>
  538. /// <para>
  539. /// This event is raised by <see cref="OnSizeChanged"/> when the driver detects or is notified
  540. /// of a terminal size change. At the time this event fires, <see cref="Cols"/>, <see cref="Rows"/>,
  541. /// and <see cref="Screen"/> have already been updated to reflect the new dimensions.
  542. /// </para>
  543. /// <para>
  544. /// <strong>In production drivers:</strong> This event fires when the OS notifies the driver of a
  545. /// terminal window resize (e.g., SIGWINCH on Unix, WINDOW_BUFFER_SIZE_EVENT on Windows).
  546. /// </para>
  547. /// <para>
  548. /// <strong>In FakeDriver:</strong> This event fires when test code calls <c>SetBufferSize</c> or
  549. /// <c>SetWindowSize</c>, allowing tests to simulate and verify resize behavior.
  550. /// </para>
  551. /// <para>
  552. /// <strong>Usage in Application:</strong> <see cref="Application"/> subscribes to this event
  553. /// during initialization and propagates resize notifications to top-level views, triggering layout
  554. /// and redraw operations.
  555. /// </para>
  556. /// </remarks>
  557. /// <seealso cref="OnSizeChanged"/>
  558. /// <seealso cref="Screen"/>
  559. public event EventHandler<SizeChangedEventArgs>? SizeChanged;
  560. #endregion Cursor Handling
  561. /// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary>
  562. /// <remarks>This is only implemented in <see cref="UnixDriver"/>.</remarks>
  563. public abstract void Suspend ();
  564. /// <summary>Sets the position of the terminal cursor to <see cref="Col"/> and <see cref="Row"/>.</summary>
  565. public abstract void UpdateCursor ();
  566. /// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary>
  567. /// <returns><see langword="true"/> if any updates to the screen were made.</returns>
  568. public abstract bool UpdateScreen ();
  569. #region Setup & Teardown
  570. /// <summary>Initializes the driver</summary>
  571. public abstract void Init ();
  572. /// <summary>Ends the execution of the console driver.</summary>
  573. public abstract void End ();
  574. #endregion
  575. #region Color Handling
  576. /// <summary>Gets whether the <see cref="IConsoleDriver"/> supports TrueColor output.</summary>
  577. public virtual bool SupportsTrueColor => true;
  578. // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
  579. // BUGBUG: Application.Force16Colors should be bool? so if SupportsTrueColor and Application.Force16Colors == false, this doesn't override
  580. /// <summary>
  581. /// Gets or sets whether the <see cref="IConsoleDriver"/> should use 16 colors instead of the default TrueColors.
  582. /// See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
  583. /// </summary>
  584. /// <remarks>
  585. /// <para>
  586. /// Will be forced to <see langword="true"/> if <see cref="IConsoleDriver.SupportsTrueColor"/> is
  587. /// <see langword="false"/>, indicating that the <see cref="IConsoleDriver"/> cannot support TrueColor.
  588. /// </para>
  589. /// </remarks>
  590. public virtual bool Force16Colors
  591. {
  592. get => Application.Force16Colors || !SupportsTrueColor;
  593. set => Application.Force16Colors = value || !SupportsTrueColor;
  594. }
  595. private int _cols;
  596. private int _rows;
  597. /// <summary>
  598. /// The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/>
  599. /// call.
  600. /// </summary>
  601. public Attribute CurrentAttribute { get; set; }
  602. /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary>
  603. /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
  604. /// <param name="c">C.</param>
  605. public Attribute SetAttribute (Attribute c)
  606. {
  607. Attribute prevAttribute = CurrentAttribute;
  608. CurrentAttribute = c;
  609. return prevAttribute;
  610. }
  611. /// <summary>Gets the current <see cref="Attribute"/>.</summary>
  612. /// <returns>The current attribute.</returns>
  613. public Attribute GetAttribute () { return CurrentAttribute; }
  614. /// <summary>Makes an <see cref="Attribute"/>.</summary>
  615. /// <param name="foreground">The foreground color.</param>
  616. /// <param name="background">The background color.</param>
  617. /// <returns>The attribute for the foreground and background colors.</returns>
  618. public virtual Attribute MakeColor (in Color foreground, in Color background)
  619. {
  620. // Encode the colors into the int value.
  621. return new (
  622. foreground,
  623. background
  624. );
  625. }
  626. #endregion Color Handling
  627. #region Mouse Handling
  628. /// <summary>Event fired when a mouse event occurs.</summary>
  629. public event EventHandler<MouseEventArgs>? MouseEvent;
  630. /// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
  631. /// <param name="a"></param>
  632. public void OnMouseEvent (MouseEventArgs a)
  633. {
  634. // Ensure ScreenPosition is set
  635. a.ScreenPosition = a.Position;
  636. MouseEvent?.Invoke (this, a);
  637. }
  638. #endregion Mouse Handling
  639. #region Keyboard Handling
  640. /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
  641. public event EventHandler<Key>? KeyDown;
  642. /// <summary>
  643. /// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
  644. /// <see cref="OnKeyUp"/>.
  645. /// </summary>
  646. /// <param name="a"></param>
  647. public void OnKeyDown (Key a) { KeyDown?.Invoke (this, a); }
  648. /// <summary>Event fired when a key is released.</summary>
  649. /// <remarks>
  650. /// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
  651. /// complete.
  652. /// </remarks>
  653. public event EventHandler<Key>? KeyUp;
  654. /// <summary>Called when a key is released. Fires the <see cref="KeyUp"/> event.</summary>
  655. /// <remarks>
  656. /// Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
  657. /// is complete.
  658. /// </remarks>
  659. /// <param name="a"></param>
  660. public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
  661. internal char _highSurrogate = '\0';
  662. internal bool IsValidInput (KeyCode keyCode, out KeyCode result)
  663. {
  664. result = keyCode;
  665. if (char.IsHighSurrogate ((char)keyCode))
  666. {
  667. _highSurrogate = (char)keyCode;
  668. return false;
  669. }
  670. if (_highSurrogate > 0 && char.IsLowSurrogate ((char)keyCode))
  671. {
  672. result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value;
  673. if ((keyCode & KeyCode.AltMask) != 0)
  674. {
  675. result |= KeyCode.AltMask;
  676. }
  677. if ((keyCode & KeyCode.CtrlMask) != 0)
  678. {
  679. result |= KeyCode.CtrlMask;
  680. }
  681. if ((keyCode & KeyCode.ShiftMask) != 0)
  682. {
  683. result |= KeyCode.ShiftMask;
  684. }
  685. _highSurrogate = '\0';
  686. return true;
  687. }
  688. if (char.IsSurrogate ((char)keyCode))
  689. {
  690. return false;
  691. }
  692. if (_highSurrogate > 0)
  693. {
  694. _highSurrogate = '\0';
  695. }
  696. return true;
  697. }
  698. #endregion
  699. private AnsiRequestScheduler? _scheduler;
  700. /// <summary>
  701. /// Queues the given <paramref name="request"/> for execution
  702. /// </summary>
  703. /// <param name="request"></param>
  704. public void QueueAnsiRequest (AnsiEscapeSequenceRequest request)
  705. {
  706. GetRequestScheduler ().SendOrSchedule (request);
  707. }
  708. internal abstract IAnsiResponseParser GetParser ();
  709. /// <summary>
  710. /// Gets the <see cref="AnsiRequestScheduler"/> for this <see cref="ConsoleDriver"/>.
  711. /// </summary>
  712. /// <returns></returns>
  713. public AnsiRequestScheduler GetRequestScheduler ()
  714. {
  715. // Lazy initialization because GetParser is virtual
  716. return _scheduler ??= new (GetParser ());
  717. }
  718. }