From 0f35043c9ac588811f22c732ac5392850f22381e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 25 Aug 2026 20:19:33 -0700 Subject: [PATCH] terminal: execute C0 controls 0x10-0x1F in the ground state Fixes #14021 The ground state UTF-8 fast paths only classified 0x00-0x0F plus 0x1B (escape) as C0 controls. The remaining C0 bytes (0x10-0x1A, 0x1C-0x1F) were decoded as ordinary codepoints and routed to print as if they were text. This resulted in incorrect grids but also very weird font fallback, e.g. U+0014 would find CJK fonts. This commit fixes this by routing every ground state C0 byte except ESC to execute as it should be. --- src/terminal/stream.zig | 91 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index e6bd55f1b..6e34c6c56 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -718,7 +718,10 @@ pub fn Stream(comptime H: type) type { var i: usize = 0; while (i < cps.len) { const cp = cps[i]; - if (cp <= 0xF) { + // C0 controls execute rather than print. ESC can + // never appear here because the decoder stops at it, + // so this is every C0 except ESC. + if (cp <= 0x1F) { @branchHint(.unlikely); self.execute(@intCast(cp)); i += 1; @@ -735,7 +738,7 @@ pub fn Stream(comptime H: type) type { scan: { if (simd.lanes(u32)) |lanes| { const V = @Vector(lanes, u32); - const threshold: V = @splat(0xF); + const threshold: V = @splat(0x1F); while (end + lanes <= cps.len) { const v: V = cps[end..][0..lanes].*; const gt = v > threshold; @@ -747,7 +750,7 @@ pub fn Stream(comptime H: type) type { end += lanes; } } - while (end < cps.len and cps[end] > 0xF) end += 1; + while (end < cps.len and cps[end] > 0x1F) end += 1; } self.handler.vt(.print_slice, .{ .cps = cps[i..end] }); i = end; @@ -1146,17 +1149,19 @@ pub fn Stream(comptime H: type) type { @setEvalBranchQuota(200_000); // C0 control - if (c <= 0xF) { + if (c <= 0x1F) { @branchHint(.unlikely); + + // ESC + if (c == 0x1B) { + self.parser.state = .escape; + self.parser.clear(); + return; + } + self.execute(@intCast(c)); return; } - // ESC - if (c == 0x1B) { - self.parser.state = .escape; - self.parser.clear(); - return; - } self.print(@intCast(c)); } @@ -3115,6 +3120,72 @@ test "simd: complete incomplete utf-8" { try testing.expectEqual(@as(u21, 0x800), s.handler.c.?); } +test "stream: ground state C0 controls are executed, not printed" { + const H = struct { + buf: [128]u21 = undefined, + len: usize = 0, + + pub fn vt( + self: *@This(), + comptime action: Action.Tag, + value: Action.Value(action), + ) void { + switch (action) { + .print => { + self.buf[self.len] = value.cp; + self.len += 1; + }, + .print_slice => for (value.cps) |cp| { + self.buf[self.len] = @intCast(cp); + self.len += 1; + }, + else => {}, + } + } + }; + + // Every C0 control except ESC must never produce a print action + // in the ground state. ESC is excluded because it begins an + // escape sequence rather than executing. + for (0..0x20) |c| { + if (c == 0x1B) continue; + + // Scalar path. + { + var s: Stream(H) = .init(.{ .handler = .{} }); + s.next('A'); + s.next(@intCast(c)); + s.next('B'); + try testing.expectEqual(@as(usize, 2), s.handler.len); + try testing.expectEqual(@as(u21, 'A'), s.handler.buf[0]); + try testing.expectEqual(@as(u21, 'B'), s.handler.buf[1]); + } + + // Batched path. + { + var s: Stream(H) = .init(.{ .handler = .{} }); + s.nextSlice(&.{ 'A', 'B', @intCast(c), 'C', 'D' }); + try testing.expectEqual(@as(usize, 4), s.handler.len); + for ("ABCD", 0..) |expected, i| { + try testing.expectEqual(@as(u21, expected), s.handler.buf[i]); + } + } + + // Batched path with a run long enough to exercise the + // vectorized printable-run scan on either side of the control. + { + var s: Stream(H) = .init(.{ .handler = .{} }); + var input: [65]u8 = @splat('A'); + input[32] = @intCast(c); + s.nextSlice(&input); + try testing.expectEqual(@as(usize, 64), s.handler.len); + for (s.handler.buf[0..s.handler.len]) |cp| { + try testing.expectEqual(@as(u21, 'A'), cp); + } + } + } +} + test "stream: cursor right (CUF)" { const H = struct { amount: u16 = 0,