diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 3f427c6b5..032ac1bc6 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -159,7 +159,10 @@ pub const Action = union(enum) { const value = @field(self, u_field.name); switch (@TypeOf(value)) { // Unicode - u21 => try std.fmt.format(writer, "'{u}'", .{value}), + u21 => try std.fmt.format(writer, "'{u}' (U+{X})", .{ value, value }), + + // Byte + u8 => try std.fmt.format(writer, "0x{x}", .{value}), // Note: we don't do ASCII (u8) because there are a lot // of invisible characters we don't want to handle right diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 05e0f0d59..1bd5f2cf3 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -443,8 +443,8 @@ pub fn restoreCursor(self: *Terminal) void { self.screen.cursor.pen = saved.pen; self.screen.charset = saved.charset; self.modes.set(.origin, saved.origin); - self.screen.cursor.x = saved.x; - self.screen.cursor.y = saved.y; + self.screen.cursor.x = @min(saved.x, self.cols - 1); + self.screen.cursor.y = @min(saved.y, self.rows - 1); self.screen.cursor.pending_wrap = saved.pending_wrap; } @@ -766,6 +766,11 @@ pub fn print(self: *Terminal, c: u21) !void { // Attach zero-width characters to our cell as grapheme data. if (width == 0) { + // If we have grapheme clustering enabled, we don't blindly attach + // any zero width character to our cells and we instead just ignore + // it. + if (self.modes.get(.grapheme_cluster)) return; + // If we're at cell zero, then this is malformed data and we don't // print anything or even store this. Zero-width characters are ALWAYS // attached to some other non-zero-width character at the time of @@ -883,10 +888,10 @@ fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell { // single-width characters into that. if (cell.attrs.wide) { const x = self.screen.cursor.x + 1; - assert(x < self.cols); - - const spacer_cell = row.getCellPtr(x); - spacer_cell.* = self.screen.cursor.pen; + if (x < self.cols) { + const spacer_cell = row.getCellPtr(x); + spacer_cell.* = self.screen.cursor.pen; + } if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) { self.clearWideSpacerHead(); @@ -1341,10 +1346,12 @@ pub fn deleteChars(self: *Terminal, count: usize) !void { } } -pub fn eraseChars(self: *Terminal, count: usize) void { +pub fn eraseChars(self: *Terminal, count_req: usize) void { const tracy = trace(@src()); defer tracy.end(); + const count = @max(count_req, 1); + // This resets the pending wrap state self.screen.cursor.pending_wrap = false; @@ -2170,6 +2177,31 @@ test "Terminal: print over wide spacer tail" { } } +test "Terminal: zero width chars with grapheme clustering can be put in their own cell" { + var t = try init(testing.allocator, 5, 5); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + try t.print('x'); + try t.print(0x7F); // zero-width control character + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("x", str); + } + + const row = t.screen.getRow(.{ .screen = 0 }); + { + const cell = row.getCell(0); + try testing.expectEqual(@as(u32, 'x'), cell.char); + try testing.expect(!cell.attrs.wide); + try testing.expect(!cell.attrs.grapheme); + } +} + test "Terminal: VS15 to make narrow character" { var t = try init(testing.allocator, 5, 5); defer t.deinit(testing.allocator); @@ -4757,6 +4789,23 @@ test "Terminal: eraseChars simple operation" { } } +test "Terminal: eraseChars minimum one" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + for ("ABC") |c| try t.print(c); + t.setCursorPos(1, 1); + t.eraseChars(0); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("XBC", str); + } +} + test "Terminal: eraseChars beyond screen edge" { const alloc = testing.allocator; var t = try init(alloc, 5, 5); @@ -5016,6 +5065,24 @@ test "Terminal: saveCursor origin mode" { } } +test "Terminal: saveCursor resize" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + t.setCursorPos(1, 10); + t.saveCursor(); + try t.resize(alloc, 5, 5); + t.restoreCursor(); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" X", str); + } +} + test "Terminal: setProtectedMode" { const alloc = testing.allocator; var t = try init(alloc, 3, 3); diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index 634779413..9b3dc7a21 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.zig @@ -197,7 +197,7 @@ pub const Parser = struct { .b = @truncate(rgb[2]), }, }; - } else if (slice.len >= 2 and slice[1] == 5) { + } else if (slice.len >= 3 and slice[1] == 5) { self.idx += 2; return Attribute{ .@"256_fg" = @truncate(slice[2]), @@ -225,7 +225,7 @@ pub const Parser = struct { .b = @truncate(rgb[2]), }, }; - } else if (slice.len >= 2 and slice[1] == 5) { + } else if (slice.len >= 3 and slice[1] == 5) { self.idx += 2; return Attribute{ .@"256_bg" = @truncate(slice[2]), @@ -532,3 +532,15 @@ test "sgr: underline, bg, and fg" { try testing.expectEqual(Attribute.Underline.single, v.underline); } } + +test "sgr: direct color fg missing color" { + // This used to crash + var p: Parser = .{ .params = &[_]u16{ 38, 5 }, .colon = false }; + while (p.next()) |_| {} +} + +test "sgr: direct color bg missing color" { + // This used to crash + var p: Parser = .{ .params = &[_]u16{ 48, 5 }, .colon = false }; + while (p.next()) |_| {} +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index e48125fce..6f27a9b06 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -154,7 +154,7 @@ pub fn Stream(comptime Handler: type) type { else log.warn("unimplemented invokeCharset: {x}", .{c}), - else => log.warn("invalid C0 character, ignoring: {x}", .{c}), + else => log.warn("invalid C0 character, ignoring: 0x{x}", .{c}), } }