using XFESystem.Console; namespace XFESystem.Console.Tests; public sealed class TerminalBufferTests { [Fact] public void TrueColorIndexedAndResetHaveExactCellStyles() { var terminal = new TerminalBuffer(40, 3); terminal.Write("\u001b[38;2;117;201;245;48;5;236mA\u001b[38;5;202mB\u001b[0mC"); Assert.Equal(new TerminalColor(117, 201, 245), terminal.GetCell(0, 0).Style.Foreground); Assert.Equal(new TerminalColor(48, 48, 48), terminal.GetCell(0, 0).Style.Background); Assert.Equal(new TerminalColor(255, 95, 0), terminal.GetCell(1, 0).Style.Foreground); Assert.Equal(TerminalStyle.Default, terminal.GetCell(2, 0).Style); Assert.Equal("ABC", terminal.GetLine(0)); } [Theory] [InlineData(30, 0)] [InlineData(31, 1)] [InlineData(32, 2)] [InlineData(33, 3)] [InlineData(34, 4)] [InlineData(35, 5)] [InlineData(36, 6)] [InlineData(37, 7)] [InlineData(90, 8)] [InlineData(91, 9)] [InlineData(92, 10)] [InlineData(93, 11)] [InlineData(94, 12)] [InlineData(95, 13)] [InlineData(96, 14)] [InlineData(97, 15)] public void StandardAndBrightAnsiColorsUseTheExpectedPalette(int sgr, byte palette) { var terminal = new TerminalBuffer(4, 2); terminal.Write($"\u001b[{sgr};{sgr + 10}mX"); Assert.Equal(TerminalColor.FromIndexed(palette), terminal.GetCell(0, 0).Style.Foreground); Assert.Equal(TerminalColor.FromIndexed(palette), terminal.GetCell(0, 0).Style.Background); } [Fact] public void AttributeResetsAreIndependentAndDefaultsCanBeConfigured() { var defaults = new TerminalStyle(new(1, 2, 3), new(4, 5, 6)); var terminal = new TerminalBuffer(8, 2, defaults); terminal.Write("\u001b[1;2;3;4;7;8;9mA\u001b[22;23;24;27;28;29mB\u001b[31;47mC\u001b[39;49mD"); var decorated = terminal.GetCell(0, 0).Style; Assert.True(decorated.Bold && decorated.Dim && decorated.Italic && decorated.Underline && decorated.Inverse && decorated.Hidden && decorated.Strike); Assert.Equal(defaults.Foreground, decorated.DisplayBackground); Assert.Equal(decorated.DisplayBackground, decorated.DisplayForeground); Assert.Equal(defaults, terminal.GetCell(1, 0).Style); Assert.Equal(defaults, terminal.GetCell(3, 0).Style); } [Fact] public void ChunkBoundariesNeverChangeEscapesOrUnicode() { const string input = "A\u001b[38;2;50;150;250m中😀e\u0301\u001b[0m\r\nB\u001b]0;Ignored title\u0007C"; var expected = new TerminalBuffer(40, 4); expected.Write(input); for (var split = 0; split <= input.Length; split++) { var actual = new TerminalBuffer(40, 4); actual.Write(input.AsSpan(0, split)); actual.Write(input.AsSpan(split)); actual.Complete(); Assert.Equal(expected.Snapshot().Cells, actual.Snapshot().Cells); Assert.Equal(expected.CursorColumn, actual.CursorColumn); Assert.Equal(expected.CursorRow, actual.CursorRow); } } [Fact] public void AllCharactersCanArriveSeparatelyIncludingSurrogatePairs() { var terminal = new TerminalBuffer(12, 2); foreach (var character in "中😀e\u0301") terminal.Write(character.ToString()); terminal.Complete(); Assert.Equal("中😀e\u0301", terminal.GetLine(0)); Assert.Equal(2, terminal.GetCell(0, 0).DisplayWidth); Assert.True(terminal.GetCell(1, 0).IsContinuation); Assert.Equal(2, terminal.GetCell(2, 0).DisplayWidth); Assert.Equal(5, terminal.CursorColumn); } [Fact] public void IncompleteAndInvalidSurrogatesAreReplacedWithoutThrowing() { var terminal = new TerminalBuffer(12, 2); terminal.Write("\ud800X\udc00\ud801"); terminal.Complete(); Assert.Equal("�X��", terminal.GetLine(0)); } [Fact] public void MalformedColorDoesNotWrapOrApplyItsComponentsAsOtherAttributes() { var terminal = new TerminalBuffer(16, 2); terminal.Write("\u001b[38;2;999;1;2mA\u001b[38;5;999mB\u001b[38;2;1mC\u001b[999999999999999999999mD"); Assert.Equal("ABCD", terminal.GetLine(0)); for (var column = 0; column < 4; column++) Assert.Equal(TerminalStyle.Default, terminal.GetCell(column, 0).Style); } [Fact] public void OscAndDcsIncludingClipboardPayloadAreDiscardedWithoutSideEffects() { var terminal = new TerminalBuffer(40, 2); terminal.Write("A\u001b]52;c;U2VjcmV0\u001b\\B\u001b]8;;https://example.com\u0007Link\u001b]8;;\u0007C\u001bPignored\u001b\\D"); Assert.Equal("ABLinkCD", terminal.GetLine(0)); Assert.DoesNotContain("Secret", terminal.GetText(), StringComparison.Ordinal); Assert.DoesNotContain('\u001b', terminal.GetText()); } [Fact] public void VeryLongSequencesUseBoundedMemoryAndRecoverAfterTheTerminator() { var terminal = new TerminalBuffer(12, 2); terminal.Write("\u001b[" + new string('1', 100_000) + "mA\u001b]" + new string('x', 100_000) + "\u0007B"); Assert.Equal("AB", terminal.GetLine(0)); Assert.Equal(TerminalStyle.Default, terminal.CurrentStyle); } [Fact] public void CarriageReturnBackspaceTabAndLineFeedFollowTerminalSemantics() { var terminal = new TerminalBuffer(20, 4); terminal.Write("abc\bD\rXY\u001b[K\nZ\r\nA\tB"); Assert.Equal("XY", terminal.GetLine(0)); Assert.Equal(" Z", terminal.GetLine(1)); Assert.Equal("A B", terminal.GetLine(2)); } [Fact] public void WrappingIsDeferredAndCarriageReturnCancelsThePendingWrap() { var terminal = new TerminalBuffer(4, 3); terminal.Write("ABCD"); Assert.Equal(0, terminal.CursorRow); Assert.Equal(3, terminal.CursorColumn); terminal.Write("\rX"); Assert.Equal("XBCD", terminal.GetLine(0)); terminal.Write("\u001b[4GYZ"); Assert.Equal("XBCY", terminal.GetLine(0)); Assert.Equal("Z", terminal.GetLine(1)); } [Fact] public void AutoWrapCanBeDisabledAndReenabled() { var terminal = new TerminalBuffer(4, 3); terminal.Write("\u001b[?7lABCDE"); Assert.Equal("ABCE", terminal.GetLine(0)); terminal.Write("\u001b[?7hFG"); Assert.Equal("ABCF", terminal.GetLine(0)); Assert.Equal("G", terminal.GetLine(1)); } [Fact] public void ScrollbackIsBoundedAndClearWithScrollbackRemovesIt() { var terminal = new TerminalBuffer(10, 2, scrollbackLimit: 2); terminal.Write("One\r\nTwo\r\nThree\r\nFour\r\nFive"); Assert.Equal(2, terminal.ScrollbackCount); Assert.Equal("Four", terminal.GetLine(0)); Assert.Equal("Five", terminal.GetLine(1)); terminal.Write("\u001b[3J"); Assert.Equal(0, terminal.ScrollbackCount); Assert.Equal("", terminal.GetLine(0)); Assert.Equal("", terminal.GetLine(1)); } [Fact] public void CursorMovementClampsHugeInputsAndSaveRestoreWorks() { var terminal = new TerminalBuffer(8, 4); terminal.Write("\u001b[2;3HX\u001b[s\u001b[2147483647B\u001b[2147483647CY\u001b[uZ"); Assert.Equal(" XZ", terminal.GetLine(1)); Assert.Equal(" Y", terminal.GetLine(3)); terminal.Write("\u001b[2147483647A\u001b[2147483647DW"); Assert.Equal("W", terminal.GetLine(0)); } [Fact] public void EraseLineAndDisplayUseTheCurrentBackgroundWithoutMovingTheCursor() { var terminal = new TerminalBuffer(8, 3); terminal.Write("abcdefgh\r\nABCDEFGH\u001b[2;4H\u001b[44m\u001b[K"); Assert.Equal("ABC", terminal.GetLine(1)); Assert.Equal(new TerminalColor(0, 0, 128), terminal.GetCell(3, 1).Style.Background); Assert.Equal(3, terminal.CursorColumn); terminal.Write("\u001b[1J"); Assert.Equal("", terminal.GetLine(0)); Assert.Equal("", terminal.GetLine(1)); } [Fact] public void OverwritingOrErasingAWideContinuationClearsItsWholeGlyph() { var terminal = new TerminalBuffer(8, 2); terminal.Write("中AB\r\u001b[2GX"); Assert.Equal(" XAB", terminal.GetLine(0)); Assert.False(terminal.GetCell(0, 0).IsContinuation); terminal.Write("\r中文\r\u001b[2G\u001b[X"); Assert.Equal(" 文", terminal.GetLine(0)); Assert.Equal(2, terminal.GetCell(2, 0).DisplayWidth); } [Fact] public void WideGlyphWrapsBeforeTheRightEdgeAndSingleColumnIsSafe() { var terminal = new TerminalBuffer(4, 3); terminal.Write("ABC中"); Assert.Equal("ABC", terminal.GetLine(0)); Assert.Equal("中", terminal.GetLine(1)); var narrow = new TerminalBuffer(1, 2); narrow.Write("中"); Assert.Equal("�", narrow.GetLine(0)); } [Fact] public void InsertDeleteAndEraseCharactersPreserveFollowingText() { var terminal = new TerminalBuffer(12, 2); terminal.Write("ABCDE\u001b[3G\u001b[2@xy"); Assert.Equal("ABxyCDE", terminal.GetLine(0)); terminal.Write("\u001b[3G\u001b[2P"); Assert.Equal("ABCDE", terminal.GetLine(0)); terminal.Write("\u001b[2G\u001b[2X"); Assert.Equal("A DE", terminal.GetLine(0)); } [Fact] public void AlternateScreenRestoresOriginalCellsCursorAndStyle() { var terminal = new TerminalBuffer(20, 4); terminal.Write("\u001b[31mMain\u001b[?1049hFull screen\r\nApp"); Assert.True(terminal.IsAlternateScreen); Assert.Equal("Full screen", terminal.GetLine(0)); terminal.Write("\u001b[?1049l!"); Assert.False(terminal.IsAlternateScreen); Assert.Equal("Main!", terminal.GetLine(0)); Assert.Equal(TerminalColor.FromIndexed(1), terminal.GetCell(4, 0).Style.Foreground); } [Fact] public void ScrollRegionKeepsHeaderAndFooterAndDoesNotCreateGlobalScrollback() { var terminal = new TerminalBuffer(12, 5); terminal.Write("Header\r\nOne\r\nTwo\r\nThree\r\nFooter\u001b[2;4r\u001b[4;1HFour\r\nFive"); Assert.Equal("Header", terminal.GetLine(0)); Assert.Equal("Two", terminal.GetLine(1)); Assert.Equal("Foure", terminal.GetLine(2)); Assert.Equal("Five", terminal.GetLine(3)); Assert.Equal("Footer", terminal.GetLine(4)); Assert.Equal(0, terminal.ScrollbackCount); } [Fact] public void SnapshotsAreDetachedAndCursorVisibilityIsPreserved() { var terminal = new TerminalBuffer(8, 2); terminal.Write("A\u001b[?25l"); var snapshot = terminal.Snapshot(); terminal.Write("\rB\u001b[?25h"); Assert.Equal("A", snapshot.GetCell(0, 0).Text); Assert.False(snapshot.CursorVisible); Assert.Equal("B", terminal.GetCell(0, 0).Text); Assert.True(terminal.CursorVisible); } [Fact] public void ResetDiscardsPartialSequencesAndRestoresTheme() { var terminal = new TerminalBuffer(8, 2); terminal.Write("\u001b[31mA\u001b[38;"); terminal.Reset(); terminal.Write("B"); Assert.Equal("B", terminal.GetLine(0)); Assert.Equal(TerminalStyle.Default, terminal.CurrentStyle); } [Fact] public void ConstructorAndCellAccessRejectInvalidGeometry() { Assert.Throws(() => new TerminalBuffer(0, 2)); Assert.Throws(() => new TerminalBuffer(2, 0)); Assert.Throws(() => new TerminalBuffer(int.MaxValue, 2)); var terminal = new TerminalBuffer(2, 2); Assert.Throws(() => terminal.GetCell(2, 0)); Assert.Throws(() => terminal.GetCell(0, -1)); Assert.Throws(() => terminal.Snapshot().GetCell(-1, 0)); } }