SixelSupportDetector.cs 6.2 KB

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