SixelSupportDetector.cs 6.6 KB

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