diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs
new file mode 100644
index 0000000000..d8dc22ad30
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs
@@ -0,0 +1,197 @@
+#nullable enable
+namespace Terminal.Gui;
+
+///
+/// Describes an ongoing ANSI request sent to the console.
+/// Use to handle the response
+/// when console answers the request.
+///
+public class AnsiEscapeSequenceRequest
+{
+ ///
+ /// Request to send e.g. see
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ ///
+ public required string Request { get; init; }
+
+ ///
+ /// Invoked when the console responds with an ANSI response code that matches the
+ ///
+ ///
+ public event EventHandler? ResponseReceived;
+
+ ///
+ ///
+ /// The terminator that uniquely identifies the type of response as responded
+ /// by the console. e.g. for
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ /// the terminator is
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator
+ ///
+ /// .
+ ///
+ ///
+ /// After sending a request, the first response with matching terminator will be matched
+ /// to the oldest outstanding request.
+ ///
+ ///
+ public required string Terminator { get; init; }
+
+ ///
+ /// Execute an ANSI escape sequence escape which may return a response or error.
+ ///
+ /// The ANSI escape sequence to request.
+ ///
+ /// When this method returns , an object containing the response with an empty
+ /// error.
+ ///
+ /// A with the response, error, terminator and value.
+ public static bool TryExecuteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest, out AnsiEscapeSequenceResponse result)
+ {
+ var response = new StringBuilder ();
+ var error = new StringBuilder ();
+ var savedIsReportingMouseMoves = false;
+ NetDriver? netDriver = null;
+ var values = new string? [] { null };
+
+ try
+ {
+ switch (Application.Driver)
+ {
+ case NetDriver:
+ netDriver = Application.Driver as NetDriver;
+ savedIsReportingMouseMoves = netDriver!.IsReportingMouseMoves;
+
+ if (savedIsReportingMouseMoves)
+ {
+ netDriver.StopReportingMouseMoves ();
+ }
+
+ while (Console.KeyAvailable)
+ {
+ netDriver._mainLoopDriver._netEvents._waitForStart.Set ();
+ netDriver._mainLoopDriver._netEvents._waitForStart.Reset ();
+
+ netDriver._mainLoopDriver._netEvents._forceRead = true;
+ }
+
+ netDriver._mainLoopDriver._netEvents._forceRead = false;
+
+ break;
+ case CursesDriver cursesDriver:
+ savedIsReportingMouseMoves = cursesDriver.IsReportingMouseMoves;
+
+ if (savedIsReportingMouseMoves)
+ {
+ cursesDriver.StopReportingMouseMoves ();
+ }
+
+ break;
+ }
+
+ if (netDriver is { })
+ {
+ NetEvents._suspendRead = true;
+ }
+ else
+ {
+ Thread.Sleep (100); // Allow time for mouse stopping and to flush the input buffer
+
+ // Flush the input buffer to avoid reading stale input
+ while (Console.KeyAvailable)
+ {
+ Console.ReadKey (true);
+ }
+ }
+
+ // Send the ANSI escape sequence
+ Console.Write (ansiRequest.Request);
+ Console.Out.Flush (); // Ensure the request is sent
+
+ // Read the response from stdin (response should come back as input)
+ Thread.Sleep (100); // Allow time for the terminal to respond
+
+ // Read input until no more characters are available or the terminator is encountered
+ while (Console.KeyAvailable)
+ {
+ // Peek the next key
+ ConsoleKeyInfo keyInfo = Console.ReadKey (true); // true to not display on the console
+
+ // Append the current key to the response
+ response.Append (keyInfo.KeyChar);
+
+ // Read until no key is available if no terminator was specified or
+ // check if the key is terminator (ANSI escape sequence ends)
+ if (!string.IsNullOrEmpty (ansiRequest.Terminator) && keyInfo.KeyChar == ansiRequest.Terminator [^1])
+ {
+ // Break out of the loop when terminator is found
+ break;
+ }
+ }
+
+ if (string.IsNullOrEmpty (ansiRequest.Terminator))
+ {
+ error.AppendLine ("Terminator request is empty.");
+ }
+ else if (!response.ToString ().EndsWith (ansiRequest.Terminator [^1]))
+ {
+ throw new InvalidOperationException ($"Terminator doesn't ends with: '{ansiRequest.Terminator [^1]}'");
+ }
+ }
+ catch (Exception ex)
+ {
+ error.AppendLine ($"Error executing ANSI request: {ex.Message}");
+ }
+ finally
+ {
+ if (string.IsNullOrEmpty (error.ToString ()))
+ {
+ (string? c1Control, string? code, values, string? terminator) = EscSeqUtils.GetEscapeResult (response.ToString ().ToCharArray ());
+ }
+
+ if (savedIsReportingMouseMoves)
+ {
+ switch (Application.Driver)
+ {
+ case NetDriver:
+ NetEvents._suspendRead = false;
+ netDriver!.StartReportingMouseMoves ();
+
+ break;
+ case CursesDriver cursesDriver:
+ cursesDriver.StartReportingMouseMoves ();
+
+ break;
+ }
+ }
+ }
+
+ AnsiEscapeSequenceResponse ansiResponse = new ()
+ {
+ Response = response.ToString (), Error = error.ToString (),
+ Terminator = string.IsNullOrEmpty (response.ToString ()) ? "" : response.ToString () [^1].ToString (), Value = values [0]
+ };
+
+ // Invoke the event if it's subscribed
+ ansiRequest.ResponseReceived?.Invoke (ansiRequest, ansiResponse);
+
+ result = ansiResponse;
+
+ return string.IsNullOrWhiteSpace (result.Error) && !string.IsNullOrWhiteSpace (result.Response);
+ }
+
+ ///
+ /// The value expected in the response e.g.
+ ///
+ /// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value
+ ///
+ /// which will have a 't' as terminator but also other different request may return the same terminator with a
+ /// different value.
+ ///
+ public string? Value { get; init; }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs
new file mode 100644
index 0000000000..df98511558
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs
@@ -0,0 +1,53 @@
+#nullable enable
+namespace Terminal.Gui;
+
+///
+/// Describes a finished ANSI received from the console.
+///
+public class AnsiEscapeSequenceResponse
+{
+ ///
+ /// Error received from e.g. see
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ ///
+ public required string Error { get; init; }
+
+ ///
+ /// Response received from e.g. see
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ /// .
+ ///
+ public required string Response { get; init; }
+
+ ///
+ ///
+ /// The terminator that uniquely identifies the type of response as responded
+ /// by the console. e.g. for
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ /// the terminator is
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator
+ ///
+ ///
+ ///
+ /// The received terminator must match to the terminator sent by the request.
+ ///
+ ///
+ public required string Terminator { get; init; }
+
+ ///
+ /// The value expected in the response e.g.
+ ///
+ /// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value
+ ///
+ /// which will have a 't' as terminator but also other different request may return the same terminator with a
+ /// different value.
+ ///
+ public string? Value { get; init; }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
index 99e560044d..a48881d4b1 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
@@ -177,11 +177,15 @@ public override bool SetCursorVisibility (CursorVisibility visibility)
return true;
}
+ public bool IsReportingMouseMoves { get; private set; }
+
public void StartReportingMouseMoves ()
{
if (!RunningUnitTests)
{
Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+
+ IsReportingMouseMoves = true;
}
}
@@ -190,6 +194,8 @@ public void StopReportingMouseMoves ()
if (!RunningUnitTests)
{
Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+
+ IsReportingMouseMoves = false;
}
}
diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
index 1eb63e34aa..5f87b1e264 100644
--- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
+++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
@@ -1316,13 +1316,9 @@ public enum DECSCUSR_Style
///
/// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR)
/// https://terminalguide.namepad.de/seq/csi_sn__p-6/
+ /// The terminal reply to . ESC [ ? (y) ; (x) ; 1 R
///
- public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n";
-
- ///
- /// The terminal reply to . ESC [ ? (y) ; (x) R
- ///
- public const string CSI_RequestCursorPositionReport_Terminator = "R";
+ public static readonly AnsiEscapeSequenceRequest CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" };
///
/// ESC [ 0 c - Send Device Attributes (Primary DA)
@@ -1341,37 +1337,35 @@ public enum DECSCUSR_Style
/// 28 = Rectangular area operations
/// 32 = Text macros
/// 42 = ISO Latin-2 character set
+ /// The terminator indicating a reply to or
+ ///
///
- public static readonly string CSI_SendDeviceAttributes = CSI + "0c";
+ public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes = new () { Request = CSI + "0c", Terminator = "c" };
///
/// ESC [ > 0 c - Send Device Attributes (Secondary DA)
/// Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220)
- ///
- public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c";
-
- ///
/// The terminator indicating a reply to or
///
///
- public const string CSI_ReportDeviceAttributes_Terminator = "c";
+ public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes2 = new () { Request = CSI + ">0c", Terminator = "c" };
///
- /// CSI 1 8 t | yes | yes | yes | report window size in chars
- /// https://terminalguide.namepad.de/seq/csi_st-18/
+ /// CSI 16 t - Request sixel resolution (width and height in pixels)
///
- public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t";
+ public static readonly AnsiEscapeSequenceRequest CSI_RequestSixelResolution = new () { Request = CSI + "16t", Terminator = "t" };
///
- /// The terminator indicating a reply to : ESC [ 8 ; height ; width t
+ /// CSI 14 t - Request window size in pixels (width x height)
///
- public const string CSI_ReportTerminalSizeInChars_Terminator = "t";
+ public static readonly AnsiEscapeSequenceRequest CSI_RequestWindowSizeInPixels = new () { Request = CSI + "14t", Terminator = "t" };
///
- /// The value of the response to indicating value 1 and 2 are the terminal
- /// size in chars.
+ /// CSI 1 8 t | yes | yes | yes | report window size in chars
+ /// https://terminalguide.namepad.de/seq/csi_st-18/
+ /// The terminator indicating a reply to : ESC [ 8 ; height ; width t
///
- public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8";
+ public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" };
#endregion
}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs
index b5729406ce..9097b37309 100644
--- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs
@@ -136,7 +136,7 @@ internal class NetEvents : IDisposable
{
private readonly ManualResetEventSlim _inputReady = new (false);
private CancellationTokenSource _inputReadyCancellationTokenSource;
- private readonly ManualResetEventSlim _waitForStart = new (false);
+ internal readonly ManualResetEventSlim _waitForStart = new (false);
//CancellationTokenSource _waitForStartCancellationTokenSource;
private readonly ManualResetEventSlim _winChange = new (false);
@@ -202,7 +202,7 @@ private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellation
{
// if there is a key available, return it without waiting
// (or dispatching work to the thread queue)
- if (Console.KeyAvailable)
+ if (Console.KeyAvailable && !_suspendRead)
{
return Console.ReadKey (intercept);
}
@@ -211,7 +211,7 @@ private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellation
{
Task.Delay (100, cancellationToken).Wait (cancellationToken);
- if (Console.KeyAvailable)
+ if (Console.KeyAvailable && !_suspendRead)
{
return Console.ReadKey (intercept);
}
@@ -222,6 +222,9 @@ private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellation
return default (ConsoleKeyInfo);
}
+ internal bool _forceRead;
+ internal static bool _suspendRead;
+
private void ProcessInputQueue ()
{
while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
@@ -237,7 +240,7 @@ private void ProcessInputQueue ()
_waitForStart.Reset ();
- if (_inputQueue.Count == 0)
+ if (_inputQueue.Count == 0 || _forceRead)
{
ConsoleKey key = 0;
ConsoleModifiers mod = 0;
@@ -591,55 +594,48 @@ private MouseButtonState MapMouseFlags (MouseFlags mouseFlags)
private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
{
- switch (terminating)
- {
- // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
- case EscSeqUtils.CSI_RequestCursorPositionReport_Terminator:
- var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
-
- if (_lastCursorPosition.Y != point.Y)
- {
- _lastCursorPosition = point;
- var eventType = EventType.WindowPosition;
- var winPositionEv = new WindowPositionEvent { CursorPosition = point };
-
- _inputQueue.Enqueue (
- new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
- );
- }
- else
- {
- return;
- }
-
- break;
+ if (terminating ==
- case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator:
- switch (values [0])
- {
- case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue:
- EnqueueWindowSizeEvent (
- Math.Max (int.Parse (values [1]), 0),
- Math.Max (int.Parse (values [2]), 0),
- Math.Max (int.Parse (values [1]), 0),
- Math.Max (int.Parse (values [2]), 0)
- );
-
- break;
- default:
- EnqueueRequestResponseEvent (c1Control, code, values, terminating);
+ // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
+ // The observation is correct because the response isn't immediate and this is useless
+ EscSeqUtils.CSI_RequestCursorPositionReport.Terminator)
+ {
+ var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
- break;
- }
+ if (_lastCursorPosition.Y != point.Y)
+ {
+ _lastCursorPosition = point;
+ var eventType = EventType.WindowPosition;
+ var winPositionEv = new WindowPositionEvent { CursorPosition = point };
- break;
- case EscSeqUtils.CSI_ReportDeviceAttributes_Terminator:
- ConsoleDriver.SupportsSixel = values.Any (v => v == "4");
- break;
- default:
+ _inputQueue.Enqueue (
+ new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
+ );
+ }
+ else
+ {
+ return;
+ }
+ }
+ else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars.Terminator)
+ {
+ if (values [0] == EscSeqUtils.CSI_ReportTerminalSizeInChars.Value)
+ {
+ EnqueueWindowSizeEvent (
+ Math.Max (int.Parse (values [1]), 0),
+ Math.Max (int.Parse (values [2]), 0),
+ Math.Max (int.Parse (values [1]), 0),
+ Math.Max (int.Parse (values [2]), 0)
+ );
+ }
+ else
+ {
EnqueueRequestResponseEvent (c1Control, code, values, terminating);
-
- break;
+ }
+ }
+ else
+ {
+ EnqueueRequestResponseEvent (c1Control, code, values, terminating);
}
_inputReady.Set ();
@@ -819,7 +815,7 @@ internal class NetDriver : ConsoleDriver
private const int COLOR_RED = 31;
private const int COLOR_WHITE = 37;
private const int COLOR_YELLOW = 33;
- private NetMainLoop _mainLoopDriver;
+ internal NetMainLoop _mainLoopDriver;
public bool IsWinPlatform { get; private set; }
public NetWinVTConsole NetWinConsole { get; private set; }
@@ -1141,13 +1137,9 @@ internal override MainLoop Init ()
_mainLoopDriver = new NetMainLoop (this);
_mainLoopDriver.ProcessInput = ProcessInput;
- _mainLoopDriver._netEvents.EscSeqRequests.Add ("c");
- // Determine if sixel is supported
- Console.Out.Write (EscSeqUtils.CSI_SendDeviceAttributes);
-
return new MainLoop (_mainLoopDriver);
}
-
+
private void ProcessInput (InputResult inputEvent)
{
switch (inputEvent.EventType)
@@ -1348,7 +1340,6 @@ private bool SetCursorPosition (int col, int row)
}
private CursorVisibility? _cachedCursorVisibility;
- private static bool _supportsSixel;
public override void UpdateCursor ()
{
@@ -1397,11 +1388,15 @@ public override bool EnsureCursorVisibility ()
#region Mouse Handling
+ public bool IsReportingMouseMoves { get; private set; }
+
public void StartReportingMouseMoves ()
{
if (!RunningUnitTests)
{
Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+
+ IsReportingMouseMoves = true;
}
}
@@ -1410,6 +1405,8 @@ public void StopReportingMouseMoves ()
if (!RunningUnitTests)
{
Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+
+ IsReportingMouseMoves = false;
}
}
@@ -1769,7 +1766,13 @@ void IMainLoopDriver.Iteration ()
{
while (_resultQueue.Count > 0)
{
- ProcessInput?.Invoke (_resultQueue.Dequeue ().Value);
+ // Always dequeue even if it's null and invoke if isn't null
+ InputResult? dequeueResult = _resultQueue.Dequeue ();
+
+ if (dequeueResult is { })
+ {
+ ProcessInput?.Invoke (dequeueResult.Value);
+ }
}
}
@@ -1825,10 +1828,16 @@ private void NetInputHandler ()
_resultQueue.Enqueue (_netEvents.DequeueInput ());
}
- while (_resultQueue.Count > 0 && _resultQueue.Peek () is null)
+ try
{
- _resultQueue.Dequeue ();
+ while (_resultQueue.Count > 0 && _resultQueue.Peek () is null)
+ {
+ // Dequeue null values
+ _resultQueue.Dequeue ();
+ }
}
+ catch (InvalidOperationException) // Peek can raise an exception
+ { }
if (_resultQueue.Count > 0)
{
diff --git a/Terminal.Gui/Drawing/SixelSupportDetector.cs b/Terminal.Gui/Drawing/SixelSupportDetector.cs
new file mode 100644
index 0000000000..4713d5e259
--- /dev/null
+++ b/Terminal.Gui/Drawing/SixelSupportDetector.cs
@@ -0,0 +1,133 @@
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui;
+
+///
+/// Uses Ansi escape sequences to detect whether sixel is supported
+/// by the terminal.
+///
+public class SixelSupportDetector
+{
+ ///
+ /// Sends Ansi escape sequences to the console to determine whether
+ /// sixel is supported (and
+ /// etc).
+ ///
+ /// Description of sixel support, may include assumptions where
+ /// expected response codes are not returned by console.
+ public SixelSupportResult Detect ()
+ {
+ var result = new SixelSupportResult ();
+
+ result.IsSupported = IsSixelSupportedByDar ();
+
+ if (result.IsSupported)
+ {
+ if (TryGetResolutionDirectly (out var res))
+ {
+ result.Resolution = res;
+ }
+ else if(TryComputeResolution(out res))
+ {
+ result.Resolution = res;
+ }
+
+ result.SupportsTransparency = IsWindowsTerminal () || IsXtermWithTransparency ();
+ }
+
+ return result;
+ }
+
+
+ private bool TryGetResolutionDirectly (out Size resolution)
+ {
+ // Expect something like:
+ //[6;20;10t
+
+ if (AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_RequestSixelResolution, out var response))
+ {
+ // Terminal supports directly responding with resolution
+ var match = Regex.Match (response.Response, @"\[\d+;(\d+);(\d+)t$");
+
+ if (match.Success)
+ {
+ if (int.TryParse (match.Groups [1].Value, out var ry) &&
+ int.TryParse (match.Groups [2].Value, out var rx))
+ {
+ resolution = new Size (rx, ry);
+
+ return true;
+ }
+ }
+ }
+
+ resolution = default;
+ return false;
+ }
+
+
+ private bool TryComputeResolution (out Size resolution)
+ {
+ // Fallback to window size in pixels and characters
+ if (AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_RequestWindowSizeInPixels, out var pixelSizeResponse)
+ && AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_ReportTerminalSizeInChars, out var charSizeResponse))
+ {
+ // Example [4;600;1200t
+ var pixelMatch = Regex.Match (pixelSizeResponse.Response, @"\[\d+;(\d+);(\d+)t$");
+
+ // Example [8;30;120t
+ var charMatch = Regex.Match (charSizeResponse.Response, @"\[\d+;(\d+);(\d+)t$");
+
+ if (pixelMatch.Success && charMatch.Success)
+ {
+ // Extract pixel dimensions
+ if (int.TryParse (pixelMatch.Groups [1].Value, out var pixelHeight)
+ && int.TryParse (pixelMatch.Groups [2].Value, out var pixelWidth)
+ &&
+
+ // Extract character dimensions
+ int.TryParse (charMatch.Groups [1].Value, out var charHeight)
+ && int.TryParse (charMatch.Groups [2].Value, out var charWidth)
+ && charWidth != 0
+ && charHeight != 0) // Avoid divide by zero
+ {
+ // Calculate the character cell size in pixels
+ var cellWidth = (int)Math.Round ((double)pixelWidth / charWidth);
+ var cellHeight = (int)Math.Round ((double)pixelHeight / charHeight);
+
+ // Set the resolution based on the character cell size
+ resolution = new Size (cellWidth, cellHeight);
+
+ return true;
+ }
+ }
+ }
+
+ resolution = default;
+ return false;
+ }
+ private bool IsSixelSupportedByDar ()
+ {
+ return AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_SendDeviceAttributes, out AnsiEscapeSequenceResponse darResponse)
+ ? darResponse.Response.Split (';').Contains ("4")
+ : false;
+ }
+
+ private bool IsWindowsTerminal ()
+ {
+ return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable ("WT_SESSION"));;
+ }
+ private bool IsXtermWithTransparency ()
+ {
+ // Check if running in real xterm (XTERM_VERSION is more reliable than TERM)
+ var xtermVersionStr = Environment.GetEnvironmentVariable ("XTERM_VERSION");
+
+ // If XTERM_VERSION exists, we are in a real xterm
+ if (!string.IsNullOrWhiteSpace (xtermVersionStr) && int.TryParse (xtermVersionStr, out var xtermVersion) && xtermVersion >= 370)
+ {
+ return true;
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Drawing/SixelSupportResult.cs b/Terminal.Gui/Drawing/SixelSupportResult.cs
new file mode 100644
index 0000000000..db42ad2370
--- /dev/null
+++ b/Terminal.Gui/Drawing/SixelSupportResult.cs
@@ -0,0 +1,33 @@
+namespace Terminal.Gui;
+
+///
+/// Describes the discovered state of sixel support and ancillary information
+/// e.g. . You can use
+/// to discover this information.
+///
+public class SixelSupportResult
+{
+ ///
+ /// Whether the terminal supports sixel graphic format.
+ /// Defaults to false.
+ ///
+ public bool IsSupported { get; set; }
+
+ ///
+ /// The number of pixels of sixel that corresponds to each Col ()
+ /// and each Row (. Defaults to 10x20.
+ ///
+ public Size Resolution { get; set; } = new (10, 20);
+
+ ///
+ /// The maximum number of colors that can be included in a sixel image. Defaults
+ /// to 256.
+ ///
+ public int MaxPaletteColors { get; set; } = 256;
+
+ ///
+ /// Whether the terminal supports transparent background sixels.
+ /// Defaults to false
+ ///
+ public bool SupportsTransparency { get; set; }
+}
diff --git a/Terminal.Gui/Views/AutocompleteFilepathContext.cs b/Terminal.Gui/Views/AutocompleteFilepathContext.cs
index f577e554fe..96ce1dfaef 100644
--- a/Terminal.Gui/Views/AutocompleteFilepathContext.cs
+++ b/Terminal.Gui/Views/AutocompleteFilepathContext.cs
@@ -6,7 +6,7 @@ namespace Terminal.Gui;
internal class AutocompleteFilepathContext : AutocompleteContext
{
public AutocompleteFilepathContext (string currentLine, int cursorPosition, FileDialogState state)
- : base (TextModel.ToRuneCellList (currentLine), cursorPosition)
+ : base (RuneCell.ToRuneCellList (currentLine), cursorPosition)
{
State = state;
}
diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs
index e5bd503630..2a8ecd7f5b 100644
--- a/Terminal.Gui/Views/TextField.cs
+++ b/Terminal.Gui/Views/TextField.cs
@@ -944,7 +944,7 @@ public override void OnDrawContent (Rectangle viewport)
int p = ScrollOffset;
var col = 0;
- int width = Frame.Width + OffSetBackground ();
+ int width = Viewport.Width + OffSetBackground ();
int tcount = _text.Count;
Attribute roc = GetReadOnlyColor ();
@@ -1130,10 +1130,10 @@ public virtual void Paste ()
}
int cols = _text [idx].GetColumns ();
- TextModel.SetCol (ref col, Frame.Width - 1, cols);
+ TextModel.SetCol (ref col, Viewport.Width - 1, cols);
}
- int pos = _cursorPosition - ScrollOffset + Math.Min (Frame.X, 0);
+ int pos = _cursorPosition - ScrollOffset + Math.Min (Viewport.X, 0);
Move (pos, 0);
return new Point (pos, 0);
@@ -1216,16 +1216,16 @@ private void Adjust ()
ScrollOffset = _cursorPosition;
need = true;
}
- else if (Frame.Width > 0
- && (ScrollOffset + _cursorPosition - (Frame.Width + offB) == 0
- || TextModel.DisplaySize (_text, ScrollOffset, _cursorPosition).size >= Frame.Width + offB))
+ else if (Viewport.Width > 0
+ && (ScrollOffset + _cursorPosition - (Viewport.Width + offB) == 0
+ || TextModel.DisplaySize (_text, ScrollOffset, _cursorPosition).size >= Viewport.Width + offB))
{
ScrollOffset = Math.Max (
TextModel.CalculateLeftColumn (
_text,
ScrollOffset,
_cursorPosition,
- Frame.Width + offB
+ Viewport.Width + offB
),
0
);
@@ -1330,7 +1330,7 @@ private List DeleteSelectedText ()
private void GenerateSuggestions ()
{
- List currentLine = TextModel.ToRuneCellList (Text);
+ List currentLine = RuneCell.ToRuneCellList (Text);
int cursorPosition = Math.Min (CursorPosition, currentLine.Count);
Autocomplete.Context = new (
diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs
index 680a29c205..eaf54eefc6 100644
--- a/Terminal.Gui/Views/TextView.cs
+++ b/Terminal.Gui/Views/TextView.cs
@@ -42,6 +42,22 @@ public override string ToString ()
return $"U+{Rune.Value:X4} '{Rune.ToString ()}'; {colorSchemeStr}";
}
+
+ /// Converts the string into a .
+ /// The string to convert.
+ /// The to use.
+ ///
+ public static List ToRuneCellList (string str, ColorScheme? colorScheme = null)
+ {
+ List cells = new ();
+
+ foreach (Rune rune in str.EnumerateRunes ())
+ {
+ cells.Add (new () { Rune = rune, ColorScheme = colorScheme });
+ }
+
+ return cells;
+ }
}
internal class TextModel
@@ -233,22 +249,6 @@ public static List> StringToLinesOfRuneCells (string content, Col
return SplitNewLines (cells);
}
- /// Converts the string into a .
- /// The string to convert.
- /// The to use.
- ///
- public static List ToRuneCellList (string str, ColorScheme? colorScheme = null)
- {
- List cells = new ();
-
- foreach (Rune rune in str.EnumerateRunes ())
- {
- cells.Add (new () { Rune = rune, ColorScheme = colorScheme });
- }
-
- return cells;
- }
-
public override string ToString ()
{
var sb = new StringBuilder ();
@@ -855,7 +855,7 @@ internal static int GetColFromX (List t, int start, int x, int tabWidth =
found = true;
}
- _lines [i] = ToRuneCellList (ReplaceText (x, textToReplace!, matchText, col));
+ _lines [i] = RuneCell.ToRuneCellList (ReplaceText (x, textToReplace!, matchText, col));
x = _lines [i];
txt = GetText (x);
pos = new (col, i);
@@ -1706,7 +1706,7 @@ public List> ToListRune (List textList)
foreach (string text in textList)
{
- runesList.Add (TextModel.ToRuneCellList (text));
+ runesList.Add (RuneCell.ToRuneCellList (text));
}
return runesList;
@@ -3601,6 +3601,8 @@ public override void OnDrawContent (Rectangle viewport)
else
{
AddRune (col, row, rune);
+ // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune
+ cols = Math.Max (cols, 1);
}
if (!TextModel.SetCol (ref col, viewport.Right, cols))
@@ -3723,7 +3725,7 @@ public void Paste ()
if (_copyWithoutSelection && contents.FirstOrDefault (x => x == '\n' || x == '\r') == 0)
{
- List runeList = contents is null ? new () : TextModel.ToRuneCellList (contents);
+ List runeList = contents is null ? new () : RuneCell.ToRuneCellList (contents);
List currentLine = GetCurrentLine ();
_historyText.Add (new () { new (currentLine) }, CursorPosition);
@@ -3812,6 +3814,11 @@ public void Paste ()
{
cols += TabWidth + 1;
}
+ else
+ {
+ // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune
+ cols = Math.Max (cols, 1);
+ }
if (!TextModel.SetCol (ref col, Viewport.Width, cols))
{
diff --git a/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs b/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs
new file mode 100644
index 0000000000..eae5085bb3
--- /dev/null
+++ b/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs
@@ -0,0 +1,134 @@
+using System.Collections.Generic;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("AnsiEscapeSequenceRequest", "Ansi Escape Sequence Request")]
+[ScenarioCategory ("Ansi Escape Sequence")]
+public sealed class AnsiEscapeSequenceRequests : Scenario
+{
+ public override void Main ()
+ {
+ // Init
+ Application.Init ();
+
+ // Setup - Create a top-level application window and configure it.
+ Window appWindow = new ()
+ {
+ Title = GetQuitKeyAndName (),
+ };
+ appWindow.Padding.Thickness = new (1);
+
+ var scrRequests = new List
+ {
+ "CSI_SendDeviceAttributes",
+ "CSI_ReportTerminalSizeInChars",
+ "CSI_RequestCursorPositionReport",
+ "CSI_SendDeviceAttributes2"
+ };
+
+ var cbRequests = new ComboBox () { Width = 40, Height = 5, ReadOnly = true, Source = new ListWrapper (new (scrRequests)) };
+ appWindow.Add (cbRequests);
+
+ var label = new Label { Y = Pos.Bottom (cbRequests) + 1, Text = "Request:" };
+ var tfRequest = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 20 };
+ appWindow.Add (label, tfRequest);
+
+ label = new Label { X = Pos.Right (tfRequest) + 1, Y = Pos.Top (tfRequest) - 1, Text = "Value:" };
+ var tfValue = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6 };
+ appWindow.Add (label, tfValue);
+
+ label = new Label { X = Pos.Right (tfValue) + 1, Y = Pos.Top (tfValue) - 1, Text = "Terminator:" };
+ var tfTerminator = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4 };
+ appWindow.Add (label, tfTerminator);
+
+ cbRequests.SelectedItemChanged += (s, e) =>
+ {
+ var selAnsiEscapeSequenceRequestName = scrRequests [cbRequests.SelectedItem];
+ AnsiEscapeSequenceRequest selAnsiEscapeSequenceRequest = null;
+ switch (selAnsiEscapeSequenceRequestName)
+ {
+ case "CSI_SendDeviceAttributes":
+ selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes;
+ break;
+ case "CSI_ReportTerminalSizeInChars":
+ selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_ReportTerminalSizeInChars;
+ break;
+ case "CSI_RequestCursorPositionReport":
+ selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_RequestCursorPositionReport;
+ break;
+ case "CSI_SendDeviceAttributes2":
+ selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes2;
+ break;
+ }
+
+ tfRequest.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Request : "";
+ tfValue.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Value ?? "" : "";
+ tfTerminator.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Terminator : "";
+ };
+ // Forces raise cbRequests.SelectedItemChanged to update TextFields
+ cbRequests.SelectedItem = 0;
+
+ label = new Label { Y = Pos.Bottom (tfRequest) + 2, Text = "Response:" };
+ var tvResponse = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true };
+ appWindow.Add (label, tvResponse);
+
+ label = new Label { X = Pos.Right (tvResponse) + 1, Y = Pos.Top (tvResponse) - 1, Text = "Error:" };
+ var tvError = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true };
+ appWindow.Add (label, tvError);
+
+ label = new Label { X = Pos.Right (tvError) + 1, Y = Pos.Top (tvError) - 1, Text = "Value:" };
+ var tvValue = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6, Height = 4, ReadOnly = true };
+ appWindow.Add (label, tvValue);
+
+ label = new Label { X = Pos.Right (tvValue) + 1, Y = Pos.Top (tvValue) - 1, Text = "Terminator:" };
+ var tvTerminator = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4, Height = 4, ReadOnly = true };
+ appWindow.Add (label, tvTerminator);
+
+ var btnResponse = new Button { X = Pos.Center (), Y = Pos.Bottom (tvResponse) + 2, Text = "Send Request", IsDefault = true };
+
+ var lblSuccess = new Label { X = Pos.Center (), Y = Pos.Bottom (btnResponse) + 1 };
+ appWindow.Add (lblSuccess);
+
+ btnResponse.Accept += (s, e) =>
+ {
+ var ansiEscapeSequenceRequest = new AnsiEscapeSequenceRequest
+ {
+ Request = tfRequest.Text,
+ Terminator = tfTerminator.Text,
+ Value = string.IsNullOrEmpty (tfValue.Text) ? null : tfValue.Text
+ };
+
+ var success = AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (
+ ansiEscapeSequenceRequest,
+ out AnsiEscapeSequenceResponse ansiEscapeSequenceResponse
+ );
+
+ tvResponse.Text = ansiEscapeSequenceResponse.Response;
+ tvError.Text = ansiEscapeSequenceResponse.Error;
+ tvValue.Text = ansiEscapeSequenceResponse.Value ?? "";
+ tvTerminator.Text = ansiEscapeSequenceResponse.Terminator;
+
+ if (success)
+ {
+ lblSuccess.ColorScheme = Colors.ColorSchemes ["Base"];
+ lblSuccess.Text = "Successful";
+ }
+ else
+ {
+ lblSuccess.ColorScheme = Colors.ColorSchemes ["Error"];
+ lblSuccess.Text = "Error";
+ }
+ };
+ appWindow.Add (btnResponse);
+
+ appWindow.Add (new Label { Y = Pos.Bottom (lblSuccess) + 2, Text = "You can send other requests by editing the TextFields." });
+
+ // Run - Start the application.
+ Application.Run (appWindow);
+ appWindow.Dispose ();
+
+ // Shutdown - Calling Application.Shutdown is required.
+ Application.Shutdown ();
+ }
+}
diff --git a/UICatalog/Scenarios/Images.cs b/UICatalog/Scenarios/Images.cs
index 6e319c8521..9fb4666daf 100644
--- a/UICatalog/Scenarios/Images.cs
+++ b/UICatalog/Scenarios/Images.cs
@@ -61,9 +61,15 @@ public class Images : Scenario
private RadioGroup _rgDistanceAlgorithm;
private NumericUpDown _popularityThreshold;
private SixelToRender _sixelImage;
+ private SixelSupportResult _sixelSupportResult;
public override void Main ()
{
+ var sixelSupportDetector = new SixelSupportDetector ();
+ _sixelSupportResult = sixelSupportDetector.Detect ();
+
+ ConsoleDriver.SupportsSixel = _sixelSupportResult.IsSupported;
+
Application.Init ();
_win = new() { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" };
@@ -158,8 +164,23 @@ private void BtnStartFireOnAccept (object sender, HandledEventArgs e)
return;
}
+ if (!_sixelSupportResult.SupportsTransparency)
+ {
+ if (MessageBox.Query (
+ "Transparency Not Supported",
+ "It looks like your terminal does not support transparent sixel backgrounds. Do you want to try anyway?",
+ "Yes",
+ "No")
+ != 0)
+ {
+ return;
+ }
+ }
+
+
_fire = new DoomFire (_win.Frame.Width * _pxX.Value, _win.Frame.Height * _pxY.Value);
_fireEncoder = new SixelEncoder ();
+ _fireEncoder.Quantizer.MaxColors = Math.Min (_fireEncoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
_fireEncoder.Quantizer.PaletteBuildingAlgorithm = new ConstPalette (_fire.Palette);
_fireFrameCounter = 0;
@@ -337,7 +358,7 @@ private void BuildSixelTab ()
{
X = Pos.Right (lblPxX),
Y = Pos.Bottom (btnStartFire) + 1,
- Value = 10
+ Value = _sixelSupportResult.Resolution.Width
};
var lblPxY = new Label
@@ -351,7 +372,7 @@ private void BuildSixelTab ()
{
X = Pos.Right (lblPxY),
Y = Pos.Bottom (_pxX),
- Value = 20
+ Value = _sixelSupportResult.Resolution.Height
};
var l1 = new Label ()
@@ -500,6 +521,7 @@ int pixelsPerCellY
)
{
var encoder = new SixelEncoder ();
+ encoder.Quantizer.MaxColors = Math.Min (encoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
encoder.Quantizer.PaletteBuildingAlgorithm = GetPaletteBuilder ();
encoder.Quantizer.DistanceAlgorithm = GetDistanceAlgorithm ();
diff --git a/UnitTests/Text/AutocompleteTests.cs b/UnitTests/Text/AutocompleteTests.cs
index c7d907e1a9..1689c70c8d 100644
--- a/UnitTests/Text/AutocompleteTests.cs
+++ b/UnitTests/Text/AutocompleteTests.cs
@@ -254,7 +254,7 @@ public void Test_GenerateSuggestions_Simple ()
ac.GenerateSuggestions (
new (
- TextModel.ToRuneCellList (tv.Text),
+ RuneCell.ToRuneCellList (tv.Text),
2
)
);
diff --git a/UnitTests/View/Mouse/MouseTests.cs b/UnitTests/View/Mouse/MouseTests.cs
index e365f98a8d..8c021e3c4c 100644
--- a/UnitTests/View/Mouse/MouseTests.cs
+++ b/UnitTests/View/Mouse/MouseTests.cs
@@ -404,6 +404,7 @@ public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_M
me.Handled = false;
view.Dispose ();
+ Application.ResetState (ignoreDisposed: true);
}
[Theory]
diff --git a/UnitTests/Views/TextFieldTests.cs b/UnitTests/Views/TextFieldTests.cs
index 5c81dffcad..4e1fec138b 100644
--- a/UnitTests/Views/TextFieldTests.cs
+++ b/UnitTests/Views/TextFieldTests.cs
@@ -2108,4 +2108,18 @@ public void Autocomplete_Visible_False_By_Default ()
Assert.True (t.Visible);
Assert.False (t.Autocomplete.Visible);
}
+
+ [Fact]
+ [AutoInitShutdown]
+ public void Draw_Esc_Rune ()
+ {
+ var tf = new TextField { Width = 5, Text = "\u001b" };
+ tf.BeginInit ();
+ tf.EndInit ();
+ tf.Draw ();
+
+ TestHelpers.AssertDriverContentsWithFrameAre ("\u241b", output);
+
+ tf.Dispose ();
+ }
}
diff --git a/UnitTests/Views/TextViewTests.cs b/UnitTests/Views/TextViewTests.cs
index 0c2bbe12f2..b69d3108fe 100644
--- a/UnitTests/Views/TextViewTests.cs
+++ b/UnitTests/Views/TextViewTests.cs
@@ -8699,4 +8699,18 @@ public void Autocomplete_Visible_False_By_Default ()
Assert.True (t.Visible);
Assert.False (t.Autocomplete.Visible);
}
+
+ [Fact]
+ [AutoInitShutdown]
+ public void Draw_Esc_Rune ()
+ {
+ var tv = new TextView { Width = 5, Height = 1, Text = "\u001b" };
+ tv.BeginInit ();
+ tv.EndInit ();
+ tv.Draw ();
+
+ TestHelpers.AssertDriverContentsWithFrameAre ("\u241b", _output);
+
+ tv.Dispose ();
+ }
}