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(); 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[ 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(); 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 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? DataReceived; public event EventHandler? 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); } } }