EscSeqUtils.cs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949
  1. #nullable enable
  2. using System.Diagnostics;
  3. using System.Globalization;
  4. // ReSharper disable InconsistentNaming
  5. namespace Terminal.Gui.Drivers;
  6. /// <summary>
  7. /// Provides a platform-independent API for managing ANSI escape sequences.
  8. /// </summary>
  9. /// <remarks>
  10. /// Useful resources:
  11. /// * https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
  12. /// * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
  13. /// * https://vt100.net/
  14. /// </remarks>
  15. public static class EscSeqUtils
  16. {
  17. /// <summary>
  18. /// Escape key code (ASCII 27/0x1B).
  19. /// </summary>
  20. public const char KeyEsc = (char)KeyCode.Esc;
  21. /// <summary>
  22. /// ESC [ - The CSI (Control Sequence Introducer).
  23. /// </summary>
  24. public const string CSI = "\u001B[";
  25. #region Screen Window Buffer
  26. /// <summary>
  27. /// Options for ANSI ESC "[xJ" - Clears part of the screen.
  28. /// </summary>
  29. public enum ClearScreenOptions
  30. {
  31. /// <summary>
  32. /// If n is 0 (or missing), clear from cursor to end of screen.
  33. /// </summary>
  34. CursorToEndOfScreen = 0,
  35. /// <summary>
  36. /// If n is 1, clear from cursor to beginning of the screen.
  37. /// </summary>
  38. CursorToBeginningOfScreen = 1,
  39. /// <summary>
  40. /// If n is 2, clear entire screen (and moves cursor to upper left on DOS ANSI.SYS).
  41. /// </summary>
  42. EntireScreen = 2,
  43. /// <summary>
  44. /// If n is 3, clear entire screen and delete all lines saved in the scrollback buffer
  45. /// </summary>
  46. EntireScreenAndScrollbackBuffer = 3
  47. }
  48. /// <summary>
  49. /// ESC [ ? 1047 h - Activate xterm alternative buffer (no backscroll)
  50. /// </summary>
  51. /// <remarks>
  52. /// From
  53. /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
  54. /// Use Alternate Screen Buffer, xterm.
  55. /// </remarks>
  56. public static readonly string CSI_ActivateAltBufferNoBackscroll = CSI + "?1047h";
  57. /// <summary>
  58. /// ESC [ ? 1047 l - Restore xterm working buffer (with backscroll)
  59. /// </summary>
  60. /// <remarks>
  61. /// From
  62. /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
  63. /// Use Normal Screen Buffer, xterm. Clear the screen first if in the Alternate Screen Buffer.
  64. /// </remarks>
  65. public static readonly string CSI_RestoreAltBufferWithBackscroll = CSI + "?1047l";
  66. /// <summary>
  67. /// ESC [ ? 1049 l - Restore cursor position and restore xterm working buffer (with backscroll)
  68. /// </summary>
  69. /// <remarks>
  70. /// From
  71. /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
  72. /// Use Normal Screen Buffer and restore cursor as in DECRC, xterm.
  73. /// resource.This combines the effects of the 1047 and 1048 modes.
  74. /// </remarks>
  75. public static readonly string CSI_RestoreCursorAndRestoreAltBufferWithBackscroll = CSI + "?1049l";
  76. /// <summary>
  77. /// ESC [ ? 1049 h - Save cursor position and activate xterm alternative buffer (no backscroll)
  78. /// </summary>
  79. /// <remarks>
  80. /// From
  81. /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
  82. /// Save cursor as in DECSC, xterm. After saving the cursor, switch to the Alternate Screen Buffer,
  83. /// clearing it first.
  84. /// This control combines the effects of the 1047 and 1048 modes.
  85. /// Use this with terminfo-based applications rather than the 47 mode.
  86. /// </remarks>
  87. public static readonly string CSI_SaveCursorAndActivateAltBufferNoBackscroll = CSI + "?1049h";
  88. /// <summary>
  89. /// ESC [ x J - Clears part of the screen. See <see cref="ClearScreenOptions"/>.
  90. /// </summary>
  91. /// <param name="option"></param>
  92. /// <returns></returns>
  93. public static string CSI_ClearScreen (ClearScreenOptions option) { return $"{CSI}{(int)option}J"; }
  94. /// <summary>
  95. /// ESC [ 8 ; height ; width t - Set Terminal Window Size
  96. /// https://terminalguide.namepad.de/seq/csi_st-8/
  97. /// </summary>
  98. public static string CSI_SetTerminalWindowSize (int height, int width) { return $"{CSI}8;{height};{width}t"; }
  99. #endregion Screen Window Buffer
  100. #region Mouse
  101. /// <summary>
  102. /// ESC [ ? 1003 l - Disable any mouse event tracking.
  103. /// </summary>
  104. public static readonly string CSI_DisableAnyEventMouse = CSI + "?1003l";
  105. /// <summary>
  106. /// ESC [ ? 1006 l - Disable SGR (Select Graphic Rendition).
  107. /// </summary>
  108. public static readonly string CSI_DisableSgrExtModeMouse = CSI + "?1006l";
  109. /// <summary>
  110. /// ESC [ ? 1015 l - Disable URXVT (Unicode Extended Virtual Terminal).
  111. /// </summary>
  112. public static readonly string CSI_DisableUrxvtExtModeMouse = CSI + "?1015l";
  113. /// <summary>
  114. /// ESC [ ? 1003 h - Enable mouse event tracking.
  115. /// </summary>
  116. public static readonly string CSI_EnableAnyEventMouse = CSI + "?1003h";
  117. /// <summary>
  118. /// ESC [ ? 1006 h - Enable SGR (Select Graphic Rendition).
  119. /// </summary>
  120. public static readonly string CSI_EnableSgrExtModeMouse = CSI + "?1006h";
  121. /// <summary>
  122. /// ESC [ ? 1015 h - Enable URXVT (Unicode Extended Virtual Terminal).
  123. /// </summary>
  124. public static readonly string CSI_EnableUrxvtExtModeMouse = CSI + "?1015h";
  125. /// <summary>
  126. /// Control sequence for disabling mouse events.
  127. /// </summary>
  128. public static readonly string CSI_DisableMouseEvents =
  129. CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse;
  130. /// <summary>
  131. /// Control sequence for enabling mouse events.
  132. /// </summary>
  133. public static readonly string CSI_EnableMouseEvents =
  134. CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse;
  135. #endregion Mouse
  136. #region Keyboard
  137. /// <summary>
  138. /// Helper to set the Control key states based on the char.
  139. /// </summary>
  140. /// <param name="ch">The char value.</param>
  141. /// <returns></returns>
  142. public static ConsoleKeyInfo MapChar (char ch) { return MapConsoleKeyInfo (new (ch, ConsoleKey.None, false, false, false)); }
  143. /// <summary>
  144. /// Ensures a console key is mapped to one that works correctly with ANSI escape sequences.
  145. /// </summary>
  146. /// <param name="consoleKeyInfo">The <see cref="ConsoleKeyInfo"/>.</param>
  147. /// <returns>The <see cref="ConsoleKeyInfo"/> modified.</returns>
  148. public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
  149. {
  150. ConsoleKeyInfo newConsoleKeyInfo = consoleKeyInfo;
  151. var key = ConsoleKey.None;
  152. char keyChar = consoleKeyInfo.KeyChar;
  153. switch ((uint)keyChar)
  154. {
  155. case 0:
  156. if (consoleKeyInfo.Key == (ConsoleKey)64)
  157. { // Ctrl+Space in Windows.
  158. newConsoleKeyInfo = new (
  159. consoleKeyInfo.KeyChar,
  160. ConsoleKey.Spacebar,
  161. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  162. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  163. (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
  164. }
  165. else if (consoleKeyInfo.Key == ConsoleKey.None)
  166. {
  167. newConsoleKeyInfo = new (
  168. consoleKeyInfo.KeyChar,
  169. ConsoleKey.Spacebar,
  170. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  171. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  172. true);
  173. }
  174. break;
  175. case uint n when n is > 0 and <= KeyEsc:
  176. if (consoleKeyInfo is { Key: 0, KeyChar: '\u001B' })
  177. {
  178. key = ConsoleKey.Escape;
  179. newConsoleKeyInfo = new (
  180. consoleKeyInfo.KeyChar,
  181. key,
  182. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  183. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  184. (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
  185. }
  186. else if (consoleKeyInfo is { Key: 0, KeyChar: '\t' })
  187. {
  188. key = ConsoleKey.Tab;
  189. newConsoleKeyInfo = new (
  190. consoleKeyInfo.KeyChar,
  191. key,
  192. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  193. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  194. (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
  195. }
  196. else if (consoleKeyInfo is { Key: 0, KeyChar: '\r' })
  197. {
  198. key = ConsoleKey.Enter;
  199. newConsoleKeyInfo = new (
  200. consoleKeyInfo.KeyChar,
  201. key,
  202. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  203. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  204. (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
  205. }
  206. else if (consoleKeyInfo is { Key: 0, KeyChar: '\n' })
  207. {
  208. key = ConsoleKey.Enter;
  209. newConsoleKeyInfo = new (
  210. consoleKeyInfo.KeyChar,
  211. key,
  212. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  213. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  214. true);
  215. }
  216. else if (consoleKeyInfo is { Key: 0, KeyChar: '\b' })
  217. {
  218. key = ConsoleKey.Backspace;
  219. newConsoleKeyInfo = new (
  220. consoleKeyInfo.KeyChar,
  221. key,
  222. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  223. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  224. true);
  225. }
  226. else if (consoleKeyInfo.Key == 0)
  227. {
  228. key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + (uint)ConsoleKey.A - 1);
  229. newConsoleKeyInfo = new (
  230. consoleKeyInfo.KeyChar,
  231. key,
  232. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  233. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  234. true);
  235. }
  236. break;
  237. case uint n when n is >= '\u001c' and <= '\u001f':
  238. key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + 24);
  239. newConsoleKeyInfo = new (
  240. (char)key,
  241. key,
  242. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  243. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  244. true);
  245. break;
  246. case 127: // DEL
  247. key = ConsoleKey.Backspace;
  248. newConsoleKeyInfo = new (
  249. consoleKeyInfo.KeyChar,
  250. key,
  251. (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
  252. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  253. (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
  254. break;
  255. default:
  256. //uint ck = ConsoleKeyMapping.MapKeyCodeToConsoleKey ((KeyCode)consoleKeyInfo.KeyChar, out bool isConsoleKey);
  257. //if (isConsoleKey)
  258. {
  259. key = consoleKeyInfo.Key;// (ConsoleKey)ck;
  260. }
  261. newConsoleKeyInfo = new (
  262. keyChar,
  263. key,
  264. GetShiftMod (consoleKeyInfo.Modifiers),
  265. (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
  266. (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
  267. break;
  268. }
  269. return newConsoleKeyInfo;
  270. bool GetShiftMod (ConsoleModifiers modifiers)
  271. {
  272. if (consoleKeyInfo.KeyChar is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z && modifiers == ConsoleModifiers.None)
  273. {
  274. return true;
  275. }
  276. return (modifiers & ConsoleModifiers.Shift) != 0;
  277. }
  278. }
  279. internal static KeyCode MapKey (ConsoleKeyInfo keyInfo)
  280. {
  281. switch (keyInfo.Key)
  282. {
  283. case ConsoleKey.Multiply:
  284. case ConsoleKey.Add:
  285. case ConsoleKey.Separator:
  286. case ConsoleKey.Subtract:
  287. case ConsoleKey.Decimal:
  288. case ConsoleKey.Divide:
  289. case ConsoleKey.OemPeriod:
  290. case ConsoleKey.OemComma:
  291. case ConsoleKey.OemPlus:
  292. case ConsoleKey.OemMinus:
  293. case ConsoleKey.Packet:
  294. case ConsoleKey.Oem1:
  295. case ConsoleKey.Oem2:
  296. case ConsoleKey.Oem3:
  297. case ConsoleKey.Oem4:
  298. case ConsoleKey.Oem5:
  299. case ConsoleKey.Oem6:
  300. case ConsoleKey.Oem7:
  301. case ConsoleKey.Oem8:
  302. case ConsoleKey.Oem102:
  303. if (keyInfo.KeyChar == 0)
  304. {
  305. // All Oem* produce a valid KeyChar and is not guaranteed to be printable ASCII, but it’s never just '\0' (null).
  306. // If that happens it's because Console.ReadKey is misreporting for AltGr + non-character keys
  307. // or if it's a combine key waiting for the next input which will determine the respective KeyChar.
  308. // This behavior only happens on Windows and not on Unix-like systems.
  309. if (keyInfo.Key != ConsoleKey.Multiply
  310. && keyInfo.Key != ConsoleKey.Add
  311. && keyInfo.Key != ConsoleKey.Decimal
  312. && keyInfo.Key != ConsoleKey.Subtract
  313. && keyInfo.Key != ConsoleKey.Divide
  314. && keyInfo.Key != ConsoleKey.OemPeriod
  315. && keyInfo.Key != ConsoleKey.OemComma
  316. && keyInfo.Key != ConsoleKey.OemPlus
  317. && keyInfo.Key != ConsoleKey.OemMinus
  318. && keyInfo.Key != ConsoleKey.Oem1
  319. && keyInfo.Key != ConsoleKey.Oem2
  320. && keyInfo.Key != ConsoleKey.Oem3
  321. && keyInfo.Key != ConsoleKey.Oem4
  322. && keyInfo.Key != ConsoleKey.Oem5
  323. && keyInfo.Key != ConsoleKey.Oem6
  324. && keyInfo.Key != ConsoleKey.Oem7
  325. && keyInfo.Key != ConsoleKey.Oem102)
  326. {
  327. // If the keyChar is 0, keyInfo.Key value is not a printable character.
  328. Debug.Assert (keyInfo.Key == 0);
  329. }
  330. return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key);
  331. }
  332. if (keyInfo.Modifiers != ConsoleModifiers.Shift)
  333. {
  334. // If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar
  335. return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
  336. }
  337. // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç")
  338. // and passing on Shift would be redundant.
  339. return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
  340. }
  341. // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
  342. if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
  343. {
  344. if (keyInfo is { Modifiers: ConsoleModifiers.Control, Key: ConsoleKey.I })
  345. {
  346. return KeyCode.Tab;
  347. }
  348. return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
  349. }
  350. // Handle control keys (e.g. CursorUp)
  351. if (keyInfo.Key != ConsoleKey.None
  352. && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
  353. {
  354. return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
  355. }
  356. if ((ConsoleKey)keyInfo.KeyChar is >= ConsoleKey.A and <= ConsoleKey.Z)
  357. {
  358. // Shifted
  359. keyInfo = new (
  360. keyInfo.KeyChar,
  361. (ConsoleKey)keyInfo.KeyChar,
  362. true,
  363. keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
  364. keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
  365. }
  366. if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z)
  367. {
  368. // Unshifted
  369. keyInfo = new (
  370. keyInfo.KeyChar,
  371. (ConsoleKey)(keyInfo.KeyChar - 32),
  372. false,
  373. keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
  374. keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
  375. }
  376. if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z)
  377. {
  378. if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)
  379. || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
  380. {
  381. // DotNetDriver doesn't support Shift-Ctrl/Shift-Alt combos
  382. return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key);
  383. }
  384. if (keyInfo.Modifiers == ConsoleModifiers.Shift)
  385. {
  386. // If ShiftMask is on add the ShiftMask
  387. if (char.IsUpper (keyInfo.KeyChar))
  388. {
  389. return (KeyCode)keyInfo.Key | KeyCode.ShiftMask;
  390. }
  391. }
  392. return (KeyCode)keyInfo.Key;
  393. }
  394. return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
  395. }
  396. #endregion Keyboard
  397. #region Cursor
  398. //ESC [ M - RI Reverse Index – Performs the reverse operation of \n, moves cursor up one line, maintains horizontal position, scrolls buffer if necessary*
  399. /// <summary>
  400. /// ESC [ 7 - Save Cursor Position in Memory**
  401. /// </summary>
  402. public static readonly string CSI_SaveCursorPosition = CSI + "7";
  403. /// <summary>
  404. /// ESC [ 8 - DECSR Restore Cursor Position from Memory**
  405. /// </summary>
  406. public static readonly string CSI_RestoreCursorPosition = CSI + "8";
  407. //ESC [ < n > A - CUU - Cursor Up Cursor up by < n >
  408. //ESC [ < n > B - CUD - Cursor Down Cursor down by < n >
  409. //ESC [ < n > C - CUF - Cursor Forward Cursor forward (Right) by < n >
  410. //ESC [ < n > D - CUB - Cursor Backward Cursor backward (Left) by < n >
  411. //ESC [ < n > E - CNL - Cursor Next Line - Cursor down < n > lines from current position
  412. //ESC [ < n > F - CPL - Cursor Previous Line Cursor up < n > lines from current position
  413. //ESC [ < n > G - CHA - Cursor Horizontal Absolute Cursor moves to < n > th position horizontally in the current line
  414. //ESC [ < n > d - VPA - Vertical Line Position Absolute Cursor moves to the < n > th position vertically in the current column
  415. /// <summary>
  416. /// ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column
  417. /// of the y line
  418. /// </summary>
  419. /// <param name="row">Origin is (1,1).</param>
  420. /// <param name="col">Origin is (1,1).</param>
  421. /// <returns></returns>
  422. public static string CSI_SetCursorPosition (int row, int col) { return $"{CSI}{row};{col}H"; }
  423. /// <summary>
  424. /// ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column
  425. /// of the y line
  426. /// </summary>
  427. /// <param name="builder">StringBuilder where to append the cursor position sequence.</param>
  428. /// <param name="row">Origin is (1,1).</param>
  429. /// <param name="col">Origin is (1,1).</param>
  430. public static void CSI_AppendCursorPosition (StringBuilder builder, int row, int col)
  431. {
  432. // InterpolatedStringHandler is composed in stack, skipping the string allocation.
  433. builder.Append ($"{CSI}{row};{col}H");
  434. }
  435. /// <summary>
  436. /// ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column
  437. /// of the y line
  438. /// </summary>
  439. /// <param name="writer">TextWriter where to write the cursor position sequence.</param>
  440. /// <param name="row">Origin is (1,1).</param>
  441. /// <param name="col">Origin is (1,1).</param>
  442. public static void CSI_WriteCursorPosition (TextWriter writer, int row, int col)
  443. {
  444. const int maxInputBufferSize =
  445. // CSI (2) + ';' + 'H'
  446. 4
  447. +
  448. // row + col (2x int sign + int max value)
  449. 2
  450. + 20;
  451. Span<char> buffer = stackalloc char [maxInputBufferSize];
  452. if (!buffer.TryWrite (CultureInfo.InvariantCulture, $"{CSI}{row};{col}H", out int charsWritten))
  453. {
  454. var tooLongCursorPositionSequence = $"{CSI}{row};{col}H";
  455. throw new InvalidOperationException (
  456. $"{nameof (CSI_WriteCursorPosition)} buffer (len: {buffer.Length}) is too short for cursor position sequence '{tooLongCursorPositionSequence}' (len: {tooLongCursorPositionSequence.Length}).");
  457. }
  458. ReadOnlySpan<char> cursorPositionSequence = buffer [..charsWritten];
  459. writer.Write (cursorPositionSequence);
  460. }
  461. //ESC [ <y> ; <x> f - HVP Horizontal Vertical Position* Cursor moves to<x>; <y> coordinate within the viewport, where <x> is the column of the<y> line
  462. //ESC [ s - ANSISYSSC Save Cursor – Ansi.sys emulation **With no parameters, performs a save cursor operation like DECSC
  463. //ESC [ u - ANSISYSRC Restore Cursor – Ansi.sys emulation **With no parameters, performs a restore cursor operation like DECRC
  464. //ESC [ ? 12 h - ATT160 Text Cursor Enable Blinking Start the cursor blinking
  465. //ESC [ ? 12 l - ATT160 Text Cursor Disable Blinking Stop blinking the cursor
  466. /// <summary>
  467. /// ESC [ ? 25 h - DECTCEM Text Cursor Enable Mode Show Show the cursor
  468. /// </summary>
  469. public static readonly string CSI_ShowCursor = CSI + "?25h";
  470. /// <summary>
  471. /// ESC [ ? 25 l - DECTCEM Text Cursor Enable Mode Hide Hide the cursor
  472. /// </summary>
  473. public static readonly string CSI_HideCursor = CSI + "?25l";
  474. //ESC [ ? 12 h - ATT160 Text Cursor Enable Blinking Start the cursor blinking
  475. //ESC [ ? 12 l - ATT160 Text Cursor Disable Blinking Stop blinking the cursor
  476. //ESC [ ? 25 h - DECTCEM Text Cursor Enable Mode Show Show the cursor
  477. //ESC [ ? 25 l - DECTCEM Text Cursor Enable Mode Hide Hide the cursor
  478. /// <summary>
  479. /// Styles for ANSI ESC "[x q" - Set Cursor Style
  480. /// </summary>
  481. public enum DECSCUSR_Style
  482. {
  483. /// <summary>
  484. /// DECSCUSR - User Shape - Default cursor shape configured by the user
  485. /// </summary>
  486. UserShape = 0,
  487. /// <summary>
  488. /// DECSCUSR - Blinking Block - Blinking block cursor shape
  489. /// </summary>
  490. BlinkingBlock = 1,
  491. /// <summary>
  492. /// DECSCUSR - Steady Block - Steady block cursor shape
  493. /// </summary>
  494. SteadyBlock = 2,
  495. /// <summary>
  496. /// DECSCUSR - Blinking Underline - Blinking underline cursor shape
  497. /// </summary>
  498. BlinkingUnderline = 3,
  499. /// <summary>
  500. /// DECSCUSR - Steady Underline - Steady underline cursor shape
  501. /// </summary>
  502. SteadyUnderline = 4,
  503. /// <summary>
  504. /// DECSCUSR - Blinking Bar - Blinking bar cursor shape
  505. /// </summary>
  506. BlinkingBar = 5,
  507. /// <summary>
  508. /// DECSCUSR - Steady Bar - Steady bar cursor shape
  509. /// </summary>
  510. SteadyBar = 6
  511. }
  512. /// <summary>
  513. /// ESC [ n SP q - Select Cursor Style (DECSCUSR)
  514. /// https://terminalguide.namepad.de/seq/csi_sq_t_space/
  515. /// </summary>
  516. /// <param name="style"></param>
  517. /// <returns></returns>
  518. public static string CSI_SetCursorStyle (DECSCUSR_Style style) { return $"{CSI}{(int)style} q"; }
  519. #endregion Cursor
  520. #region Colors
  521. /// <summary>
  522. /// ESC [ (n) m - SGR - Set Graphics Rendition - Set the format of the screen and text as specified by (n)
  523. /// This command is special in that the (n) position can accept between 0 and 16 parameters separated by semicolons.
  524. /// When no parameters are specified, it is treated the same as a single 0 parameter.
  525. /// https://terminalguide.namepad.de/seq/csi_sm/
  526. /// </summary>
  527. public static string CSI_SetGraphicsRendition (params int [] parameters) { return $"{CSI}{string.Join (";", parameters)}m"; }
  528. /// <summary>
  529. /// ESC [ (n) m - Uses <see cref="CSI_SetGraphicsRendition(int[])"/> to set the foreground color.
  530. /// </summary>
  531. /// <param name="code">One of the 16 color codes.</param>
  532. /// <returns></returns>
  533. public static string CSI_SetForegroundColor (AnsiColorCode code) { return CSI_SetGraphicsRendition ((int)code); }
  534. /// <summary>
  535. /// ESC [ (n) m - Uses <see cref="CSI_SetGraphicsRendition(int[])"/> to set the background color.
  536. /// </summary>
  537. /// <param name="code">One of the 16 color codes.</param>
  538. /// <returns></returns>
  539. public static string CSI_SetBackgroundColor (AnsiColorCode code) { return CSI_SetGraphicsRendition ((int)code + 10); }
  540. /// <summary>
  541. /// ESC[38;5;{id}m - Set foreground color (256 colors)
  542. /// </summary>
  543. public static string CSI_SetForegroundColor256 (int color) { return $"{CSI}38;5;{color}m"; }
  544. /// <summary>
  545. /// ESC[48;5;{id}m - Set background color (256 colors)
  546. /// </summary>
  547. public static string CSI_SetBackgroundColor256 (int color) { return $"{CSI}48;5;{color}m"; }
  548. /// <summary>
  549. /// ESC[38;2;{r};{g};{b}m Set foreground color as RGB.
  550. /// </summary>
  551. public static string CSI_SetForegroundColorRGB (int r, int g, int b) { return $"{CSI}38;2;{r};{g};{b}m"; }
  552. /// <summary>
  553. /// ESC[38;2;{r};{g};{b}m Append foreground color as RGB to StringBuilder.
  554. /// </summary>
  555. public static void CSI_AppendForegroundColorRGB (StringBuilder builder, int r, int g, int b)
  556. {
  557. // InterpolatedStringHandler is composed in stack, skipping the string allocation.
  558. builder.Append ($"{CSI}38;2;{r};{g};{b}m");
  559. }
  560. /// <summary>
  561. /// ESC[48;2;{r};{g};{b}m Set background color as RGB.
  562. /// </summary>
  563. public static string CSI_SetBackgroundColorRGB (int r, int g, int b) { return $"{CSI}48;2;{r};{g};{b}m"; }
  564. /// <summary>
  565. /// ESC[48;2;{r};{g};{b}m Append background color as RGB to StringBuilder.
  566. /// </summary>
  567. public static void CSI_AppendBackgroundColorRGB (StringBuilder builder, int r, int g, int b)
  568. {
  569. // InterpolatedStringHandler is composed in stack, skipping the string allocation.
  570. builder.Append ($"{CSI}48;2;{r};{g};{b}m");
  571. }
  572. #endregion Colors
  573. #region Text Styles
  574. /// <summary>
  575. /// Appends an ANSI SGR (Select Graphic Rendition) escape sequence to switch printed text from one
  576. /// <see cref="TextStyle"/> to another.
  577. /// </summary>
  578. /// <param name="output"><see cref="StringBuilder"/> to add escape sequence to.</param>
  579. /// <param name="prev">Previous <see cref="TextStyle"/> to change away from.</param>
  580. /// <param name="next">Next <see cref="TextStyle"/> to change to.</param>
  581. /// <remarks>
  582. /// <para>
  583. /// Unlike colors, most text styling options are not mutually exclusive with each other, and can be applied
  584. /// independently. This creates a problem when
  585. /// switching from one style to another: For instance, if your previous style is just bold, and your next style is
  586. /// just italic, then simply adding the
  587. /// sequence to enable italic text would cause the text to remain bold. This method automatically handles this
  588. /// problem, enabling and disabling styles as
  589. /// necessary to apply exactly the next style.
  590. /// </para>
  591. /// </remarks>
  592. internal static void CSI_AppendTextStyleChange (StringBuilder output, TextStyle prev, TextStyle next)
  593. {
  594. // Do nothing if styles are the same, as no changes are necessary.
  595. if (prev == next)
  596. {
  597. return;
  598. }
  599. // Bitwise operations to determine flag changes. A ^ B are the flags different between two flag sets. These different flags that exist in the next flag
  600. // set (diff & next) are the ones that were enabled in the switch, those that exist in the previous flag set (diff & prev) are the ones that were
  601. // disabled.
  602. TextStyle diff = prev ^ next;
  603. TextStyle enabled = diff & next;
  604. TextStyle disabled = diff & prev;
  605. // List of escape codes to apply.
  606. List<int> sgr = new ();
  607. if (disabled != TextStyle.None)
  608. {
  609. // Special case: Both bold and faint have the same disabling code. While unusual, it can be valid to have both enabled at the same time, so when
  610. // one and only one of them is being disabled, we need to re-enable the other afterward. We can check what flags remain enabled by taking
  611. // prev & next, as this is the set of flags both have.
  612. if (disabled.HasFlag (TextStyle.Bold))
  613. {
  614. sgr.Add (22);
  615. if ((prev & next).HasFlag (TextStyle.Faint))
  616. {
  617. sgr.Add (2);
  618. }
  619. }
  620. if (disabled.HasFlag (TextStyle.Faint))
  621. {
  622. sgr.Add (22);
  623. if ((prev & next).HasFlag (TextStyle.Bold))
  624. {
  625. sgr.Add (1);
  626. }
  627. }
  628. if (disabled.HasFlag (TextStyle.Italic))
  629. {
  630. sgr.Add (23);
  631. }
  632. if (disabled.HasFlag (TextStyle.Underline))
  633. {
  634. sgr.Add (24);
  635. }
  636. if (disabled.HasFlag (TextStyle.Blink))
  637. {
  638. sgr.Add (25);
  639. }
  640. if (disabled.HasFlag (TextStyle.Reverse))
  641. {
  642. sgr.Add (27);
  643. }
  644. if (disabled.HasFlag (TextStyle.Strikethrough))
  645. {
  646. sgr.Add (29);
  647. }
  648. }
  649. if (enabled != TextStyle.None)
  650. {
  651. if (enabled.HasFlag (TextStyle.Bold))
  652. {
  653. sgr.Add (1);
  654. }
  655. if (enabled.HasFlag (TextStyle.Faint))
  656. {
  657. sgr.Add (2);
  658. }
  659. if (enabled.HasFlag (TextStyle.Italic))
  660. {
  661. sgr.Add (3);
  662. }
  663. if (enabled.HasFlag (TextStyle.Underline))
  664. {
  665. sgr.Add (4);
  666. }
  667. if (enabled.HasFlag (TextStyle.Blink))
  668. {
  669. sgr.Add (5);
  670. }
  671. if (enabled.HasFlag (TextStyle.Reverse))
  672. {
  673. sgr.Add (7);
  674. }
  675. if (enabled.HasFlag (TextStyle.Strikethrough))
  676. {
  677. sgr.Add (9);
  678. }
  679. }
  680. output.Append ("\x1b[");
  681. output.Append (string.Join (';', sgr));
  682. output.Append ('m');
  683. }
  684. #endregion Text Styles
  685. #region Requests
  686. /// <summary>
  687. /// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR)
  688. /// https://terminalguide.namepad.de/seq/csi_sn__p-6/
  689. /// The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) ; 1 R
  690. /// </summary>
  691. public static readonly AnsiEscapeSequence CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" };
  692. /// <summary>
  693. /// The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) R
  694. /// </summary>
  695. public const string CSI_RequestCursorPositionReport_Terminator = "R";
  696. /// <summary>
  697. /// ESC [ 0 c - Send Device Attributes (Primary DA)
  698. /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Application-Program-Command-functions
  699. /// https://www.xfree86.org/current/ctlseqs.html
  700. /// Windows Terminal v1.17 and below emits “\x1b[?1;0c”, indicating "VT101 with No Options".
  701. /// Windows Terminal v1.18+ emits: \x1b[?61;6;7;22;23;24;28;32;42c"
  702. /// See https://github.com/microsoft/terminal/pull/14906
  703. /// 61 - The device conforms to level 1 of the character cell display architecture
  704. /// (See https://github.com/microsoft/terminal/issues/15693#issuecomment-1633304497)
  705. /// 6 = Selective erase
  706. /// 7 = Soft fonts
  707. /// 22 = Color text
  708. /// 23 = Greek character sets
  709. /// 24 = Turkish character sets
  710. /// 28 = Rectangular area operations
  711. /// 32 = Text macros
  712. /// 42 = ISO Latin-2 character set
  713. /// The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
  714. /// <see cref="CSI_SendDeviceAttributes2"/>
  715. /// </summary>
  716. public static readonly AnsiEscapeSequence CSI_SendDeviceAttributes = new () { Request = CSI + "0c", Terminator = "c" };
  717. /// <summary>
  718. /// ESC [ > 0 c - Send Device Attributes (Secondary DA)
  719. /// Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220)
  720. /// </summary>
  721. public static readonly AnsiEscapeSequence CSI_SendDeviceAttributes2 = new () { Request = CSI + ">0c", Terminator = "c" };
  722. /// <summary>
  723. /// CSI 16 t - Request sixel resolution (width and height in pixels)
  724. /// </summary>
  725. public static readonly AnsiEscapeSequence CSI_RequestSixelResolution = new () { Request = CSI + "16t", Terminator = "t" };
  726. /// <summary>
  727. /// CSI 14 t - Request window size in pixels (width x height)
  728. /// </summary>
  729. public static readonly AnsiEscapeSequence CSI_RequestWindowSizeInPixels = new () { Request = CSI + "14t", Terminator = "t" };
  730. /// <summary>
  731. /// CSI 1 8 t | yes | yes | yes | report window size in chars
  732. /// https://terminalguide.namepad.de/seq/csi_st-18/
  733. /// The terminator indicating a reply to <see cref="CSI_ReportWindowSizeInChars"/> : ESC [ 8 ; height ; width t
  734. /// </summary>
  735. public static readonly AnsiEscapeSequence CSI_ReportWindowSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" };
  736. /// <summary>
  737. /// The terminator indicating a reply to <see cref="CSI_ReportWindowSizeInChars"/> : ESC [ 8 ; height ; width t
  738. /// </summary>
  739. public const string CSI_ReportWindowSizeInChars_Terminator = "t";
  740. /// <summary>
  741. /// The value of the response to <see cref="CSI_ReportWindowSizeInChars"/> indicating value 1 and 2 are the terminal
  742. /// size in chars.
  743. /// </summary>
  744. public const string CSI_ReportWindowSizeInChars_ResponseValue = "8";
  745. #endregion Requests
  746. #region OSC
  747. /// <summary>
  748. /// OSC (Operating System Command) escape sequence prefix.
  749. /// </summary>
  750. /// <remarks>
  751. /// OSC sequences are used for operating system-specific commands like setting window title,
  752. /// hyperlinks (OSC 8), and other terminal emulator features.
  753. /// </remarks>
  754. public const string OSC = "\u001B]";
  755. /// <summary>
  756. /// String Terminator (ST) - terminates OSC sequences.
  757. /// </summary>
  758. /// <remarks>
  759. /// Can also be represented as BEL (0x07) in some terminals, but ST is more modern.
  760. /// </remarks>
  761. public const string ST = "\u001B\\";
  762. /// <summary>
  763. /// Starts a hyperlink using OSC 8 escape sequence.
  764. /// </summary>
  765. /// <param name="url">The URL to link to (e.g., "https://github.com").</param>
  766. /// <param name="id">Optional hyperlink ID for matching start/end pairs. Use null for automatic matching.</param>
  767. /// <returns>The OSC 8 start sequence.</returns>
  768. /// <remarks>
  769. /// OSC 8 format: ESC ] 8 ; params ; URL ST
  770. /// Supported in Windows Terminal, iTerm2, and other modern terminals.
  771. /// Must be followed by visible text, then terminated with <see cref="OSC_EndHyperlink"/>.
  772. /// </remarks>
  773. public static string OSC_StartHyperlink (string url, string? id = null)
  774. {
  775. // Format: ESC ] 8 ; params ; URL ST
  776. // params can include "id=value" for matching start/end
  777. string parameters = string.IsNullOrEmpty (id) ? "" : $"id={id}";
  778. return $"{OSC}8;{parameters};{url}{ST}";
  779. }
  780. /// <summary>
  781. /// Ends a hyperlink using OSC 8 escape sequence.
  782. /// </summary>
  783. /// <returns>The OSC 8 end sequence.</returns>
  784. /// <remarks>
  785. /// This terminates the hyperlink started by <see cref="OSC_StartHyperlink"/>.
  786. /// Format: ESC ] 8 ; ; ST (empty URL ends the hyperlink).
  787. /// </remarks>
  788. public static string OSC_EndHyperlink ()
  789. {
  790. // Format: ESC ] 8 ; ; ST (empty URL ends hyperlink)
  791. return $"{OSC}8;;{ST}";
  792. }
  793. #endregion OSC
  794. /// <summary>
  795. /// Convert a <see cref="ConsoleKeyInfo"/> array to string.
  796. /// </summary>
  797. /// <param name="consoleKeyInfos"></param>
  798. /// <returns>The string representing the array.</returns>
  799. public static string ToString (ConsoleKeyInfo [] consoleKeyInfos)
  800. {
  801. StringBuilder sb = new ();
  802. foreach (ConsoleKeyInfo keyChar in consoleKeyInfos)
  803. {
  804. sb.Append (keyChar.KeyChar);
  805. }
  806. return sb.ToString ();
  807. }
  808. }