using TinyTUI.Text; namespace TinyTUI.Tests.Text; public sealed class TerminalTextMeasurerTests { private readonly TerminalTextMeasurer _measurer = new(); [Fact] public void GetWidthHandlesAnsiTabsAndEmojiGraphemes() { Assert.Equal(5, _measurer.GetWidth("\t\e[31m界\e[0m")); Assert.Equal(2, _measurer.GetWidth("🇨")); Assert.Equal(2, _measurer.GetWidth("🇨🇳")); Assert.Equal(2, _measurer.GetWidth("👨‍💻")); Assert.Equal(2, _measurer.GetWidth("⚡️")); } [Fact] public void SliceTracksTabColumnsAndMeasuredWidth() { var strictTabSlice = _measurer.Slice("out 192M\t.pi/skill-tests/results-ha", 0, 10, strict: true); Assert.Equal("out 192M", strictTabSlice.Text); Assert.Equal(8, strictTabSlice.Width); Assert.Equal(_measurer.GetWidth(strictTabSlice.Text), strictTabSlice.Width); var afterTabSlice = _measurer.Slice("out 192M\t.pi/skill-tests/results-ha", 13, 10, strict: true); Assert.Equal("i/skill-te", afterTabSlice.Text); Assert.Equal(_measurer.GetWidth(afterTabSlice.Text), afterTabSlice.Width); } [Fact] public void TruncateKeepsAnsiPrefixAndBracketsEllipsis() { var ansiText = $"\e[31m{string.Concat(Enumerable.Repeat("hello ", 100))}\e[0m"; var truncated = _measurer.Truncate(ansiText, 20, "…"); Assert.True(_measurer.GetWidth(truncated) <= 20); Assert.Contains("\e[31m", truncated, StringComparison.Ordinal); Assert.EndsWith("\e[0m…\e[0m", truncated, StringComparison.Ordinal); Assert.Equal("\e[0m🙂\e[0m", _measurer.Truncate("abcdef", 2, "🙂")); Assert.Equal(string.Empty, _measurer.Truncate("abcdef", 1, "🙂")); } [Fact] public void WrapRestoresAnsiAndOsc8AcrossLines() { var plainWrapped = _measurer.Wrap("hello world this is a test", 10); Assert.True(plainWrapped.Count > 1); Assert.All(plainWrapped, line => Assert.True(_measurer.GetWidth(line) <= 10)); var redWrapped = _measurer.Wrap($"\e[31mhello world this is red\e[0m", 10); Assert.All(redWrapped.Skip(1), line => Assert.StartsWith("\e[31m", line, StringComparison.Ordinal)); Assert.All(redWrapped.Take(redWrapped.Count - 1), line => Assert.False(line.EndsWith("\e[0m", StringComparison.Ordinal))); var underlinedUrl = _measurer.Wrap($"read this thread \e[4mhttps://example.com/very/long/path/that/will/definitely/wrap\e[24m", 40); Assert.Equal("read this thread", underlinedUrl[0]); Assert.StartsWith("\e[4m", underlinedUrl[1], StringComparison.Ordinal); Assert.Contains(underlinedUrl.Take(underlinedUrl.Count - 1), line => line.EndsWith("\e[24m", StringComparison.Ordinal)); var hyperlink = "\e]8;;https://example.com\a0123456789\e]8;;\a"; var hyperlinkWrapped = _measurer.Wrap(hyperlink, 6); Assert.True(hyperlinkWrapped.Count > 1); Assert.All(hyperlinkWrapped, line => Assert.Contains("\e]8;;https://example.com\a", line, StringComparison.Ordinal)); Assert.All(hyperlinkWrapped.Take(hyperlinkWrapped.Count - 1), line => Assert.EndsWith("\e]8;;\a", line, StringComparison.Ordinal)); } }