Skip to content

Commit c65c505

Browse files
committed
Merge branch 'v2_3767_ansi-escape-sequence-all-drivers' into sixel-encoder-tinkering
2 parents 64d286c + 9536913 commit c65c505

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2145
-1219
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#nullable enable
2+
namespace Terminal.Gui;
3+
4+
/// <summary>
5+
/// Describes an ongoing ANSI request sent to the console.
6+
/// Use <see cref="ResponseReceived"/> to handle the response
7+
/// when console answers the request.
8+
/// </summary>
9+
public class AnsiEscapeSequenceRequest
10+
{
11+
/// <summary>
12+
/// Request to send e.g. see
13+
/// <see>
14+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
15+
/// </see>
16+
/// </summary>
17+
public required string Request { get; init; }
18+
19+
/// <summary>
20+
/// Invoked when the console responds with an ANSI response code that matches the
21+
/// <see cref="Terminator"/>
22+
/// </summary>
23+
public event EventHandler<AnsiEscapeSequenceResponse>? ResponseReceived;
24+
25+
/// <summary>
26+
/// <para>
27+
/// The terminator that uniquely identifies the type of response as responded
28+
/// by the console. e.g. for
29+
/// <see>
30+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
31+
/// </see>
32+
/// the terminator is
33+
/// <see>
34+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
35+
/// </see>
36+
/// .
37+
/// </para>
38+
/// <para>
39+
/// After sending a request, the first response with matching terminator will be matched
40+
/// to the oldest outstanding request.
41+
/// </para>
42+
/// </summary>
43+
public required string Terminator { get; init; }
44+
45+
/// <summary>
46+
/// Execute an ANSI escape sequence escape which may return a response or error.
47+
/// </summary>
48+
/// <param name="ansiRequest">The ANSI escape sequence to request.</param>
49+
/// <param name="result">
50+
/// When this method returns <see langword="true"/>, an object containing the response with an empty
51+
/// error.
52+
/// </param>
53+
/// <returns>A <see cref="AnsiEscapeSequenceResponse"/> with the response, error, terminator and value.</returns>
54+
public static bool TryExecuteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest, out AnsiEscapeSequenceResponse result)
55+
{
56+
var response = new StringBuilder ();
57+
var error = new StringBuilder ();
58+
var savedIsReportingMouseMoves = false;
59+
NetDriver? netDriver = null;
60+
var values = new string? [] { null };
61+
62+
try
63+
{
64+
switch (Application.Driver)
65+
{
66+
case NetDriver:
67+
netDriver = Application.Driver as NetDriver;
68+
savedIsReportingMouseMoves = netDriver!.IsReportingMouseMoves;
69+
70+
if (savedIsReportingMouseMoves)
71+
{
72+
netDriver.StopReportingMouseMoves ();
73+
}
74+
75+
while (Console.KeyAvailable)
76+
{
77+
netDriver._mainLoopDriver._netEvents._waitForStart.Set ();
78+
netDriver._mainLoopDriver._netEvents._waitForStart.Reset ();
79+
80+
netDriver._mainLoopDriver._netEvents._forceRead = true;
81+
}
82+
83+
netDriver._mainLoopDriver._netEvents._forceRead = false;
84+
85+
break;
86+
case CursesDriver cursesDriver:
87+
savedIsReportingMouseMoves = cursesDriver.IsReportingMouseMoves;
88+
89+
if (savedIsReportingMouseMoves)
90+
{
91+
cursesDriver.StopReportingMouseMoves ();
92+
}
93+
94+
break;
95+
}
96+
97+
if (netDriver is { })
98+
{
99+
NetEvents._suspendRead = true;
100+
}
101+
else
102+
{
103+
Thread.Sleep (100); // Allow time for mouse stopping and to flush the input buffer
104+
105+
// Flush the input buffer to avoid reading stale input
106+
while (Console.KeyAvailable)
107+
{
108+
Console.ReadKey (true);
109+
}
110+
}
111+
112+
// Send the ANSI escape sequence
113+
Console.Write (ansiRequest.Request);
114+
Console.Out.Flush (); // Ensure the request is sent
115+
116+
// Read the response from stdin (response should come back as input)
117+
Thread.Sleep (100); // Allow time for the terminal to respond
118+
119+
// Read input until no more characters are available or the terminator is encountered
120+
while (Console.KeyAvailable)
121+
{
122+
// Peek the next key
123+
ConsoleKeyInfo keyInfo = Console.ReadKey (true); // true to not display on the console
124+
125+
// Append the current key to the response
126+
response.Append (keyInfo.KeyChar);
127+
128+
// Read until no key is available if no terminator was specified or
129+
// check if the key is terminator (ANSI escape sequence ends)
130+
if (!string.IsNullOrEmpty (ansiRequest.Terminator) && keyInfo.KeyChar == ansiRequest.Terminator [^1])
131+
{
132+
// Break out of the loop when terminator is found
133+
break;
134+
}
135+
}
136+
137+
if (string.IsNullOrEmpty (ansiRequest.Terminator))
138+
{
139+
error.AppendLine ("Terminator request is empty.");
140+
}
141+
else if (!response.ToString ().EndsWith (ansiRequest.Terminator [^1]))
142+
{
143+
throw new InvalidOperationException ($"Terminator doesn't ends with: '{ansiRequest.Terminator [^1]}'");
144+
}
145+
}
146+
catch (Exception ex)
147+
{
148+
error.AppendLine ($"Error executing ANSI request: {ex.Message}");
149+
}
150+
finally
151+
{
152+
if (string.IsNullOrEmpty (error.ToString ()))
153+
{
154+
(string? c1Control, string? code, values, string? terminator) = EscSeqUtils.GetEscapeResult (response.ToString ().ToCharArray ());
155+
}
156+
157+
if (savedIsReportingMouseMoves)
158+
{
159+
switch (Application.Driver)
160+
{
161+
case NetDriver:
162+
NetEvents._suspendRead = false;
163+
netDriver!.StartReportingMouseMoves ();
164+
165+
break;
166+
case CursesDriver cursesDriver:
167+
cursesDriver.StartReportingMouseMoves ();
168+
169+
break;
170+
}
171+
}
172+
}
173+
174+
AnsiEscapeSequenceResponse ansiResponse = new ()
175+
{
176+
Response = response.ToString (), Error = error.ToString (),
177+
Terminator = string.IsNullOrEmpty (response.ToString ()) ? "" : response.ToString () [^1].ToString (), Value = values [0]
178+
};
179+
180+
// Invoke the event if it's subscribed
181+
ansiRequest.ResponseReceived?.Invoke (ansiRequest, ansiResponse);
182+
183+
result = ansiResponse;
184+
185+
return string.IsNullOrWhiteSpace (result.Error) && !string.IsNullOrWhiteSpace (result.Response);
186+
}
187+
188+
/// <summary>
189+
/// The value expected in the response e.g.
190+
/// <see>
191+
/// <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
192+
/// </see>
193+
/// which will have a 't' as terminator but also other different request may return the same terminator with a
194+
/// different value.
195+
/// </summary>
196+
public string? Value { get; init; }
197+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#nullable enable
2+
namespace Terminal.Gui;
3+
4+
/// <summary>
5+
/// Describes a finished ANSI received from the console.
6+
/// </summary>
7+
public class AnsiEscapeSequenceResponse
8+
{
9+
/// <summary>
10+
/// Error received from e.g. see
11+
/// <see>
12+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
13+
/// </see>
14+
/// </summary>
15+
public required string Error { get; init; }
16+
17+
/// <summary>
18+
/// Response received from e.g. see
19+
/// <see>
20+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
21+
/// </see>
22+
/// .
23+
/// </summary>
24+
public required string Response { get; init; }
25+
26+
/// <summary>
27+
/// <para>
28+
/// The terminator that uniquely identifies the type of response as responded
29+
/// by the console. e.g. for
30+
/// <see>
31+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
32+
/// </see>
33+
/// the terminator is
34+
/// <see>
35+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
36+
/// </see>
37+
/// </para>
38+
/// <para>
39+
/// The received terminator must match to the terminator sent by the request.
40+
/// </para>
41+
/// </summary>
42+
public required string Terminator { get; init; }
43+
44+
/// <summary>
45+
/// The value expected in the response e.g.
46+
/// <see>
47+
/// <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
48+
/// </see>
49+
/// which will have a 't' as terminator but also other different request may return the same terminator with a
50+
/// different value.
51+
/// </summary>
52+
public string? Value { get; init; }
53+
}

Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,15 @@ public override bool SetCursorVisibility (CursorVisibility visibility)
177177
return true;
178178
}
179179

180+
public bool IsReportingMouseMoves { get; private set; }
181+
180182
public void StartReportingMouseMoves ()
181183
{
182184
if (!RunningUnitTests)
183185
{
184186
Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
187+
188+
IsReportingMouseMoves = true;
185189
}
186190
}
187191

@@ -190,6 +194,8 @@ public void StopReportingMouseMoves ()
190194
if (!RunningUnitTests)
191195
{
192196
Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
197+
198+
IsReportingMouseMoves = false;
193199
}
194200
}
195201

Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,13 +1316,9 @@ public enum DECSCUSR_Style
13161316
/// <summary>
13171317
/// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR)
13181318
/// https://terminalguide.namepad.de/seq/csi_sn__p-6/
1319+
/// The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) ; 1 R
13191320
/// </summary>
1320-
public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n";
1321-
1322-
/// <summary>
1323-
/// The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) R
1324-
/// </summary>
1325-
public const string CSI_RequestCursorPositionReport_Terminator = "R";
1321+
public static readonly AnsiEscapeSequenceRequest CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" };
13261322

13271323
/// <summary>
13281324
/// ESC [ 0 c - Send Device Attributes (Primary DA)
@@ -1341,20 +1337,18 @@ public enum DECSCUSR_Style
13411337
/// 28 = Rectangular area operations
13421338
/// 32 = Text macros
13431339
/// 42 = ISO Latin-2 character set
1340+
/// The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
1341+
/// <see cref="CSI_SendDeviceAttributes2"/>
13441342
/// </summary>
1345-
public static readonly string CSI_SendDeviceAttributes = CSI + "0c";
1343+
public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes = new () { Request = CSI + "0c", Terminator = "c" };
13461344

13471345
/// <summary>
13481346
/// ESC [ > 0 c - Send Device Attributes (Secondary DA)
13491347
/// Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220)
1350-
/// </summary>
1351-
public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c";
1352-
1353-
/// <summary>
13541348
/// The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
13551349
/// <see cref="CSI_SendDeviceAttributes2"/>
13561350
/// </summary>
1357-
public const string CSI_ReportDeviceAttributes_Terminator = "c";
1351+
public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes2 = new () { Request = CSI + ">0c", Terminator = "c" };
13581352

13591353
/*
13601354
TODO: depends on https://github.com/gui-cs/Terminal.Gui/pull/3768
@@ -1372,19 +1366,9 @@ public enum DECSCUSR_Style
13721366
/// <summary>
13731367
/// CSI 1 8 t | yes | yes | yes | report window size in chars
13741368
/// https://terminalguide.namepad.de/seq/csi_st-18/
1375-
/// </summary>
1376-
public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t";
1377-
1378-
/// <summary>
13791369
/// The terminator indicating a reply to <see cref="CSI_ReportTerminalSizeInChars"/> : ESC [ 8 ; height ; width t
13801370
/// </summary>
1381-
public const string CSI_ReportTerminalSizeInChars_Terminator = "t";
1382-
1383-
/// <summary>
1384-
/// The value of the response to <see cref="CSI_ReportTerminalSizeInChars"/> indicating value 1 and 2 are the terminal
1385-
/// size in chars.
1386-
/// </summary>
1387-
public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8";
1371+
public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" };
13881372

13891373
#endregion
13901374
}

0 commit comments

Comments
 (0)