Commit ce8b2bd

Jay Hemnani  ·  2026-10-16 09:27:21 -0400 EDT
parent 36e024e
fix: keep UTF-8 text in run and tail output (#274)

stripAnsi fed each byte to the bare VT parser, which expects decoded
input. It dropped every non-ASCII byte, and a UTF-8 continuation byte in
the C1 range (Ý is C3 9D, 0x9D is the 8-bit OSC introducer) started a
sequence that swallowed the text after it. Pass bytes >= 0x80 through
when the parser is not inside a sequence.
1 files changed,  +20, -0
+20, -0
......@@ -493,6 +493,11 @@ pub fn stripAnsi(alloc: std.mem.Allocator, data: []const u8) ![]const u8 {
493493
494494 var parser = ghostty_vt.Parser.init();
495495 for (data) |c| {
496+ // Bytes >= 0x80 outside a sequence are UTF-8 text, not C1 controls.
497+ if (c >= 0x80 and parser.state == .ground) {
498+ result.append(alloc, c) catch unreachable;
499+ continue;
500+ }
496501 const actions = parser.next(c);
497502 for (actions) |action_opt| {
498503 const action = action_opt orelse continue;
......@@ -2294,6 +2299,21 @@ test "stripAnsi: only escape sequences" {
22942299 try testing.expectEqualStrings("", result);
22952300 }
22962301
2302+test "stripAnsi: keeps UTF-8 text" {
2303+ const alloc = testing.allocator;
2304+ const result = try stripAnsi(alloc, "\x1b[1mcafé\x1b[0m – 日本 🎉\n");
2305+ defer alloc.free(result);
2306+ try testing.expectEqualStrings("café – 日本 🎉\n", result);
2307+}
2308+
2309+test "stripAnsi: UTF-8 bytes do not start escape sequences" {
2310+ const alloc = testing.allocator;
2311+ // Ý is C3 9D, and 0x9D on its own is the 8-bit OSC introducer
2312+ const result = try stripAnsi(alloc, "Ýmir\nline two\n");
2313+ defer alloc.free(result);
2314+ try testing.expectEqualStrings("Ýmir\nline two\n", result);
2315+}
2316+
22972317 test "isUserInput: kitty keyboard status reply is a terminal response" {
22982318 // CSI ? flags u answers the program's CSI ? u query; no key was pressed
22992319 try testing.expect(!isUserInput("\x1b[?1u"));