CursesDriver.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. #nullable enable
  2. //
  3. // Driver.cs: Curses-based Driver
  4. //
  5. using System.Diagnostics;
  6. using System.Runtime.InteropServices;
  7. using Terminal.Gui.ConsoleDrivers;
  8. using Unix.Terminal;
  9. namespace Terminal.Gui;
  10. /// <summary>A Linux/Mac driver based on the Curses library.</summary>
  11. internal class CursesDriver : ConsoleDriver
  12. {
  13. public override string GetVersionInfo () { return $"{Curses.curses_version ()}"; }
  14. internal override int Cols
  15. {
  16. get => Curses.Cols;
  17. set
  18. {
  19. Curses.Cols = value;
  20. ClearContents ();
  21. }
  22. }
  23. internal override int Rows
  24. {
  25. get => Curses.Lines;
  26. set
  27. {
  28. Curses.Lines = value;
  29. ClearContents ();
  30. }
  31. }
  32. public override bool IsRuneSupported (Rune rune)
  33. {
  34. // See Issue #2615 - CursesDriver is broken with non-BMP characters
  35. return base.IsRuneSupported (rune) && rune.IsBmp;
  36. }
  37. public override void Move (int col, int row)
  38. {
  39. base.Move (col, row);
  40. if (RunningUnitTests)
  41. {
  42. return;
  43. }
  44. if (IsValidLocation (default, col, row))
  45. {
  46. Curses.move (row, col);
  47. }
  48. else
  49. {
  50. // Not a valid location (outside screen or clip region)
  51. // Move within the clip region, then AddRune will actually move to Col, Row
  52. Rectangle clipRect = Clip.GetBounds ();
  53. Curses.move (clipRect.Y, clipRect.X);
  54. }
  55. }
  56. public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
  57. {
  58. KeyCode key;
  59. if (consoleKey == ConsoleKey.Packet)
  60. {
  61. //var mod = new ConsoleModifiers ();
  62. //if (shift)
  63. //{
  64. // mod |= ConsoleModifiers.Shift;
  65. //}
  66. //if (alt)
  67. //{
  68. // mod |= ConsoleModifiers.Alt;
  69. //}
  70. //if (control)
  71. //{
  72. // mod |= ConsoleModifiers.Control;
  73. //}
  74. var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
  75. cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
  76. key = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (cKeyInfo);
  77. }
  78. else
  79. {
  80. key = (KeyCode)keyChar;
  81. }
  82. OnKeyDown (new (key));
  83. OnKeyUp (new (key));
  84. //OnKeyPressed (new KeyEventArgsEventArgs (key));
  85. }
  86. public void StartReportingMouseMoves ()
  87. {
  88. if (!RunningUnitTests)
  89. {
  90. Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_EnableMouseEvents);
  91. }
  92. }
  93. public void StopReportingMouseMoves ()
  94. {
  95. if (!RunningUnitTests)
  96. {
  97. Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_DisableMouseEvents);
  98. }
  99. }
  100. public override void Suspend ()
  101. {
  102. StopReportingMouseMoves ();
  103. if (!RunningUnitTests)
  104. {
  105. Platform.Suspend ();
  106. if (Force16Colors)
  107. {
  108. Curses.Window.Standard.redrawwin ();
  109. Curses.refresh ();
  110. }
  111. }
  112. StartReportingMouseMoves ();
  113. }
  114. public override void UpdateCursor ()
  115. {
  116. EnsureCursorVisibility ();
  117. if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
  118. {
  119. if (Force16Colors)
  120. {
  121. Curses.move (Row, Col);
  122. Curses.raw ();
  123. Curses.noecho ();
  124. Curses.refresh ();
  125. }
  126. else
  127. {
  128. _mainLoopDriver?.WriteRaw (AnsiEscapeSequenceRequestUtils.CSI_SetCursorPosition (Row + 1, Col + 1));
  129. }
  130. }
  131. }
  132. public override bool UpdateScreen ()
  133. {
  134. bool updated = false;
  135. if (Force16Colors)
  136. {
  137. for (var row = 0; row < Rows; row++)
  138. {
  139. if (!_dirtyLines! [row])
  140. {
  141. continue;
  142. }
  143. _dirtyLines [row] = false;
  144. for (var col = 0; col < Cols; col++)
  145. {
  146. if (Contents! [row, col].IsDirty == false)
  147. {
  148. continue;
  149. }
  150. if (RunningUnitTests)
  151. {
  152. // In unit tests, we don't want to actually write to the screen.
  153. continue;
  154. }
  155. Curses.attrset (Contents [row, col].Attribute.GetValueOrDefault ().PlatformColor);
  156. Rune rune = Contents [row, col].Rune;
  157. if (rune.IsBmp)
  158. {
  159. // BUGBUG: CursesDriver doesn't render CharMap correctly for wide chars (and other Unicode) - Curses is doing something funky with glyphs that report GetColums() of 1 yet are rendered wide. E.g. 0x2064 (invisible times) is reported as 1 column but is rendered as 2. WindowsDriver & NetDriver correctly render this as 1 column, overlapping the next cell.
  160. if (rune.GetColumns () < 2)
  161. {
  162. Curses.mvaddch (row, col, rune.Value);
  163. }
  164. else /*if (col + 1 < Cols)*/
  165. {
  166. Curses.mvaddwstr (row, col, rune.ToString ());
  167. }
  168. }
  169. else
  170. {
  171. Curses.mvaddwstr (row, col, rune.ToString ());
  172. if (rune.GetColumns () > 1 && col + 1 < Cols)
  173. {
  174. // TODO: This is a hack to deal with non-BMP and wide characters.
  175. //col++;
  176. Curses.mvaddch (row, ++col, '*');
  177. }
  178. }
  179. }
  180. }
  181. if (!RunningUnitTests)
  182. {
  183. Curses.move (Row, Col);
  184. _window?.wrefresh ();
  185. }
  186. }
  187. else
  188. {
  189. if (RunningUnitTests
  190. || Console.WindowHeight < 1
  191. || Contents!.Length != Rows * Cols
  192. || Rows != Console.WindowHeight)
  193. {
  194. return updated;
  195. }
  196. var top = 0;
  197. var left = 0;
  198. int rows = Rows;
  199. int cols = Cols;
  200. var output = new StringBuilder ();
  201. Attribute? redrawAttr = null;
  202. int lastCol = -1;
  203. CursorVisibility? savedVisibility = _currentCursorVisibility;
  204. SetCursorVisibility (CursorVisibility.Invisible);
  205. for (int row = top; row < rows; row++)
  206. {
  207. if (Console.WindowHeight < 1)
  208. {
  209. return updated;
  210. }
  211. if (!_dirtyLines! [row])
  212. {
  213. continue;
  214. }
  215. if (!SetCursorPosition (0, row))
  216. {
  217. return updated;
  218. }
  219. _dirtyLines [row] = false;
  220. output.Clear ();
  221. for (int col = left; col < cols; col++)
  222. {
  223. lastCol = -1;
  224. var outputWidth = 0;
  225. for (; col < cols; col++)
  226. {
  227. updated = true;
  228. if (!Contents [row, col].IsDirty)
  229. {
  230. if (output.Length > 0)
  231. {
  232. WriteToConsole (output, ref lastCol, row, ref outputWidth);
  233. }
  234. else if (lastCol == -1)
  235. {
  236. lastCol = col;
  237. }
  238. if (lastCol + 1 < cols)
  239. {
  240. lastCol++;
  241. }
  242. continue;
  243. }
  244. if (lastCol == -1)
  245. {
  246. lastCol = col;
  247. }
  248. Attribute attr = Contents [row, col].Attribute!.Value;
  249. // Performance: Only send the escape sequence if the attribute has changed.
  250. if (attr != redrawAttr)
  251. {
  252. redrawAttr = attr;
  253. output.Append (
  254. AnsiEscapeSequenceRequestUtils.CSI_SetForegroundColorRGB (
  255. attr.Foreground.R,
  256. attr.Foreground.G,
  257. attr.Foreground.B
  258. )
  259. );
  260. output.Append (
  261. AnsiEscapeSequenceRequestUtils.CSI_SetBackgroundColorRGB (
  262. attr.Background.R,
  263. attr.Background.G,
  264. attr.Background.B
  265. )
  266. );
  267. }
  268. outputWidth++;
  269. Rune rune = Contents [row, col].Rune;
  270. output.Append (rune);
  271. if (Contents [row, col].CombiningMarks.Count > 0)
  272. {
  273. // AtlasEngine does not support NON-NORMALIZED combining marks in a way
  274. // compatible with the driver architecture. Any CMs (except in the first col)
  275. // are correctly combined with the base char, but are ALSO treated as 1 column
  276. // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`.
  277. //
  278. // For now, we just ignore the list of CMs.
  279. //foreach (var combMark in Contents [row, col].CombiningMarks) {
  280. // output.Append (combMark);
  281. //}
  282. // WriteToConsole (output, ref lastCol, row, ref outputWidth);
  283. }
  284. else if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
  285. {
  286. WriteToConsole (output, ref lastCol, row, ref outputWidth);
  287. SetCursorPosition (col - 1, row);
  288. }
  289. Contents [row, col].IsDirty = false;
  290. }
  291. }
  292. if (output.Length > 0)
  293. {
  294. SetCursorPosition (lastCol, row);
  295. Console.Write (output);
  296. }
  297. }
  298. // SIXELS
  299. foreach (SixelToRender s in Application.Sixel)
  300. {
  301. SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
  302. Console.Write (s.SixelData);
  303. }
  304. SetCursorPosition (0, 0);
  305. _currentCursorVisibility = savedVisibility;
  306. void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
  307. {
  308. SetCursorPosition (lastCol, row);
  309. Console.Write (output);
  310. output.Clear ();
  311. lastCol += outputWidth;
  312. outputWidth = 0;
  313. }
  314. }
  315. return updated;
  316. }
  317. #region Color Handling
  318. public override bool SupportsTrueColor => true;
  319. /// <summary>Creates an Attribute from the provided curses-based foreground and background color numbers</summary>
  320. /// <param name="foreground">Contains the curses color number for the foreground (color, plus any attributes)</param>
  321. /// <param name="background">Contains the curses color number for the background (color, plus any attributes)</param>
  322. /// <returns></returns>
  323. private static Attribute MakeColor (short foreground, short background)
  324. {
  325. //var v = (short)((ushort)foreground | (background << 4));
  326. var v = (short)(((ushort)(foreground & 0xffff) << 16) | (background & 0xffff));
  327. // TODO: for TrueColor - Use InitExtendedPair
  328. Curses.InitColorPair (v, foreground, background);
  329. return new (
  330. Curses.ColorPair (v),
  331. CursesColorNumberToColorName16 (foreground),
  332. CursesColorNumberToColorName16 (background)
  333. );
  334. }
  335. /// <inheritdoc/>
  336. /// <remarks>
  337. /// In the CursesDriver, colors are encoded as an int. The foreground color is stored in the most significant 4
  338. /// bits, and the background color is stored in the least significant 4 bits. The Terminal.GUi Color values are
  339. /// converted to curses color encoding before being encoded.
  340. /// </remarks>
  341. public override Attribute MakeColor (in Color foreground, in Color background)
  342. {
  343. if (!RunningUnitTests && Force16Colors)
  344. {
  345. return MakeColor (
  346. ColorNameToCursesColorNumber (foreground.GetClosestNamedColor16 ()),
  347. ColorNameToCursesColorNumber (background.GetClosestNamedColor16 ())
  348. );
  349. }
  350. return new (
  351. 0,
  352. foreground,
  353. background
  354. );
  355. }
  356. private static short ColorNameToCursesColorNumber (ColorName16 color)
  357. {
  358. switch (color)
  359. {
  360. case ColorName16.Black:
  361. return Curses.COLOR_BLACK;
  362. case ColorName16.Blue:
  363. return Curses.COLOR_BLUE;
  364. case ColorName16.Green:
  365. return Curses.COLOR_GREEN;
  366. case ColorName16.Cyan:
  367. return Curses.COLOR_CYAN;
  368. case ColorName16.Red:
  369. return Curses.COLOR_RED;
  370. case ColorName16.Magenta:
  371. return Curses.COLOR_MAGENTA;
  372. case ColorName16.Yellow:
  373. return Curses.COLOR_YELLOW;
  374. case ColorName16.Gray:
  375. return Curses.COLOR_WHITE;
  376. case ColorName16.DarkGray:
  377. return Curses.COLOR_GRAY;
  378. case ColorName16.BrightBlue:
  379. return Curses.COLOR_BLUE | Curses.COLOR_GRAY;
  380. case ColorName16.BrightGreen:
  381. return Curses.COLOR_GREEN | Curses.COLOR_GRAY;
  382. case ColorName16.BrightCyan:
  383. return Curses.COLOR_CYAN | Curses.COLOR_GRAY;
  384. case ColorName16.BrightRed:
  385. return Curses.COLOR_RED | Curses.COLOR_GRAY;
  386. case ColorName16.BrightMagenta:
  387. return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY;
  388. case ColorName16.BrightYellow:
  389. return Curses.COLOR_YELLOW | Curses.COLOR_GRAY;
  390. case ColorName16.White:
  391. return Curses.COLOR_WHITE | Curses.COLOR_GRAY;
  392. }
  393. throw new ArgumentException ("Invalid color code");
  394. }
  395. private static ColorName16 CursesColorNumberToColorName16 (short color)
  396. {
  397. switch (color)
  398. {
  399. case Curses.COLOR_BLACK:
  400. return ColorName16.Black;
  401. case Curses.COLOR_BLUE:
  402. return ColorName16.Blue;
  403. case Curses.COLOR_GREEN:
  404. return ColorName16.Green;
  405. case Curses.COLOR_CYAN:
  406. return ColorName16.Cyan;
  407. case Curses.COLOR_RED:
  408. return ColorName16.Red;
  409. case Curses.COLOR_MAGENTA:
  410. return ColorName16.Magenta;
  411. case Curses.COLOR_YELLOW:
  412. return ColorName16.Yellow;
  413. case Curses.COLOR_WHITE:
  414. return ColorName16.Gray;
  415. case Curses.COLOR_GRAY:
  416. return ColorName16.DarkGray;
  417. case Curses.COLOR_BLUE | Curses.COLOR_GRAY:
  418. return ColorName16.BrightBlue;
  419. case Curses.COLOR_GREEN | Curses.COLOR_GRAY:
  420. return ColorName16.BrightGreen;
  421. case Curses.COLOR_CYAN | Curses.COLOR_GRAY:
  422. return ColorName16.BrightCyan;
  423. case Curses.COLOR_RED | Curses.COLOR_GRAY:
  424. return ColorName16.BrightRed;
  425. case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY:
  426. return ColorName16.BrightMagenta;
  427. case Curses.COLOR_YELLOW | Curses.COLOR_GRAY:
  428. return ColorName16.BrightYellow;
  429. case Curses.COLOR_WHITE | Curses.COLOR_GRAY:
  430. return ColorName16.White;
  431. }
  432. throw new ArgumentException ("Invalid curses color code");
  433. }
  434. #endregion
  435. private CursorVisibility? _currentCursorVisibility;
  436. private CursorVisibility? _initialCursorVisibility;
  437. /// <inheritdoc/>
  438. public override bool EnsureCursorVisibility ()
  439. {
  440. if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
  441. {
  442. GetCursorVisibility (out CursorVisibility cursorVisibility);
  443. _currentCursorVisibility = cursorVisibility;
  444. SetCursorVisibility (CursorVisibility.Invisible);
  445. return false;
  446. }
  447. SetCursorVisibility (_currentCursorVisibility ?? CursorVisibility.Default);
  448. return _currentCursorVisibility == CursorVisibility.Default;
  449. }
  450. /// <inheritdoc/>
  451. public override bool GetCursorVisibility (out CursorVisibility visibility)
  452. {
  453. visibility = CursorVisibility.Invisible;
  454. if (!_currentCursorVisibility.HasValue)
  455. {
  456. return false;
  457. }
  458. visibility = _currentCursorVisibility.Value;
  459. return true;
  460. }
  461. /// <inheritdoc/>
  462. public override bool SetCursorVisibility (CursorVisibility visibility)
  463. {
  464. if (_initialCursorVisibility.HasValue == false)
  465. {
  466. return false;
  467. }
  468. if (!RunningUnitTests)
  469. {
  470. Curses.curs_set (((int)visibility >> 16) & 0x000000FF);
  471. }
  472. if (visibility != CursorVisibility.Invisible)
  473. {
  474. _mainLoopDriver?.WriteRaw (
  475. AnsiEscapeSequenceRequestUtils.CSI_SetCursorStyle (
  476. (AnsiEscapeSequenceRequestUtils.DECSCUSR_Style)
  477. (((int)visibility >> 24)
  478. & 0xFF)
  479. )
  480. );
  481. }
  482. _currentCursorVisibility = visibility;
  483. return true;
  484. }
  485. private bool SetCursorPosition (int col, int row)
  486. {
  487. // + 1 is needed because non-Windows is based on 1 instead of 0 and
  488. // Console.CursorTop/CursorLeft isn't reliable.
  489. Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_SetCursorPosition (row + 1, col + 1));
  490. return true;
  491. }
  492. #region Init/End/MainLoop
  493. public Curses.Window? _window;
  494. private UnixMainLoop? _mainLoopDriver;
  495. internal override MainLoop Init ()
  496. {
  497. _mainLoopDriver = new (this);
  498. if (!RunningUnitTests)
  499. {
  500. _window = Curses.initscr ();
  501. Curses.set_escdelay (10);
  502. // Ensures that all procedures are performed at some previous closing.
  503. Curses.doupdate ();
  504. //
  505. // We are setting Invisible as default, so we could ignore XTerm DECSUSR setting
  506. //
  507. switch (Curses.curs_set (0))
  508. {
  509. case 0:
  510. _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Invisible;
  511. break;
  512. case 1:
  513. _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Underline;
  514. Curses.curs_set (1);
  515. break;
  516. case 2:
  517. _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Box;
  518. Curses.curs_set (2);
  519. break;
  520. default:
  521. _currentCursorVisibility = _initialCursorVisibility = null;
  522. break;
  523. }
  524. if (!Curses.HasColors)
  525. {
  526. throw new InvalidOperationException ("V2 - This should never happen. File an Issue if it does.");
  527. }
  528. Curses.raw ();
  529. Curses.noecho ();
  530. Curses.Window.Standard.keypad (true);
  531. Curses.StartColor ();
  532. Curses.UseDefaultColors ();
  533. if (!RunningUnitTests)
  534. {
  535. Curses.timeout (0);
  536. }
  537. }
  538. CurrentAttribute = new (ColorName16.White, ColorName16.Black);
  539. if (Environment.OSVersion.Platform == PlatformID.Win32NT)
  540. {
  541. Clipboard = new FakeDriver.FakeClipboard ();
  542. }
  543. else
  544. {
  545. if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
  546. {
  547. Clipboard = new MacOSXClipboard ();
  548. }
  549. else
  550. {
  551. if (Is_WSL_Platform ())
  552. {
  553. Clipboard = new WSLClipboard ();
  554. }
  555. else
  556. {
  557. Clipboard = new CursesClipboard ();
  558. }
  559. }
  560. }
  561. ClearContents ();
  562. StartReportingMouseMoves ();
  563. if (!RunningUnitTests)
  564. {
  565. Curses.CheckWinChange ();
  566. ClearContents ();
  567. if (Force16Colors)
  568. {
  569. Curses.refresh ();
  570. }
  571. }
  572. return new (_mainLoopDriver);
  573. }
  574. internal void ProcessInput (UnixMainLoop.PollData inputEvent)
  575. {
  576. switch (inputEvent.EventType)
  577. {
  578. case UnixMainLoop.EventType.Key:
  579. ConsoleKeyInfo consoleKeyInfo = inputEvent.KeyEvent;
  580. KeyCode map = AnsiEscapeSequenceRequestUtils.MapKey (consoleKeyInfo);
  581. if (map == KeyCode.Null)
  582. {
  583. break;
  584. }
  585. OnKeyDown (new (map));
  586. OnKeyUp (new (map));
  587. break;
  588. case UnixMainLoop.EventType.Mouse:
  589. var me = new MouseEventArgs { Position = inputEvent.MouseEvent.Position, Flags = inputEvent.MouseEvent.MouseFlags };
  590. OnMouseEvent (me);
  591. break;
  592. case UnixMainLoop.EventType.WindowSize:
  593. Size size = new (inputEvent.WindowSizeEvent.Size.Width, inputEvent.WindowSizeEvent.Size.Height);
  594. ProcessWinChange (size);
  595. break;
  596. default:
  597. throw new ArgumentOutOfRangeException ();
  598. }
  599. }
  600. private void ProcessWinChange (Size size)
  601. {
  602. if (!RunningUnitTests && Curses.ChangeWindowSize (size.Height, size.Width))
  603. {
  604. ClearContents ();
  605. OnSizeChanged (new (new (Cols, Rows)));
  606. }
  607. }
  608. internal override void End ()
  609. {
  610. _ansiResponseTokenSource?.Cancel ();
  611. _ansiResponseTokenSource?.Dispose ();
  612. _waitAnsiResponse.Dispose ();
  613. StopReportingMouseMoves ();
  614. SetCursorVisibility (CursorVisibility.Default);
  615. if (RunningUnitTests)
  616. {
  617. return;
  618. }
  619. // throws away any typeahead that has been typed by
  620. // the user and has not yet been read by the program.
  621. Curses.flushinp ();
  622. Curses.endwin ();
  623. }
  624. #endregion Init/End/MainLoop
  625. public static bool Is_WSL_Platform ()
  626. {
  627. // xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell
  628. //if (new CursesClipboard ().IsSupported) {
  629. // // If xclip is installed on Linux under WSL, this will return true.
  630. // return false;
  631. //}
  632. (int exitCode, string result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true);
  633. if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL"))
  634. {
  635. return true;
  636. }
  637. return false;
  638. }
  639. private readonly ManualResetEventSlim _waitAnsiResponse = new (false);
  640. private CancellationTokenSource? _ansiResponseTokenSource;
  641. /// <inheritdoc/>
  642. public override bool TryWriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
  643. {
  644. if (_mainLoopDriver is null)
  645. {
  646. return false;
  647. }
  648. _ansiResponseTokenSource ??= new ();
  649. try
  650. {
  651. lock (ansiRequest._responseLock)
  652. {
  653. ansiRequest.ResponseFromInput += (s, e) =>
  654. {
  655. Debug.Assert (s == ansiRequest);
  656. Debug.Assert (e == ansiRequest.AnsiEscapeSequenceResponse);
  657. _waitAnsiResponse.Set ();
  658. };
  659. AnsiEscapeSequenceRequests.Add (ansiRequest);
  660. WriteRaw (ansiRequest.Request);
  661. _mainLoopDriver._forceRead = true;
  662. }
  663. if (!_ansiResponseTokenSource.IsCancellationRequested)
  664. {
  665. _mainLoopDriver._waitForInput.Set ();
  666. _waitAnsiResponse.Wait (_ansiResponseTokenSource.Token);
  667. }
  668. }
  669. catch (OperationCanceledException)
  670. {
  671. return false;
  672. }
  673. lock (ansiRequest._responseLock)
  674. {
  675. _mainLoopDriver._forceRead = false;
  676. if (AnsiEscapeSequenceRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus? request))
  677. {
  678. if (AnsiEscapeSequenceRequests.Statuses.Count > 0
  679. && string.IsNullOrEmpty (request.AnsiRequest.AnsiEscapeSequenceResponse?.Response))
  680. {
  681. lock (request.AnsiRequest._responseLock)
  682. {
  683. // Bad request or no response at all
  684. AnsiEscapeSequenceRequests.Statuses.TryDequeue (out _);
  685. }
  686. }
  687. }
  688. _waitAnsiResponse.Reset ();
  689. return ansiRequest.AnsiEscapeSequenceResponse is { Valid: true };
  690. }
  691. }
  692. /// <inheritdoc/>
  693. internal override void WriteRaw (string ansi) { _mainLoopDriver?.WriteRaw (ansi); }
  694. }
  695. // TODO: One type per file - move to another file
  696. internal static class Platform
  697. {
  698. private static int _suspendSignal;
  699. /// <summary>Suspends the process by sending SIGTSTP to itself</summary>
  700. /// <returns>True if the suspension was successful.</returns>
  701. public static bool Suspend ()
  702. {
  703. int signal = GetSuspendSignal ();
  704. if (signal == -1)
  705. {
  706. return false;
  707. }
  708. killpg (0, signal);
  709. return true;
  710. }
  711. private static int GetSuspendSignal ()
  712. {
  713. if (_suspendSignal != 0)
  714. {
  715. return _suspendSignal;
  716. }
  717. nint buf = Marshal.AllocHGlobal (8192);
  718. if (uname (buf) != 0)
  719. {
  720. Marshal.FreeHGlobal (buf);
  721. _suspendSignal = -1;
  722. return _suspendSignal;
  723. }
  724. try
  725. {
  726. switch (Marshal.PtrToStringAnsi (buf))
  727. {
  728. case "Darwin":
  729. case "DragonFly":
  730. case "FreeBSD":
  731. case "NetBSD":
  732. case "OpenBSD":
  733. _suspendSignal = 18;
  734. break;
  735. case "Linux":
  736. // TODO: should fetch the machine name and
  737. // if it is MIPS return 24
  738. _suspendSignal = 20;
  739. break;
  740. case "Solaris":
  741. _suspendSignal = 24;
  742. break;
  743. default:
  744. _suspendSignal = -1;
  745. break;
  746. }
  747. return _suspendSignal;
  748. }
  749. finally
  750. {
  751. Marshal.FreeHGlobal (buf);
  752. }
  753. }
  754. [DllImport ("libc")]
  755. private static extern int killpg (int pgrp, int pid);
  756. [DllImport ("libc")]
  757. private static extern int uname (nint buf);
  758. }