SixelSupportDetector.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. using System.Text.RegularExpressions;
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// Uses Ansi escape sequences to detect whether sixel is supported
  5. /// by the terminal.
  6. /// </summary>
  7. public class SixelSupportDetector
  8. {
  9. /// <summary>
  10. /// Sends Ansi escape sequences to the console to determine whether
  11. /// sixel is supported (and <see cref="SixelSupportResult.Resolution"/>
  12. /// etc).
  13. /// </summary>
  14. /// <returns>
  15. /// Description of sixel support, may include assumptions where
  16. /// expected response codes are not returned by console.
  17. /// </returns>
  18. public void Detect (Action<SixelSupportResult> resultCallback)
  19. {
  20. var result = new SixelSupportResult ();
  21. result.SupportsTransparency = IsWindowsTerminal () || IsXtermWithTransparency ();
  22. IsSixelSupportedByDar (result, resultCallback);
  23. }
  24. private void TryGetResolutionDirectly (SixelSupportResult result, Action<SixelSupportResult> resultCallback)
  25. {
  26. // Expect something like:
  27. //<esc>[6;20;10t
  28. QueueRequest (
  29. EscSeqUtils.CSI_RequestSixelResolution,
  30. r =>
  31. {
  32. // Terminal supports directly responding with resolution
  33. Match match = Regex.Match (r, @"\[\d+;(\d+);(\d+)t$");
  34. if (match.Success)
  35. {
  36. if (int.TryParse (match.Groups [1].Value, out int ry) && int.TryParse (match.Groups [2].Value, out int rx))
  37. {
  38. result.Resolution = new (rx, ry);
  39. }
  40. }
  41. // Finished
  42. resultCallback.Invoke (result);
  43. },
  44. // Request failed, so try to compute instead
  45. () => TryComputeResolution (result, resultCallback));
  46. }
  47. private void TryComputeResolution (SixelSupportResult result, Action<SixelSupportResult> resultCallback)
  48. {
  49. string windowSize;
  50. string sizeInChars;
  51. QueueRequest (
  52. EscSeqUtils.CSI_RequestWindowSizeInPixels,
  53. r1 =>
  54. {
  55. windowSize = r1;
  56. QueueRequest (
  57. EscSeqUtils.CSI_ReportTerminalSizeInChars,
  58. r2 =>
  59. {
  60. sizeInChars = r2;
  61. ComputeResolution (result, windowSize, sizeInChars);
  62. resultCallback (result);
  63. },
  64. () => resultCallback (result));
  65. },
  66. () => resultCallback (result));
  67. }
  68. private void ComputeResolution (SixelSupportResult result, string windowSize, string sizeInChars)
  69. {
  70. // Fallback to window size in pixels and characters
  71. // Example [4;600;1200t
  72. Match pixelMatch = Regex.Match (windowSize, @"\[\d+;(\d+);(\d+)t$");
  73. // Example [8;30;120t
  74. Match charMatch = Regex.Match (sizeInChars, @"\[\d+;(\d+);(\d+)t$");
  75. if (pixelMatch.Success && charMatch.Success)
  76. {
  77. // Extract pixel dimensions
  78. if (int.TryParse (pixelMatch.Groups [1].Value, out int pixelHeight)
  79. && int.TryParse (pixelMatch.Groups [2].Value, out int pixelWidth)
  80. &&
  81. // Extract character dimensions
  82. int.TryParse (charMatch.Groups [1].Value, out int charHeight)
  83. && int.TryParse (charMatch.Groups [2].Value, out int charWidth)
  84. && charWidth != 0
  85. && charHeight != 0) // Avoid divide by zero
  86. {
  87. // Calculate the character cell size in pixels
  88. var cellWidth = (int)Math.Round ((double)pixelWidth / charWidth);
  89. var cellHeight = (int)Math.Round ((double)pixelHeight / charHeight);
  90. // Set the resolution based on the character cell size
  91. result.Resolution = new (cellWidth, cellHeight);
  92. }
  93. }
  94. }
  95. private void IsSixelSupportedByDar (SixelSupportResult result, Action<SixelSupportResult> resultCallback)
  96. {
  97. QueueRequest (
  98. EscSeqUtils.CSI_SendDeviceAttributes,
  99. r =>
  100. {
  101. result.IsSupported = ResponseIndicatesSupport (r);
  102. if (result.IsSupported)
  103. {
  104. TryGetResolutionDirectly (result, resultCallback);
  105. }
  106. else
  107. {
  108. resultCallback (result);
  109. }
  110. },
  111. () => resultCallback (result));
  112. }
  113. private static void QueueRequest (AnsiEscapeSequence req, Action<string> responseCallback, Action abandoned)
  114. {
  115. var newRequest = new AnsiEscapeSequenceRequest
  116. {
  117. Request = req.Request,
  118. Terminator = req.Terminator,
  119. ResponseReceived = responseCallback,
  120. Abandoned = abandoned
  121. };
  122. Application.Driver?.QueueAnsiRequest (newRequest);
  123. }
  124. private static bool ResponseIndicatesSupport (string response) { return response.Split (';').Contains ("4"); }
  125. private static bool IsWindowsTerminal ()
  126. {
  127. return !string.IsNullOrWhiteSpace (Environment.GetEnvironmentVariable ("WT_SESSION"));
  128. ;
  129. }
  130. private static bool IsXtermWithTransparency ()
  131. {
  132. // Check if running in real xterm (XTERM_VERSION is more reliable than TERM)
  133. string xtermVersionStr = Environment.GetEnvironmentVariable ("XTERM_VERSION");
  134. // If XTERM_VERSION exists, we are in a real xterm
  135. if (!string.IsNullOrWhiteSpace (xtermVersionStr) && int.TryParse (xtermVersionStr, out int xtermVersion) && xtermVersion >= 370)
  136. {
  137. return true;
  138. }
  139. return false;
  140. }
  141. }