Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/config/io.zig
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,30 @@ pub const ReadableIO = union(enum) {
try v.formatEntry(formatterpkg.entryFormatter("a", &buf.writer));
try std.testing.expectEqualSlices(u8, "a = raw:foo\n", buf.written());
}

// The embedded apprt stores initial input as a Zig-escaped raw
// value, so parsing it must give back the exact input bytes.
test "cloneParsed restores Zig-escaped bytes" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();

const inputs = [_][]const u8{
"printf '\x1b]0;라마바OSC테스트\x07'\r",
"日本語のタイトル",
"\u{1F680} \u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}",
"e\u{301}cole \u{1112}\u{1161}\u{11AB}",
"invalid \xff\xfe\x80 bytes",
};
for (inputs) |input| {
var escaped: std.Io.Writer.Allocating = .init(alloc);
try std.zig.stringEscape(input, &escaped.writer);
const v: Self = .{ .raw = try escaped.toOwnedSliceSentinel(0) };
const parsed = try v.cloneParsed(alloc);
try testing.expectEqualSlices(u8, input, parsed.raw);
}
}
};

pub const RepeatableReadableIO = struct {
Expand Down
24 changes: 20 additions & 4 deletions src/config/string.zig
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,23 @@ pub fn parse(out: []u8, bytes: []const u8) ![]u8 {
continue;
}

// Zig's \xNN syntax represents a single byte, while the other
// escape sequences represent Unicode codepoints.
const is_byte_escape = src_i + 1 < bytes.len and
bytes[src_i + 1] == 'x';

// Parse the escape sequence
switch (std.zig.string_literal.parseEscapeSequence(
bytes,
&src_i,
)) {
.failure => return error.InvalidString,
.success => |cp| dst_i += try std.unicode.utf8Encode(
cp,
out[dst_i..],
),
.success => |cp| if (is_byte_escape) {
out[dst_i] = @intCast(cp);
dst_i += 1;
} else {
dst_i += try std.unicode.utf8Encode(cp, out[dst_i..]);
},
}
}

Expand Down Expand Up @@ -100,6 +107,15 @@ test "parse: escapes" {
}
}

test "parse: hex escapes are bytes" {
var buf: [128]u8 = undefined;
const result = try parse(
&buf,
"\\xe6\\x97\\xa5\\xe6\\x9c\\xac\\xe8\\xaa\\x9e",
);
try std.testing.expectEqualStrings("日本語", result);
}

test "codepointIterator: empty" {
var it = codepointIterator("");
try std.testing.expectEqual(null, try it.next());
Expand Down
49 changes: 46 additions & 3 deletions src/terminal/formatter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1143,9 +1143,15 @@ pub const PageFormatter = struct {
};

// If this row is blank, accumulate to avoid a bunch of extra
// work later. If it isn't blank, make sure we dump all our
// blanks.
if (!Cell.hasTextAny(cells_subset)) {
// work later. A styled blank row is content for styled output:
// dropping it loses full-width background bands during replay.
const has_styled_content = if (formatStyled(self.opts.emit)) styled: {
for (cells_subset) |cell| {
if (!cell.isEmpty() or cell.hasStyling()) break :styled true;
}
break :styled false;
} else false;
if (!Cell.hasTextAny(cells_subset) and !has_styled_content) {
blank_rows += 1;
continue;
}
Expand Down Expand Up @@ -6718,6 +6724,43 @@ test "Page VT background color on trailing blank cells" {
try testing.expect(has_red_bg_line1);
}

test "Page VT preserves a fully styled blank row" {
const testing = std.testing;
const alloc = testing.allocator;
const io = testing.io;

var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();

var t = try Terminal.init(io, alloc, .{
.cols = 20,
.rows = 5,
});
defer t.deinit(alloc);

var s = t.vtStream();
defer s.deinit();
// Erase the complete first row with a background color and then put text
// on the next row. The first row has no text cells, but every cell carries
// the background style that a replay must preserve.
s.nextSlice("\x1b[41m\x1b[2K\x1b[0m\r\nline2");

const page = t.screens.active.pages.pages.last.?.page();
var opts: Options = .vt;
opts.trim = false;
var formatter: PageFormatter = .init(page, opts);
try formatter.format(&builder.writer);
const output = builder.writer.buffered();

const first_break = std.mem.indexOf(u8, output, "\r\n") orelse {
return error.TestUnexpectedResult;
};
// SGR 41 is stored as palette background 1, which the VT formatter
// writes in its indexed form.
try testing.expect(std.mem.indexOf(u8, output[0..first_break], "\x1b[48;5;1m") != null);
try testing.expectEqual(@as(usize, 20), std.mem.count(u8, output[0..first_break], " "));
}

test "Page HTML with hyperlinks" {
const testing = std.testing;
const alloc = testing.allocator;
Expand Down
Loading