zcatui/src/terminal.zig
reugenio 2a62c0f60b Inicio proyecto zcatui - TUI library para Zig
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>
2025-12-08 01:56:44 +01:00

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;
}