using System.Globalization;
namespace Charac.Client;
/// Tracks remote VT mouse modes and keeps stale mouse reports out of the shell.
internal sealed class TerminalMouseTracking
{
private readonly byte[] _outputSequence = new byte[64];
private readonly byte[] _inputSequence = new byte[64];
private int _outputLength;
private int _inputLength;
private int _trackingModes;
private int _forwardMouse;
private int _alternateScreen;
public bool HasPendingInput => _inputLength != 0;
public bool InAlternateScreen => Volatile.Read(ref _alternateScreen) != 0;
public void ObserveOutput(ReadOnlySpan data)
{
foreach (var value in data)
{
if (_outputLength == 0)
{
if (value == 0x1b)
_outputSequence[_outputLength++] = value;
continue;
}
if (_outputLength == 1 && value != (byte)'[' ||
_outputLength == 2 && value != (byte)'?')
{
_outputLength = value == 0x1b ? 1 : 0;
continue;
}
if (_outputLength == _outputSequence.Length)
{
_outputLength = 0;
continue;
}
_outputSequence[_outputLength++] = value;
if (value is (byte)'h' or (byte)'l')
{
UpdateModes(value == (byte)'h');
_outputLength = 0;
}
else if (_outputLength > 3 && value is not ((byte)';') &&
(value < (byte)'0' || value > (byte)'9'))
_outputLength = 0;
}
}
public byte[] FilterInput(ReadOnlySpan data)
{
using var output = new MemoryStream(data.Length + _inputLength);
foreach (var value in data)
{
if (_inputLength == 0)
{
if (value == 0x1b)
_inputSequence[_inputLength++] = value;
else
output.WriteByte(value);
continue;
}
var expected = _inputLength switch
{
1 => (byte)'[',
2 => (byte)'<',
_ => (byte)0
};
if (expected != 0 && value != expected || _inputLength >= 3 &&
value is not ((byte)'M' or (byte)'m' or (byte)';') &&
(value < (byte)'0' || value > (byte)'9') ||
_inputLength == _inputSequence.Length)
{
output.Write(_inputSequence.AsSpan(0, _inputLength));
_inputLength = 0;
if (value == 0x1b)
_inputSequence[_inputLength++] = value;
else
output.WriteByte(value);
continue;
}
_inputSequence[_inputLength++] = value;
if (value is (byte)'M' or (byte)'m')
{
if (Volatile.Read(ref _forwardMouse) != 0)
output.Write(_inputSequence.AsSpan(0, _inputLength));
_inputLength = 0;
}
}
return output.ToArray();
}
public byte[] FlushPendingInput()
{
var pending = _inputSequence[.._inputLength];
_inputLength = 0;
return pending;
}
private void UpdateModes(bool enable)
{
var text = System.Text.Encoding.ASCII.GetString(_outputSequence.AsSpan(3, _outputLength - 4));
foreach (var part in text.Split(';'))
{
if (!int.TryParse(part, NumberStyles.None, CultureInfo.InvariantCulture, out var mode))
continue;
if (mode == 1049)
{
Volatile.Write(ref _alternateScreen, enable ? 1 : 0);
if (!enable)
_trackingModes = 0;
}
else
{
var bit = mode switch { 1000 => 1, 1002 => 2, 1003 => 4, _ => 0 };
if (enable)
_trackingModes |= bit;
else
_trackingModes &= ~bit;
}
}
Volatile.Write(ref _forwardMouse, _trackingModes != 0 ? 1 : 0);
}
}