build: Migrar a Zig 0.16

- Crear src/utils/time.zig para centralizar helpers de tiempo (timestamp, milliTimestamp, nanoTimestamp).
- Reemplazar std.io.fixedBufferStream por std.Io.Writer.fixed.
- Migrar de std.fs.cwd() a std.Io.Dir.cwd() y actualizar firmas de lectura/escritura.
- Reemplazar std.Thread.sleep por std.posix.system.nanosleep.
- Corregir capturas descartadas en switches (text_input => {}).
- Actualizar tests y ejemplos para compatibilidad con std.Io y nuevos helpers.
- Actualizar build.zig.zon a 0.16.0.

Co-Authored-By: Gemini <noreply@google.com>
This commit is contained in:
R.Eugenio 2026-01-18 02:01:04 +01:00
parent b3a5f94ea4
commit f9179b4e9a
18 changed files with 111 additions and 68 deletions

View file

@ -2,7 +2,7 @@
.fingerprint = 0x30a5cd33d0b0066c, .fingerprint = 0x30a5cd33d0b0066c,
.name = .zcatgui, .name = .zcatgui,
.version = "0.1.0", .version = "0.1.0",
.minimum_zig_version = "0.15.0", .minimum_zig_version = "0.16.0",
.dependencies = .{ .dependencies = .{
.zcatttf = .{ .zcatttf = .{

View file

@ -75,7 +75,8 @@ pub fn main() !void {
frame += 1; frame += 1;
// Cap at ~60 FPS // Control frame rate (approx 60 FPS)
std.Thread.sleep(16 * std.time.ns_per_ms); const ts = std.posix.timespec{ .sec = 0, .nsec = 16 * std.time.ns_per_ms };
_ = std.posix.system.nanosleep(&ts, null);
} }
} }

View file

@ -109,7 +109,7 @@ pub fn main() !void {
.s => { .s => {
if (recorder.eventCount() > 0) { if (recorder.eventCount() > 0) {
recorder.save("macro.zcm") catch |err| { recorder.save(std.Options.debug_io, "macro.zcm") catch |err| {
std.debug.print("Save failed: {}\n", .{err}); std.debug.print("Save failed: {}\n", .{err});
}; };
status_message = "Saved to macro.zcm"; status_message = "Saved to macro.zcm";
@ -118,7 +118,7 @@ pub fn main() !void {
}, },
.l => { .l => {
recorder.load("macro.zcm") catch |err| { recorder.load(std.Options.debug_io, "macro.zcm") catch |err| {
std.debug.print("Load failed: {}\n", .{err}); std.debug.print("Load failed: {}\n", .{err});
}; };
status_message = "Loaded from macro.zcm"; status_message = "Loaded from macro.zcm";
@ -169,7 +169,8 @@ pub fn main() !void {
backend.present(&fb); backend.present(&fb);
// Cap at ~60 FPS // Cap at ~60 FPS
std.Thread.sleep(16 * std.time.ns_per_ms); const ts = std.posix.timespec{ .sec = 0, .nsec = 16 * std.time.ns_per_ms };
_ = std.posix.system.nanosleep(&ts, null);
} }
std.debug.print("Final status: {s}\n", .{status_message}); std.debug.print("Final status: {s}\n", .{status_message});

View file

@ -334,7 +334,8 @@ pub fn main() !void {
frame += 1; frame += 1;
// Cap at ~60 FPS // Cap at ~60 FPS
std.Thread.sleep(16 * std.time.ns_per_ms); const ts = std.posix.timespec{ .sec = 0, .nsec = 16 * std.time.ns_per_ms };
_ = std.posix.system.nanosleep(&ts, null);
} }
print("\n=== Demo Complete ===\n", .{}); print("\n=== Demo Complete ===\n", .{});

View file

@ -310,8 +310,7 @@ pub const Info = struct {
/// Format as accessible text announcement /// Format as accessible text announcement
pub fn announce(self: Self, buf: []u8) []const u8 { pub fn announce(self: Self, buf: []u8) []const u8 {
var stream = std.io.fixedBufferStream(buf); var writer = std.Io.Writer.fixed(buf);
const writer = stream.writer();
// Label // Label
if (self.label.len > 0) { if (self.label.len > 0) {
@ -319,40 +318,44 @@ pub const Info = struct {
} }
// Role // Role
writer.print(", {s}", .{self.role.name()}) catch return buf[0..stream.pos]; writer.print(", {s}", .{self.role.name()}) catch return buf[0..writer.end];
// Value // Value
if (self.value) |v| { if (self.value) |v| {
writer.print(", {s}", .{v}) catch return buf[0..stream.pos]; writer.print(", {s}", .{v}) catch return buf[0..writer.end];
} else if (self.value_now) |v| { } else if (self.value_now) |v| {
if (self.value_min != null and self.value_max != null) { if (self.value_min != null and self.value_max != null) {
writer.print(", {d:.0} of {d:.0}", .{ v, self.value_max.? }) catch return buf[0..stream.pos]; writer.print(", {d:.0} of {d:.0}", .{ v, self.value_max.? }) catch return buf[0..writer.end];
} else {
writer.print(", {d:.0}", .{v}) catch return buf[0..writer.end];
} }
} }
// State // State
if (self.state.disabled) { if (self.state.focused) {
writer.writeAll(", disabled") catch return buf[0..stream.pos]; writer.writeAll(", focused") catch return buf[0..writer.end];
}
if (self.state.checked) {
writer.writeAll(", checked") catch return buf[0..stream.pos];
}
if (self.state.expanded) {
writer.writeAll(", expanded") catch return buf[0..stream.pos];
} }
if (self.state.selected) { if (self.state.selected) {
writer.writeAll(", selected") catch return buf[0..stream.pos]; writer.writeAll(", selected") catch return buf[0..writer.end];
} }
if (self.state.invalid) { if (self.state.disabled) {
writer.writeAll(", invalid") catch return buf[0..stream.pos]; writer.writeAll(", disabled") catch return buf[0..writer.end];
}
if (self.state.checked) {
writer.writeAll(", checked") catch return buf[0..writer.end];
}
if (self.state.expanded) {
writer.writeAll(", expanded") catch return buf[0..writer.end];
} else if (self.role == .menu or self.role == .treeitem) {
writer.writeAll(", collapsed") catch return buf[0..writer.end];
} }
// Position // Shortcut
if (self.pos_in_set != null and self.set_size != null) { if (self.shortcut.len > 0) {
writer.print(", {d} of {d}", .{ self.pos_in_set.?, self.set_size.? }) catch return buf[0..stream.pos]; writer.print(", shortcut {s}", .{self.shortcut}) catch return buf[0..writer.end];
} }
return buf[0..stream.pos]; return buf[0..writer.end];
} }
}; };

View file

@ -257,7 +257,7 @@ pub const MainLoop = struct {
.resize => |size| { .resize => |size| {
self.handleResize(size.width, size.height) catch {}; self.handleResize(size.width, size.height) catch {};
}, },
.text_input => |_| { .text_input => {
self.needs_redraw = true; self.needs_redraw = true;
}, },
} }

View file

@ -249,8 +249,7 @@ pub const ShortcutManager = struct {
/// Format a shortcut as human-readable text /// Format a shortcut as human-readable text
pub fn formatShortcut(buf: []u8, shortcut: Shortcut) []const u8 { pub fn formatShortcut(buf: []u8, shortcut: Shortcut) []const u8 {
var stream = std.io.fixedBufferStream(buf); var writer = std.Io.Writer.fixed(buf);
const writer = stream.writer();
if (shortcut.modifiers.ctrl) { if (shortcut.modifiers.ctrl) {
writer.writeAll("Ctrl+") catch return ""; writer.writeAll("Ctrl+") catch return "";
@ -268,7 +267,7 @@ pub fn formatShortcut(buf: []u8, shortcut: Shortcut) []const u8 {
const key_name = keyName(shortcut.key); const key_name = keyName(shortcut.key);
writer.writeAll(key_name) catch return ""; writer.writeAll(key_name) catch return "";
return buf[0..stream.pos]; return buf[0..writer.end];
} }
/// Get human-readable name for a key /// Get human-readable name for a key

View file

@ -115,12 +115,12 @@ pub const MacroRecorder = struct {
} }
/// Save recorded events to a file /// Save recorded events to a file
pub fn save(self: *Self, path: []const u8) !void { pub fn save(self: *Self, io: std.Io, path: []const u8) !void {
const file = try std.fs.cwd().createFile(path, .{}); const file = try std.Io.Dir.cwd().createFile(io, path, .{});
defer file.close(); defer file.close(io);
// Write header directly // Write header directly
_ = try file.write("ZCATGUI_MACRO_V1\n"); _ = try file.writeStreamingAll(io, "ZCATGUI_MACRO_V1\n");
// Write events // Write events
var line_buf: [128]u8 = undefined; var line_buf: [128]u8 = undefined;
@ -132,20 +132,20 @@ pub const MacroRecorder = struct {
event.char orelse 0, event.char orelse 0,
@as(u8, if (event.pressed) 1 else 0), @as(u8, if (event.pressed) 1 else 0),
}) catch continue; }) catch continue;
_ = try file.write(line); _ = try file.writeStreamingAll(io, line);
} }
} }
/// Load events from a file /// Load events from a file
pub fn load(self: *Self, path: []const u8) !void { pub fn load(self: *Self, io: std.Io, path: []const u8) !void {
const file = try std.fs.cwd().openFile(path, .{}); const file = try std.Io.Dir.cwd().openFile(io, path, .{});
defer file.close(); defer file.close(io);
// Read file content into buffer // Read file content into buffer
const stat = try file.stat(); const stat = try file.stat(io);
const content = try self.allocator.alloc(u8, stat.size); const content = try self.allocator.alloc(u8, @intCast(stat.size));
defer self.allocator.free(content); defer self.allocator.free(content);
_ = try file.readAll(content); _ = try file.readPositional(io, &.{content}, 0);
// Parse content line by line // Parse content line by line
var lines = std.mem.splitScalar(u8, content, '\n'); var lines = std.mem.splitScalar(u8, content, '\n');
@ -203,7 +203,11 @@ pub const MacroPlayer = struct {
for (events) |event| { for (events) |event| {
inject_fn(event); inject_fn(event);
if (delay_ms > 0) { if (delay_ms > 0) {
std.Thread.sleep(delay_ms * std.time.ns_per_ms); const ts = std.posix.timespec{
.sec = @intCast(@divTrunc(delay_ms, 1000)),
.nsec = @intCast((delay_ms % 1000) * std.time.ns_per_ms),
};
_ = std.posix.system.nanosleep(&ts, null);
} }
} }
} }

View file

@ -126,7 +126,7 @@ pub const DetailPanelBase = struct {
/// Transiciona automaticamente de saved -> viewing despues de 2 segundos. /// Transiciona automaticamente de saved -> viewing despues de 2 segundos.
pub fn updateSavedTimer(self: *Self) void { pub fn updateSavedTimer(self: *Self) void {
if (self.state == .saved) { if (self.state == .saved) {
const elapsed = std.time.milliTimestamp() - self.saved_timer; const elapsed = zcatgui.utils.milliTimestamp() - self.saved_timer;
if (elapsed >= 2000) { if (elapsed >= 2000) {
self.state = .viewing; self.state = .viewing;
} }
@ -141,7 +141,7 @@ pub const DetailPanelBase = struct {
/// Inicia el timer para volver a viewing. /// Inicia el timer para volver a viewing.
pub fn markSaved(self: *Self) void { pub fn markSaved(self: *Self) void {
self.state = .saved; self.state = .saved;
self.saved_timer = std.time.milliTimestamp(); self.saved_timer = zcatgui.utils.milliTimestamp();
self.error_message = null; self.error_message = null;
} }

View file

@ -101,14 +101,14 @@ pub const TtfFont = struct {
const Self = @This(); const Self = @This();
/// Load font from file /// Load font from file
pub fn loadFromFile(allocator: Allocator, path: []const u8) !Self { pub fn loadFromFile(io: std.Io, allocator: Allocator, path: []const u8) !Self {
const file = try std.fs.cwd().openFile(path, .{}); const file = try std.Io.Dir.cwd().openFile(io, path, .{});
defer file.close(); defer file.close(io);
const stat = try file.stat(); const stat = try file.stat(io);
const data = try allocator.alloc(u8, stat.size); const data = try allocator.alloc(u8, @intCast(stat.size));
const bytes_read = try file.readAll(data); const bytes_read = try file.readPositional(io, &.{data}, 0);
if (bytes_read != stat.size) { if (bytes_read != stat.size) {
allocator.free(data); allocator.free(data);
return error.IncompleteRead; return error.IncompleteRead;
@ -565,9 +565,10 @@ test "decodeUtf8" {
test "TtfFont with zcatttf" { test "TtfFont with zcatttf" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const io = std.testing.io;
// Try to load DroidSans from system // Try to load DroidSans from system
var font = TtfFont.loadFromFile(allocator, "/usr/share/fonts/google-droid-sans-fonts/DroidSans.ttf") catch { var font = TtfFont.loadFromFile(io, allocator, "/usr/share/fonts/google-droid-sans-fonts/DroidSans.ttf") catch {
// Font not available on this system, skip test // Font not available on this system, skip test
return; return;
}; };

View file

@ -17,6 +17,7 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const time = @import("time.zig");
/// High-resolution timer for benchmarking /// High-resolution timer for benchmarking
pub const Timer = struct { pub const Timer = struct {
@ -35,13 +36,13 @@ pub const Timer = struct {
} }
pub fn start(self: *Self) void { pub fn start(self: *Self) void {
self.start_time = std.time.nanoTimestamp(); self.start_time = time.nanoTimestamp();
self.running = true; self.running = true;
} }
pub fn stop(self: *Self) void { pub fn stop(self: *Self) void {
if (self.running) { if (self.running) {
self.elapsed_ns = std.time.nanoTimestamp() - self.start_time; self.elapsed_ns = time.nanoTimestamp() - self.start_time;
self.running = false; self.running = false;
} }
} }
@ -54,7 +55,7 @@ pub const Timer = struct {
pub fn elapsedNs(self: Self) i128 { pub fn elapsedNs(self: Self) i128 {
if (self.running) { if (self.running) {
return std.time.nanoTimestamp() - self.start_time; return time.nanoTimestamp() - self.start_time;
} }
return self.elapsed_ns; return self.elapsed_ns;
} }
@ -318,11 +319,11 @@ pub const FrameTimer = struct {
} }
pub fn beginFrame(self: *Self) void { pub fn beginFrame(self: *Self) void {
self.frame_start = std.time.nanoTimestamp(); self.frame_start = time.nanoTimestamp();
} }
pub fn endFrame(self: *Self) void { pub fn endFrame(self: *Self) void {
self.last_frame_ns = std.time.nanoTimestamp() - self.frame_start; self.last_frame_ns = time.nanoTimestamp() - self.frame_start;
self.frame_count += 1; self.frame_count += 1;
self.total_time_ns += self.last_frame_ns; self.total_time_ns += self.last_frame_ns;
self.min_frame_ns = @min(self.min_frame_ns, self.last_frame_ns); self.min_frame_ns = @min(self.min_frame_ns, self.last_frame_ns);
@ -505,7 +506,8 @@ pub fn benchmarkCommands(allocator: Allocator) !void {
test "Timer basic" { test "Timer basic" {
var timer = Timer.init(); var timer = Timer.init();
timer.start(); timer.start();
std.Thread.sleep(1_000_000); // 1ms const ts = std.posix.timespec{ .sec = 0, .nsec = 1_000_000 };
_ = std.posix.system.nanosleep(&ts, null);
timer.stop(); timer.stop();
try std.testing.expect(timer.elapsedNs() > 0); try std.testing.expect(timer.elapsedNs() > 0);
@ -517,7 +519,8 @@ test "Benchmark basic" {
for (0..10) |_| { for (0..10) |_| {
bench.startIteration(); bench.startIteration();
std.Thread.sleep(100_000); // 0.1ms const ts = std.posix.timespec{ .sec = 0, .nsec = 100_000 };
_ = std.posix.system.nanosleep(&ts, null);
bench.endIteration(); bench.endIteration();
} }
@ -544,7 +547,8 @@ test "FrameTimer" {
for (0..5) |_| { for (0..5) |_| {
timer.beginFrame(); timer.beginFrame();
std.Thread.sleep(1_000_000); // 1ms const ts = std.posix.timespec{ .sec = 0, .nsec = 1_000_000 };
_ = std.posix.system.nanosleep(&ts, null);
timer.endFrame(); timer.endFrame();
} }

19
src/utils/time.zig Normal file
View file

@ -0,0 +1,19 @@
const std = @import("std");
/// Returns the current Unix timestamp in seconds
pub fn timestamp() i64 {
const ts = std.posix.clock_gettime(std.posix.CLOCK.REALTIME) catch return 0;
return ts.sec;
}
/// Returns the current Unix timestamp in milliseconds
pub fn milliTimestamp() i64 {
const ts = std.posix.clock_gettime(std.posix.CLOCK.REALTIME) catch return 0;
return ts.sec * 1000 + @divTrunc(ts.nsec, 1_000_000);
}
/// Returns the current Unix timestamp in nanoseconds
pub fn nanoTimestamp() i128 {
const ts = std.posix.clock_gettime(std.posix.CLOCK.REALTIME) catch return 0;
return @as(i128, ts.sec) * 1_000_000_000 + ts.nsec;
}

View file

@ -13,6 +13,7 @@ pub const arena = @import("arena.zig");
pub const pool = @import("pool.zig"); pub const pool = @import("pool.zig");
pub const benchmark = @import("benchmark.zig"); pub const benchmark = @import("benchmark.zig");
pub const testing_utils = @import("testing.zig"); pub const testing_utils = @import("testing.zig");
pub const time = @import("time.zig");
// Re-exports // Re-exports
pub const FrameArena = arena.FrameArena; pub const FrameArena = arena.FrameArena;
@ -27,6 +28,10 @@ pub const Timer = benchmark.Timer;
pub const FrameTimer = benchmark.FrameTimer; pub const FrameTimer = benchmark.FrameTimer;
pub const AllocationTracker = benchmark.AllocationTracker; pub const AllocationTracker = benchmark.AllocationTracker;
pub const timestamp = time.timestamp;
pub const milliTimestamp = time.milliTimestamp;
pub const nanoTimestamp = time.nanoTimestamp;
// Testing utilities // Testing utilities
pub const TestRunner = testing_utils.TestRunner; pub const TestRunner = testing_utils.TestRunner;
pub const SnapshotTester = testing_utils.SnapshotTester; pub const SnapshotTester = testing_utils.SnapshotTester;

View file

@ -9,6 +9,7 @@ const Command = @import("../core/command.zig");
const Layout = @import("../core/layout.zig"); const Layout = @import("../core/layout.zig");
const Style = @import("../core/style.zig"); const Style = @import("../core/style.zig");
const Input = @import("../core/input.zig"); const Input = @import("../core/input.zig");
const utils = @import("../utils/utils.zig");
/// Date structure /// Date structure
pub const Date = struct { pub const Date = struct {
@ -29,7 +30,7 @@ pub const Date = struct {
/// Get today's date (simplified - uses epoch calculation) /// Get today's date (simplified - uses epoch calculation)
pub fn today() Self { pub fn today() Self {
const ts = std.time.timestamp(); const ts = utils.timestamp();
return fromTimestamp(ts); return fromTimestamp(ts);
} }

View file

@ -9,6 +9,7 @@ const Context = @import("../core/context.zig").Context;
const Command = @import("../core/command.zig"); const Command = @import("../core/command.zig");
const Layout = @import("../core/layout.zig"); const Layout = @import("../core/layout.zig");
const Style = @import("../core/style.zig"); const Style = @import("../core/style.zig");
const utils = @import("../utils/utils.zig");
/// Image pixel format /// Image pixel format
pub const Format = enum { pub const Format = enum {
@ -166,7 +167,7 @@ pub const ImageCache = struct {
/// Get image from cache /// Get image from cache
pub fn get(self: *Self, id: u64) ?*CachedImage { pub fn get(self: *Self, id: u64) ?*CachedImage {
if (self.entries.getPtr(id)) |entry| { if (self.entries.getPtr(id)) |entry| {
entry.last_used = std.time.milliTimestamp(); entry.last_used = utils.milliTimestamp();
return entry; return entry;
} }
return null; return null;
@ -184,7 +185,7 @@ pub const ImageCache = struct {
// Add entry // Add entry
self.entries.put(id, .{ self.entries.put(id, .{
.data = data, .data = data,
.last_used = std.time.milliTimestamp(), .last_used = utils.milliTimestamp(),
.size_bytes = size, .size_bytes = size,
}) catch return; }) catch return;

View file

@ -8,6 +8,7 @@ const Layout = @import("../../core/layout.zig");
const Style = @import("../../core/style.zig"); const Style = @import("../../core/style.zig");
const Rect = Layout.Rect; const Rect = Layout.Rect;
const Color = Style.Color; const Color = Style.Color;
const utils = @import("../../utils/utils.zig");
const render = @import("render.zig"); const render = @import("render.zig");
@ -51,7 +52,7 @@ pub const SpinnerState = struct {
last_update: i64 = 0, last_update: i64 = 0,
pub fn update(self: *SpinnerState, speed: f32) void { pub fn update(self: *SpinnerState, speed: f32) void {
const now = std.time.milliTimestamp(); const now = utils.milliTimestamp();
if (self.last_update == 0) { if (self.last_update == 0) {
self.last_update = now; self.last_update = now;
return; return;

View file

@ -29,6 +29,7 @@ const Layout = @import("../core/layout.zig");
const Style = @import("../core/style.zig"); const Style = @import("../core/style.zig");
const Rect = Layout.Rect; const Rect = Layout.Rect;
const Color = Style.Color; const Color = Style.Color;
const utils = @import("../utils/utils.zig");
// ============================================================================= // =============================================================================
// Types // Types
@ -146,7 +147,7 @@ pub const Toast = struct {
.toast_type = toast_type, .toast_type = toast_type,
.message = undefined, .message = undefined,
.message_len = @min(message.len, 256), .message_len = @min(message.len, 256),
.created_ms = std.time.milliTimestamp(), .created_ms = utils.milliTimestamp(),
.duration_ms = duration_ms, .duration_ms = duration_ms,
.dismissing = false, .dismissing = false,
.animation = 0, .animation = 0,
@ -174,13 +175,13 @@ pub const Toast = struct {
pub fn shouldDismiss(self: *const Self) bool { pub fn shouldDismiss(self: *const Self) bool {
if (self.duration_ms == 0) return false; if (self.duration_ms == 0) return false;
const elapsed = std.time.milliTimestamp() - self.created_ms; const elapsed = utils.milliTimestamp() - self.created_ms;
return elapsed >= self.duration_ms; return elapsed >= self.duration_ms;
} }
pub fn getRemainingMs(self: *const Self) i64 { pub fn getRemainingMs(self: *const Self) i64 {
if (self.duration_ms == 0) return -1; if (self.duration_ms == 0) return -1;
const elapsed = std.time.milliTimestamp() - self.created_ms; const elapsed = utils.milliTimestamp() - self.created_ms;
return @max(0, @as(i64, self.duration_ms) - elapsed); return @max(0, @as(i64, self.duration_ms) - elapsed);
} }
}; };

View file

@ -28,6 +28,7 @@ const Layout = @import("../core/layout.zig");
const Style = @import("../core/style.zig"); const Style = @import("../core/style.zig");
const Rect = Layout.Rect; const Rect = Layout.Rect;
const Color = Style.Color; const Color = Style.Color;
const utils = @import("../utils/utils.zig");
// ============================================================================= // =============================================================================
// Configuration // Configuration
@ -123,7 +124,7 @@ pub const State = struct {
/// Update tooltip visibility based on hover /// Update tooltip visibility based on hover
pub fn update(self: *Self, target: Rect, mouse_x: i32, mouse_y: i32, delay_ms: u32) void { pub fn update(self: *Self, target: Rect, mouse_x: i32, mouse_y: i32, delay_ms: u32) void {
const hovering = self.isHovering(target, mouse_x, mouse_y); const hovering = self.isHovering(target, mouse_x, mouse_y);
const now = std.time.milliTimestamp(); const now = utils.milliTimestamp();
if (hovering) { if (hovering) {
if (self.hover_start_ms == 0) { if (self.hover_start_ms == 0) {