CursesDriver.cs 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165
  1. #nullable enable
  2. //
  3. // Driver.cs: Curses-based Driver
  4. //
  5. using System.Runtime.InteropServices;
  6. using Unix.Terminal;
  7. namespace Terminal.Gui.Drivers;
  8. /// <summary>A Linux/Mac driver based on the Curses library.</summary>
  9. internal class CursesDriver : ConsoleDriver
  10. {
  11. public override string GetVersionInfo () { return $"{Curses.curses_version ()}"; }
  12. public override int Cols
  13. {
  14. get => Curses.Cols;
  15. set
  16. {
  17. Curses.Cols = value;
  18. ClearContents ();
  19. }
  20. }
  21. public override int Rows
  22. {
  23. get => Curses.Lines;
  24. set
  25. {
  26. Curses.Lines = value;
  27. ClearContents ();
  28. }
  29. }
  30. public override bool IsRuneSupported (Rune rune)
  31. {
  32. // See Issue #2615 - CursesDriver is broken with non-BMP characters
  33. return base.IsRuneSupported (rune) && rune.IsBmp;
  34. }
  35. public override void Move (int col, int row)
  36. {
  37. base.Move (col, row);
  38. if (RunningUnitTests)
  39. {
  40. return;
  41. }
  42. if (IsValidLocation (default, col, row))
  43. {
  44. Curses.move (row, col);
  45. }
  46. else
  47. {
  48. // Not a valid location (outside screen or clip region)
  49. // Move within the clip region, then AddRune will actually move to Col, Row
  50. Rectangle clipRect = Clip!.GetBounds ();
  51. Curses.move (clipRect.Y, clipRect.X);
  52. }
  53. }
  54. public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
  55. {
  56. KeyCode key;
  57. if (consoleKey == ConsoleKey.Packet)
  58. {
  59. //var mod = new ConsoleModifiers ();
  60. //if (shift)
  61. //{
  62. // mod |= ConsoleModifiers.Shift;
  63. //}
  64. //if (alt)
  65. //{
  66. // mod |= ConsoleModifiers.Alt;
  67. //}
  68. //if (control)
  69. //{
  70. // mod |= ConsoleModifiers.Control;
  71. //}
  72. var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
  73. cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
  74. key = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (cKeyInfo);
  75. }
  76. else
  77. {
  78. key = (KeyCode)keyChar;
  79. }
  80. OnKeyDown (new (key));
  81. OnKeyUp (new (key));
  82. //OnKeyPressed (new KeyEventArgsEventArgs (key));
  83. }
  84. public void StartReportingMouseMoves ()
  85. {
  86. if (!RunningUnitTests)
  87. {
  88. Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
  89. }
  90. }
  91. public void StopReportingMouseMoves ()
  92. {
  93. if (!RunningUnitTests)
  94. {
  95. Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
  96. }
  97. }
  98. public override void Suspend ()
  99. {
  100. StopReportingMouseMoves ();
  101. if (!RunningUnitTests)
  102. {
  103. Platform.Suspend ();
  104. }
  105. StartReportingMouseMoves ();
  106. }
  107. public override void UpdateCursor ()
  108. {
  109. EnsureCursorVisibility ();
  110. if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
  111. {
  112. _mainLoopDriver?.WriteRaw (EscSeqUtils.CSI_SetCursorPosition (Row + 1, Col + 1));
  113. }
  114. }
  115. public override bool UpdateScreen ()
  116. {
  117. bool updated = false;
  118. if (RunningUnitTests
  119. || Console.WindowHeight < 1
  120. || Contents?.Length != Rows * Cols
  121. || Rows != Console.WindowHeight)
  122. {
  123. return updated;
  124. }
  125. var top = 0;
  126. var left = 0;
  127. int rows = Rows;
  128. int cols = Cols;
  129. var output = new StringBuilder ();
  130. Attribute? redrawAttr = null;
  131. int lastCol = -1;
  132. CursorVisibility? savedVisibility = _currentCursorVisibility;
  133. SetCursorVisibility (CursorVisibility.Invisible);
  134. for (int row = top; row < rows; row++)
  135. {
  136. if (Console.WindowHeight < 1)
  137. {
  138. return updated;
  139. }
  140. if (!_dirtyLines! [row])
  141. {
  142. continue;
  143. }
  144. if (!SetCursorPosition (0, row))
  145. {
  146. return updated;
  147. }
  148. updated = true;
  149. _dirtyLines [row] = false;
  150. output.Clear ();
  151. for (int col = left; col < cols; col++)
  152. {
  153. lastCol = -1;
  154. var outputWidth = 0;
  155. for (; col < cols; col++)
  156. {
  157. if (!Contents [row, col].IsDirty)
  158. {
  159. if (output.Length > 0)
  160. {
  161. WriteToConsole (output, ref lastCol, row, ref outputWidth);
  162. }
  163. else if (lastCol == -1)
  164. {
  165. lastCol = col;
  166. }
  167. if (lastCol + 1 < cols)
  168. {
  169. lastCol++;
  170. }
  171. continue;
  172. }
  173. if (lastCol == -1)
  174. {
  175. lastCol = col;
  176. }
  177. Attribute attr = Contents [row, col].Attribute!.Value;
  178. // Performance: Only send the escape sequence if the attribute has changed.
  179. if (attr != redrawAttr)
  180. {
  181. redrawAttr = attr;
  182. if (Force16Colors)
  183. {
  184. output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
  185. output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));
  186. }
  187. else
  188. {
  189. output.Append (
  190. EscSeqUtils.CSI_SetForegroundColorRGB (
  191. attr.Foreground.R,
  192. attr.Foreground.G,
  193. attr.Foreground.B
  194. )
  195. );
  196. output.Append (
  197. EscSeqUtils.CSI_SetBackgroundColorRGB (
  198. attr.Background.R,
  199. attr.Background.G,
  200. attr.Background.B
  201. )
  202. );
  203. }
  204. }
  205. outputWidth++;
  206. Rune rune = Contents [row, col].Rune;
  207. output.Append (rune);
  208. if (Contents [row, col].CombiningMarks.Count > 0)
  209. {
  210. // AtlasEngine does not support NON-NORMALIZED combining marks in a way
  211. // compatible with the driver architecture. Any CMs (except in the first col)
  212. // are correctly combined with the base char, but are ALSO treated as 1 column
  213. // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`.
  214. //
  215. // For now, we just ignore the list of CMs.
  216. //foreach (var combMark in Contents [row, col].CombiningMarks) {
  217. // output.Append (combMark);
  218. //}
  219. // WriteToConsole (output, ref lastCol, row, ref outputWidth);
  220. }
  221. else if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
  222. {
  223. WriteToConsole (output, ref lastCol, row, ref outputWidth);
  224. SetCursorPosition (col - 1, row);
  225. }
  226. Contents [row, col].IsDirty = false;
  227. }
  228. }
  229. if (output.Length > 0)
  230. {
  231. SetCursorPosition (lastCol, row);
  232. Console.Write (output);
  233. }
  234. foreach (var s in Application.Sixel)
  235. {
  236. if (!string.IsNullOrWhiteSpace (s.SixelData))
  237. {
  238. SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
  239. Console.Write (s.SixelData);
  240. }
  241. }
  242. }
  243. SetCursorPosition (0, 0);
  244. _currentCursorVisibility = savedVisibility;
  245. void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
  246. {
  247. SetCursorPosition (lastCol, row);
  248. Console.Write (output);
  249. output.Clear ();
  250. lastCol += outputWidth;
  251. outputWidth = 0;
  252. }
  253. return updated;
  254. }
  255. #region Color Handling
  256. public override bool SupportsTrueColor => true;
  257. /// <summary>Creates an Attribute from the provided curses-based foreground and background color numbers</summary>
  258. /// <param name="foreground">Contains the curses color number for the foreground (color, plus any attributes)</param>
  259. /// <param name="background">Contains the curses color number for the background (color, plus any attributes)</param>
  260. /// <returns></returns>
  261. private static Attribute MakeColor (short foreground, short background)
  262. {
  263. //var v = (short)((ushort)foreground | (background << 4));
  264. var v = (short)(((ushort)(foreground & 0xffff) << 16) | (background & 0xffff));
  265. // TODO: for TrueColor - Use InitExtendedPair
  266. Curses.InitColorPair (v, foreground, background);
  267. return new (
  268. Curses.ColorPair (v),
  269. CursesColorNumberToColorName16 (foreground),
  270. CursesColorNumberToColorName16 (background)
  271. );
  272. }
  273. private static short ColorNameToCursesColorNumber (ColorName16 color)
  274. {
  275. switch (color)
  276. {
  277. case ColorName16.Black:
  278. return Curses.COLOR_BLACK;
  279. case ColorName16.Blue:
  280. return Curses.COLOR_BLUE;
  281. case ColorName16.Green:
  282. return Curses.COLOR_GREEN;
  283. case ColorName16.Cyan:
  284. return Curses.COLOR_CYAN;
  285. case ColorName16.Red:
  286. return Curses.COLOR_RED;
  287. case ColorName16.Magenta:
  288. return Curses.COLOR_MAGENTA;
  289. case ColorName16.Yellow:
  290. return Curses.COLOR_YELLOW;
  291. case ColorName16.Gray:
  292. return Curses.COLOR_WHITE;
  293. case ColorName16.DarkGray:
  294. return Curses.COLOR_GRAY;
  295. case ColorName16.BrightBlue:
  296. return Curses.COLOR_BLUE | Curses.COLOR_GRAY;
  297. case ColorName16.BrightGreen:
  298. return Curses.COLOR_GREEN | Curses.COLOR_GRAY;
  299. case ColorName16.BrightCyan:
  300. return Curses.COLOR_CYAN | Curses.COLOR_GRAY;
  301. case ColorName16.BrightRed:
  302. return Curses.COLOR_RED | Curses.COLOR_GRAY;
  303. case ColorName16.BrightMagenta:
  304. return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY;
  305. case ColorName16.BrightYellow:
  306. return Curses.COLOR_YELLOW | Curses.COLOR_GRAY;
  307. case ColorName16.White:
  308. return Curses.COLOR_WHITE | Curses.COLOR_GRAY;
  309. }
  310. throw new ArgumentException ("Invalid color code");
  311. }
  312. private static ColorName16 CursesColorNumberToColorName16 (short color)
  313. {
  314. switch (color)
  315. {
  316. case Curses.COLOR_BLACK:
  317. return ColorName16.Black;
  318. case Curses.COLOR_BLUE:
  319. return ColorName16.Blue;
  320. case Curses.COLOR_GREEN:
  321. return ColorName16.Green;
  322. case Curses.COLOR_CYAN:
  323. return ColorName16.Cyan;
  324. case Curses.COLOR_RED:
  325. return ColorName16.Red;
  326. case Curses.COLOR_MAGENTA:
  327. return ColorName16.Magenta;
  328. case Curses.COLOR_YELLOW:
  329. return ColorName16.Yellow;
  330. case Curses.COLOR_WHITE:
  331. return ColorName16.Gray;
  332. case Curses.COLOR_GRAY:
  333. return ColorName16.DarkGray;
  334. case Curses.COLOR_BLUE | Curses.COLOR_GRAY:
  335. return ColorName16.BrightBlue;
  336. case Curses.COLOR_GREEN | Curses.COLOR_GRAY:
  337. return ColorName16.BrightGreen;
  338. case Curses.COLOR_CYAN | Curses.COLOR_GRAY:
  339. return ColorName16.BrightCyan;
  340. case Curses.COLOR_RED | Curses.COLOR_GRAY:
  341. return ColorName16.BrightRed;
  342. case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY:
  343. return ColorName16.BrightMagenta;
  344. case Curses.COLOR_YELLOW | Curses.COLOR_GRAY:
  345. return ColorName16.BrightYellow;
  346. case Curses.COLOR_WHITE | Curses.COLOR_GRAY:
  347. return ColorName16.White;
  348. }
  349. throw new ArgumentException ("Invalid curses color code");
  350. }
  351. #endregion
  352. private CursorVisibility? _currentCursorVisibility;
  353. private CursorVisibility? _initialCursorVisibility;
  354. private void EnsureCursorVisibility ()
  355. {
  356. if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
  357. {
  358. GetCursorVisibility (out CursorVisibility cursorVisibility);
  359. _currentCursorVisibility = cursorVisibility;
  360. SetCursorVisibility (CursorVisibility.Invisible);
  361. return;
  362. }
  363. SetCursorVisibility (_currentCursorVisibility ?? CursorVisibility.Default);
  364. }
  365. /// <inheritdoc/>
  366. public override bool GetCursorVisibility (out CursorVisibility visibility)
  367. {
  368. visibility = CursorVisibility.Invisible;
  369. if (!_currentCursorVisibility.HasValue)
  370. {
  371. return false;
  372. }
  373. visibility = _currentCursorVisibility.Value;
  374. return true;
  375. }
  376. private EscSeqUtils.DECSCUSR_Style? _currentDecscusrStyle;
  377. /// <inheritdoc/>
  378. public override bool SetCursorVisibility (CursorVisibility visibility)
  379. {
  380. if (_initialCursorVisibility.HasValue == false)
  381. {
  382. return false;
  383. }
  384. if (!RunningUnitTests)
  385. {
  386. Curses.curs_set (((int)visibility >> 16) & 0x000000FF);
  387. Curses.leaveok (_window!.Handle, !Force16Colors);
  388. }
  389. if (visibility != CursorVisibility.Invisible)
  390. {
  391. if (_currentDecscusrStyle is null || _currentDecscusrStyle != (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF))
  392. {
  393. _currentDecscusrStyle = (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF);
  394. _mainLoopDriver?.WriteRaw (
  395. EscSeqUtils.CSI_SetCursorStyle ((EscSeqUtils.DECSCUSR_Style)_currentDecscusrStyle)
  396. );
  397. }
  398. }
  399. _currentCursorVisibility = visibility;
  400. return true;
  401. }
  402. private bool SetCursorPosition (int col, int row)
  403. {
  404. // + 1 is needed because non-Windows is based on 1 instead of 0 and
  405. // Console.CursorTop/CursorLeft isn't reliable.
  406. Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
  407. return true;
  408. }
  409. #region Init/End/MainLoop
  410. private Curses.Window? _window;
  411. private UnixMainLoop? _mainLoopDriver;
  412. private object? _processInputToken;
  413. public override MainLoop Init ()
  414. {
  415. _mainLoopDriver = new (this);
  416. if (!RunningUnitTests)
  417. {
  418. _window = Curses.initscr ();
  419. Curses.set_escdelay (10);
  420. // Ensures that all procedures are performed at some previous closing.
  421. Curses.doupdate ();
  422. //
  423. // We are setting Invisible as default, so we could ignore XTerm DECSUSR setting
  424. //
  425. switch (Curses.curs_set (0))
  426. {
  427. case 0:
  428. _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Invisible;
  429. break;
  430. case 1:
  431. _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Underline;
  432. Curses.curs_set (1);
  433. break;
  434. case 2:
  435. _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Box;
  436. Curses.curs_set (2);
  437. break;
  438. default:
  439. _currentCursorVisibility = _initialCursorVisibility = null;
  440. break;
  441. }
  442. if (!Curses.HasColors)
  443. {
  444. throw new InvalidOperationException ("V2 - This should never happen. File an Issue if it does.");
  445. }
  446. Curses.raw ();
  447. Curses.noecho ();
  448. Curses.Window.Standard.keypad (true);
  449. Curses.StartColor ();
  450. Curses.UseDefaultColors ();
  451. if (!RunningUnitTests)
  452. {
  453. Curses.timeout (0);
  454. }
  455. _processInputToken = _mainLoopDriver.AddWatch (
  456. 0,
  457. UnixMainLoop.Condition.PollIn,
  458. x =>
  459. {
  460. ProcessInput ();
  461. return true;
  462. }
  463. );
  464. }
  465. CurrentAttribute = new (ColorName16.White, ColorName16.Black);
  466. if (Environment.OSVersion.Platform == PlatformID.Win32NT)
  467. {
  468. Clipboard = new FakeDriver.FakeClipboard ();
  469. }
  470. else
  471. {
  472. if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
  473. {
  474. Clipboard = new MacOSXClipboard ();
  475. }
  476. else
  477. {
  478. if (Is_WSL_Platform ())
  479. {
  480. Clipboard = new WSLClipboard ();
  481. }
  482. else
  483. {
  484. Clipboard = new CursesClipboard ();
  485. }
  486. }
  487. }
  488. ClearContents ();
  489. StartReportingMouseMoves ();
  490. if (!RunningUnitTests)
  491. {
  492. Curses.CheckWinChange ();
  493. // On Init this call is needed no mater Force16Colors or not
  494. Curses.refresh ();
  495. EscSeqUtils.ContinuousButtonPressed += EscSeqUtils_ContinuousButtonPressed;
  496. }
  497. return new (_mainLoopDriver);
  498. }
  499. private readonly AnsiResponseParser _parser = new ();
  500. /// <inheritdoc />
  501. internal override IAnsiResponseParser GetParser () => _parser;
  502. internal void ProcessInput ()
  503. {
  504. int wch;
  505. int code = Curses.get_wch (out wch);
  506. //System.Diagnostics.Debug.WriteLine ($"code: {code}; wch: {wch}");
  507. if (code == Curses.ERR)
  508. {
  509. return;
  510. }
  511. var k = KeyCode.Null;
  512. if (code == Curses.KEY_CODE_YES)
  513. {
  514. while (code == Curses.KEY_CODE_YES && wch == Curses.KeyResize)
  515. {
  516. ProcessWinChange ();
  517. code = Curses.get_wch (out wch);
  518. }
  519. if (wch == 0)
  520. {
  521. return;
  522. }
  523. if (wch == Curses.KeyMouse)
  524. {
  525. int wch2 = wch;
  526. while (wch2 == Curses.KeyMouse)
  527. {
  528. // BUGBUG: Fix this nullable issue.
  529. Key kea = null;
  530. ConsoleKeyInfo [] cki =
  531. {
  532. new ((char)KeyCode.Esc, 0, false, false, false),
  533. new ('[', 0, false, false, false),
  534. new ('<', 0, false, false, false)
  535. };
  536. code = 0;
  537. // BUGBUG: Fix this nullable issue.
  538. HandleEscSeqResponse (ref code, ref k, ref wch2, ref kea, ref cki);
  539. }
  540. return;
  541. }
  542. k = MapCursesKey (wch);
  543. if (wch >= 277 && wch <= 288)
  544. {
  545. // Shift+(F1 - F12)
  546. wch -= 12;
  547. k = KeyCode.ShiftMask | MapCursesKey (wch);
  548. }
  549. else if (wch >= 289 && wch <= 300)
  550. {
  551. // Ctrl+(F1 - F12)
  552. wch -= 24;
  553. k = KeyCode.CtrlMask | MapCursesKey (wch);
  554. }
  555. else if (wch >= 301 && wch <= 312)
  556. {
  557. // Ctrl+Shift+(F1 - F12)
  558. wch -= 36;
  559. k = KeyCode.CtrlMask | KeyCode.ShiftMask | MapCursesKey (wch);
  560. }
  561. else if (wch >= 313 && wch <= 324)
  562. {
  563. // Alt+(F1 - F12)
  564. wch -= 48;
  565. k = KeyCode.AltMask | MapCursesKey (wch);
  566. }
  567. else if (wch >= 325 && wch <= 327)
  568. {
  569. // Shift+Alt+(F1 - F3)
  570. wch -= 60;
  571. k = KeyCode.ShiftMask | KeyCode.AltMask | MapCursesKey (wch);
  572. }
  573. else if (wch == 520) // Ctrl+Delete
  574. {
  575. k = KeyCode.CtrlMask | KeyCode.Delete;
  576. }
  577. OnKeyDown (new Key (k));
  578. OnKeyUp (new Key (k));
  579. return;
  580. }
  581. // Special handling for ESC, we want to try to catch ESC+letter to simulate alt-letter as well as Alt-Fkey
  582. if (wch == 27)
  583. {
  584. Curses.timeout (10);
  585. code = Curses.get_wch (out int wch2);
  586. if (code == Curses.KEY_CODE_YES)
  587. {
  588. k = KeyCode.AltMask | MapCursesKey (wch);
  589. }
  590. // BUGBUG: Fix this nullable issue.
  591. Key key = null;
  592. if (code == 0)
  593. {
  594. // The ESC-number handling, debatable.
  595. // Simulates the AltMask itself by pressing Alt + Space.
  596. // Needed for macOS
  597. if (wch2 == (int)KeyCode.Space)
  598. {
  599. k = KeyCode.AltMask | KeyCode.Space;
  600. }
  601. else if (wch2 - (int)KeyCode.Space >= (uint)KeyCode.A
  602. && wch2 - (int)KeyCode.Space <= (uint)KeyCode.Z)
  603. {
  604. k = (KeyCode)((uint)KeyCode.AltMask + (wch2 - (int)KeyCode.Space));
  605. }
  606. else if (wch2 >= (uint)KeyCode.A - 64 && wch2 <= (uint)KeyCode.Z - 64)
  607. {
  608. k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + (wch2 + 64));
  609. }
  610. else if (wch2 >= (uint)KeyCode.D0 && wch2 <= (uint)KeyCode.D9)
  611. {
  612. k = (KeyCode)((uint)KeyCode.AltMask + (uint)KeyCode.D0 + (wch2 - (uint)KeyCode.D0));
  613. }
  614. else
  615. {
  616. ConsoleKeyInfo [] cki =
  617. [
  618. new ((char)KeyCode.Esc, 0, false, false, false), new ((char)wch2, 0, false, false, false)
  619. ];
  620. // BUGBUG: Fix this nullable issue.
  621. HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
  622. return;
  623. }
  624. //else if (wch2 == Curses.KeyCSI)
  625. //{
  626. // ConsoleKeyInfo [] cki =
  627. // {
  628. // new ((char)KeyCode.Esc, 0, false, false, false), new ('[', 0, false, false, false)
  629. // };
  630. // HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
  631. // return;
  632. //}
  633. //else
  634. //{
  635. // // Unfortunately there are no way to differentiate Ctrl+Alt+alfa and Ctrl+Shift+Alt+alfa.
  636. // if (((KeyCode)wch2 & KeyCode.CtrlMask) != 0)
  637. // {
  638. // k = (KeyCode)((uint)KeyCode.CtrlMask + (wch2 & ~(int)KeyCode.CtrlMask));
  639. // }
  640. // if (wch2 == 0)
  641. // {
  642. // k = KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Space;
  643. // }
  644. // //else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
  645. // //{
  646. // // k = KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Space;
  647. // //}
  648. // else if (wch2 < 256)
  649. // {
  650. // k = (KeyCode)wch2; // | KeyCode.AltMask;
  651. // }
  652. // else
  653. // {
  654. // k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + wch2);
  655. // }
  656. //}
  657. key = new Key (k);
  658. }
  659. else
  660. {
  661. key = Key.Esc;
  662. }
  663. OnKeyDown (key);
  664. OnKeyUp (key);
  665. }
  666. else if (wch == 8) // Ctrl+Backspace
  667. {
  668. k = KeyCode.Backspace | KeyCode.CtrlMask;
  669. OnKeyDown (new Key (k));
  670. OnKeyUp (new Key (k));
  671. }
  672. else if (wch == Curses.KeyTab)
  673. {
  674. k = MapCursesKey (wch);
  675. OnKeyDown (new Key (k));
  676. OnKeyUp (new Key (k));
  677. }
  678. else if (wch == 127)
  679. {
  680. // Backspace needed for macOS
  681. k = KeyCode.Backspace;
  682. OnKeyDown (new Key (k));
  683. OnKeyUp (new Key (k));
  684. }
  685. else
  686. {
  687. // Unfortunately there are no way to differentiate Ctrl+alfa and Ctrl+Shift+alfa.
  688. k = (KeyCode)wch;
  689. if (wch == 0)
  690. {
  691. k = KeyCode.CtrlMask | KeyCode.Space;
  692. }
  693. else if (wch >= (uint)KeyCode.A - 64 && wch <= (uint)KeyCode.Z - 64)
  694. {
  695. if ((KeyCode)(wch + 64) != KeyCode.J)
  696. {
  697. k = KeyCode.CtrlMask | (KeyCode)(wch + 64);
  698. }
  699. }
  700. else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
  701. {
  702. k = (KeyCode)wch | KeyCode.ShiftMask;
  703. }
  704. if (wch == '\n' || wch == '\r')
  705. {
  706. k = KeyCode.Enter;
  707. }
  708. // Strip the KeyCode.Space flag off if it's set
  709. //if (k != KeyCode.Space && k.HasFlag (KeyCode.Space))
  710. if (Key.GetIsKeyCodeAtoZ (k) && (k & KeyCode.Space) != 0)
  711. {
  712. k &= ~KeyCode.Space;
  713. }
  714. if (IsValidInput (k, out k))
  715. {
  716. OnKeyDown (new (k));
  717. OnKeyUp (new (k));
  718. }
  719. }
  720. }
  721. internal void ProcessWinChange ()
  722. {
  723. if (!RunningUnitTests && Curses.CheckWinChange ())
  724. {
  725. ClearContents ();
  726. OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
  727. }
  728. }
  729. static string ConvertToString (ConsoleKeyInfo [] keyInfos)
  730. {
  731. char [] chars = new char [keyInfos.Length];
  732. for (int i = 0; i < keyInfos.Length; i++)
  733. {
  734. chars [i] = keyInfos [i].KeyChar;
  735. }
  736. return new string (chars);
  737. }
  738. private void HandleEscSeqResponse (
  739. ref int code,
  740. ref KeyCode k,
  741. ref int wch2,
  742. ref Key keyEventArgs,
  743. ref ConsoleKeyInfo [] cki
  744. )
  745. {
  746. ConsoleKey ck = 0;
  747. ConsoleModifiers mod = 0;
  748. while (code == 0)
  749. {
  750. code = Curses.get_wch (out wch2);
  751. var consoleKeyInfo = new ConsoleKeyInfo ((char)wch2, 0, false, false, false);
  752. if (wch2 == 0 || wch2 == 27 || wch2 == Curses.KeyMouse)
  753. {
  754. // Give ansi parser a chance to deal with the escape sequence
  755. if (cki != null && string.IsNullOrEmpty(_parser.ProcessInput (ConvertToString(cki))))
  756. {
  757. // Parser fully consumed all keys meaning keys are processed - job done
  758. return;
  759. }
  760. // Ansi parser could not deal with it either because it is not expecting
  761. // the given terminator (e.g. mouse) or did not understand format somehow.
  762. // Carry on with the older code for processing curses escape codes
  763. // BUGBUG: Fix this nullable issue.
  764. EscSeqUtils.DecodeEscSeq (
  765. ref consoleKeyInfo,
  766. ref ck,
  767. cki,
  768. ref mod,
  769. out _,
  770. out _,
  771. out _,
  772. out _,
  773. out bool isKeyMouse,
  774. out List<MouseFlags> mouseFlags,
  775. out Point pos,
  776. out _,
  777. EscSeqUtils.ProcessMouseEvent
  778. );
  779. if (isKeyMouse)
  780. {
  781. foreach (MouseFlags mf in mouseFlags)
  782. {
  783. OnMouseEvent (new () { Flags = mf, Position = pos });
  784. }
  785. // BUGBUG: Fix this nullable issue.
  786. cki = null;
  787. if (wch2 == 27)
  788. {
  789. cki = EscSeqUtils.ResizeArray (
  790. new ConsoleKeyInfo (
  791. (char)KeyCode.Esc,
  792. 0,
  793. false,
  794. false,
  795. false
  796. ),
  797. cki
  798. );
  799. }
  800. }
  801. else
  802. {
  803. k = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo);
  804. keyEventArgs = new Key (k);
  805. OnKeyDown (keyEventArgs);
  806. }
  807. }
  808. else
  809. {
  810. cki = EscSeqUtils.ResizeArray (consoleKeyInfo, cki);
  811. }
  812. }
  813. }
  814. private void EscSeqUtils_ContinuousButtonPressed (object? sender, MouseEventArgs e)
  815. {
  816. OnMouseEvent (e);
  817. }
  818. private static KeyCode MapCursesKey (int cursesKey)
  819. {
  820. switch (cursesKey)
  821. {
  822. case Curses.KeyF1: return KeyCode.F1;
  823. case Curses.KeyF2: return KeyCode.F2;
  824. case Curses.KeyF3: return KeyCode.F3;
  825. case Curses.KeyF4: return KeyCode.F4;
  826. case Curses.KeyF5: return KeyCode.F5;
  827. case Curses.KeyF6: return KeyCode.F6;
  828. case Curses.KeyF7: return KeyCode.F7;
  829. case Curses.KeyF8: return KeyCode.F8;
  830. case Curses.KeyF9: return KeyCode.F9;
  831. case Curses.KeyF10: return KeyCode.F10;
  832. case Curses.KeyF11: return KeyCode.F11;
  833. case Curses.KeyF12: return KeyCode.F12;
  834. case Curses.KeyUp: return KeyCode.CursorUp;
  835. case Curses.KeyDown: return KeyCode.CursorDown;
  836. case Curses.KeyLeft: return KeyCode.CursorLeft;
  837. case Curses.KeyRight: return KeyCode.CursorRight;
  838. case Curses.KeyHome: return KeyCode.Home;
  839. case Curses.KeyEnd: return KeyCode.End;
  840. case Curses.KeyNPage: return KeyCode.PageDown;
  841. case Curses.KeyPPage: return KeyCode.PageUp;
  842. case Curses.KeyDeleteChar: return KeyCode.Delete;
  843. case Curses.KeyInsertChar: return KeyCode.Insert;
  844. case Curses.KeyTab: return KeyCode.Tab;
  845. case Curses.KeyBackTab: return KeyCode.Tab | KeyCode.ShiftMask;
  846. case Curses.KeyBackspace: return KeyCode.Backspace;
  847. case Curses.ShiftKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask;
  848. case Curses.ShiftKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask;
  849. case Curses.ShiftKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask;
  850. case Curses.ShiftKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask;
  851. case Curses.ShiftKeyHome: return KeyCode.Home | KeyCode.ShiftMask;
  852. case Curses.ShiftKeyEnd: return KeyCode.End | KeyCode.ShiftMask;
  853. case Curses.ShiftKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask;
  854. case Curses.ShiftKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask;
  855. case Curses.AltKeyUp: return KeyCode.CursorUp | KeyCode.AltMask;
  856. case Curses.AltKeyDown: return KeyCode.CursorDown | KeyCode.AltMask;
  857. case Curses.AltKeyLeft: return KeyCode.CursorLeft | KeyCode.AltMask;
  858. case Curses.AltKeyRight: return KeyCode.CursorRight | KeyCode.AltMask;
  859. case Curses.AltKeyHome: return KeyCode.Home | KeyCode.AltMask;
  860. case Curses.AltKeyEnd: return KeyCode.End | KeyCode.AltMask;
  861. case Curses.AltKeyNPage: return KeyCode.PageDown | KeyCode.AltMask;
  862. case Curses.AltKeyPPage: return KeyCode.PageUp | KeyCode.AltMask;
  863. case Curses.CtrlKeyUp: return KeyCode.CursorUp | KeyCode.CtrlMask;
  864. case Curses.CtrlKeyDown: return KeyCode.CursorDown | KeyCode.CtrlMask;
  865. case Curses.CtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.CtrlMask;
  866. case Curses.CtrlKeyRight: return KeyCode.CursorRight | KeyCode.CtrlMask;
  867. case Curses.CtrlKeyHome: return KeyCode.Home | KeyCode.CtrlMask;
  868. case Curses.CtrlKeyEnd: return KeyCode.End | KeyCode.CtrlMask;
  869. case Curses.CtrlKeyNPage: return KeyCode.PageDown | KeyCode.CtrlMask;
  870. case Curses.CtrlKeyPPage: return KeyCode.PageUp | KeyCode.CtrlMask;
  871. case Curses.ShiftCtrlKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.CtrlMask;
  872. case Curses.ShiftCtrlKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.CtrlMask;
  873. case Curses.ShiftCtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.CtrlMask;
  874. case Curses.ShiftCtrlKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.CtrlMask;
  875. case Curses.ShiftCtrlKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.CtrlMask;
  876. case Curses.ShiftCtrlKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.CtrlMask;
  877. case Curses.ShiftCtrlKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.CtrlMask;
  878. case Curses.ShiftCtrlKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.CtrlMask;
  879. case Curses.ShiftAltKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.AltMask;
  880. case Curses.ShiftAltKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.AltMask;
  881. case Curses.ShiftAltKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.AltMask;
  882. case Curses.ShiftAltKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.AltMask;
  883. case Curses.ShiftAltKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.AltMask;
  884. case Curses.ShiftAltKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.AltMask;
  885. case Curses.ShiftAltKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.AltMask;
  886. case Curses.ShiftAltKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.AltMask;
  887. case Curses.AltCtrlKeyNPage: return KeyCode.PageDown | KeyCode.AltMask | KeyCode.CtrlMask;
  888. case Curses.AltCtrlKeyPPage: return KeyCode.PageUp | KeyCode.AltMask | KeyCode.CtrlMask;
  889. case Curses.AltCtrlKeyHome: return KeyCode.Home | KeyCode.AltMask | KeyCode.CtrlMask;
  890. case Curses.AltCtrlKeyEnd: return KeyCode.End | KeyCode.AltMask | KeyCode.CtrlMask;
  891. default: return KeyCode.Null;
  892. }
  893. }
  894. public override void End ()
  895. {
  896. EscSeqUtils.ContinuousButtonPressed -= EscSeqUtils_ContinuousButtonPressed;
  897. StopReportingMouseMoves ();
  898. SetCursorVisibility (CursorVisibility.Default);
  899. if (_mainLoopDriver is { } && _processInputToken != null)
  900. {
  901. _mainLoopDriver.RemoveWatch (_processInputToken);
  902. }
  903. if (RunningUnitTests)
  904. {
  905. return;
  906. }
  907. // throws away any typeahead that has been typed by
  908. // the user and has not yet been read by the program.
  909. Curses.flushinp ();
  910. Curses.endwin ();
  911. }
  912. #endregion Init/End/MainLoop
  913. public static bool Is_WSL_Platform ()
  914. {
  915. // xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell
  916. //if (new CursesClipboard ().IsSupported) {
  917. // // If xclip is installed on Linux under WSL, this will return true.
  918. // return false;
  919. //}
  920. (int exitCode, string result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true);
  921. if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL"))
  922. {
  923. return true;
  924. }
  925. return false;
  926. }
  927. /// <inheritdoc/>
  928. public override void WriteRaw (string ansi) { _mainLoopDriver?.WriteRaw (ansi); }
  929. }
  930. // TODO: One type per file - move to another file
  931. internal static class Platform
  932. {
  933. private static int _suspendSignal;
  934. /// <summary>Suspends the process by sending SIGTSTP to itself</summary>
  935. /// <returns>True if the suspension was successful.</returns>
  936. public static bool Suspend ()
  937. {
  938. int signal = GetSuspendSignal ();
  939. if (signal == -1)
  940. {
  941. return false;
  942. }
  943. killpg (0, signal);
  944. return true;
  945. }
  946. private static int GetSuspendSignal ()
  947. {
  948. if (_suspendSignal != 0)
  949. {
  950. return _suspendSignal;
  951. }
  952. nint buf = Marshal.AllocHGlobal (8192);
  953. if (uname (buf) != 0)
  954. {
  955. Marshal.FreeHGlobal (buf);
  956. _suspendSignal = -1;
  957. return _suspendSignal;
  958. }
  959. try
  960. {
  961. switch (Marshal.PtrToStringAnsi (buf))
  962. {
  963. case "Darwin":
  964. case "DragonFly":
  965. case "FreeBSD":
  966. case "NetBSD":
  967. case "OpenBSD":
  968. _suspendSignal = 18;
  969. break;
  970. case "Linux":
  971. // TODO: should fetch the machine name and
  972. // if it is MIPS return 24
  973. _suspendSignal = 20;
  974. break;
  975. case "Solaris":
  976. _suspendSignal = 24;
  977. break;
  978. default:
  979. _suspendSignal = -1;
  980. break;
  981. }
  982. return _suspendSignal;
  983. }
  984. finally
  985. {
  986. Marshal.FreeHGlobal (buf);
  987. }
  988. }
  989. [DllImport ("libc")]
  990. private static extern int killpg (int pgrp, int pid);
  991. [DllImport ("libc")]
  992. private static extern int uname (nint buf);
  993. }