af9ad109e1
- negotiate Kitty keyboard protocol during terminal session startup - fallback to modifyOtherKeys when Kitty protocol is unavailable - cover protocol startup and restore behavior with terminal session tests
153 lines
5.0 KiB
C#
153 lines
5.0 KiB
C#
using TinyTUI.Stdio;
|
|
using TinyTUI.Terminal;
|
|
using TinyTUI.Tests.TestInfrastructure;
|
|
|
|
namespace TinyTUI.Tests.Terminal;
|
|
|
|
public sealed class ConsoleTerminalSessionTests
|
|
{
|
|
[Fact]
|
|
public void StartWritesKittyKeyboardProtocolQuery()
|
|
{
|
|
var input = new FakeTerminalInput();
|
|
var output = new VirtualTerminalOutput();
|
|
using var session = new ConsoleTerminalSession(input, output, TimeSpan.FromSeconds(10));
|
|
|
|
session.Start();
|
|
|
|
Assert.True(input.Started);
|
|
Assert.Contains("\e[?25l", output.Buffer.ToString(), StringComparison.Ordinal);
|
|
Assert.Contains("\e[?2004h", output.Buffer.ToString(), StringComparison.Ordinal);
|
|
Assert.Contains("\e[>7u\e[?u\e[c", output.Buffer.ToString(), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void KittyFlagsResponseMarksProtocolActiveAndIsNotForwarded()
|
|
{
|
|
var input = new FakeTerminalInput();
|
|
var output = new VirtualTerminalOutput();
|
|
using var session = new ConsoleTerminalSession(input, output, TimeSpan.FromSeconds(10));
|
|
var received = new List<string>();
|
|
session.DataReceived += (_, data) => received.Add(data);
|
|
|
|
session.Start();
|
|
input.RaiseData("\e[?7u");
|
|
input.RaiseData("a");
|
|
|
|
Assert.True(session.KittyProtocolActive);
|
|
Assert.False(session.ModifyOtherKeysActive);
|
|
Assert.DoesNotContain("\e[>4;2m", output.Buffer.ToString(), StringComparison.Ordinal);
|
|
Assert.Equal(["a"], received);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeviceAttributesResponseFallsBackToModifyOtherKeys()
|
|
{
|
|
var input = new FakeTerminalInput();
|
|
var output = new VirtualTerminalOutput();
|
|
using var session = new ConsoleTerminalSession(input, output, TimeSpan.FromSeconds(10));
|
|
|
|
session.Start();
|
|
input.RaiseData("\e[?1;2c");
|
|
|
|
Assert.False(session.KittyProtocolActive);
|
|
Assert.True(session.ModifyOtherKeysActive);
|
|
Assert.Contains("\e[>4;2m", output.Buffer.ToString(), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TimeoutFallsBackToModifyOtherKeys()
|
|
{
|
|
var input = new FakeTerminalInput();
|
|
var output = new VirtualTerminalOutput();
|
|
using var session = new ConsoleTerminalSession(input, output, TimeSpan.FromMilliseconds(20));
|
|
|
|
session.Start();
|
|
await WaitUntil(() => session.ModifyOtherKeysActive);
|
|
|
|
Assert.False(session.KittyProtocolActive);
|
|
Assert.Contains("\e[>4;2m", output.Buffer.ToString(), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void StopRestoresKeyboardProtocolsAndTerminalModes()
|
|
{
|
|
var input = new FakeTerminalInput();
|
|
var output = new VirtualTerminalOutput();
|
|
using var session = new ConsoleTerminalSession(input, output, TimeSpan.FromSeconds(10));
|
|
|
|
session.Start();
|
|
input.RaiseData("\e[?1;2c");
|
|
output.ClearWrites();
|
|
|
|
session.Stop();
|
|
session.Stop();
|
|
|
|
Assert.False(input.Started);
|
|
Assert.False(session.KittyProtocolActive);
|
|
Assert.False(session.ModifyOtherKeysActive);
|
|
Assert.Equal(1, output.Writes.Count(value => value == "\e[<u"));
|
|
Assert.Equal(1, output.Writes.Count(value => value == "\e[>4;0m"));
|
|
Assert.Equal(1, output.Writes.Count(value => value == "\e[?2004l"));
|
|
Assert.Equal(1, output.Writes.Count(value => value == "\e[?25h"));
|
|
}
|
|
|
|
[Fact]
|
|
public void SplitKittyFlagsResponseIsBufferedDuringNegotiation()
|
|
{
|
|
var input = new FakeTerminalInput();
|
|
var output = new VirtualTerminalOutput();
|
|
using var session = new ConsoleTerminalSession(input, output, TimeSpan.FromSeconds(10));
|
|
var received = new List<string>();
|
|
session.DataReceived += (_, data) => received.Add(data);
|
|
|
|
session.Start();
|
|
input.RaiseData("\e[?");
|
|
input.RaiseData("7u");
|
|
input.RaiseData("x");
|
|
|
|
Assert.True(session.KittyProtocolActive);
|
|
Assert.Equal(["x"], received);
|
|
}
|
|
|
|
private static async Task WaitUntil(Func<bool> condition)
|
|
{
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
|
|
|
while (!condition())
|
|
{
|
|
if (cts.IsCancellationRequested)
|
|
throw new TimeoutException("Condition was not met before timeout");
|
|
|
|
await Task.Delay(5, CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeTerminalInput : ITerminalInput
|
|
{
|
|
public TerminalSize CurrentSize { get; private set; } = new(80, 24);
|
|
|
|
public bool Started { get; private set; }
|
|
|
|
public event EventHandler<string>? DataReceived;
|
|
|
|
public event EventHandler<TerminalSize>? Resized;
|
|
|
|
public void Start() => Started = true;
|
|
|
|
public void Stop() => Started = false;
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
|
|
public void RaiseData(string data) => DataReceived?.Invoke(this, data);
|
|
|
|
public void Resize(int columns, int rows)
|
|
{
|
|
CurrentSize = new TerminalSize(columns, rows);
|
|
Resized?.Invoke(this, CurrentSize);
|
|
}
|
|
}
|
|
}
|