SixelSupportDetector.cs 6.5 KB

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