Skip to content

Commit c72c8ba

Browse files
committed
fix: resolve CSI terminator, snapshot resize, and escape UTF-8 bugs
Address three PR review findings: isCSITerminator missed the full ECMA-48 range (0x40-0x7E), causing ~-terminated sequences like function keys to consume subsequent content. The Strip() regex had the same gap. - Expand isCSITerminator to full 0x40-0x7E range and add ~ to Strip regex - Handle pty.Setsize error on snapshot temporary resize - Use utf8.DecodeRuneInString for multi-byte runes in escape.Interpret - Add tests for tilde-terminated CSI, UTF-8 escape, and look-ahead
1 parent f97dd7b commit c72c8ba

File tree

6 files changed

+36
-5
lines changed

6 files changed

+36
-5
lines changed

internal/ansi/clear_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,17 @@ func TestFrameDetector_CursorJumpTopLookAhead(t *testing.T) {
12741274
t.Error("should NOT truncate when only cursor positioning/control follows")
12751275
}
12761276
})
1277+
1278+
t.Run("jump followed by tilde-terminated CSI then content - truncation fires", func(t *testing.T) {
1279+
d := NewFrameDetector(TruncationStrategy{CursorJumpTop: true})
1280+
1281+
d.Process(buildRows(1, 20))
1282+
1283+
result := d.Process([]byte(cursorPos(1) + "\x1b[15~hello world"))
1284+
if !result.Truncate {
1285+
t.Error("should truncate when content follows after tilde-terminated sequence")
1286+
}
1287+
})
12771288
}
12781289

12791290
func TestFrameDetector_CursorHomeLookAhead(t *testing.T) {

internal/ansi/strip.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
var ansiPatterns = []*regexp.Regexp{
11-
regexp.MustCompile(`\x1b\[[0-9;]*[A-Za-z]`), // CSI sequences (colors, cursor, etc)
11+
regexp.MustCompile(`\x1b\[[0-9;]*[A-Za-z~]`), // CSI sequences (colors, cursor, etc)
1212
regexp.MustCompile(`\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)`), // OSC sequences
1313
regexp.MustCompile(`\x1b[()][AB012]`), // Character set selection
1414
regexp.MustCompile(`\x1b[=>]`), // Keypad modes
@@ -455,7 +455,7 @@ func parseEraseDisplay(s string, i int) (mode, end int, ok bool) {
455455
}
456456

457457
func isCSITerminator(c byte) bool {
458-
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '@' || c == '`'
458+
return c >= 0x40 && c <= 0x7E
459459
}
460460

461461
// hasPrintableAhead checks if printable content follows within maxBytes,

internal/ansi/strip_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,21 @@ func TestStrip(t *testing.T) {
345345
input: "\x1b[1;1Hhello world\x1b[2;1Hrow2\x1b[1;6H\x1b[1J",
346346
expected: " world\nrow2",
347347
},
348+
{
349+
name: "function key escape sequences",
350+
input: "before\x1b[15~after",
351+
expected: "beforeafter",
352+
},
353+
{
354+
name: "delete key escape sequence",
355+
input: "text\x1b[3~more",
356+
expected: "textmore",
357+
},
358+
{
359+
name: "cursor positioning with tilde-terminated sequences mixed in",
360+
input: "\x1b[1;1H\x1b[15~hello",
361+
expected: "hello",
362+
},
348363
}
349364

350365
for _, tt := range tests {

internal/daemon/server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,9 @@ func (s *Server) handleSnapshot(req Request) Response {
651651

652652
tempCols := clampUint16(meta.Cols + 1)
653653
tempRows := clampUint16(meta.Rows + 1)
654-
pty.Setsize(ptmx, &pty.Winsize{Cols: tempCols, Rows: tempRows})
654+
if err := pty.Setsize(ptmx, &pty.Winsize{Cols: tempCols, Rows: tempRows}); err != nil {
655+
return Response{Success: false, Error: fmt.Sprintf("temporary resize for snapshot: %v", err)}
656+
}
655657
if cmd != nil && cmd.Process != nil {
656658
cmd.Process.Signal(syscall.SIGWINCH)
657659
}

internal/escape/escape.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"strconv"
66
"strings"
7+
"unicode/utf8"
78
)
89

910
// Interpret processes escape sequences in a string.
@@ -82,8 +83,9 @@ func Interpret(s string) (string, error) {
8283
i += 2
8384

8485
default:
85-
result.WriteByte(s[i+1])
86-
i += 2
86+
r, size := utf8.DecodeRuneInString(s[i+1:])
87+
result.WriteRune(r)
88+
i += 1 + size
8789
}
8890
}
8991

internal/escape/escape_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func TestInterpret_UnknownSequences(t *testing.T) {
4444
{"slash", `\/`, "/"},
4545
{"at sign", `\@`, "@"},
4646
{"hash", `\#`, "#"},
47+
{"multi-byte UTF-8 after backslash", `\é`, "é"},
4748
}
4849
for _, tt := range tests {
4950
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)