mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-26 17:11:40 +00:00
terminal: execute C0 controls 0x10-0x1F in the ground state (#14022)
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.
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user