SixelSupportDetector.cs 6.5 KB

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