Mejoras de performance v1.1
Buffer optimizations: - Symbol: nuevo tipo compacto para almacenar UTF-8 (4 bytes max) - Evita conversion codepoint->UTF8 en cada render - fromCodepoint() y fromSlice() para crear symbols - slice() para output directo sin conversion - Cell: refactorizado para usar Symbol - Eliminado campo 'dirty' (innecesario con diff) - Nuevo metodo eql() para comparacion eficiente - char() accessor para compatibilidad legacy - Buffer.diff(): nuevo sistema de renderizado diferencial - DiffIterator compara buffers celda a celda - Solo retorna celdas que cambiaron - Reduce drasticamente I/O a terminal - Buffer.resize(): nuevo metodo para redimensionar - Preserva contenido existente donde posible - Backend.writeSymbol(): escribe UTF-8 directo - Mas eficiente que writeChar() con conversion - Terminal.flush(): usa diff iterator - Solo escribe celdas modificadas Tests: 18 tests (9 nuevos para Symbol, Cell, diff) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
560ed1b355
commit
1df3172afc
4 changed files with 382 additions and 54 deletions
|
|
@ -186,13 +186,20 @@ pub const AnsiBackend = struct {
|
|||
}
|
||||
}
|
||||
|
||||
/// Writes a single character.
|
||||
/// Writes a single character (codepoint).
|
||||
pub fn writeChar(self: *AnsiBackend, char: u21) !void {
|
||||
var buf: [4]u8 = undefined;
|
||||
const len = std.unicode.utf8Encode(char, &buf) catch return;
|
||||
_ = self.stdout.write(buf[0..len]) catch {};
|
||||
}
|
||||
|
||||
/// Writes a UTF-8 encoded symbol directly (more efficient than writeChar).
|
||||
pub fn writeSymbol(self: *AnsiBackend, symbol: []const u8) !void {
|
||||
if (symbol.len > 0) {
|
||||
_ = self.stdout.write(symbol) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Flushes output to the terminal.
|
||||
pub fn flush(self: *AnsiBackend) !void {
|
||||
// stdout is typically unbuffered or line-buffered
|
||||
|
|
|
|||
386
src/buffer.zig
386
src/buffer.zig
|
|
@ -129,26 +129,96 @@ pub const Margin = struct {
|
|||
}
|
||||
};
|
||||
|
||||
/// Compact UTF-8 symbol storage (up to 4 bytes).
|
||||
///
|
||||
/// This is more memory-efficient than storing a u21 codepoint because:
|
||||
/// 1. Most common characters (ASCII) use only 1 byte
|
||||
/// 2. No conversion needed when writing to terminal (already UTF-8)
|
||||
/// 3. Supports grapheme clusters up to 4 bytes (covers most use cases)
|
||||
pub const Symbol = struct {
|
||||
/// UTF-8 encoded bytes (up to 4 bytes, padded with zeros)
|
||||
data: [4]u8 = .{ ' ', 0, 0, 0 },
|
||||
/// Length of the UTF-8 sequence (1-4)
|
||||
len: u3 = 1,
|
||||
|
||||
pub const default_val: Symbol = .{};
|
||||
pub const space: Symbol = .{};
|
||||
|
||||
/// Creates a Symbol from a UTF-8 string slice.
|
||||
/// Takes only the first grapheme (up to 4 bytes).
|
||||
pub fn fromSlice(bytes: []const u8) Symbol {
|
||||
if (bytes.len == 0) return Symbol.space;
|
||||
|
||||
var sym: Symbol = .{ .data = .{ 0, 0, 0, 0 }, .len = 0 };
|
||||
const copy_len = @min(bytes.len, 4);
|
||||
for (0..copy_len) |i| {
|
||||
sym.data[i] = bytes[i];
|
||||
}
|
||||
sym.len = @intCast(copy_len);
|
||||
return sym;
|
||||
}
|
||||
|
||||
/// Creates a Symbol from a Unicode codepoint.
|
||||
pub fn fromCodepoint(cp: u21) Symbol {
|
||||
var sym: Symbol = .{ .data = .{ 0, 0, 0, 0 }, .len = 0 };
|
||||
|
||||
if (cp < 0x80) {
|
||||
sym.data[0] = @intCast(cp);
|
||||
sym.len = 1;
|
||||
} else if (cp < 0x800) {
|
||||
sym.data[0] = @intCast(0xC0 | (cp >> 6));
|
||||
sym.data[1] = @intCast(0x80 | (cp & 0x3F));
|
||||
sym.len = 2;
|
||||
} else if (cp < 0x10000) {
|
||||
sym.data[0] = @intCast(0xE0 | (cp >> 12));
|
||||
sym.data[1] = @intCast(0x80 | ((cp >> 6) & 0x3F));
|
||||
sym.data[2] = @intCast(0x80 | (cp & 0x3F));
|
||||
sym.len = 3;
|
||||
} else {
|
||||
sym.data[0] = @intCast(0xF0 | (cp >> 18));
|
||||
sym.data[1] = @intCast(0x80 | ((cp >> 12) & 0x3F));
|
||||
sym.data[2] = @intCast(0x80 | ((cp >> 6) & 0x3F));
|
||||
sym.data[3] = @intCast(0x80 | (cp & 0x3F));
|
||||
sym.len = 4;
|
||||
}
|
||||
return sym;
|
||||
}
|
||||
|
||||
/// Returns the symbol as a slice for output.
|
||||
pub fn slice(self: Symbol) []const u8 {
|
||||
return self.data[0..self.len];
|
||||
}
|
||||
|
||||
/// Checks equality with another Symbol.
|
||||
pub fn eql(self: Symbol, other: Symbol) bool {
|
||||
return std.mem.eql(u8, &self.data, &other.data) and self.len == other.len;
|
||||
}
|
||||
};
|
||||
|
||||
/// A single cell in the terminal buffer.
|
||||
///
|
||||
/// Contains a character (as Unicode codepoint) and styling information.
|
||||
/// Contains a character (as UTF-8 Symbol) and styling information.
|
||||
/// Optimized for memory layout and cache efficiency.
|
||||
pub const Cell = struct {
|
||||
/// The character to display (Unicode codepoint, space by default).
|
||||
char: u21 = ' ',
|
||||
/// The character to display (UTF-8 encoded).
|
||||
symbol: Symbol = Symbol.space,
|
||||
/// Foreground color.
|
||||
fg: Color = .reset,
|
||||
/// Background color.
|
||||
bg: Color = .reset,
|
||||
/// Text modifiers (bold, italic, etc.).
|
||||
modifiers: Modifier = .{},
|
||||
/// Whether this cell has been modified and needs redraw.
|
||||
dirty: bool = true,
|
||||
|
||||
pub const empty: Cell = .{};
|
||||
|
||||
/// Creates a cell with the given character.
|
||||
pub fn init(char: u21) Cell {
|
||||
return .{ .char = char };
|
||||
/// Creates a cell with the given character (codepoint).
|
||||
pub fn init(cp: u21) Cell {
|
||||
return .{ .symbol = Symbol.fromCodepoint(cp) };
|
||||
}
|
||||
|
||||
/// Creates a cell from a UTF-8 string.
|
||||
pub fn fromStr(s: []const u8) Cell {
|
||||
return .{ .symbol = Symbol.fromSlice(s) };
|
||||
}
|
||||
|
||||
/// Sets the style of the cell.
|
||||
|
|
@ -157,13 +227,45 @@ pub const Cell = struct {
|
|||
if (s.background) |bg_color| self.bg = bg_color;
|
||||
self.modifiers = self.modifiers.insert(s.add_modifiers);
|
||||
self.modifiers = self.modifiers.remove(s.sub_modifiers);
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Sets the character from a codepoint.
|
||||
pub fn setChar(self: *Cell, cp: u21) void {
|
||||
self.symbol = Symbol.fromCodepoint(cp);
|
||||
}
|
||||
|
||||
/// Sets the character from a UTF-8 string.
|
||||
pub fn setSymbol(self: *Cell, s: []const u8) void {
|
||||
self.symbol = Symbol.fromSlice(s);
|
||||
}
|
||||
|
||||
/// Resets the cell to empty.
|
||||
pub fn reset(self: *Cell) void {
|
||||
self.* = Cell.empty;
|
||||
}
|
||||
|
||||
/// Checks if two cells are equal (same content and style).
|
||||
pub fn eql(self: Cell, other: Cell) bool {
|
||||
return self.symbol.eql(other.symbol) and
|
||||
std.meta.eql(self.fg, other.fg) and
|
||||
std.meta.eql(self.bg, other.bg) and
|
||||
std.meta.eql(self.modifiers, other.modifiers);
|
||||
}
|
||||
|
||||
/// Legacy accessor for char (returns first codepoint or space).
|
||||
pub fn char(self: Cell) u21 {
|
||||
const bytes = self.symbol.slice();
|
||||
if (bytes.len == 0) return ' ';
|
||||
const decoded = std.unicode.utf8Decode(bytes) catch return ' ';
|
||||
return decoded;
|
||||
}
|
||||
};
|
||||
|
||||
/// A single cell update for differential rendering.
|
||||
pub const CellUpdate = struct {
|
||||
x: u16,
|
||||
y: u16,
|
||||
cell: Cell,
|
||||
};
|
||||
|
||||
/// A buffer holding a grid of cells representing terminal state.
|
||||
|
|
@ -192,6 +294,37 @@ pub const Buffer = struct {
|
|||
self.allocator.free(self.cells);
|
||||
}
|
||||
|
||||
/// Resizes the buffer to a new area.
|
||||
/// Preserves existing content where possible.
|
||||
pub fn resize(self: *Buffer, new_rect: Rect) !void {
|
||||
if (new_rect.width == self.area.width and new_rect.height == self.area.height) {
|
||||
self.area = new_rect;
|
||||
return;
|
||||
}
|
||||
|
||||
const new_size = new_rect.area();
|
||||
const new_cells = try self.allocator.alloc(Cell, new_size);
|
||||
@memset(new_cells, Cell.empty);
|
||||
|
||||
// Copy existing content
|
||||
const min_width = @min(self.area.width, new_rect.width);
|
||||
const min_height = @min(self.area.height, new_rect.height);
|
||||
|
||||
var y: u16 = 0;
|
||||
while (y < min_height) : (y += 1) {
|
||||
const old_row_start = @as(usize, y) * @as(usize, self.area.width);
|
||||
const new_row_start = @as(usize, y) * @as(usize, new_rect.width);
|
||||
@memcpy(
|
||||
new_cells[new_row_start..][0..min_width],
|
||||
self.cells[old_row_start..][0..min_width],
|
||||
);
|
||||
}
|
||||
|
||||
self.allocator.free(self.cells);
|
||||
self.cells = new_cells;
|
||||
self.area = new_rect;
|
||||
}
|
||||
|
||||
/// Gets the index in the cells array for position (x, y).
|
||||
fn indexAt(self: *const Buffer, x: u16, y: u16) ?usize {
|
||||
if (!self.area.contains(x, y)) {
|
||||
|
|
@ -214,10 +347,15 @@ pub const Buffer = struct {
|
|||
return self.cells[idx];
|
||||
}
|
||||
|
||||
/// Alias for getPtr for compatibility.
|
||||
pub fn getCell(self: *Buffer, x: u16, y: u16) ?*Cell {
|
||||
return self.getPtr(x, y);
|
||||
}
|
||||
|
||||
/// Sets a single character at (x, y) with the given style.
|
||||
pub fn setChar(self: *Buffer, x: u16, y: u16, char: u21, s: Style) void {
|
||||
pub fn setChar(self: *Buffer, x: u16, y: u16, cp: u21, s: Style) void {
|
||||
if (self.getPtr(x, y)) |cell| {
|
||||
cell.char = char;
|
||||
cell.setChar(cp);
|
||||
cell.setStyle(s);
|
||||
}
|
||||
}
|
||||
|
|
@ -238,7 +376,7 @@ pub const Buffer = struct {
|
|||
}
|
||||
|
||||
/// Fills the entire buffer (or a sub-area) with a character and style.
|
||||
pub fn fill(self: *Buffer, rect: Rect, char: u21, s: Style) void {
|
||||
pub fn fill(self: *Buffer, rect: Rect, cp: u21, s: Style) void {
|
||||
const target = self.area.intersection(rect);
|
||||
if (target.isEmpty()) return;
|
||||
|
||||
|
|
@ -246,7 +384,7 @@ pub const Buffer = struct {
|
|||
while (y < target.bottom()) : (y += 1) {
|
||||
var cur_x = target.x;
|
||||
while (cur_x < target.right()) : (cur_x += 1) {
|
||||
self.setChar(cur_x, y, char, s);
|
||||
self.setChar(cur_x, y, cp, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -256,25 +394,174 @@ pub const Buffer = struct {
|
|||
@memset(self.cells, Cell.empty);
|
||||
}
|
||||
|
||||
/// Marks all cells as dirty (need redraw).
|
||||
/// Marks all cells for redraw (legacy compatibility).
|
||||
/// With the new diff-based system, this is a no-op but kept for API compatibility.
|
||||
pub fn markDirty(self: *Buffer) void {
|
||||
for (self.cells) |*cell| {
|
||||
cell.dirty = true;
|
||||
_ = self;
|
||||
// No-op: diff-based rendering doesn't use per-cell dirty flags
|
||||
}
|
||||
|
||||
/// Marks all cells as clean (legacy compatibility).
|
||||
pub fn markClean(self: *Buffer) void {
|
||||
_ = self;
|
||||
// No-op: diff-based rendering doesn't use per-cell dirty flags
|
||||
}
|
||||
|
||||
/// Computes the diff between this buffer and another.
|
||||
/// Returns an iterator over changed cells.
|
||||
/// This buffer is the "new" state, other is the "old" state.
|
||||
pub fn diff(self: *const Buffer, other: *const Buffer) DiffIterator {
|
||||
return DiffIterator{
|
||||
.new_buf = self,
|
||||
.old_buf = other,
|
||||
.index = 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Copies content from another buffer into this one.
|
||||
/// Only copies the overlapping region.
|
||||
pub fn merge(self: *Buffer, other: *const Buffer) void {
|
||||
const target = self.area.intersection(other.area);
|
||||
if (target.isEmpty()) return;
|
||||
|
||||
var y = target.y;
|
||||
while (y < target.bottom()) : (y += 1) {
|
||||
var cur_x = target.x;
|
||||
while (cur_x < target.right()) : (cur_x += 1) {
|
||||
if (other.get(cur_x, y)) |cell| {
|
||||
if (self.getPtr(cur_x, y)) |self_cell| {
|
||||
self_cell.* = cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks all cells as clean.
|
||||
pub fn markClean(self: *Buffer) void {
|
||||
for (self.cells) |*cell| {
|
||||
cell.dirty = false;
|
||||
/// Sets the style for an entire area without changing content.
|
||||
pub fn setStyle(self: *Buffer, rect: Rect, s: Style) void {
|
||||
const target = self.area.intersection(rect);
|
||||
if (target.isEmpty()) return;
|
||||
|
||||
var y = target.y;
|
||||
while (y < target.bottom()) : (y += 1) {
|
||||
var cur_x = target.x;
|
||||
while (cur_x < target.right()) : (cur_x += 1) {
|
||||
if (self.getPtr(cur_x, y)) |cell| {
|
||||
cell.setStyle(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Iterator for buffer differences.
|
||||
pub const DiffIterator = struct {
|
||||
new_buf: *const Buffer,
|
||||
old_buf: *const Buffer,
|
||||
index: usize,
|
||||
|
||||
/// Returns the next changed cell, or null if done.
|
||||
pub fn next(self: *DiffIterator) ?CellUpdate {
|
||||
const total = self.new_buf.cells.len;
|
||||
|
||||
while (self.index < total) {
|
||||
const idx = self.index;
|
||||
self.index += 1;
|
||||
|
||||
const new_cell = self.new_buf.cells[idx];
|
||||
|
||||
// Check if old buffer has same index
|
||||
const old_cell = if (idx < self.old_buf.cells.len)
|
||||
self.old_buf.cells[idx]
|
||||
else
|
||||
Cell.empty;
|
||||
|
||||
// If cells differ, return the update
|
||||
if (!new_cell.eql(old_cell)) {
|
||||
const local_x: u16 = @intCast(idx % self.new_buf.area.width);
|
||||
const local_y: u16 = @intCast(idx / self.new_buf.area.width);
|
||||
return CellUpdate{
|
||||
.x = self.new_buf.area.x + local_x,
|
||||
.y = self.new_buf.area.y + local_y,
|
||||
.cell = new_cell,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Counts remaining differences without consuming the iterator.
|
||||
pub fn countRemaining(self: *DiffIterator) usize {
|
||||
var count: usize = 0;
|
||||
var temp = self.*;
|
||||
while (temp.next()) |_| {
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
test "Symbol fromCodepoint ASCII" {
|
||||
const sym = Symbol.fromCodepoint('A');
|
||||
try std.testing.expectEqual(@as(u3, 1), sym.len);
|
||||
try std.testing.expectEqualStrings("A", sym.slice());
|
||||
}
|
||||
|
||||
test "Symbol fromCodepoint UTF-8" {
|
||||
// 2-byte UTF-8 (e.g., copyright symbol)
|
||||
const sym2 = Symbol.fromCodepoint(0xA9); // ©
|
||||
try std.testing.expectEqual(@as(u3, 2), sym2.len);
|
||||
|
||||
// 3-byte UTF-8 (e.g., box drawing)
|
||||
const sym3 = Symbol.fromCodepoint(0x2500); // ─
|
||||
try std.testing.expectEqual(@as(u3, 3), sym3.len);
|
||||
|
||||
// 4-byte UTF-8 (e.g., emoji)
|
||||
const sym4 = Symbol.fromCodepoint(0x1F600); // grinning face
|
||||
try std.testing.expectEqual(@as(u3, 4), sym4.len);
|
||||
}
|
||||
|
||||
test "Symbol fromSlice" {
|
||||
const sym = Symbol.fromSlice("X");
|
||||
try std.testing.expectEqualStrings("X", sym.slice());
|
||||
|
||||
const sym_multi = Symbol.fromSlice("AB");
|
||||
try std.testing.expectEqualStrings("AB", sym_multi.slice());
|
||||
}
|
||||
|
||||
test "Symbol equality" {
|
||||
const sym1 = Symbol.fromCodepoint('A');
|
||||
const sym2 = Symbol.fromCodepoint('A');
|
||||
const sym3 = Symbol.fromCodepoint('B');
|
||||
|
||||
try std.testing.expect(sym1.eql(sym2));
|
||||
try std.testing.expect(!sym1.eql(sym3));
|
||||
}
|
||||
|
||||
test "Cell init and char" {
|
||||
const cell = Cell.init('X');
|
||||
try std.testing.expectEqual(@as(u21, 'X'), cell.char());
|
||||
}
|
||||
|
||||
test "Cell equality" {
|
||||
var cell1 = Cell.init('A');
|
||||
cell1.fg = Color.red;
|
||||
|
||||
var cell2 = Cell.init('A');
|
||||
cell2.fg = Color.red;
|
||||
|
||||
var cell3 = Cell.init('A');
|
||||
cell3.fg = Color.blue;
|
||||
|
||||
try std.testing.expect(cell1.eql(cell2));
|
||||
try std.testing.expect(!cell1.eql(cell3));
|
||||
}
|
||||
|
||||
test "Rect basic operations" {
|
||||
const r = Rect.init(10, 20, 100, 50);
|
||||
|
||||
|
|
@ -317,7 +604,7 @@ test "Buffer creation and access" {
|
|||
buf.setChar(5, 5, 'X', Style.default.fg(Color.red));
|
||||
|
||||
const cell = buf.get(5, 5).?;
|
||||
try std.testing.expectEqual(@as(u21, 'X'), cell.char);
|
||||
try std.testing.expectEqual(@as(u21, 'X'), cell.char());
|
||||
try std.testing.expectEqual(Color.red, cell.fg);
|
||||
}
|
||||
|
||||
|
|
@ -329,9 +616,56 @@ test "Buffer setString" {
|
|||
const written = buf.setString(0, 0, "Hello", Style{});
|
||||
try std.testing.expectEqual(@as(u16, 5), written);
|
||||
|
||||
try std.testing.expectEqual(@as(u21, 'H'), buf.get(0, 0).?.char);
|
||||
try std.testing.expectEqual(@as(u21, 'e'), buf.get(1, 0).?.char);
|
||||
try std.testing.expectEqual(@as(u21, 'l'), buf.get(2, 0).?.char);
|
||||
try std.testing.expectEqual(@as(u21, 'l'), buf.get(3, 0).?.char);
|
||||
try std.testing.expectEqual(@as(u21, 'o'), buf.get(4, 0).?.char);
|
||||
try std.testing.expectEqual(@as(u21, 'H'), buf.get(0, 0).?.char());
|
||||
try std.testing.expectEqual(@as(u21, 'e'), buf.get(1, 0).?.char());
|
||||
try std.testing.expectEqual(@as(u21, 'l'), buf.get(2, 0).?.char());
|
||||
try std.testing.expectEqual(@as(u21, 'l'), buf.get(3, 0).?.char());
|
||||
try std.testing.expectEqual(@as(u21, 'o'), buf.get(4, 0).?.char());
|
||||
}
|
||||
|
||||
test "Buffer diff empty buffers" {
|
||||
const allocator = std.testing.allocator;
|
||||
var buf1 = try Buffer.init(allocator, Rect.init(0, 0, 10, 10));
|
||||
defer buf1.deinit();
|
||||
var buf2 = try Buffer.init(allocator, Rect.init(0, 0, 10, 10));
|
||||
defer buf2.deinit();
|
||||
|
||||
var diff_iter = buf1.diff(&buf2);
|
||||
try std.testing.expectEqual(@as(?CellUpdate, null), diff_iter.next());
|
||||
}
|
||||
|
||||
test "Buffer diff with changes" {
|
||||
const allocator = std.testing.allocator;
|
||||
var buf1 = try Buffer.init(allocator, Rect.init(0, 0, 10, 10));
|
||||
defer buf1.deinit();
|
||||
var buf2 = try Buffer.init(allocator, Rect.init(0, 0, 10, 10));
|
||||
defer buf2.deinit();
|
||||
|
||||
// Modify buf1
|
||||
buf1.setChar(5, 5, 'X', Style.default);
|
||||
|
||||
var diff_iter = buf1.diff(&buf2);
|
||||
const update = diff_iter.next();
|
||||
try std.testing.expect(update != null);
|
||||
try std.testing.expectEqual(@as(u16, 5), update.?.x);
|
||||
try std.testing.expectEqual(@as(u16, 5), update.?.y);
|
||||
try std.testing.expectEqual(@as(u21, 'X'), update.?.cell.char());
|
||||
|
||||
// No more differences
|
||||
try std.testing.expectEqual(@as(?CellUpdate, null), diff_iter.next());
|
||||
}
|
||||
|
||||
test "Buffer resize" {
|
||||
const allocator = std.testing.allocator;
|
||||
var buf = try Buffer.init(allocator, Rect.init(0, 0, 10, 10));
|
||||
defer buf.deinit();
|
||||
|
||||
buf.setChar(5, 5, 'X', Style.default);
|
||||
|
||||
try buf.resize(Rect.init(0, 0, 20, 20));
|
||||
try std.testing.expectEqual(@as(u16, 20), buf.area.width);
|
||||
try std.testing.expectEqual(@as(u16, 20), buf.area.height);
|
||||
|
||||
// Original content should be preserved
|
||||
try std.testing.expectEqual(@as(u21, 'X'), buf.get(5, 5).?.char());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ pub const buffer = @import("buffer.zig");
|
|||
pub const Cell = buffer.Cell;
|
||||
pub const Buffer = buffer.Buffer;
|
||||
pub const Rect = buffer.Rect;
|
||||
pub const Symbol = buffer.Symbol;
|
||||
pub const Margin = buffer.Margin;
|
||||
pub const CellUpdate = buffer.CellUpdate;
|
||||
pub const DiffIterator = buffer.DiffIterator;
|
||||
|
||||
pub const text = @import("text.zig");
|
||||
pub const Span = text.Span;
|
||||
|
|
|
|||
|
|
@ -109,34 +109,17 @@ pub const Terminal = struct {
|
|||
///
|
||||
/// Compares current and previous buffers, only outputting differences.
|
||||
fn flush(self: *Terminal) !void {
|
||||
const rect = self.current_buffer.area;
|
||||
|
||||
var y: u16 = rect.y;
|
||||
while (y < rect.bottom()) : (y += 1) {
|
||||
var x: u16 = rect.x;
|
||||
while (x < rect.right()) : (x += 1) {
|
||||
const current = self.current_buffer.get(x, y) orelse continue;
|
||||
const previous = self.previous_buffer.get(x, y);
|
||||
|
||||
// Only update if changed
|
||||
const needs_update = if (previous) |prev|
|
||||
current.char != prev.char or
|
||||
!colorEqual(current.fg, prev.fg) or
|
||||
!colorEqual(current.bg, prev.bg) or
|
||||
!modifierEqual(current.modifiers, prev.modifiers)
|
||||
else
|
||||
true;
|
||||
|
||||
if (needs_update) {
|
||||
try self.backend.moveCursor(x, y);
|
||||
try self.backend.setStyle(current.fg, current.bg, current.modifiers);
|
||||
try self.backend.writeChar(current.char);
|
||||
// Use diff iterator for efficient rendering
|
||||
var diff_iter = self.current_buffer.diff(&self.previous_buffer);
|
||||
while (diff_iter.next()) |update| {
|
||||
try self.backend.moveCursor(update.x, update.y);
|
||||
try self.backend.setStyle(update.cell.fg, update.cell.bg, update.cell.modifiers);
|
||||
// Write the symbol directly (already UTF-8 encoded)
|
||||
try self.backend.writeSymbol(update.cell.symbol.slice());
|
||||
|
||||
// Update previous buffer
|
||||
if (self.previous_buffer.getPtr(x, y)) |prev| {
|
||||
prev.* = current;
|
||||
}
|
||||
}
|
||||
if (self.previous_buffer.getPtr(update.x, update.y)) |prev| {
|
||||
prev.* = update.cell;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue