Librería TUI inspirada en ratatui (Rust), implementada en Zig 0.15.2. Estructura inicial: - src/style.zig: Color, Style, Modifier - src/buffer.zig: Cell, Buffer, Rect - src/layout.zig: Layout, Constraint, Direction - src/terminal.zig: Terminal abstraction - src/backend/backend.zig: ANSI escape sequences - src/widgets/block.zig: Block con borders y título - src/widgets/paragraph.zig: Paragraph con wrapping - examples/hello.zig: Demo funcional 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
184 lines
5.8 KiB
Zig
184 lines
5.8 KiB
Zig
//! Terminal abstraction for zcatui.
|
|
//!
|
|
//! The Terminal struct provides the main entry point for TUI applications.
|
|
//! It handles initialization, drawing, and cleanup of the terminal state.
|
|
//!
|
|
//! ## Example
|
|
//!
|
|
//! ```zig
|
|
//! var term = try Terminal.init(allocator);
|
|
//! defer term.deinit();
|
|
//!
|
|
//! try term.draw(renderFn);
|
|
//! ```
|
|
|
|
const std = @import("std");
|
|
const buffer = @import("buffer.zig");
|
|
const Buffer = buffer.Buffer;
|
|
const Rect = buffer.Rect;
|
|
const backend_mod = @import("backend/backend.zig");
|
|
const AnsiBackend = backend_mod.AnsiBackend;
|
|
|
|
/// Terminal provides the main interface for TUI applications.
|
|
pub const Terminal = struct {
|
|
allocator: std.mem.Allocator,
|
|
backend: AnsiBackend,
|
|
current_buffer: Buffer,
|
|
previous_buffer: Buffer,
|
|
|
|
/// Initializes the terminal for TUI mode.
|
|
///
|
|
/// Enables raw mode, hides cursor, and clears screen.
|
|
pub fn init(allocator: std.mem.Allocator) !Terminal {
|
|
var backend = AnsiBackend.init();
|
|
|
|
// Get terminal size
|
|
const size = backend.getSize();
|
|
const rect = Rect.init(0, 0, size.width, size.height);
|
|
|
|
var current_buffer = try Buffer.init(allocator, rect);
|
|
var previous_buffer = try Buffer.init(allocator, rect);
|
|
|
|
// Enter alternate screen, hide cursor, enable raw mode
|
|
try backend.enterAlternateScreen();
|
|
try backend.hideCursor();
|
|
try backend.enableRawMode();
|
|
try backend.clear();
|
|
|
|
// Mark all cells as needing redraw
|
|
current_buffer.markDirty();
|
|
previous_buffer.markClean();
|
|
|
|
return .{
|
|
.allocator = allocator,
|
|
.backend = backend,
|
|
.current_buffer = current_buffer,
|
|
.previous_buffer = previous_buffer,
|
|
};
|
|
}
|
|
|
|
/// Cleans up terminal state.
|
|
///
|
|
/// Shows cursor, exits alternate screen, and restores terminal mode.
|
|
pub fn deinit(self: *Terminal) void {
|
|
self.backend.disableRawMode() catch {};
|
|
self.backend.showCursor() catch {};
|
|
self.backend.leaveAlternateScreen() catch {};
|
|
|
|
self.current_buffer.deinit();
|
|
self.previous_buffer.deinit();
|
|
}
|
|
|
|
/// Returns the current terminal area.
|
|
pub fn area(self: *const Terminal) Rect {
|
|
return self.current_buffer.area;
|
|
}
|
|
|
|
/// Returns a pointer to the current buffer for rendering.
|
|
pub fn buffer(self: *Terminal) *Buffer {
|
|
return &self.current_buffer;
|
|
}
|
|
|
|
/// Draws the UI by calling the provided render function.
|
|
///
|
|
/// The render function receives the terminal area and buffer,
|
|
/// and should render all widgets to the buffer.
|
|
pub fn draw(self: *Terminal, comptime render_fn: fn (Rect, *Buffer) void) !void {
|
|
// Clear buffer
|
|
self.current_buffer.clear();
|
|
|
|
// Call user's render function
|
|
render_fn(self.area(), &self.current_buffer);
|
|
|
|
// Flush changes to terminal
|
|
try self.flush();
|
|
}
|
|
|
|
/// Draws using a context-aware render function.
|
|
pub fn drawWithContext(
|
|
self: *Terminal,
|
|
context: anytype,
|
|
comptime render_fn: fn (@TypeOf(context), Rect, *Buffer) void,
|
|
) !void {
|
|
self.current_buffer.clear();
|
|
render_fn(context, self.area(), &self.current_buffer);
|
|
try self.flush();
|
|
}
|
|
|
|
/// Flushes buffer changes to the terminal.
|
|
///
|
|
/// 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);
|
|
|
|
// Update previous buffer
|
|
if (self.previous_buffer.getPtr(x, y)) |prev| {
|
|
prev.* = current;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
try self.backend.flush();
|
|
}
|
|
|
|
/// Resizes the terminal buffers.
|
|
pub fn resize(self: *Terminal, width: u16, height: u16) !void {
|
|
const new_rect = Rect.init(0, 0, width, height);
|
|
|
|
self.current_buffer.deinit();
|
|
self.previous_buffer.deinit();
|
|
|
|
self.current_buffer = try Buffer.init(self.allocator, new_rect);
|
|
self.previous_buffer = try Buffer.init(self.allocator, new_rect);
|
|
|
|
self.current_buffer.markDirty();
|
|
}
|
|
|
|
/// Clears the terminal screen.
|
|
pub fn clear(self: *Terminal) !void {
|
|
try self.backend.clear();
|
|
self.current_buffer.clear();
|
|
self.previous_buffer.clear();
|
|
self.current_buffer.markDirty();
|
|
}
|
|
};
|
|
|
|
// Helper functions for comparison
|
|
fn colorEqual(a: @import("style.zig").Color, b: @import("style.zig").Color) bool {
|
|
return std.meta.eql(a, b);
|
|
}
|
|
|
|
fn modifierEqual(a: @import("style.zig").Modifier, b: @import("style.zig").Modifier) bool {
|
|
return std.meta.eql(a, b);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
test "Terminal type exists" {
|
|
// Basic compilation test - actual terminal tests require real terminal
|
|
_ = Terminal;
|
|
}
|