# Plan de Trabajo zcatui v2.2 **Fecha:** 2025-12-08 **Objetivo:** Implementar todas las mejoras identificadas **Estimación:** 13 fases de trabajo --- ## Resumen de Tareas | Fase | Descripción | Archivos Nuevos | Complejidad | |------|-------------|-----------------|-------------| | 1 | Logo Widget | 1 widget | Baja | | 2 | Examples v2.1 | 7 examples | Media | | 3 | Mouse Drag & Drop | 2-3 módulos | Alta | | 4 | Keyboard Shortcuts Configurables | 1 módulo | Media | | 5 | Async Event Loop (io_uring/epoll) | 2-3 módulos | Muy Alta | | 6 | Widget Composition Ergonómico | 1-2 módulos | Media | | 7 | Layout Ratio Constraints | 1 módulo (edit) | Baja | | 8 | Terminal Resize Handler | 1 módulo (edit) | Media | | 9 | Sixel Images | 1 módulo | Alta | | 10 | Errores tipo Elm | 1 módulo | Media | | 11 | Debug Mode | 1-2 módulos | Media | | 12 | Performance Profiling | 1-2 módulos | Alta | | 13 | Package Manager (build.zig.zon) | 2 archivos | Baja | --- ## Fase 1: Logo Widget ### Objetivo Widget para renderizar ASCII art / logos con soporte para colores y animaciones. ### Archivo - `src/widgets/logo.zig` ### Funcionalidades ```zig const Logo = @import("widgets/logo.zig").Logo; // ASCII art estático const logo = Logo.init() .setText( \\ _______ _______ _______ _______ \\ |___ | _ |_ _| | | \\ ___| | | | | | | | \\ |_______|___|___| |___| |_______| ) .setStyle(Style{}.fg(Color.cyan).bold()) .setAlignment(.center); // Con gradiente de colores const logo_gradient = Logo.init() .setText(ascii_art) .setGradient(&[_]Color{ .red, .yellow, .green, .cyan, .blue }); // Animación de aparición const logo_animated = Logo.init() .setText(ascii_art) .setAnimation(.typewriter) // o .fade_in, .slide_down .setAnimationSpeed(50); // ms por caracter ``` ### Tipos de animación - `.none` - Estático - `.typewriter` - Aparece caracter por caracter - `.fade_in` - Fade usando caracteres ░▒▓█ - `.slide_down` - Línea por línea desde arriba - `.slide_up` - Línea por línea desde abajo - `.rainbow` - Colores rotativos continuos - `.pulse` - Parpadeo suave ### Tests - Renderizado básico - Alineación (left, center, right) - Gradientes - Animaciones --- ## Fase 2: Examples para Widgets v2.1 ### Objetivo Crear 7 demos ejecutables para los widgets nuevos. ### Archivos 1. `examples/spinner_demo.zig` 2. `examples/help_demo.zig` 3. `examples/viewport_demo.zig` 4. `examples/progress_demo.zig` 5. `examples/markdown_demo.zig` 6. `examples/dirtree_demo.zig` 7. `examples/syntax_demo.zig` ### Detalle de cada demo #### 2.1 spinner_demo.zig ``` ┌─ Spinner Styles ─────────────────────────┐ │ │ │ Dots: ⠋ Loading... │ │ Line: | Processing... │ │ Arc: ◜ Compiling... │ │ Pulse: ● Connecting... │ │ Bounce: ⠁ Syncing... │ │ Clock: ◴ Waiting... │ │ Moon: ◐ Downloading... │ │ Box: ▖ Building... │ │ Circle: ◴ Testing... │ │ Custom: ⣾ Custom spinner... │ │ │ │ Press 1-0 to select style, q to quit │ └──────────────────────────────────────────┘ ``` #### 2.2 help_demo.zig ``` ┌─ Keyboard Shortcuts ─────────────────────┐ │ │ │ Navigation │ │ ────────── │ │ ↑/k Move up │ │ ↓/j Move down │ │ ←/h Move left │ │ →/l Move right │ │ Home Go to start │ │ End Go to end │ │ │ │ Actions │ │ ─────── │ │ Enter Select item │ │ Space Toggle selection │ │ / Search │ │ ? Show this help │ │ │ │ Press any key to close │ └──────────────────────────────────────────┘ ``` #### 2.3 viewport_demo.zig ``` ┌─ Viewport Demo ──────────────────────────┐ │ Line 1: Lorem ipsum dolor sit amet... │▲ │ Line 2: Consectetur adipiscing elit... │█ │ Line 3: Sed do eiusmod tempor... │█ │ Line 4: Incididunt ut labore et... │█ │ Line 5: Dolore magna aliqua... │░ │ Line 6: Ut enim ad minim veniam... │░ │ Line 7: Quis nostrud exercitation... │░ │ Line 8: Ullamco laboris nisi ut... │░ │ │░ │ [Scroll: 1-100 of 500 lines] │▼ └──────────────────────────────────────────┘ ↑/↓: Scroll PgUp/PgDn: Page Home/End ``` #### 2.4 progress_demo.zig ``` ┌─ Multi-Step Progress ────────────────────┐ │ │ │ Installation Progress │ │ │ │ ✓ Download ████████████████ 100% │ │ ✓ Extract ████████████████ 100% │ │ → Compile ████████░░░░░░░░ 52% │ │ ○ Test ░░░░░░░░░░░░░░░░ 0% │ │ ○ Install ░░░░░░░░░░░░░░░░ 0% │ │ │ │ Overall: ████████████░░░░░░░░ 63% │ │ │ │ Compiling: src/widgets/chart.zig │ │ ETA: 2m 34s │ └──────────────────────────────────────────┘ ``` #### 2.5 markdown_demo.zig ``` ┌─ Markdown Viewer ────────────────────────┐ │ │ │ # Welcome to zcatui │ │ │ │ A **TUI library** for Zig with: │ │ │ │ - 34 widgets │ │ - Event handling │ │ - Animations │ │ │ │ ## Code Example │ │ ┌────────────────────────────────────┐ │ │ │ const zcatui = @import("zcatui"); │ │ │ │ const term = zcatui.Terminal; │ │ │ └────────────────────────────────────┘ │ │ │ │ > This is a blockquote │ │ │ └──────────────────────────────────────────┘ ``` #### 2.6 dirtree_demo.zig ``` ┌─ File Browser ───────────────────────────┐ │ 📁 /home/user/projects/zcatui │ ├──────────────────────────────────────────┤ │ 📁 src/ │ │ 📁 widgets/ │ │ 📄 block.zig 2.1K │ │ 📄 list.zig 4.3K │ │ > 📄 root.zig 1.2K │ │ 📄 buffer.zig 3.5K │ │ 📁 examples/ │ │ 📁 docs/ │ │ 📄 build.zig 890B │ │ 📄 README.md 2.4K │ ├──────────────────────────────────────────┤ │ 12 files, 4 dirs, 45.2 KB total │ └──────────────────────────────────────────┘ Enter: Open Backspace: Up Tab: Preview ``` #### 2.7 syntax_demo.zig ``` ┌─ Syntax Highlighter ─────────────────────┐ │ Language: [Zig ▼] │ ├──────────────────────────────────────────┤ │ 1 │ const std = @import("std"); │ │ 2 │ │ │ 3 │ pub fn main() !void { │ │ 4 │ const stdout = std.io.getStd(); │ │ 5 │ try stdout.print("Hello\n", .{});│ │ 6 │ } │ │ 7 │ │ │ 8 │ // This is a comment │ │ 9 │ const x: u32 = 42; │ │ 10 │ const str = "string literal"; │ ├──────────────────────────────────────────┤ │ Zig | 10 lines | UTF-8 │ └──────────────────────────────────────────┘ 1-5: Language q: Quit ``` ### Actualizar build.zig Añadir los 7 nuevos ejemplos al build system. --- ## Fase 3: Mouse Drag & Drop + Resize Panels ### Objetivo Sistema completo de drag & drop para redimensionar paneles con el ratón. ### Archivos - `src/drag.zig` - Sistema de drag & drop - `src/resizable.zig` - Wrapper para hacer widgets redimensionables - Modificar `src/widgets/panel.zig` - Integrar resize ### Arquitectura ```zig // drag.zig pub const DragState = struct { active: bool = false, start_pos: Position, current_pos: Position, target: DragTarget, pub const DragTarget = union(enum) { panel_border: struct { panel_id: u32, edge: Edge, // .left, .right, .top, .bottom }, splitter: struct { splitter_id: u32, orientation: Orientation, }, window: u32, custom: *anyopaque, }; }; pub const DragManager = struct { state: ?DragState = null, on_drag_start: ?*const fn(DragState) void = null, on_drag_move: ?*const fn(DragState, Position) void = null, on_drag_end: ?*const fn(DragState, Position) void = null, /// Procesar evento de ratón pub fn handleMouseEvent(self: *DragManager, event: MouseEvent) DragResult { // Detectar inicio de drag (click en borde) // Actualizar posición durante drag // Finalizar en release } /// Obtener cursor apropiado para la posición pub fn getCursor(self: *const DragManager, pos: Position) Cursor { // Retorna .resize_ew, .resize_ns, etc. } }; ``` ```zig // resizable.zig pub fn Resizable(comptime Widget: type) type { return struct { inner: Widget, min_size: Size = .{ .width = 10, .height = 3 }, max_size: ?Size = null, resize_edges: EdgeSet = EdgeSet.all(), pub fn render(self: *@This(), area: Rect, buf: *Buffer) void { // Renderizar widget interno self.inner.render(area, buf); // Renderizar handles de resize en bordes if (self.resize_edges.contains(.right)) { self.renderResizeHandle(buf, area, .right); } // ... } pub fn handleDrag(self: *@This(), drag: DragState, delta: Position) void { // Ajustar tamaño según el drag } }; } // Uso: var resizable_panel = Resizable(Panel).init(my_panel) .setMinSize(.{ .width = 20, .height = 5 }) .setResizeEdges(.{ .right = true, .bottom = true }); ``` ### Splitter Widget ```zig // Para dividir áreas con drag pub const Splitter = struct { orientation: Orientation, position: f32, // 0.0 - 1.0 (porcentaje) min_first: u16 = 10, min_second: u16 = 10, pub fn render(self: Splitter, area: Rect, buf: *Buffer) void { // Renderizar línea divisoria // Con handle visual para drag } pub fn getAreas(self: Splitter, area: Rect) struct { first: Rect, second: Rect } { // Calcular las dos áreas resultantes } }; ``` ### Cursores de terminal ```zig pub const Cursor = enum { default, pointer, text, resize_ew, // ↔ horizontal resize_ns, // ↕ vertical resize_nwse, // ↘ diagonal resize_nesw, // ↙ diagonal move, // ✥ mover not_allowed, // 🚫 /// Secuencia ANSI para cambiar cursor (si el terminal lo soporta) pub fn toAnsi(self: Cursor) []const u8 { return switch (self) { .default => "\x1b[0 q", .pointer => "\x1b[1 q", // etc. }; } }; ``` ### Tests - Drag start/move/end lifecycle - Resize constraints (min/max) - Splitter position calculation - Edge detection --- ## Fase 4: Keyboard Shortcuts Configurables ### Objetivo Sistema de shortcuts reconfigurables para evitar conflictos con la aplicación host. ### Archivo - `src/shortcuts.zig` ### Diseño ```zig pub const Shortcut = struct { key: KeyCode, modifiers: Modifiers = .{}, pub const Modifiers = packed struct { ctrl: bool = false, alt: bool = false, shift: bool = false, super: bool = false, }; pub fn matches(self: Shortcut, event: KeyEvent) bool { return self.key == event.code and self.modifiers.ctrl == event.ctrl and self.modifiers.alt == event.alt and self.modifiers.shift == event.shift; } /// Parse from string: "Ctrl+Shift+S", "Alt+F4", "Enter" pub fn parse(str: []const u8) !Shortcut { ... } /// Format to string pub fn format(self: Shortcut, buf: []u8) []const u8 { ... } }; pub const Action = enum { // Navigation move_up, move_down, move_left, move_right, page_up, page_down, go_start, go_end, // Selection select, toggle, select_all, select_none, // Edit delete, copy, paste, cut, undo, redo, // UI focus_next, focus_prev, close, cancel, confirm, help, search, // Resize resize_grow_h, resize_shrink_h, resize_grow_v, resize_shrink_v, // Custom custom_1, custom_2, // ... hasta custom_20 }; pub const ShortcutMap = struct { bindings: std.EnumMap(Action, Shortcut), /// Preset por defecto (vim-like) pub const vim_preset = ShortcutMap{ .bindings = .{ .move_up = .{ .key = .char, .char = 'k' }, .move_down = .{ .key = .char, .char = 'j' }, .move_left = .{ .key = .char, .char = 'h' }, .move_right = .{ .key = .char, .char = 'l' }, // ... }, }; /// Preset arrows (tradicional) pub const arrows_preset = ShortcutMap{ .bindings = .{ .move_up = .{ .key = .up }, .move_down = .{ .key = .down }, // ... }, }; /// Preset emacs pub const emacs_preset = ShortcutMap{ .bindings = .{ .move_up = .{ .key = .char, .char = 'p', .modifiers = .{ .ctrl = true } }, .move_down = .{ .key = .char, .char = 'n', .modifiers = .{ .ctrl = true } }, // ... }, }; /// Rebind una acción pub fn rebind(self: *ShortcutMap, action: Action, shortcut: Shortcut) void { self.bindings.put(action, shortcut); } /// Obtener acción para un evento pub fn getAction(self: *const ShortcutMap, event: KeyEvent) ?Action { var iter = self.bindings.iterator(); while (iter.next()) |entry| { if (entry.value.matches(event)) { return entry.key; } } return null; } /// Cargar desde archivo de configuración pub fn loadFromFile(allocator: Allocator, path: []const u8) !ShortcutMap { ... } /// Guardar a archivo pub fn saveToFile(self: *const ShortcutMap, path: []const u8) !void { ... } /// Detectar conflictos pub fn findConflicts(self: *const ShortcutMap) []Conflict { // Retorna shortcuts que mapean al mismo key } }; /// Contexto de shortcuts (para diferentes modos) pub const ShortcutContext = struct { name: []const u8, map: ShortcutMap, parent: ?*const ShortcutContext = null, // Herencia pub fn getAction(self: *const ShortcutContext, event: KeyEvent) ?Action { // Buscar en este contexto, luego en parent if (self.map.getAction(event)) |action| { return action; } if (self.parent) |p| { return p.getAction(event); } return null; } }; ``` ### Formato de archivo de configuración ``` # ~/.config/zcatui/shortcuts.conf # Navigation move_up = k, Up move_down = j, Down move_left = h, Left move_right = l, Right # With modifiers page_up = Ctrl+u, PageUp page_down = Ctrl+d, PageDown go_start = g g, Home go_end = G, End # Actions select = Enter, Space delete = d d, Delete copy = y y, Ctrl+c paste = p, Ctrl+v # Resize (Ctrl+Arrow) resize_grow_h = Ctrl+Right resize_shrink_h = Ctrl+Left resize_grow_v = Ctrl+Down resize_shrink_v = Ctrl+Up ``` ### Tests - Parse shortcuts from strings - Match events - Conflict detection - File load/save - Context inheritance --- ## Fase 5: Async Event Loop (io_uring/epoll) ### Objetivo Event loop asíncrono para alto rendimiento, con fallback a epoll para compatibilidad. ### Archivos - `src/async/loop.zig` - Event loop principal - `src/async/io_uring.zig` - Backend io_uring (Linux 5.1+) - `src/async/epoll.zig` - Backend epoll (fallback) - `src/async/poll.zig` - Backend poll (máxima compatibilidad) ### Arquitectura ```zig // loop.zig pub const EventLoop = struct { backend: Backend, callbacks: CallbackRegistry, timers: TimerQueue, running: bool = false, pub const Backend = union(enum) { io_uring: IoUring, epoll: Epoll, poll: Poll, }; /// Detectar mejor backend disponible pub fn init(allocator: Allocator) !EventLoop { // Intentar io_uring primero if (IoUring.isSupported()) { return .{ .backend = .{ .io_uring = try IoUring.init() } }; } // Fallback a epoll if (Epoll.isSupported()) { return .{ .backend = .{ .epoll = try Epoll.init() } }; } // Último recurso: poll return .{ .backend = .{ .poll = try Poll.init() } }; } /// Registrar file descriptor para lectura pub fn addReader(self: *EventLoop, fd: i32, callback: ReadCallback) !void { switch (self.backend) { .io_uring => |*io| try io.addReader(fd, callback), .epoll => |*ep| try ep.addReader(fd, callback), .poll => |*p| try p.addReader(fd, callback), } } /// Registrar timer pub fn setTimeout(self: *EventLoop, ms: u64, callback: TimerCallback) TimerId { return self.timers.add(ms, callback, .once); } pub fn setInterval(self: *EventLoop, ms: u64, callback: TimerCallback) TimerId { return self.timers.add(ms, callback, .repeating); } /// Ejecutar una iteración pub fn poll(self: *EventLoop, timeout_ms: ?u32) !void { // 1. Procesar timers vencidos self.timers.processExpired(); // 2. Poll I/O events const events = switch (self.backend) { .io_uring => |*io| try io.poll(timeout_ms), .epoll => |*ep| try ep.poll(timeout_ms), .poll => |*p| try p.poll(timeout_ms), }; // 3. Dispatch callbacks for (events) |event| { if (self.callbacks.get(event.fd)) |cb| { cb(event); } } } /// Run loop principal pub fn run(self: *EventLoop) !void { self.running = true; while (self.running) { try self.poll(null); } } /// Detener el loop pub fn stop(self: *EventLoop) void { self.running = false; } }; ``` ```zig // io_uring.zig pub const IoUring = struct { ring: linux.io_uring, pub fn isSupported() bool { // Check kernel version >= 5.1 var uname: linux.utsname = undefined; _ = linux.uname(&uname); // Parse version... return kernel_version >= .{ .major = 5, .minor = 1 }; } pub fn init() !IoUring { var ring: linux.io_uring = undefined; const ret = linux.io_uring_setup(256, &ring); if (ret < 0) return error.IoUringSetupFailed; return .{ .ring = ring }; } pub fn addReader(self: *IoUring, fd: i32, callback: ReadCallback) !void { const sqe = linux.io_uring_get_sqe(&self.ring); linux.io_uring_prep_read(sqe, fd, ...); // ... } pub fn poll(self: *IoUring, timeout: ?u32) ![]Event { // Submit and wait for completions _ = linux.io_uring_submit(&self.ring); // ... } }; ``` ```zig // epoll.zig pub const Epoll = struct { epfd: i32, events: [64]linux.epoll_event, pub fn isSupported() bool { return @import("builtin").os.tag == .linux; } pub fn init() !Epoll { const epfd = linux.epoll_create1(0); if (epfd < 0) return error.EpollCreateFailed; return .{ .epfd = epfd, .events = undefined }; } pub fn addReader(self: *Epoll, fd: i32, callback: ReadCallback) !void { var ev = linux.epoll_event{ .events = linux.EPOLLIN, .data = .{ .fd = fd }, }; _ = linux.epoll_ctl(self.epfd, linux.EPOLL_CTL_ADD, fd, &ev); } pub fn poll(self: *Epoll, timeout: ?u32) ![]Event { const n = linux.epoll_wait(self.epfd, &self.events, 64, timeout orelse -1); // Convert to Events... } }; ``` ### Integración con Terminal ```zig // En terminal.zig pub const AsyncTerminal = struct { terminal: Terminal, loop: EventLoop, event_callback: ?*const fn(Event) void = null, pub fn init(allocator: Allocator) !AsyncTerminal { var term = try Terminal.init(allocator); var loop = try EventLoop.init(allocator); // Registrar stdin para eventos try loop.addReader(std.os.STDIN_FILENO, handleStdinReady); return .{ .terminal = term, .loop = loop }; } /// Callback cuando stdin tiene datos fn handleStdinReady(self: *AsyncTerminal) void { if (self.terminal.pollEvent()) |event| { if (self.event_callback) |cb| { cb(event); } } } /// Render con rate limiting pub fn scheduleRender(self: *AsyncTerminal, delay_ms: u32) void { self.loop.setTimeout(delay_ms, renderFrame); } }; ``` ### Tests - io_uring availability detection - epoll fallback - Timer accuracy - Event dispatch - Terminal integration --- ## Fase 6: Widget Composition Ergonómico ### Objetivo API más ergonómica para componer widgets complejos. ### Archivo - `src/compose.zig` ### Diseño actual vs propuesto ```zig // ACTUAL (verboso) fn render(area: Rect, buf: *Buffer) void { const layout = Layout.init(.vertical, &[_]Constraint{ .{ .length = 3 }, .{ .min = 1 }, .{ .length = 1 }, }); const chunks = layout.split(area); const header = Block.init().title("Header").borders(.all); header.render(chunks[0], buf); const content = Paragraph.init().text("Content"); content.render(chunks[1], buf); const footer = Paragraph.init().text("Footer"); footer.render(chunks[2], buf); } // PROPUESTO (fluent/declarativo) fn render(area: Rect, buf: *Buffer) void { compose(buf, area) .column(.{ .header(3, Block.init().title("Header").borders(.all)), .flex(1, Paragraph.init().text("Content")), .footer(1, Paragraph.init().text("Footer")), }); } // O estilo builder: fn render(area: Rect, buf: *Buffer) void { VStack.init() .child(Block.init().title("Header"), .{ .height = 3 }) .child(Paragraph.init().text("Content"), .{ .flex = 1 }) .child(Paragraph.init().text("Footer"), .{ .height = 1 }) .render(area, buf); } ``` ### Implementación ```zig // compose.zig /// Stack vertical de widgets pub fn VStack(comptime N: usize) type { return struct { children: [N]Child, spacing: u16 = 0, alignment: Alignment = .start, pub const Child = struct { widget: Widget, constraint: Constraint, }; pub fn init() @This() { return .{ .children = undefined }; } pub fn child(self: *@This(), widget: anytype, constraint: Constraint) *@This() { // Add child... return self; } pub fn spacing(self: *@This(), s: u16) *@This() { self.spacing = s; return self; } pub fn render(self: @This(), area: Rect, buf: *Buffer) void { // Calculate layout and render children } }; } /// Stack horizontal pub fn HStack(comptime N: usize) type { ... } /// Z-Stack (overlay) pub fn ZStack(comptime N: usize) type { ... } /// Conditional rendering pub fn when(condition: bool, widget: anytype) ?@TypeOf(widget) { return if (condition) widget else null; } /// Composición declarativa con comptime pub fn compose(buf: *Buffer, area: Rect) Composer { return .{ .buf = buf, .area = area }; } pub const Composer = struct { buf: *Buffer, area: Rect, pub fn column(self: Composer, children: anytype) void { const T = @TypeOf(children); const fields = @typeInfo(T).@"struct".fields; // Build constraints array var constraints: [fields.len]Constraint = undefined; inline for (fields, 0..) |field, i| { constraints[i] = @field(children, field.name).constraint; } // Split area const layout = Layout.init(.vertical, &constraints); const chunks = layout.split(self.area); // Render each child inline for (fields, 0..) |field, i| { @field(children, field.name).widget.render(chunks[i], self.buf); } } pub fn row(self: Composer, children: anytype) void { ... } pub fn overlay(self: Composer, children: anytype) void { ... } }; /// Helper para crear child con constraint pub fn sized(widget: anytype, height: u16) Child(@TypeOf(widget)) { return .{ .widget = widget, .constraint = .{ .length = height } }; } pub fn flex(widget: anytype, factor: u16) Child(@TypeOf(widget)) { return .{ .widget = widget, .constraint = .{ .ratio = .{ factor, 1 } } }; } pub fn fill(widget: anytype) Child(@TypeOf(widget)) { return .{ .widget = widget, .constraint = .{ .min = 0 } }; } ``` ### Ejemplo completo ```zig const App = struct { state: AppState, pub fn render(self: *App, area: Rect, buf: *Buffer) void { compose(buf, area).column(.{ // Header fijo sized( Block.init() .title("My App") .borders(.all) .style(self.theme.header), 3 ), // Content area flexible flex( compose(buf, area).row(.{ // Sidebar sized(self.renderSidebar(), 20), // Main content fill(self.renderContent()), }), 1 ), // Footer condicional when(self.state.show_footer, sized(StatusBar.init().text(self.status), 1) ), }); } }; ``` ### Tests - VStack/HStack layout - Nested composition - Conditional rendering - Constraint propagation --- ## Fase 7: Layout Ratio Constraints ### Objetivo Añadir constraint por ratio (fracción) además de length/min/max/percentage. ### Archivo - Modificar `src/layout.zig` ### Implementación ```zig // En Constraint pub const Constraint = union(enum) { /// Fixed size in cells length: u16, /// Minimum size min: u16, /// Maximum size max: u16, /// Percentage of available space (0-100) percentage: u16, /// Ratio of remaining space (numerator, denominator) ratio: struct { num: u16, den: u16 }, /// Helper constructors pub fn fixed(size: u16) Constraint { return .{ .length = size }; } pub fn percent(p: u16) Constraint { return .{ .percentage = @min(p, 100) }; } pub fn ratio(num: u16, den: u16) Constraint { return .{ .ratio = .{ .num = num, .den = den } }; } // Ejemplo: 1/3 del espacio pub fn third() Constraint { return ratio(1, 3); } // Ejemplo: 2/3 del espacio pub fn twoThirds() Constraint { return ratio(2, 3); } }; ``` ### Algoritmo de split actualizado ```zig pub fn split(self: Layout, area: Rect) []Rect { const total = if (self.direction == .horizontal) area.width else area.height; var remaining = total; var results: [MAX_CHUNKS]Rect = undefined; // Fase 1: Calcular tamaños fijos for (self.constraints) |c| { switch (c) { .length => |len| remaining -= @min(len, remaining), else => {}, } } // Fase 2: Calcular ratios var total_ratio_parts: u32 = 0; for (self.constraints) |c| { switch (c) { .ratio => |r| total_ratio_parts += r.num, else => {}, } } // Fase 3: Asignar tamaños var pos: u16 = if (self.direction == .horizontal) area.x else area.y; for (self.constraints, 0..) |c, i| { const size: u16 = switch (c) { .length => |len| len, .min => |min| @max(min, remaining / (self.constraints.len - i)), .max => |max| @min(max, remaining / (self.constraints.len - i)), .percentage => |p| @as(u16, @intCast((@as(u32, total) * p) / 100)), .ratio => |r| blk: { const ratio_space = remaining; // Espacio para ratios break :blk @as(u16, @intCast((@as(u32, ratio_space) * r.num) / total_ratio_parts)); }, }; results[i] = if (self.direction == .horizontal) Rect{ .x = pos, .y = area.y, .width = size, .height = area.height } else Rect{ .x = area.x, .y = pos, .width = area.width, .height = size }; pos += size + self.spacing; } return results[0..self.constraints.len]; } ``` ### Ejemplos de uso ```zig // Dividir en tercios const layout = Layout.init(.horizontal, &[_]Constraint{ .ratio(1, 3), // 1/3 .ratio(1, 3), // 1/3 .ratio(1, 3), // 1/3 }); // Golden ratio (aproximado) const golden = Layout.init(.horizontal, &[_]Constraint{ .ratio(382, 1000), // ~38.2% .ratio(618, 1000), // ~61.8% }); // Sidebar + content const sidebar_layout = Layout.init(.horizontal, &[_]Constraint{ .length(20), // Sidebar fijo .ratio(1, 1), // Content ocupa el resto }); // Three column con proporciones const three_col = Layout.init(.horizontal, &[_]Constraint{ .ratio(1, 4), // 25% .ratio(2, 4), // 50% .ratio(1, 4), // 25% }); ``` ### Tests - Ratio simple (1/2, 1/3) - Ratios múltiples - Combinación ratio + fixed - Edge cases (ratio > espacio disponible) --- ## Fase 8: Terminal Resize Handler Mejorado ### Objetivo Manejo robusto de resize de terminal con debounce y callbacks. ### Archivo - Modificar `src/terminal.zig` - Nuevo: `src/resize.zig` ### Implementación ```zig // resize.zig pub const ResizeHandler = struct { current_size: Size, callbacks: std.ArrayListUnmanaged(ResizeCallback), debounce_ms: u32 = 100, last_resize: i64 = 0, pending_size: ?Size = null, allocator: std.mem.Allocator, pub const ResizeCallback = *const fn (old: Size, new: Size) void; pub fn init(allocator: std.mem.Allocator) ResizeHandler { return .{ .current_size = getTerminalSize(), .callbacks = .{}, .allocator = allocator, }; } /// Registrar callback para resize pub fn onResize(self: *ResizeHandler, callback: ResizeCallback) !void { try self.callbacks.append(self.allocator, callback); } /// Procesar señal SIGWINCH pub fn handleSignal(self: *ResizeHandler) void { const new_size = getTerminalSize(); if (!new_size.eql(self.current_size)) { self.pending_size = new_size; self.last_resize = std.time.milliTimestamp(); } } /// Llamar en el event loop para procesar resize con debounce pub fn poll(self: *ResizeHandler) ?struct { old: Size, new: Size } { if (self.pending_size) |new_size| { const now = std.time.milliTimestamp(); if (now - self.last_resize >= self.debounce_ms) { const old_size = self.current_size; self.current_size = new_size; self.pending_size = null; // Notificar callbacks for (self.callbacks.items) |cb| { cb(old_size, new_size); } return .{ .old = old_size, .new = new_size }; } } return null; } /// Obtener tamaño actual pub fn getSize(self: *const ResizeHandler) Size { return self.current_size; } }; /// Obtener tamaño del terminal pub fn getTerminalSize() Size { var ws: std.os.linux.winsize = undefined; const ret = std.os.linux.ioctl( std.os.STDOUT_FILENO, std.os.linux.T.IOCGWINSZ, @intFromPtr(&ws) ); if (ret == 0) { return .{ .width = ws.ws_col, .height = ws.ws_row, }; } // Fallback: variables de entorno const cols = std.os.getenv("COLUMNS") orelse "80"; const rows = std.os.getenv("LINES") orelse "24"; return .{ .width = std.fmt.parseInt(u16, cols, 10) catch 80, .height = std.fmt.parseInt(u16, rows, 10) catch 24, }; } /// Instalar handler de SIGWINCH pub fn installResizeSignalHandler(handler: *ResizeHandler) !void { const S = struct { var global_handler: ?*ResizeHandler = null; fn sigwinchHandler(_: c_int) callconv(.C) void { if (global_handler) |h| { h.handleSignal(); } } }; S.global_handler = handler; var sa = std.os.linux.Sigaction{ .handler = .{ .handler = S.sigwinchHandler }, .mask = std.os.linux.empty_sigset, .flags = 0, }; _ = std.os.linux.sigaction(std.os.linux.SIG.WINCH, &sa, null); } ``` ### Integración con Terminal ```zig // En terminal.zig pub const Terminal = struct { // ... campos existentes ... resize_handler: ResizeHandler, pub fn init(allocator: Allocator) !Terminal { var self = Terminal{ // ... .resize_handler = ResizeHandler.init(allocator), }; try installResizeSignalHandler(&self.resize_handler); return self; } pub fn pollEvent(self: *Terminal) ?Event { // Verificar resize primero if (self.resize_handler.poll()) |resize| { return Event{ .resize = resize }; } // Luego verificar input... } pub fn onResize(self: *Terminal, callback: ResizeHandler.ResizeCallback) !void { try self.resize_handler.onResize(callback); } }; ``` ### Layout responsivo ```zig pub const ResponsiveLayout = struct { breakpoints: struct { small: u16 = 60, // < 60 cols medium: u16 = 100, // 60-100 cols large: u16 = 140, // 100-140 cols // > 140 = xlarge }, pub fn getBreakpoint(self: ResponsiveLayout, width: u16) Breakpoint { if (width < self.breakpoints.small) return .small; if (width < self.breakpoints.medium) return .medium; if (width < self.breakpoints.large) return .large; return .xlarge; } pub const Breakpoint = enum { small, medium, large, xlarge }; }; // Uso fn render(app: *App, area: Rect, buf: *Buffer) void { const bp = ResponsiveLayout{}.getBreakpoint(area.width); switch (bp) { .small => app.renderMobile(area, buf), .medium => app.renderTablet(area, buf), .large, .xlarge => app.renderDesktop(area, buf), } } ``` ### Tests - Size detection - Signal handling - Debounce - Callback dispatch - Responsive breakpoints --- ## Fase 9: Sixel Images ### Objetivo Soporte para imágenes Sixel (alternativa a Kitty protocol, más compatible). ### Archivo - `src/sixel.zig` ### Documentación Sixel Sixel es un formato de gráficos para terminales que data de los años 80 (DEC). Muchos terminales modernos lo soportan: xterm, mlterm, mintty, Windows Terminal, foot. ### Formato Sixel ``` ESC P q ESC \ Donde es: - " Pan ; Pad ; Ph ; Pv - Atributos raster - # Pc ; Pu ; Px ; Py ; Pz - Definir color - ! Pn - Repetir caracter Pn veces - $ - Carriage return (sin line feed) - - - Line feed (nueva banda de 6 píxeles) - ? a ~ - Datos (cada caracter = 6 píxeles verticales) ``` ### Implementación ```zig // sixel.zig pub const SixelEncoder = struct { output: std.ArrayListUnmanaged(u8), palette: [256]Color, palette_size: u8, allocator: std.mem.Allocator, pub fn init(allocator: std.mem.Allocator) SixelEncoder { return .{ .output = .{}, .palette = undefined, .palette_size = 0, .allocator = allocator, }; } pub fn deinit(self: *SixelEncoder) void { self.output.deinit(self.allocator); } /// Codificar imagen RGBA a Sixel pub fn encode(self: *SixelEncoder, pixels: []const u8, width: u32, height: u32) ![]const u8 { self.output.clearRetainingCapacity(); // Inicio secuencia Sixel try self.output.appendSlice(self.allocator, "\x1bPq"); // Cuantizar colores a paleta (máx 256) try self.buildPalette(pixels, width, height); // Escribir definiciones de paleta try self.writePalette(); // Codificar píxeles en bandas de 6 try self.encodePixels(pixels, width, height); // Fin secuencia try self.output.appendSlice(self.allocator, "\x1b\\"); return self.output.items; } fn buildPalette(self: *SixelEncoder, pixels: []const u8, width: u32, height: u32) !void { // Algoritmo de cuantización (median cut o similar) var color_counts = std.AutoHashMap(u32, u32).init(self.allocator); defer color_counts.deinit(); // Contar colores únicos var i: usize = 0; while (i < pixels.len) : (i += 4) { const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; const key = (@as(u32, r) << 16) | (@as(u32, g) << 8) | b; const entry = try color_counts.getOrPut(key); if (!entry.found_existing) { entry.value_ptr.* = 0; } entry.value_ptr.* += 1; } // Reducir a 256 colores si es necesario // (implementar median cut aquí) // Por ahora, tomar los primeros 256 var iter = color_counts.iterator(); self.palette_size = 0; while (iter.next()) |entry| { if (self.palette_size >= 256) break; const key = entry.key_ptr.*; self.palette[self.palette_size] = Color.rgb( @truncate(key >> 16), @truncate(key >> 8), @truncate(key), ); self.palette_size += 1; } } fn writePalette(self: *SixelEncoder) !void { for (self.palette[0..self.palette_size], 0..) |color, i| { const rgb = switch (color) { .true_color => |c| c, else => continue, }; // Formato: #Pc;2;R;G;B (donde R,G,B son 0-100) var buf: [32]u8 = undefined; const str = std.fmt.bufPrint(&buf, "#{d};2;{d};{d};{d}", .{ i, @as(u32, rgb.r) * 100 / 255, @as(u32, rgb.g) * 100 / 255, @as(u32, rgb.b) * 100 / 255, }) catch continue; try self.output.appendSlice(self.allocator, str); } } fn encodePixels(self: *SixelEncoder, pixels: []const u8, width: u32, height: u32) !void { // Procesar en bandas de 6 píxeles de alto var y: u32 = 0; while (y < height) : (y += 6) { const band_height = @min(6, height - y); // Para cada color en la paleta for (0..self.palette_size) |color_idx| { var has_pixels = false; var run_start: ?u32 = null; var run_char: u8 = 0; for (0..width) |x| { var sixel_bits: u8 = 0; // Construir los 6 bits verticales for (0..band_height) |dy| { const py = y + dy; const idx = (py * width + x) * 4; if (self.getColorIndex(pixels[idx..][0..3]) == color_idx) { sixel_bits |= @as(u8, 1) << @intCast(dy); has_pixels = true; } } const char = sixel_bits + 63; // '?' = 0, '~' = 63 // Run-length encoding if (run_start == null) { run_start = @intCast(x); run_char = char; } else if (char != run_char) { try self.writeRun(run_char, x - run_start.?); run_start = @intCast(x); run_char = char; } } // Escribir último run if (run_start) |start| { try self.writeRun(run_char, width - start); } // Carriage return (volver al inicio de la banda) if (has_pixels) { try self.output.append(self.allocator, '$'); } } // Line feed (siguiente banda de 6 píxeles) try self.output.append(self.allocator, '-'); } } fn writeRun(self: *SixelEncoder, char: u8, count: u32) !void { if (count == 1) { try self.output.append(self.allocator, char); } else if (count > 1) { var buf: [16]u8 = undefined; const str = std.fmt.bufPrint(&buf, "!{d}", .{count}) catch return; try self.output.appendSlice(self.allocator, str); try self.output.append(self.allocator, char); } } fn getColorIndex(self: *SixelEncoder, rgb: *const [3]u8) usize { // Encontrar color más cercano en la paleta var best_idx: usize = 0; var best_dist: u32 = std.math.maxInt(u32); for (self.palette[0..self.palette_size], 0..) |color, i| { const c = switch (color) { .true_color => |c| c, else => continue, }; const dr = @as(i32, rgb[0]) - @as(i32, c.r); const dg = @as(i32, rgb[1]) - @as(i32, c.g); const db = @as(i32, rgb[2]) - @as(i32, c.b); const dist: u32 = @intCast(dr * dr + dg * dg + db * db); if (dist < best_dist) { best_dist = dist; best_idx = i; } } return best_idx; } }; /// Detectar soporte Sixel en el terminal pub fn sixelSupported() bool { // Enviar DA1 query y parsear respuesta // Si respuesta contiene ";4;" o ";4c", soporta sixel // Por ahora, detectar por $TERM const term = std.os.getenv("TERM") orelse return false; return std.mem.indexOf(u8, term, "xterm") != null or std.mem.indexOf(u8, term, "mlterm") != null or std.mem.indexOf(u8, term, "mintty") != null or std.mem.indexOf(u8, term, "foot") != null; } /// Widget para mostrar imagen Sixel pub const SixelImage = struct { data: []const u8, width: u32, height: u32, cached_sixel: ?[]const u8 = null, pub fn fromRgba(data: []const u8, width: u32, height: u32) SixelImage { return .{ .data = data, .width = width, .height = height, }; } pub fn render(self: *SixelImage, area: Rect, buf: *Buffer, allocator: std.mem.Allocator) void { if (!sixelSupported()) { // Fallback a bloques Unicode self.renderFallback(area, buf); return; } // Generar sixel data si no está cacheado if (self.cached_sixel == null) { var encoder = SixelEncoder.init(allocator); defer encoder.deinit(); self.cached_sixel = encoder.encode(self.data, self.width, self.height) catch null; } if (self.cached_sixel) |sixel| { // Escribir secuencia sixel al buffer // (requiere soporte especial en Buffer para datos raw) buf.setRawSequence(area.x, area.y, sixel); } } fn renderFallback(self: *SixelImage, area: Rect, buf: *Buffer) void { // Renderizar usando half-block characters (▀▄█ ) // 2 píxeles por caracter vertical for (0..@min(area.height * 2, self.height)) |y| { for (0..@min(area.width, self.width)) |x| { const top_idx = (y * 2 * self.width + x) * 4; const bot_idx = ((y * 2 + 1) * self.width + x) * 4; // Simplificado: usar ▀ con colores const top_color = Color.rgb( self.data[top_idx], self.data[top_idx + 1], self.data[top_idx + 2], ); buf.setCell( area.x + @as(u16, @intCast(x)), area.y + @as(u16, @intCast(y)), Cell{ .symbol = .{ .short = .{ '▀', 0, 0, 0 } }, .style = Style{}.fg(top_color), }, ); } } } }; ``` ### Tests - Palette generation - Sixel encoding - Run-length compression - Fallback rendering - Terminal detection --- ## Fase 10: Errores tipo Elm ### Objetivo Mensajes de error hermosos y útiles, inspirados en Elm y Rust. ### Archivo - `src/errors.zig` ### Diseño de errores Elm-style ``` ── CONSTRAINT ERROR ─────────────────────────── src/layout.zig:45 I found a layout constraint that doesn't make sense: 45 │ .ratio = .{ .num = 3, .den = 0 }, ^^^ The denominator of a ratio cannot be zero. This would cause a division by zero when calculating the layout. Hint: Did you mean to use a fixed length instead? .length = 3 ── STYLE ERROR ──────────────────────────────── src/widgets/block.zig:12 I found conflicting style settings: 12 │ .fg = Color.red, 13 │ .foreground = Color.blue, ^^^^^^^^^^ You're setting the foreground color twice with different values. Use either `fg` or `foreground`, but not both. ── TYPE ERROR ───────────────────────────────── examples/demo.zig:23 I was expecting a `Rect` but found a `Size`: 23 │ widget.render(my_size, buffer); ^^^^^^^ The `render` function needs the position AND size of the area, but `Size` only has width and height. Try converting it to a Rect: widget.render(Rect.fromSize(0, 0, my_size), buffer); ``` ### Implementación ```zig // errors.zig pub const DiagnosticLevel = enum { @"error", warning, hint, note, }; pub const SourceLocation = struct { file: []const u8, line: u32, column: u32, source_line: ?[]const u8 = null, }; pub const Diagnostic = struct { level: DiagnosticLevel, title: []const u8, message: []const u8, location: ?SourceLocation = null, highlights: []const Highlight = &.{}, hints: []const []const u8 = &.{}, notes: []const []const u8 = &.{}, pub const Highlight = struct { start: u32, end: u32, label: ?[]const u8 = null, }; }; pub const DiagnosticFormatter = struct { allocator: std.mem.Allocator, colors: bool = true, max_width: u16 = 80, pub fn format(self: DiagnosticFormatter, diag: Diagnostic) ![]u8 { var out = std.ArrayListUnmanaged(u8){}; errdefer out.deinit(self.allocator); const writer = out.writer(self.allocator); // Header con color try self.writeHeader(writer, diag); // Mensaje principal try writer.writeAll("\n"); try self.writeWrapped(writer, diag.message, 0); try writer.writeAll("\n\n"); // Código fuente con highlight if (diag.location) |loc| { try self.writeSourceSnippet(writer, loc, diag.highlights); try writer.writeAll("\n"); } // Hints for (diag.hints) |hint| { try self.writeHint(writer, hint); } // Notes for (diag.notes) |note| { try self.writeNote(writer, note); } return out.toOwnedSlice(self.allocator); } fn writeHeader(self: DiagnosticFormatter, writer: anytype, diag: Diagnostic) !void { // ── ERROR ──────────────────────────── file.zig:12 const color = if (self.colors) switch (diag.level) { .@"error" => "\x1b[31m", // Red .warning => "\x1b[33m", // Yellow .hint => "\x1b[36m", // Cyan .note => "\x1b[34m", // Blue } else ""; const reset = if (self.colors) "\x1b[0m" else ""; try writer.print("{s}── {s} ", .{ color, @tagName(diag.level) }); // Línea decorativa var remaining = self.max_width - 20; if (diag.location) |loc| { remaining -= @min(remaining, loc.file.len + 10); } for (0..remaining) |_| try writer.writeAll("─"); // Ubicación if (diag.location) |loc| { try writer.print(" {s}:{d}{s}", .{ loc.file, loc.line, reset }); } else { try writer.print("{s}", .{reset}); } } fn writeSourceSnippet(self: DiagnosticFormatter, writer: anytype, loc: SourceLocation, highlights: []const Diagnostic.Highlight) !void { const gutter_width = 5; const line_num_str = try std.fmt.allocPrint(self.allocator, "{d}", .{loc.line}); defer self.allocator.free(line_num_str); // Padding para número de línea const padding = gutter_width - line_num_str.len - 1; for (0..padding) |_| try writer.writeAll(" "); // Número de línea if (self.colors) try writer.writeAll("\x1b[90m"); try writer.print("{s} │ ", .{line_num_str}); if (self.colors) try writer.writeAll("\x1b[0m"); // Código fuente if (loc.source_line) |src| { try writer.writeAll(src); } try writer.writeAll("\n"); // Underline highlights for (0..gutter_width) |_| try writer.writeAll(" "); try writer.writeAll("│ "); if (self.colors) try writer.writeAll("\x1b[31m"); for (highlights) |h| { for (0..h.start) |_| try writer.writeAll(" "); for (h.start..h.end) |_| try writer.writeAll("^"); } if (self.colors) try writer.writeAll("\x1b[0m"); } fn writeHint(self: DiagnosticFormatter, writer: anytype, hint: []const u8) !void { if (self.colors) try writer.writeAll("\x1b[36m"); try writer.writeAll("Hint: "); if (self.colors) try writer.writeAll("\x1b[0m"); try self.writeWrapped(writer, hint, 6); try writer.writeAll("\n\n"); } fn writeNote(self: DiagnosticFormatter, writer: anytype, note: []const u8) !void { if (self.colors) try writer.writeAll("\x1b[34m"); try writer.writeAll("Note: "); if (self.colors) try writer.writeAll("\x1b[0m"); try self.writeWrapped(writer, note, 6); try writer.writeAll("\n"); } fn writeWrapped(self: DiagnosticFormatter, writer: anytype, text: []const u8, indent: u16) !void { var col: u16 = indent; var words = std.mem.tokenizeAny(u8, text, " \t\n"); while (words.next()) |word| { if (col + word.len + 1 > self.max_width) { try writer.writeAll("\n"); for (0..indent) |_| try writer.writeAll(" "); col = indent; } if (col > indent) { try writer.writeAll(" "); col += 1; } try writer.writeAll(word); col += @intCast(word.len); } } }; // Errores predefinidos para zcatui pub const ZcatuiError = union(enum) { constraint_zero_denominator: struct { line: u32, col: u32, file: []const u8 }, invalid_color_format: struct { value: []const u8 }, buffer_out_of_bounds: struct { x: u16, y: u16, width: u16, height: u16 }, widget_not_rendered: struct { widget_type: []const u8 }, // ... más errores pub fn toDiagnostic(self: ZcatuiError, allocator: std.mem.Allocator) !Diagnostic { return switch (self) { .constraint_zero_denominator => |e| .{ .level = .@"error", .title = "CONSTRAINT ERROR", .message = "The denominator of a ratio constraint cannot be zero. This would cause a division by zero when calculating the layout.", .location = .{ .file = e.file, .line = e.line, .column = e.col, }, .hints = &.{ "Did you mean to use a fixed length instead?", }, }, // ... más casos }; } }; ``` ### Macro de error (comptime) ```zig pub fn err(comptime fmt: []const u8, args: anytype) void { const diag = Diagnostic{ .level = .@"error", .message = std.fmt.comptimePrint(fmt, args), }; const formatter = DiagnosticFormatter{ .allocator = std.heap.page_allocator }; const output = formatter.format(diag) catch return; defer std.heap.page_allocator.free(output); std.debug.print("{s}\n", .{output}); } // Uso if (denominator == 0) { errors.err("Ratio denominator cannot be zero in constraint at {s}:{d}", .{ @src().file, @src().line }); return error.InvalidConstraint; } ``` ### Tests - Formatting con colores - Wrapping de texto largo - Source snippet display - Multiple highlights --- ## Fase 11: Debug Mode ### Objetivo Modo de depuración visual para inspeccionar buffers, eventos, layouts. ### Archivos - `src/debug.zig` - `src/widgets/debug_overlay.zig` ### Funcionalidades ```zig // debug.zig pub const DebugMode = struct { enabled: bool = false, show_layout_bounds: bool = true, show_focus_ring: bool = true, show_event_log: bool = true, show_render_stats: bool = true, show_buffer_grid: bool = false, event_log: std.BoundedArray(DebugEvent, 100) = .{}, render_stats: RenderStats = .{}, pub const DebugEvent = struct { timestamp: i64, event_type: []const u8, details: []const u8, }; pub const RenderStats = struct { frame_count: u64 = 0, last_frame_time_ns: u64 = 0, cells_rendered: u64 = 0, cells_changed: u64 = 0, avg_frame_time_ns: u64 = 0, }; /// Toggle debug mode pub fn toggle(self: *DebugMode) void { self.enabled = !self.enabled; } /// Log un evento pub fn logEvent(self: *DebugMode, event_type: []const u8, details: []const u8) void { if (!self.enabled) return; self.event_log.append(.{ .timestamp = std.time.milliTimestamp(), .event_type = event_type, .details = details, }) catch { // Si está lleno, remover el más viejo _ = self.event_log.orderedRemove(0); self.event_log.append(.{ .timestamp = std.time.milliTimestamp(), .event_type = event_type, .details = details, }) catch {}; }; } /// Marcar inicio de frame pub fn frameStart(self: *DebugMode) void { self.render_stats.frame_start = std.time.nanoTimestamp(); } /// Marcar fin de frame pub fn frameEnd(self: *DebugMode, cells_rendered: u64, cells_changed: u64) void { const elapsed = std.time.nanoTimestamp() - self.render_stats.frame_start; self.render_stats.frame_count += 1; self.render_stats.last_frame_time_ns = @intCast(elapsed); self.render_stats.cells_rendered = cells_rendered; self.render_stats.cells_changed = cells_changed; // Running average self.render_stats.avg_frame_time_ns = (self.render_stats.avg_frame_time_ns * (self.render_stats.frame_count - 1) + self.render_stats.last_frame_time_ns) / self.render_stats.frame_count; } }; /// Widget overlay de debug pub const DebugOverlay = struct { debug: *DebugMode, position: Position = .top_right, pub const Position = enum { top_left, top_right, bottom_left, bottom_right }; pub fn render(self: DebugOverlay, area: Rect, buf: *Buffer) void { if (!self.debug.enabled) return; // Calcular posición del overlay const overlay_width: u16 = 40; const overlay_height: u16 = @min(20, area.height); const overlay_area = switch (self.position) { .top_right => Rect{ .x = area.x + area.width - overlay_width, .y = area.y, .width = overlay_width, .height = overlay_height, }, // ... otros casos }; // Fondo semi-transparente (usando color oscuro) for (overlay_area.y..overlay_area.y + overlay_area.height) |y| { for (overlay_area.x..overlay_area.x + overlay_area.width) |x| { buf.setCell(@intCast(x), @intCast(y), Cell{ .symbol = .{ .short = .{ ' ', 0, 0, 0 } }, .style = Style{}.bg(Color.rgb(20, 20, 30)), }); } } // Título buf.setString( overlay_area.x + 1, overlay_area.y, "DEBUG", Style{}.fg(Color.yellow).bold(), ); // Render stats if (self.debug.show_render_stats) { self.renderStats(buf, overlay_area); } // Event log if (self.debug.show_event_log) { self.renderEventLog(buf, overlay_area); } } fn renderStats(self: DebugOverlay, buf: *Buffer, area: Rect) void { const stats = self.debug.render_stats; var y = area.y + 2; // FPS const fps = if (stats.avg_frame_time_ns > 0) 1_000_000_000 / stats.avg_frame_time_ns else 0; var line_buf: [64]u8 = undefined; const fps_str = std.fmt.bufPrint(&line_buf, "FPS: {d}", .{fps}) catch "FPS: ?"; buf.setString(area.x + 1, y, fps_str, Style{}.fg(Color.green)); y += 1; const frame_str = std.fmt.bufPrint(&line_buf, "Frame: {d}", .{stats.frame_count}) catch "Frame: ?"; buf.setString(area.x + 1, y, frame_str, Style{}.fg(Color.white)); y += 1; const cells_str = std.fmt.bufPrint(&line_buf, "Cells: {d}/{d}", .{ stats.cells_changed, stats.cells_rendered, }) catch "Cells: ?"; buf.setString(area.x + 1, y, cells_str, Style{}.fg(Color.white)); } fn renderEventLog(self: DebugOverlay, buf: *Buffer, area: Rect) void { var y = area.y + 6; const max_events = @min(self.debug.event_log.len, area.height - 8); buf.setString(area.x + 1, y, "Events:", Style{}.fg(Color.cyan)); y += 1; // Mostrar eventos más recientes const start = if (self.debug.event_log.len > max_events) self.debug.event_log.len - max_events else 0; for (self.debug.event_log.slice()[start..]) |event| { var line_buf: [64]u8 = undefined; const line = std.fmt.bufPrint(&line_buf, "{s}: {s}", .{ event.event_type, event.details, }) catch continue; buf.setString(area.x + 2, y, line[0..@min(line.len, area.width - 3)], Style{}.fg(Color.white)); y += 1; } } }; /// Visualizador de layout bounds pub fn renderLayoutBounds(area: Rect, buf: *Buffer, label: []const u8) void { // Dibujar borde punteado const style = Style{}.fg(Color.magenta); // Top for (area.x..area.x + area.width) |x| { if (x % 2 == 0) { buf.setCell(@intCast(x), area.y, Cell{ .symbol = .{ .short = .{ '·', 0, 0, 0 } }, .style = style, }); } } // Bottom for (area.x..area.x + area.width) |x| { if (x % 2 == 0) { buf.setCell(@intCast(x), area.y + area.height - 1, Cell{ .symbol = .{ .short = .{ '·', 0, 0, 0 } }, .style = style, }); } } // Label if (label.len > 0) { buf.setString(area.x, area.y, label, style.bold()); } } ``` ### Shortcut de activación ```zig // F12 o Ctrl+Shift+D para toggle debug if (event.key == .f12 or (event.key == .char and event.char == 'd' and event.ctrl and event.shift)) { debug_mode.toggle(); } ``` ### Tests - Event logging - Render stats calculation - Overlay positioning - Layout bounds visualization --- ## Fase 12: Performance Profiling ### Objetivo Sistema de profiling para identificar cuellos de botella en el rendering. ### Archivos - `src/profiler.zig` - `src/widgets/profiler_view.zig` ### Implementación ```zig // profiler.zig pub const Profiler = struct { spans: std.ArrayListUnmanaged(Span), stack: std.BoundedArray(SpanId, 32), allocator: std.mem.Allocator, enabled: bool = false, pub const SpanId = u32; pub const Span = struct { name: []const u8, parent: ?SpanId, start_ns: i128, end_ns: i128 = 0, metadata: ?[]const u8 = null, pub fn durationNs(self: Span) u64 { return @intCast(self.end_ns - self.start_ns); } pub fn durationMs(self: Span) f64 { return @as(f64, @floatFromInt(self.durationNs())) / 1_000_000.0; } }; pub fn init(allocator: std.mem.Allocator) Profiler { return .{ .spans = .{}, .stack = .{}, .allocator = allocator, }; } pub fn deinit(self: *Profiler) void { self.spans.deinit(self.allocator); } /// Comenzar un span pub fn begin(self: *Profiler, name: []const u8) SpanId { if (!self.enabled) return 0; const parent = if (self.stack.len > 0) self.stack.get(self.stack.len - 1) else null; const id: SpanId = @intCast(self.spans.items.len); self.spans.append(self.allocator, .{ .name = name, .parent = parent, .start_ns = std.time.nanoTimestamp(), }) catch return 0; self.stack.append(id) catch {}; return id; } /// Terminar un span pub fn end(self: *Profiler, id: SpanId) void { if (!self.enabled) return; if (id >= self.spans.items.len) return; self.spans.items[id].end_ns = std.time.nanoTimestamp(); _ = self.stack.pop(); } /// Helper para scope automático pub fn scope(self: *Profiler, name: []const u8) ScopeGuard { return .{ .profiler = self, .id = self.begin(name), }; } pub const ScopeGuard = struct { profiler: *Profiler, id: SpanId, pub fn deinit(self: ScopeGuard) void { self.profiler.end(self.id); } }; /// Comenzar nuevo frame (clear spans) pub fn newFrame(self: *Profiler) void { self.spans.clearRetainingCapacity(); self.stack.len = 0; } /// Generar reporte pub fn generateReport(self: *const Profiler, allocator: std.mem.Allocator) !Report { var report = Report{ .total_time_ns = 0, .span_stats = std.StringHashMap(SpanStats).init(allocator), }; // Calcular estadísticas por nombre de span for (self.spans.items) |span| { const duration = span.durationNs(); report.total_time_ns += duration; const entry = try report.span_stats.getOrPut(span.name); if (!entry.found_existing) { entry.value_ptr.* = .{}; } entry.value_ptr.count += 1; entry.value_ptr.total_ns += duration; entry.value_ptr.min_ns = @min(entry.value_ptr.min_ns, duration); entry.value_ptr.max_ns = @max(entry.value_ptr.max_ns, duration); } return report; } pub const SpanStats = struct { count: u64 = 0, total_ns: u64 = 0, min_ns: u64 = std.math.maxInt(u64), max_ns: u64 = 0, pub fn avgNs(self: SpanStats) u64 { return if (self.count > 0) self.total_ns / self.count else 0; } pub fn avgMs(self: SpanStats) f64 { return @as(f64, @floatFromInt(self.avgNs())) / 1_000_000.0; } }; pub const Report = struct { total_time_ns: u64, span_stats: std.StringHashMap(SpanStats), pub fn deinit(self: *Report) void { self.span_stats.deinit(); } /// Formatear como texto pub fn format(self: *const Report, allocator: std.mem.Allocator) ![]u8 { var out = std.ArrayListUnmanaged(u8){}; const writer = out.writer(allocator); try writer.writeAll("=== PROFILE REPORT ===\n\n"); try writer.print("Total frame time: {d:.2} ms\n\n", .{ @as(f64, @floatFromInt(self.total_time_ns)) / 1_000_000.0, }); try writer.writeAll("Span Count Total Avg Min Max\n"); try writer.writeAll("─────────────────────────────────────────────────────────────────\n"); var iter = self.span_stats.iterator(); while (iter.next()) |entry| { const stats = entry.value_ptr.*; try writer.print("{s:<24} {d:>5} {d:>6.2}ms {d:>6.2}ms {d:>6.2}ms {d:>6.2}ms\n", .{ entry.key_ptr.*, stats.count, @as(f64, @floatFromInt(stats.total_ns)) / 1_000_000.0, stats.avgMs(), @as(f64, @floatFromInt(stats.min_ns)) / 1_000_000.0, @as(f64, @floatFromInt(stats.max_ns)) / 1_000_000.0, }); } return out.toOwnedSlice(allocator); } }; }; /// Uso con defer pub fn profiledRender(profiler: *Profiler, widget: anytype, area: Rect, buf: *Buffer) void { const guard = profiler.scope(@typeName(@TypeOf(widget))); defer guard.deinit(); widget.render(area, buf); } ``` ### Instrumentación automática (comptime) ```zig /// Wrapper que añade profiling a cualquier widget pub fn Profiled(comptime Widget: type) type { return struct { inner: Widget, profiler: *Profiler, pub fn render(self: @This(), area: Rect, buf: *Buffer) void { const guard = self.profiler.scope(@typeName(Widget)); defer guard.deinit(); self.inner.render(area, buf); } // Delegar otros métodos... pub usingnamespace if (@hasDecl(Widget, "handleEvent")) struct { pub fn handleEvent(self: *@This(), event: Event) bool { return self.inner.handleEvent(event); } } else struct {}; }; } // Uso var profiled_list = Profiled(List([]const u8)){ .inner = my_list, .profiler = &profiler, }; profiled_list.render(area, buf); ``` ### Flame graph output ```zig /// Exportar a formato flame graph (para visualizar con flamegraph.pl) pub fn exportFlameGraph(self: *const Profiler, allocator: std.mem.Allocator) ![]u8 { var out = std.ArrayListUnmanaged(u8){}; const writer = out.writer(allocator); for (self.spans.items) |span| { // Construir stack path var path = std.ArrayListUnmanaged([]const u8){}; defer path.deinit(allocator); var current: ?SpanId = @intCast(&span - self.spans.items.ptr); while (current) |id| { const s = self.spans.items[id]; try path.insert(allocator, 0, s.name); current = s.parent; } // Formato: stack;path;names count for (path.items, 0..) |name, i| { if (i > 0) try writer.writeAll(";"); try writer.writeAll(name); } try writer.print(" {d}\n", .{span.durationNs() / 1000}); // microseconds } return out.toOwnedSlice(allocator); } ``` ### Tests - Span begin/end - Nested spans - Report generation - Flame graph export --- ## Fase 13: Package Manager (build.zig.zon) ### Objetivo Hacer zcatui instalable via Zig package manager. ### Archivos - `build.zig.zon` (nuevo) - Modificar `build.zig` ### build.zig.zon ```zon .{ .name = "zcatui", .version = "2.2.0", .minimum_zig_version = "0.15.0", .dependencies = .{ // Sin dependencias externas }, .paths = .{ "build.zig", "build.zig.zon", "src", "LICENSE", "README.md", }, } ``` ### Modificaciones a build.zig ```zig const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // ============================================ // MÓDULO PRINCIPAL (para uso como dependencia) // ============================================ const zcatui_mod = b.addModule("zcatui", .{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); // ============================================ // LIBRERÍA ESTÁTICA (opcional) // ============================================ const lib = b.addStaticLibrary(.{ .name = "zcatui", .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); b.installArtifact(lib); // ============================================ // TESTS // ============================================ const tests = b.addTest(.{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); const run_tests = b.addRunArtifact(tests); const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_tests.step); // ============================================ // EXAMPLES // ============================================ const examples = [_][]const u8{ "hello", "events_demo", "list_demo", "table_demo", "dashboard", "input_demo", "animation_demo", "clipboard_demo", "menu_demo", "form_demo", "panel_demo", // Nuevos v2.1 "spinner_demo", "help_demo", "viewport_demo", "progress_demo", "markdown_demo", "dirtree_demo", "syntax_demo", }; for (examples) |name| { const exe = b.addExecutable(.{ .name = name, .root_source_file = b.path(b.fmt("examples/{s}.zig", .{name})), .target = target, .optimize = optimize, }); exe.root_module.addImport("zcatui", zcatui_mod); const run_cmd = b.addRunArtifact(exe); const run_step = b.step(name, b.fmt("Run {s} example", .{name})); run_step.dependOn(&run_cmd.step); b.installArtifact(exe); } // ============================================ // DOCUMENTACIÓN // ============================================ const docs = b.addStaticLibrary(.{ .name = "zcatui-docs", .root_source_file = b.path("src/root.zig"), .target = target, .optimize = .Debug, }); const install_docs = b.addInstallDirectory(.{ .source_dir = docs.getEmittedDocs(), .install_dir = .prefix, .install_subdir = "docs", }); const docs_step = b.step("docs", "Generate documentation"); docs_step.dependOn(&install_docs.step); } ``` ### Uso desde otro proyecto ```zig // En el proyecto del usuario: build.zig.zon .{ .name = "my-tui-app", .version = "0.1.0", .dependencies = .{ .zcatui = .{ .url = "https://git.reugenio.com/reugenio/zcatui/archive/v2.2.0.tar.gz", .hash = "1234567890abcdef...", // Se calcula automáticamente }, }, } // En build.zig const zcatui_dep = b.dependency("zcatui", .{ .target = target, .optimize = optimize, }); exe.root_module.addImport("zcatui", zcatui_dep.module("zcatui")); // En código const zcatui = @import("zcatui"); pub fn main() !void { var term = try zcatui.Terminal.init(allocator); defer term.deinit(); // ... } ``` ### Tests - Build como módulo - Build como librería estática - Import desde proyecto externo --- ## Resumen de Entregables | Fase | Archivos Nuevos | Modificados | Tests | |------|-----------------|-------------|-------| | 1 | logo.zig | - | 4 | | 2 | 7 examples | build.zig | - | | 3 | drag.zig, resizable.zig | panel.zig | 8 | | 4 | shortcuts.zig | - | 6 | | 5 | async/loop.zig, io_uring.zig, epoll.zig | terminal.zig | 10 | | 6 | compose.zig | - | 5 | | 7 | - | layout.zig | 4 | | 8 | resize.zig | terminal.zig | 5 | | 9 | sixel.zig | - | 5 | | 10 | errors.zig | - | 4 | | 11 | debug.zig, debug_overlay.zig | - | 4 | | 12 | profiler.zig, profiler_view.zig | - | 4 | | 13 | build.zig.zon | build.zig | - | **Total estimado:** - ~20 archivos nuevos - ~10 archivos modificados - ~60 tests nuevos - ~5000-8000 líneas de código --- ## Orden de Ejecución Recomendado 1. **Fase 7** (Ratio constraints) - Base para otros layouts 2. **Fase 1** (Logo widget) - Simple, prepara terreno 3. **Fase 4** (Shortcuts) - Necesario para fases 3 y 11 4. **Fase 6** (Widget composition) - Mejora DX para examples 5. **Fase 2** (Examples) - Demuestra funcionalidad existente 6. **Fase 8** (Resize handler) - Necesario para fase 3 7. **Fase 3** (Drag & drop) - Feature compleja 8. **Fase 10** (Errores Elm) - Mejora debugging 9. **Fase 11** (Debug mode) - Usa errores mejorados 10. **Fase 12** (Profiling) - Complementa debug 11. **Fase 9** (Sixel) - Feature independiente 12. **Fase 5** (Async) - Más compleja, puede ser opcional 13. **Fase 13** (Package) - Final, cuando todo esté estable --- ## Notas Finales - Cada fase es independiente y puede commitearse por separado - Tests deben pasar antes de avanzar a siguiente fase - Documentar inline con `///` doc comments - Mantener compatibilidad con API existente - Seguir patrones establecidos (builder, render, etc.)