- CLAUDE.md: actualizado a v1.1, seccion de optimizaciones - docs/ARCHITECTURE.md: detalle de optimizaciones implementadas - docs/API.md: nuevos tipos Symbol, DiffIterator, CellUpdate 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
13 KiB
zcatui - Arquitectura
Documentación técnica de la arquitectura de zcatui
Visión General
zcatui es una librería TUI (Terminal User Interface) para Zig, inspirada en ratatui de Rust. Utiliza un patrón de renderizado inmediato con buffers intermedios.
Diagrama de Arquitectura
┌─────────────────────────────────────────────────────────────────┐
│ Application │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Widget1 │ │ Widget2 │ │ Widget3 │ │ Widget4 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ └────────────┴─────┬──────┴────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Buffer │ Grid de Cells │
│ │ (current)│ Cada Cell: char + style │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Diff │ Compara con buffer anterior │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Terminal │ Solo envía cambios │
│ └────┬─────┘ │
│ │ │
└─────────────────────────┼────────────────────────────────────────┘
│
▼
┌──────────┐
│ stdout │ ANSI escape sequences
└──────────┘
Componentes Core
1. Cell (buffer.zig)
La unidad mínima de renderizado. Representa un único carácter en la terminal.
pub const Cell = struct {
symbol: Symbol, // UTF-8 grapheme (hasta 4 bytes)
style: Style, // Foreground, background, modifiers
pub fn reset(self: *Cell) void { ... }
pub fn setChar(self: *Cell, ch: u21) void { ... }
pub fn setSymbol(self: *Cell, symbol: []const u8) void { ... }
};
2. Buffer (buffer.zig)
Grid bidimensional de Cells. Maneja el estado de renderizado.
pub const Buffer = struct {
area: Rect,
cells: []Cell,
allocator: Allocator,
pub fn init(allocator: Allocator, area: Rect) !Buffer { ... }
pub fn getCell(self: *Buffer, x: u16, y: u16) ?*Cell { ... }
pub fn setString(self: *Buffer, x: u16, y: u16, text: []const u8, style: Style) u16 { ... }
pub fn setSpan(self: *Buffer, x: u16, y: u16, span: Span, width: u16) u16 { ... }
};
3. Rect (buffer.zig)
Representa un área rectangular en la terminal.
pub const Rect = struct {
x: u16,
y: u16,
width: u16,
height: u16,
pub fn init(x: u16, y: u16, width: u16, height: u16) Rect { ... }
pub fn inner(self: Rect, margin: u16) Rect { ... }
pub fn intersection(self: Rect, other: Rect) Rect { ... }
pub fn isEmpty(self: Rect) bool { ... }
pub fn left(self: Rect) u16 { ... }
pub fn right(self: Rect) u16 { ... }
pub fn top(self: Rect) u16 { ... }
pub fn bottom(self: Rect) u16 { ... }
};
4. Style (style.zig)
Combinación de colores y modificadores de texto.
pub const Style = struct {
foreground: ?Color = null,
background: ?Color = null,
add_modifiers: Modifier = .{},
sub_modifiers: Modifier = .{},
pub const default: Style = .{};
pub fn fg(self: Style, color: Color) Style { ... }
pub fn bg(self: Style, color: Color) Style { ... }
pub fn bold(self: Style) Style { ... }
pub fn italic(self: Style) Style { ... }
pub fn patch(self: Style, other: Style) Style { ... }
};
5. Color (style.zig)
Soporte para colores de 16, 256 y RGB.
pub const Color = union(enum) {
reset,
black, red, green, yellow, blue, magenta, cyan, white,
light_black, light_red, light_green, light_yellow,
light_blue, light_magenta, light_cyan, light_white,
indexed: u8, // 256 colores
rgb: struct { r: u8, g: u8, b: u8 },
};
6. Text Types (text.zig)
Tipos para manejar texto estilizado.
// Span: texto con estilo único
pub const Span = struct {
content: []const u8,
style: Style,
};
// Line: múltiples spans en una línea
pub const Line = struct {
spans: []const Span,
alignment: Alignment,
};
// Text: múltiples líneas
pub const Text = struct {
lines: []const Line,
alignment: Alignment,
};
7. Layout (layout.zig)
Sistema de distribución de espacio.
pub const Layout = struct {
direction: Direction,
constraints: []const Constraint,
pub fn horizontal(constraints: []const Constraint) Layout { ... }
pub fn vertical(constraints: []const Constraint) Layout { ... }
pub fn split(self: Layout, area: Rect, result: []Rect) void { ... }
};
pub const Constraint = union(enum) {
length: u16, // Exactamente N celdas
min: u16, // Mínimo N celdas
max: u16, // Máximo N celdas
percentage: u16, // N% del espacio disponible
ratio: struct { num: u32, den: u32 },
fill: u16, // Llenar espacio restante
};
Sistema de Widgets
Patrón de Widget
Todos los widgets implementan el método render:
pub fn render(self: WidgetType, area: Rect, buf: *Buffer) void {
// 1. Validar área
if (area.isEmpty()) return;
// 2. Renderizar block/wrapper si existe
const inner = if (self.block) |b| blk: {
b.render(area, buf);
break :blk b.inner(area);
} else area;
// 3. Renderizar contenido
// ...
}
StatefulWidget Pattern
Para widgets con estado mutable:
pub fn renderStateful(self: WidgetType, area: Rect, buf: *Buffer, state: *State) void {
// Similar a render, pero puede modificar state
// Útil para: scroll position, selection, etc.
}
Fluent Builder Pattern
Los widgets usan setters encadenables:
const list = List.init(items)
.setBlock(block)
.setHighlightStyle(style)
.setHighlightSymbol("> ")
.setDirection(.top_to_bottom);
Sistema de Símbolos
Estructura
symbols/
├── line.zig # ─ │ ┌ ┐ └ ┘ ├ ┤ etc.
├── border.zig # Sets de bordes: plain, rounded, double, thick
├── block.zig # █ ▀ ▄ ▌ ▐ etc.
├── bar.zig # ▏▎▍▌▋▊▉█ (barras horizontales)
├── braille.zig # Patrones braille (256 combinaciones)
├── half_block.zig # ▀ ▄ para resolución 1x2
├── scrollbar.zig # Símbolos para scrollbars
└── marker.zig # Marcadores para charts: dot, block, braille, etc.
Braille Grid (2x4 dots per cell)
┌───┬───┐
│ 0 │ 3 │ Bit layout:
├───┼───┤
│ 1 │ 4 │ byte = Σ (2^bit) para cada dot activo
├───┼───┤
│ 2 │ 5 │ Base: U+2800 (braille blank)
├───┼───┤ Resultado: chr(0x2800 + byte)
│ 6 │ 7 │
└───┴───┘
Backend ANSI
Escape Sequences Soportadas
| Función | Secuencia |
|---|---|
| Clear screen | \x1b[2J |
| Move cursor | \x1b[{row};{col}H |
| Hide cursor | \x1b[?25l |
| Show cursor | \x1b[?25h |
| Reset style | \x1b[0m |
| Bold | \x1b[1m |
| Dim | \x1b[2m |
| Italic | \x1b[3m |
| Underline | \x1b[4m |
| FG color (16) | \x1b[{30-37}m |
| BG color (16) | \x1b[{40-47}m |
| FG color (256) | \x1b[38;5;{n}m |
| BG color (256) | \x1b[48;5;{n}m |
| FG color (RGB) | \x1b[38;2;{r};{g};{b}m |
| BG color (RGB) | \x1b[48;2;{r};{g};{b}m |
| Alternate screen | \x1b[?1049h |
| Main screen | \x1b[?1049l |
Algoritmos Clave
Bresenham's Line Algorithm (Canvas)
Usado para dibujar líneas en el canvas:
fn drawLine(x0: i32, y0: i32, x1: i32, y1: i32, color: Color) void {
var dx = @abs(x1 - x0);
var dy = @abs(y1 - y0);
var sx: i32 = if (x0 < x1) 1 else -1;
var sy: i32 = if (y0 < y1) 1 else -1;
var err = dx - dy;
while (true) {
self.set(x0, y0, color);
if (x0 == x1 and y0 == y1) break;
const e2 = 2 * err;
if (e2 > -dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
}
Zeller's Congruence (Calendar)
Para calcular el día de la semana:
pub fn dayOfWeek(year: i16, month: u4, day: u5) u3 {
var y = year;
var m = month;
if (m < 3) { m += 12; y -= 1; }
const q = day;
const k = @mod(y, 100);
const j = @divFloor(y, 100);
var h = q + @divFloor(13 * (m + 1), 5) + k + @divFloor(k, 4) + @divFloor(j, 4) - 2 * j;
return @intCast(@mod(@mod(h, 7) + 6, 7)); // 0=Sunday
}
Consideraciones de Memoria
Stack Allocation
La mayoría de estructuras usan stack allocation con tamaños fijos:
CalendarEventStore: máximo 32 eventosSymbol: máximo 4 bytes UTF-8- Arrays de constraints: tamaño fijo en compilación
Heap Allocation
Solo se usa heap para:
Buffer.cells: array de celdas (puede ser grande)- Strings dinámicos pasados por el usuario
Sin GC
Zig no tiene garbage collector. Los widgets no poseen memoria, solo referencias. El usuario es responsable de la lifetime de los datos.
Testing
Estrategia
- Unit tests en cada módulo
- Render tests comparando buffers
- Property-based donde aplica (ej: Rect.intersection es conmutativa)
Ejecutar tests
zig build test # Todos los tests
zig build test --summary all # Con resumen detallado
Extensibilidad
Crear un Widget Personalizado
const MyWidget = struct {
data: []const u8,
style: Style,
block: ?Block = null,
pub fn init(data: []const u8) MyWidget {
return .{ .data = data, .style = Style.default };
}
pub fn setStyle(self: MyWidget, s: Style) MyWidget {
var w = self;
w.style = s;
return w;
}
pub fn setBlock(self: MyWidget, b: Block) MyWidget {
var w = self;
w.block = b;
return w;
}
pub fn render(self: MyWidget, area: Rect, buf: *Buffer) void {
if (area.isEmpty()) return;
const inner = if (self.block) |b| blk: {
b.render(area, buf);
break :blk b.inner(area);
} else area;
_ = buf.setString(inner.left(), inner.top(), self.data, self.style);
}
};
Performance
Optimizaciones Implementadas (v1.1)
-
Symbol compacto: Tipo que almacena UTF-8 directamente (4 bytes max)
- Evita conversión codepoint→UTF8 en cada render
fromCodepoint()yfromSlice()para crear symbolsslice()retorna bytes listos para output
-
Buffer diff: Sistema de renderizado diferencial
DiffIteratorcompara buffers celda a celda- Solo retorna celdas que cambiaron entre frames
- Reduce dramáticamente I/O a terminal
-
Cell.eql(): Comparación eficiente de celdas
- Compara symbol, fg, bg, modifiers
- Base del sistema de diff
-
writeSymbol(): Output UTF-8 directo
- Escribe bytes sin conversión
- Más eficiente que
writeChar()con encoding
-
Pre-computed symbols: Braille patterns pre-calculados (256 patrones)
-
Saturating arithmetic: Uso de
-|y+|para evitar overflow checks
Áreas de Mejora Futuras (v1.2+)
- Buffer pooling para reutilización de memoria
- Lazy widget evaluation
- SIMD para operaciones de buffer masivas
- Dirty region tracking (solo re-renderizar áreas modificadas)