Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #3767. Allowing any driver to request ANSI escape sequence with immediate response. #3768

Open
wants to merge 93 commits into
base: v2_develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
9de21e0
Fixes #3767. Allowing any driver to request ANSI escape sequence with…
BDisp Sep 30, 2024
0b3d219
Using a more appropriate request for cursor position.
BDisp Sep 30, 2024
c89a9c8
Add AnsiEscapeSequenceRequest and AnsiEscapeSequenceResponse classes.
BDisp Sep 30, 2024
21c3155
Prevents empty response error.
BDisp Oct 1, 2024
7142a04
Add AnsiEscapeSequenceRequests scenario.
BDisp Oct 1, 2024
dd31796
Improving scenario layout.
BDisp Oct 1, 2024
8d52fe7
Fix NetDriver read key issue.
BDisp Oct 2, 2024
1d20bce
Change file name.
BDisp Oct 2, 2024
c320fc2
Improves null dequeues handling.
BDisp Oct 2, 2024
8050202
Replace ExecuteAnsiRequest with TryParse.
BDisp Oct 5, 2024
bdc6fe6
Code cleanup.
BDisp Oct 5, 2024
922f586
Replace from TryParse to TryExecuteAnsiRequest.
BDisp Oct 5, 2024
2c76ed2
Fix exception throwing if no terminator is specified.
BDisp Oct 7, 2024
331a49e
Fix merge errors.
BDisp Oct 9, 2024
e5c30eb
Make AnsiEscapeSequenceRequest agnostic of each driver.
BDisp Oct 13, 2024
7249de0
Cannot run with unit tests.
BDisp Oct 13, 2024
f850e73
Fix unit test.
BDisp Oct 13, 2024
67d497c
Fixes CursesDriver stale buffer.
BDisp Oct 13, 2024
b35b9f5
Add abstract WriteAnsi into the ConsoleDriver for each driver handlin…
BDisp Oct 14, 2024
a2e4d82
Add WriteAnsiDefault method to common code.
BDisp Oct 14, 2024
cc1d668
Fix Window Terminal Preview using WindowsDriver.
BDisp Oct 14, 2024
5ce3996
Prevents throwing if selected item is equal to minus 1.
BDisp Oct 14, 2024
d963941
Preparing NetDriver to handle ansi response on demand.
BDisp Oct 15, 2024
0de8262
View.Mouse cleanup - WIP
tig Oct 15, 2024
c318748
View.Mouse cleanup - WIP3
tig Oct 15, 2024
2ad9887
View.Mouse cleanup - Fixed combobox
tig Oct 15, 2024
ec14b62
Merged MouseEvent and MouseEventEventArgs into MouseEventArgs
tig Oct 15, 2024
9dcfe02
Fixed Time/DateField crash
tig Oct 15, 2024
59396ae
Fixed Time/DateField crash 2
tig Oct 15, 2024
49096fa
Fix merge errors.
BDisp Oct 15, 2024
884011e
Improving WriteAnsi method to return the response.
BDisp Oct 16, 2024
9348328
Rename to WriteAnsiRequest.
BDisp Oct 16, 2024
1645d1f
Ensure dequeue on bad requests or without response in NetDriver.
BDisp Oct 16, 2024
34fb5b5
Non-blocking ReadConsoleInput in WindowsDriver.
BDisp Oct 16, 2024
8a6928e
Merge branch 'v2_develop' into v2_3767_ansi-escape-sequence-all-drivers
BDisp Oct 17, 2024
7fa098f
Remove unnecessary IsSuspendRead property.
BDisp Oct 18, 2024
9759b97
Remove unused variable.
BDisp Oct 18, 2024
1ecff5e
Fix ansi multi-thread requests handling in the NetDriver.
BDisp Oct 18, 2024
9a840dd
Remove unnecessary WriteAnsiRequestDefault method.
BDisp Oct 19, 2024
cc4f7e9
Simplifying code.
BDisp Oct 19, 2024
542d82d
Remove response from error.
BDisp Oct 19, 2024
9749142
Set _forceRead as true before set _waitForProbe.
BDisp Oct 20, 2024
9eb7023
Improves CursesDriver to allow non-blocking input polling.
BDisp Oct 20, 2024
4090ea4
Avoids response starting without Esc char on stressing tests.
BDisp Oct 20, 2024
5ef34b8
Refactoring code.
BDisp Oct 21, 2024
ee0f93d
Merge branch 'v2_develop' into v2_3767_ansi-escape-sequence-all-drivers
BDisp Oct 28, 2024
c1063a0
Remove unneeded abstract ConsoleDriver objects.
BDisp Oct 31, 2024
1b6963f
Improves a lot of console key mappings.
BDisp Oct 31, 2024
ba607f1
Commenting GetIsKeyCodeAtoZ contradiction debug check.
BDisp Oct 31, 2024
ee8d040
Fix NetDriver and WindowsDriver.
BDisp Oct 31, 2024
8ab7ec5
Fix for WindowsDriver.
BDisp Oct 31, 2024
de45b4b
Refactoring a lot CursesDriver.
BDisp Oct 31, 2024
6f25337
Cleanup unused code and comments.
BDisp Nov 1, 2024
126bcef
Add EscSeqRequests support to WindowsDriver.
BDisp Nov 1, 2024
5b39c3d
Fix unit test.
BDisp Nov 1, 2024
c89efe5
Add tab view for single/multi request sends
tznind Nov 2, 2024
0ddc11c
Add bulk send to scenario
tznind Nov 2, 2024
64ad1a9
Fix a bug which was sending the terminator to the mainloop.
BDisp Nov 2, 2024
a2872cd
Ensures a new iteration when _eventReady is already set.
BDisp Nov 2, 2024
1d58ba4
Merge pull request #201 from tznind/single-multi
BDisp Nov 2, 2024
68b41a3
Set CanFocus true and only SetNeedsDisplay if were sent or answered.
BDisp Nov 2, 2024
c999fc0
Trying fix unit tests.
BDisp Nov 2, 2024
3c4564c
Explain what the colors represent.
BDisp Nov 2, 2024
2e0bc01
Fixes #3807. WindowsDriver doesn't process characters with accents.
BDisp Nov 4, 2024
2d8bfd5
Fix comment.
BDisp Nov 4, 2024
808896d
Fix bug where some key were not been split.
BDisp Nov 4, 2024
782325f
Change to ConcurrentQueue.
BDisp Nov 4, 2024
e2f3b7b
Using empty string instead of space.
BDisp Nov 4, 2024
e35b37f
Add InvalidRequestTerminator property.
BDisp Nov 4, 2024
da21ef1
Improves drivers responses to being more reliable.
BDisp Nov 4, 2024
81eb301
Fix escSeqRequests that may be null running unit tests.
BDisp Nov 4, 2024
173f820
Disable HACK_CHECK_WINCHANGED.
BDisp Nov 4, 2024
472ec45
Remove _screenBuffer and only using the alternate buffer.
BDisp Nov 5, 2024
ad4f6c4
Fix unit test.
BDisp Nov 5, 2024
f73c817
Fix unit tests.
BDisp Nov 5, 2024
7921ae3
Returns ansiRequest.Response instead of a variable for more thread safe.
BDisp Nov 5, 2024
ecbbed0
For re-run CI.
BDisp Nov 5, 2024
2919d55
Code Review
tig Nov 6, 2024
4ac79c5
Merge pull request #203 from tig/BDisp-v2_3767_ansi-escape-sequence-a…
BDisp Nov 6, 2024
7d9ae7f
Ad more unit test to handling with IsLetterOrDigit, IsPunctuation and…
BDisp Nov 6, 2024
eb98707
Revert Key class changes.
BDisp Nov 6, 2024
0b11e20
Change Response to a nullable string.
BDisp Nov 6, 2024
bdcc0ff
Return null instead if empty and fix a bug that was returning the ter…
BDisp Nov 6, 2024
629cea8
Handling null response.
BDisp Nov 7, 2024
f87c2b1
Rename EscSeq to AnsiEscapeSequence and move to his folder.
BDisp Nov 7, 2024
dba089f
Change category.
BDisp Nov 7, 2024
5efba6a
Code cleanup.
BDisp Nov 7, 2024
b7b9e01
Fix error with Key.Space using lowercase letters.
BDisp Nov 7, 2024
a64f68c
Moving MapKey method to the AnsiEscapeSequenceRequestUtils class.
BDisp Nov 7, 2024
cc21bd4
Fix a bug where a esc key was ignored.
BDisp Nov 7, 2024
3e952df
Change to BlockingCollection, thanks to @tznind.
BDisp Nov 7, 2024
44d5997
Split more WindowDriver classes and #nullable enable.
BDisp Nov 7, 2024
873b578
#nullable enable.
BDisp Nov 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Terminal.Gui/Application/Application.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ public static void Refresh ()
{
if (tl.LayoutNeeded)
{
tl.SetRelativeLayout (new (Driver!.Cols, Driver.Rows));
tl.LayoutSubviews ();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#nullable enable
namespace Terminal.Gui;

/// <summary>
/// Describes an ongoing ANSI request sent to the console.
/// Use <see cref="ResponseReceived"/> to handle the response
/// when console answers the request.
/// </summary>
public class AnsiEscapeSequenceRequest
{
internal readonly object _responseLock = new (); // Per-instance lock

/// <summary>
/// Request to send e.g. see
/// <see>
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
/// </see>
/// </summary>
public required string Request { get; init; }
BDisp marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Response received from the request.
/// </summary>
public string Response { get; internal set; } = string.Empty;
BDisp marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Invoked when the console responds with an ANSI response code that matches the
/// <see cref="Terminator"/>
/// </summary>
public event EventHandler<AnsiEscapeSequenceResponse>? ResponseReceived;

/// <summary>
/// <para>
/// The terminator that uniquely identifies the type of response as responded
/// by the console. e.g. for
/// <see>
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
/// </see>
/// the terminator is
/// <see>
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
/// </see>
/// .
/// </para>
/// <para>
/// After sending a request, the first response with matching terminator will be matched
/// to the oldest outstanding request.
/// </para>
/// </summary>
public required string Terminator { get; init; }

/// <summary>
/// Execute an ANSI escape sequence escape which may return a response or error.
/// </summary>
/// <param name="ansiRequest">The ANSI escape sequence to request.</param>
/// <param name="result">
/// When this method returns <see langword="true"/>, an object containing the response with an empty
/// error.
/// </param>
/// <returns>A <see cref="AnsiEscapeSequenceResponse"/> with the response, error, terminator and value.</returns>
public static bool TryExecuteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest, out AnsiEscapeSequenceResponse result)
{
var error = new StringBuilder ();
var values = new string? [] { null };

try
{
ConsoleDriver? driver = Application.Driver;

// Send the ANSI escape sequence
ansiRequest.Response = driver?.WriteAnsiRequest (ansiRequest)!;

if (!string.IsNullOrEmpty (ansiRequest.Response) && !ansiRequest.Response.StartsWith (EscSeqUtils.KeyEsc))
{
throw new InvalidOperationException ("Invalid escape character!");
}

if (string.IsNullOrEmpty (ansiRequest.Terminator))
{
throw new InvalidOperationException ("Terminator request is empty.");
}

if (!ansiRequest.Response.EndsWith (ansiRequest.Terminator [^1]))
{
string resp = string.IsNullOrEmpty (ansiRequest.Response) ? "" : ansiRequest.Response.Last ().ToString ();

throw new InvalidOperationException ($"Terminator ends with '{resp}'\nand doesn't end with: '{ansiRequest.Terminator [^1]}'");
}
}
catch (Exception ex)
{
error.AppendLine ($"Error executing ANSI request:\n{ex.Message}");
}
finally
{
if (string.IsNullOrEmpty (error.ToString ()))
{
(string? _, string? _, values, string? _) = EscSeqUtils.GetEscapeResult (ansiRequest.Response.ToCharArray ());
}
}

AnsiEscapeSequenceResponse ansiResponse = new ()
{
Response = ansiRequest.Response, Error = error.ToString (),
Terminator = string.IsNullOrEmpty (ansiRequest.Response) ? "" : ansiRequest.Response [^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);
}

/// <summary>
/// The value expected in the response e.g.
/// <see>
/// <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
/// </see>
/// which will have a 't' as terminator but also other different request may return the same terminator with a
/// different value.
/// </summary>
public string? Value { get; init; }

internal void RaiseResponseFromInput (AnsiEscapeSequenceRequest ansiRequest, string response) { ResponseFromInput?.Invoke (ansiRequest, response); }

internal event EventHandler<string>? ResponseFromInput;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#nullable enable
namespace Terminal.Gui;

/// <summary>
/// Describes a finished ANSI received from the console.
/// </summary>
public class AnsiEscapeSequenceResponse
{
/// <summary>
/// Error received from e.g. see
/// <see>
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
/// </see>
/// </summary>
public required string Error { get; init; }

/// <summary>
/// Response received from e.g. see
/// <see>
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
/// </see>
/// .
/// </summary>
public required string Response { get; init; }

/// <summary>
/// <para>
/// The terminator that uniquely identifies the type of response as responded
/// by the console. e.g. for
/// <see>
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
/// </see>
/// the terminator is
/// <see>
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
/// </see>
/// </para>
/// <para>
/// The received terminator must match to the terminator sent by the request.
/// </para>
/// </summary>
public required string Terminator { get; init; }

/// <summary>
/// The value expected in the response e.g.
/// <see>
/// <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
/// </see>
/// which will have a 't' as terminator but also other different request may return the same terminator with a
/// different value.
/// </summary>
public string? Value { get; init; }
}
19 changes: 16 additions & 3 deletions Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public void AddRune (Rune rune)
// are correctly combined with the base char, but are ALSO treated as 1 column
// width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`.
//
// Until this is addressed (see Issue #), we do our best by
// Until this is addressed (see Issue #), we do our best by
// a) Attempting to normalize any CM with the base char to it's left
// b) Ignoring any CMs that don't normalize
if (Col > 0)
Expand All @@ -167,7 +167,7 @@ public void AddRune (Rune rune)
if (normalized.Length == 1)
{
// It normalized! We can just set the Cell to the left with the
// normalized codepoint
// normalized codepoint
Contents [Row, Col - 1].Rune = (Rune)normalized [0];

// Ignore. Don't move to next column because we're already there
Expand Down Expand Up @@ -377,7 +377,7 @@ public void FillRect (Rectangle rect, Rune rune = default)
{
Contents [r, c] = new Cell
{
Rune = (rune != default ? rune : (Rune)' '),
Rune = rune != default ? rune : (Rune)' ',
Attribute = CurrentAttribute, IsDirty = true
};
_dirtyLines! [r] = true;
Expand Down Expand Up @@ -608,6 +608,19 @@ public void OnMouseEvent (MouseEventArgs a)
/// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);

/// <summary>
/// Provide handling for the terminal write ANSI escape sequence request.
/// </summary>
/// <param name="ansiRequest">The <see cref="AnsiEscapeSequenceRequest"/> object.</param>
/// <returns>The request response.</returns>
public abstract string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be virtual with a default implementation that does the common stuff?
Looking at the implementations of this method, there is TONs of duplicated code.
We should figure out how to find just the things that are unique to each driver and
create more fine-grained APIs to handle those.


/// <summary>
/// Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
/// </summary>
/// <param name="ansi"></param>
public abstract void WriteRaw (string ansi);
BDisp marked this conversation as resolved.
Show resolved Hide resolved

#endregion
}

Expand Down
39 changes: 37 additions & 2 deletions Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ public static ConsoleModifiers MapToConsoleModifiers (KeyCode key)
{
var modifiers = new ConsoleModifiers ();

if (key.HasFlag (KeyCode.ShiftMask))
if (key.HasFlag (KeyCode.ShiftMask) || char.IsUpper ((char)key))
{
modifiers |= ConsoleModifiers.Shift;
}
Expand Down Expand Up @@ -590,7 +590,8 @@ internal static uint GetKeyCharFromUnicodeChar (

if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter)
{
consoleKey = char.ToUpper (stFormD [i]);
char ck = char.ToUpper (stFormD [i]);
consoleKey = (uint)(ck > 0 && ck <= 255 ? char.ToUpper (stFormD [i]) : 0);
scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0);

if (scode is { })
Expand Down Expand Up @@ -704,6 +705,32 @@ internal static uint MapKeyCodeToConsoleKey (KeyCode keyValue, out bool isConsol
return (uint)ConsoleKey.F24;
case KeyCode.Tab | KeyCode.ShiftMask:
return (uint)ConsoleKey.Tab;
case KeyCode.Space:
return (uint)ConsoleKey.Spacebar;
default:
uint c = (char)keyValue;

if (c is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z)
{
return c;
}

if ((c - 32) is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z)
{
return (c - 32);
}

if (Enum.IsDefined (typeof (ConsoleKey), keyValue.ToString ()))
{
return (uint)keyValue;
}

// DEL
if ((uint)keyValue == 127)
{
return (uint)ConsoleKey.Backspace;
}
break;
}

isConsoleKey = false;
Expand Down Expand Up @@ -867,6 +894,14 @@ public static KeyCode MapConsoleKeyInfoToKeyCode (ConsoleKeyInfo consoleKeyInfo)
case ConsoleKey.Tab:
keyCode = KeyCode.Tab;

break;
case ConsoleKey.Spacebar:
keyCode = KeyCode.Space;

break;
case ConsoleKey.Backspace:
keyCode = KeyCode.Backspace;

break;
default:
if ((int)consoleKeyInfo.KeyChar is >= 1 and <= 26)
Expand Down
Loading
Loading