diff --git a/internal/linebuffer/linebuffer.go b/internal/linebuffer/linebuffer.go index af58d32..536dbec 100644 --- a/internal/linebuffer/linebuffer.go +++ b/internal/linebuffer/linebuffer.go @@ -118,7 +118,7 @@ func (l LineBuffer) Truncate(xOffset, width int) string { return visible } -func reapplyANSI(original, truncated string, truncOffset int, ansiCodeIndexes [][]int) string { +func reapplyANSI(original, truncated string, truncByteOffset int, ansiCodeIndexes [][]int) string { var result []byte var lenAnsiAdded int isReset := true @@ -129,7 +129,7 @@ func reapplyANSI(original, truncated string, truncOffset int, ansiCodeIndexes [] for len(ansiCodeIndexes) > 0 { candidateAnsi := ansiCodeIndexes[0] codeStart, codeEnd := candidateAnsi[0], candidateAnsi[1] - originalIdx := truncOffset + i + lenAnsiAdded + originalIdx := truncByteOffset + i + lenAnsiAdded if codeStart <= originalIdx { code := original[codeStart:codeEnd] isReset = code == "\x1b[m" diff --git a/internal/linebuffer/linebuffer_test.go b/internal/linebuffer/linebuffer_test.go index a0f76c5..aeaabdb 100644 --- a/internal/linebuffer/linebuffer_test.go +++ b/internal/linebuffer/linebuffer_test.go @@ -16,7 +16,7 @@ func TestTruncateLine(t *testing.T) { expected string }{ { - name: "zero width zero truncOffset", + name: "zero width zero truncByteOffset", s: "1234567890123456789012345", xOffset: 0, width: 0, @@ -24,7 +24,7 @@ func TestTruncateLine(t *testing.T) { expected: "", }, { - name: "zero width positive truncOffset", + name: "zero width positive truncByteOffset", s: "1234567890123456789012345", xOffset: 5, width: 0, @@ -32,7 +32,7 @@ func TestTruncateLine(t *testing.T) { expected: "", }, { - name: "zero width negative truncOffset", + name: "zero width negative truncByteOffset", s: "1234567890123456789012345", xOffset: -5, width: 0, @@ -72,7 +72,7 @@ func TestTruncateLine(t *testing.T) { expected: ".....", }, { - name: "zero truncOffset, sufficient width", + name: "zero truncByteOffset, sufficient width", s: "1234567890123456789012345", xOffset: 0, width: 30, @@ -80,7 +80,7 @@ func TestTruncateLine(t *testing.T) { expected: "1234567890123456789012345", }, { - name: "zero truncOffset, sufficient width, space at end", + name: "zero truncByteOffset, sufficient width, space at end", s: "1234567890123456789012345 ", xOffset: 0, width: 30, @@ -88,7 +88,7 @@ func TestTruncateLine(t *testing.T) { expected: "1234567890123456789012345 ", }, { - name: "zero truncOffset, insufficient width", + name: "zero truncByteOffset, insufficient width", s: "1234567890123456789012345", xOffset: 0, width: 15, @@ -96,7 +96,7 @@ func TestTruncateLine(t *testing.T) { expected: "123456789012...", }, { - name: "positive truncOffset, insufficient width", + name: "positive truncByteOffset, insufficient width", s: "1234567890123456789012345", xOffset: 5, width: 15, @@ -104,7 +104,7 @@ func TestTruncateLine(t *testing.T) { expected: "...901234567...", }, { - name: "positive truncOffset, exactly at end", + name: "positive truncByteOffset, exactly at end", s: "1234567890123456789012345", xOffset: 15, width: 10, @@ -112,7 +112,7 @@ func TestTruncateLine(t *testing.T) { expected: "...9012345", }, { - name: "positive truncOffset, over the end", + name: "positive truncByteOffset, over the end", s: "1234567890123456789012345", xOffset: 20, width: 10, @@ -120,7 +120,7 @@ func TestTruncateLine(t *testing.T) { expected: "...45", }, { - name: "positive truncOffset, ansi", + name: "positive truncByteOffset, ansi", s: "\x1b[38;2;255;0;0ma really really long line\x1b[m", xOffset: 15, width: 15, @@ -128,7 +128,7 @@ func TestTruncateLine(t *testing.T) { expected: "\x1b[38;2;255;0;0m long line\x1b[m", }, { - name: "zero truncOffset, insufficient width, ansi", + name: "zero truncByteOffset, insufficient width, ansi", s: "\x1b[38;2;255;0;0m1234567890123456789012345\x1b[m", xOffset: 0, width: 15, @@ -136,7 +136,7 @@ func TestTruncateLine(t *testing.T) { expected: "\x1b[38;2;255;0;0m123456789012...\x1b[m", }, { - name: "positive truncOffset, insufficient width, ansi", + name: "positive truncByteOffset, insufficient width, ansi", s: "\x1b[38;2;255;0;0m1234567890123456789012345\x1b[m", xOffset: 5, width: 15, @@ -144,7 +144,7 @@ func TestTruncateLine(t *testing.T) { expected: "\x1b[38;2;255;0;0m...901234567...\x1b[m", }, { - name: "no truncOffset, insufficient width, inline ansi", + name: "no truncByteOffset, insufficient width, inline ansi", s: "|\x1b[38;2;169;15;15mfl..-1\x1b[m| {\"timestamp\": \"2024-09-29T22:30:28.730520\"}", xOffset: 0, width: 15, @@ -152,7 +152,7 @@ func TestTruncateLine(t *testing.T) { expected: "|\x1b[38;2;169;15;15mfl..-1\x1b[m| {\"t...", }, { - name: "truncOffset overflow, ansi", + name: "truncByteOffset overflow, ansi", s: "\x1b[38;2;0;0;255mthird line that is fairly long\x1b[m", xOffset: 41, width: 10, @@ -160,7 +160,7 @@ func TestTruncateLine(t *testing.T) { expected: "", }, { - name: "truncOffset overflow, ansi 2", + name: "truncByteOffset overflow, ansi 2", s: "\x1b[38;2;0;0;255mfourth\x1b[m", xOffset: 41, width: 10, @@ -168,7 +168,7 @@ func TestTruncateLine(t *testing.T) { expected: "", }, { - name: "truncOffset start space ansi", + name: "truncByteOffset start space ansi", // 0123456789012345 67890 // 0 123456789012345678901234 s: "\x1b[38;2;255;0;0ma\x1b[m really really long line", @@ -222,172 +222,207 @@ func TestTruncateLine(t *testing.T) { func TestReapplyAnsi(t *testing.T) { tests := []struct { - name string - original string - truncated string - truncOffset int - expected string + name string + original string + truncated string + truncByteOffset int + expected string }{ { - name: "no ansi, no truncOffset", - original: "1234567890123456789012345", - truncated: "12345", - truncOffset: 0, - expected: "12345", + name: "no ansi, no truncByteOffset", + original: "1234567890123456789012345", + truncated: "12345", + truncByteOffset: 0, + expected: "12345", }, { - name: "no ansi, truncOffset", - original: "1234567890123456789012345", - truncated: "2345", - truncOffset: 1, - expected: "2345", + name: "no ansi, truncByteOffset", + original: "1234567890123456789012345", + truncated: "2345", + truncByteOffset: 1, + expected: "2345", }, { - name: "surrounding ansi, no truncOffset", - original: "\x1b[38;2;255;0;0m12345\x1b[m", - truncated: "123", - truncOffset: 0, - expected: "\x1b[38;2;255;0;0m123\x1b[m", + name: "surrounding ansi, no truncByteOffset", + original: "\x1b[38;2;255;0;0m12345\x1b[m", + truncated: "123", + truncByteOffset: 0, + expected: "\x1b[38;2;255;0;0m123\x1b[m", }, { - name: "surrounding ansi, truncOffset", - original: "\x1b[38;2;255;0;0m12345\x1b[m", - truncated: "234", - truncOffset: 1, - expected: "\x1b[38;2;255;0;0m234\x1b[m", + name: "surrounding ansi, truncByteOffset", + original: "\x1b[38;2;255;0;0m12345\x1b[m", + truncated: "234", + truncByteOffset: 1, + expected: "\x1b[38;2;255;0;0m234\x1b[m", }, { - name: "left ansi, no truncOffset", - original: "\x1b[38;2;255;0;0m1\x1b[m" + "2345", - truncated: "123", - truncOffset: 0, - expected: "\x1b[38;2;255;0;0m1\x1b[m" + "23", + name: "left ansi, no truncByteOffset", + original: "\x1b[38;2;255;0;0m1\x1b[m" + "2345", + truncated: "123", + truncByteOffset: 0, + expected: "\x1b[38;2;255;0;0m1\x1b[m" + "23", }, { - name: "left ansi, truncOffset", - original: "\x1b[38;2;255;0;0m12\x1b[m" + "345", - truncated: "234", - truncOffset: 1, - expected: "\x1b[38;2;255;0;0m2\x1b[m" + "34", + name: "left ansi, truncByteOffset", + original: "\x1b[38;2;255;0;0m12\x1b[m" + "345", + truncated: "234", + truncByteOffset: 1, + expected: "\x1b[38;2;255;0;0m2\x1b[m" + "34", }, { - name: "right ansi, no truncOffset", - original: "1" + "\x1b[38;2;255;0;0m2345\x1b[m", - truncated: "123", - truncOffset: 0, - expected: "1" + "\x1b[38;2;255;0;0m23\x1b[m", + name: "right ansi, no truncByteOffset", + original: "1" + "\x1b[38;2;255;0;0m2345\x1b[m", + truncated: "123", + truncByteOffset: 0, + expected: "1" + "\x1b[38;2;255;0;0m23\x1b[m", }, { - name: "right ansi, truncOffset", - original: "12" + "\x1b[38;2;255;0;0m345\x1b[m", - truncated: "234", - truncOffset: 1, - expected: "2" + "\x1b[38;2;255;0;0m34\x1b[m", + name: "right ansi, truncByteOffset", + original: "12" + "\x1b[38;2;255;0;0m345\x1b[m", + truncated: "234", + truncByteOffset: 1, + expected: "2" + "\x1b[38;2;255;0;0m34\x1b[m", }, { - name: "left and right ansi, no truncOffset", - original: "\x1b[38;2;255;0;0m1\x1b[m" + "2" + "\x1b[38;2;255;0;0m345\x1b[m", - truncated: "123", - truncOffset: 0, - expected: "\x1b[38;2;255;0;0m1\x1b[m" + "2" + "\x1b[38;2;255;0;0m3\x1b[m", + name: "left and right ansi, no truncByteOffset", + original: "\x1b[38;2;255;0;0m1\x1b[m" + "2" + "\x1b[38;2;255;0;0m345\x1b[m", + truncated: "123", + truncByteOffset: 0, + expected: "\x1b[38;2;255;0;0m1\x1b[m" + "2" + "\x1b[38;2;255;0;0m3\x1b[m", }, { - name: "left and right ansi, truncOffset", - original: "\x1b[38;2;255;0;0m12\x1b[m" + "3" + "\x1b[38;2;255;0;0m45\x1b[m", - truncated: "234", - truncOffset: 1, - expected: "\x1b[38;2;255;0;0m2\x1b[m" + "3" + "\x1b[38;2;255;0;0m4\x1b[m", + name: "left and right ansi, truncByteOffset", + original: "\x1b[38;2;255;0;0m12\x1b[m" + "3" + "\x1b[38;2;255;0;0m45\x1b[m", + truncated: "234", + truncByteOffset: 1, + expected: "\x1b[38;2;255;0;0m2\x1b[m" + "3" + "\x1b[38;2;255;0;0m4\x1b[m", }, { - name: "truncated right ansi, no truncOffset", - original: "\x1b[38;2;255;0;0m1\x1b[m" + "234" + "\x1b[38;2;255;0;0m5\x1b[m", - truncated: "123", - truncOffset: 0, - expected: "\x1b[38;2;255;0;0m1\x1b[m" + "23", + name: "truncated right ansi, no truncByteOffset", + original: "\x1b[38;2;255;0;0m1\x1b[m" + "234" + "\x1b[38;2;255;0;0m5\x1b[m", + truncated: "123", + truncByteOffset: 0, + expected: "\x1b[38;2;255;0;0m1\x1b[m" + "23", }, { - name: "truncated right ansi, truncOffset", - original: "\x1b[38;2;255;0;0m12\x1b[m" + "34" + "\x1b[38;2;255;0;0m5\x1b[m", - truncated: "234", - truncOffset: 1, - expected: "\x1b[38;2;255;0;0m2\x1b[m" + "34", + name: "truncated right ansi, truncByteOffset", + original: "\x1b[38;2;255;0;0m12\x1b[m" + "34" + "\x1b[38;2;255;0;0m5\x1b[m", + truncated: "234", + truncByteOffset: 1, + expected: "\x1b[38;2;255;0;0m2\x1b[m" + "34", }, { - name: "truncated left ansi, truncOffset", - original: "\x1b[38;2;255;0;0m1\x1b[m" + "23" + "\x1b[38;2;255;0;0m45\x1b[m", - truncated: "234", - truncOffset: 1, - expected: "23" + "\x1b[38;2;255;0;0m4\x1b[m", + name: "truncated left ansi, truncByteOffset", + original: "\x1b[38;2;255;0;0m1\x1b[m" + "23" + "\x1b[38;2;255;0;0m45\x1b[m", + truncated: "234", + truncByteOffset: 1, + expected: "23" + "\x1b[38;2;255;0;0m4\x1b[m", }, { - name: "nested color sequences", - original: "\x1b[31m1\x1b[32m2\x1b[33m3\x1b[m\x1b[m\x1b[m45", - truncated: "123", - truncOffset: 0, - expected: "\x1b[31m1\x1b[32m2\x1b[33m3\x1b[m", + name: "nested color sequences", + original: "\x1b[31m1\x1b[32m2\x1b[33m3\x1b[m\x1b[m\x1b[m45", + truncated: "123", + truncByteOffset: 0, + expected: "\x1b[31m1\x1b[32m2\x1b[33m3\x1b[m", }, { - name: "nested color sequences with truncOffset", - original: "\x1b[31m1\x1b[32m2\x1b[33m3\x1b[m\x1b[m\x1b[m45", - truncated: "234", - truncOffset: 1, - expected: "\x1b[31m\x1b[32m2\x1b[33m3\x1b[m4", + name: "nested color sequences with truncByteOffset", + original: "\x1b[31m1\x1b[32m2\x1b[33m3\x1b[m\x1b[m\x1b[m45", + truncated: "234", + truncByteOffset: 1, + expected: "\x1b[31m\x1b[32m2\x1b[33m3\x1b[m4", }, { - name: "nested style sequences", - original: "\x1b[1m1\x1b[4m2\x1b[3m3\x1b[m\x1b[m\x1b[m45", - truncated: "123", - truncOffset: 0, - expected: "\x1b[1m1\x1b[4m2\x1b[3m3\x1b[m", + name: "nested style sequences", + original: "\x1b[1m1\x1b[4m2\x1b[3m3\x1b[m\x1b[m\x1b[m45", + truncated: "123", + truncByteOffset: 0, + expected: "\x1b[1m1\x1b[4m2\x1b[3m3\x1b[m", }, { - name: "mixed nested sequences", - original: "\x1b[31m1\x1b[1m2\x1b[4;32m3\x1b[m\x1b[m\x1b[m45", - truncated: "234", - truncOffset: 1, - expected: "\x1b[31m\x1b[1m2\x1b[4;32m3\x1b[m4", + name: "mixed nested sequences", + original: "\x1b[31m1\x1b[1m2\x1b[4;32m3\x1b[m\x1b[m\x1b[m45", + truncated: "234", + truncByteOffset: 1, + expected: "\x1b[31m\x1b[1m2\x1b[4;32m3\x1b[m4", }, { - name: "deeply nested sequences", - original: "\x1b[31m1\x1b[1m2\x1b[4m3\x1b[32m4\x1b[m\x1b[m\x1b[m\x1b[m5", - truncated: "123", - truncOffset: 0, - expected: "\x1b[31m1\x1b[1m2\x1b[4m3\x1b[m", + name: "deeply nested sequences", + original: "\x1b[31m1\x1b[1m2\x1b[4m3\x1b[32m4\x1b[m\x1b[m\x1b[m\x1b[m5", + truncated: "123", + truncByteOffset: 0, + expected: "\x1b[31m1\x1b[1m2\x1b[4m3\x1b[m", }, { - name: "partial nested sequences", - original: "1\x1b[31m2\x1b[1m3\x1b[4m4\x1b[m\x1b[m\x1b[m5", - truncated: "234", - truncOffset: 1, - expected: "\x1b[31m2\x1b[1m3\x1b[4m4\x1b[m", + name: "partial nested sequences", + original: "1\x1b[31m2\x1b[1m3\x1b[4m4\x1b[m\x1b[m\x1b[m5", + truncated: "234", + truncByteOffset: 1, + expected: "\x1b[31m2\x1b[1m3\x1b[4m4\x1b[m", }, { - name: "overlapping nested sequences", - original: "\x1b[31m1\x1b[1m2\x1b[m3\x1b[4m4\x1b[m5", - truncated: "234", - truncOffset: 1, - expected: "\x1b[31m\x1b[1m2\x1b[m3\x1b[4m4\x1b[m", + name: "overlapping nested sequences", + original: "\x1b[31m1\x1b[1m2\x1b[m3\x1b[4m4\x1b[m5", + truncated: "234", + truncByteOffset: 1, + expected: "\x1b[31m\x1b[1m2\x1b[m3\x1b[4m4\x1b[m", }, { - name: "complex RGB nested sequences", - original: "\x1b[38;2;255;0;0m1\x1b[1m2\x1b[38;2;0;255;0m3\x1b[m\x1b[m45", - truncated: "123", - truncOffset: 0, - expected: "\x1b[38;2;255;0;0m1\x1b[1m2\x1b[38;2;0;255;0m3\x1b[m", + name: "complex RGB nested sequences", + original: "\x1b[38;2;255;0;0m1\x1b[1m2\x1b[38;2;0;255;0m3\x1b[m\x1b[m45", + truncated: "123", + truncByteOffset: 0, + expected: "\x1b[38;2;255;0;0m1\x1b[1m2\x1b[38;2;0;255;0m3\x1b[m", }, { - name: "nested sequences with background colors", - original: "\x1b[31;44m1\x1b[1m2\x1b[32;45m3\x1b[m\x1b[m45", - truncated: "234", - truncOffset: 1, - expected: "\x1b[31;44m\x1b[1m2\x1b[32;45m3\x1b[m4", + name: "nested sequences with background colors", + original: "\x1b[31;44m1\x1b[1m2\x1b[32;45m3\x1b[m\x1b[m45", + truncated: "234", + truncByteOffset: 1, + expected: "\x1b[31;44m\x1b[1m2\x1b[32;45m3\x1b[m4", + }, + { + name: "emoji basic", + original: "1️⃣2️⃣3️⃣4️⃣5️⃣", + truncated: "1️⃣2️⃣3️⃣", + truncByteOffset: 0, + expected: "1️⃣2️⃣3️⃣", + }, + { + name: "emoji with ansi", + original: "\x1b[31m1️⃣\x1b[32m2️⃣\x1b[33m3️⃣\x1b[m", + truncated: "1️⃣2️⃣", + truncByteOffset: 0, + expected: "\x1b[31m1️⃣\x1b[32m2️⃣\x1b[m", + }, + { + name: "chinese characters", + original: "你好世界星星", + truncated: "你好世", + truncByteOffset: 0, + expected: "你好世", + }, + { + name: "simple with ansi and offset", + original: "\x1b[31ma\x1b[32mb\x1b[33mc\x1b[mde", + truncated: "bcd", + truncByteOffset: 1, + expected: "\x1b[31m\x1b[32mb\x1b[33mc\x1b[md", + }, + { + name: "chinese with ansi and offset", + original: "\x1b[31m你\x1b[32m好\x1b[33m世\x1b[m界星", + truncated: "好世界", + truncByteOffset: 3, // 你 is 3 bytes + expected: "\x1b[31m\x1b[32m好\x1b[33m世\x1b[m界", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ansiCodeIndexes := constants.AnsiRegex.FindAllStringIndex(tt.original, -1) - actual := reapplyANSI(tt.original, tt.truncated, tt.truncOffset, ansiCodeIndexes) + actual := reapplyANSI(tt.original, tt.truncated, tt.truncByteOffset, ansiCodeIndexes) util.CmpStr(t, tt.expected, actual) }) }