From ef45ce693497f495a6d4b9c00db7f2a881662837 Mon Sep 17 00:00:00 2001 From: reugenio Date: Thu, 18 Dec 2025 12:56:51 +0100 Subject: [PATCH] feat: Add FileWatcher and loadFromString MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FileWatcher: Observa archivo para detectar cambios via mtime - Intervalo configurable entre verificaciones - reset() y updateMtime() para control manual - loadFromString(): Carga config desde string (para defaults embebidos) - Tests para ambas funcionalidades 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/zcatconfig.zig | 164 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/src/zcatconfig.zig b/src/zcatconfig.zig index 85718a2..1e4b252 100644 --- a/src/zcatconfig.zig +++ b/src/zcatconfig.zig @@ -806,6 +806,126 @@ const loadFn = load; /// Funcion save renombrada para evitar conflicto con metodo const saveFn = save; +// ============================================================================= +// FILE WATCHER +// ============================================================================= + +/// Observa un archivo para detectar cambios (via mtime) +/// +/// Uso tipico: +/// ```zig +/// var watcher = FileWatcher.init("config.txt", 1000); // check cada 1s +/// // En main loop: +/// if (watcher.checkForChanges()) { +/// // Recargar configuracion +/// } +/// ``` +pub const FileWatcher = struct { + path: []const u8, + last_mtime: i128 = 0, + check_interval_ms: i64 = 1000, + last_check: i64 = 0, + + /// Inicializa un FileWatcher para el path dado + /// @param path: ruta del archivo a observar + /// @param check_interval_ms: intervalo minimo entre verificaciones (default 1000ms) + pub fn init(path: []const u8, check_interval_ms: i64) FileWatcher { + return .{ + .path = path, + .check_interval_ms = check_interval_ms, + }; + } + + /// Verifica si el archivo ha cambiado desde la ultima verificacion + /// Respeta el intervalo minimo entre verificaciones + /// @return true si el archivo cambio, false si no o si hubo error + pub fn checkForChanges(self: *FileWatcher) bool { + const now = std.time.milliTimestamp(); + + // Respetar intervalo minimo + if (now - self.last_check < self.check_interval_ms) { + return false; + } + self.last_check = now; + + // Obtener mtime del archivo + const stat = std.fs.cwd().statFile(self.path) catch { + return false; + }; + + const file_mtime = stat.mtime; + + // Primera vez - guardar mtime inicial + if (self.last_mtime == 0) { + self.last_mtime = file_mtime; + return false; + } + + // Verificar si cambio + if (file_mtime != self.last_mtime) { + self.last_mtime = file_mtime; + return true; + } + + return false; + } + + /// Reinicia el watcher (olvida el mtime anterior) + pub fn reset(self: *FileWatcher) void { + self.last_mtime = 0; + self.last_check = 0; + } + + /// Fuerza actualizar el mtime sin detectar cambio + /// Util despues de guardar el archivo + pub fn updateMtime(self: *FileWatcher) void { + const stat = std.fs.cwd().statFile(self.path) catch return; + self.last_mtime = stat.mtime; + } +}; + +// ============================================================================= +// LOAD FROM STRING +// ============================================================================= + +/// Carga configuracion desde un string (util para defaults embebidos) +/// Similar a load() pero sin leer archivo +pub fn loadFromString( + comptime variables: []const ConfigVariable, + comptime ConfigType: type, + config: *ConfigType, + content: []const u8, +) void { + const EngineType = Engine(variables, ConfigType); + + // Parsear linea por linea (igual que load()) + var lines = std.mem.splitScalar(u8, content, '\n'); + while (lines.next()) |line| { + const trimmed = std.mem.trim(u8, line, " \t\r"); + if (trimmed.len == 0) continue; + if (trimmed[0] == '#') continue; + if (trimmed[0] != '@') continue; + + // Buscar separador ':' + const colon_pos = std.mem.indexOf(u8, trimmed, ":") orelse continue; + const key = std.mem.trim(u8, trimmed[0..colon_pos], " \t"); + const after_colon = trimmed[colon_pos + 1 ..]; + + // Quitar comentario inline + const comment_pos = findCommentStart(after_colon); + const value_with_spaces = if (comment_pos) |pos| + after_colon[0..pos] + else + after_colon; + const value = std.mem.trim(u8, value_with_spaces, " \t"); + + // Aplicar el valor + EngineType.set(config, key, value) catch { + continue; + }; + } +} + // ============================================================================= // TESTS // ============================================================================= @@ -965,3 +1085,47 @@ test "ConfigResult toString" { const color_result = ConfigResult{ .color = Color.rgb(255, 0, 128) }; try std.testing.expectEqualStrings("RGB(255,0,128)", color_result.toString(&buf)); } + +test "FileWatcher init" { + const watcher = FileWatcher.init("test.txt", 500); + try std.testing.expectEqualStrings("test.txt", watcher.path); + try std.testing.expectEqual(@as(i64, 500), watcher.check_interval_ms); + try std.testing.expectEqual(@as(i128, 0), watcher.last_mtime); +} + +test "loadFromString basic" { + const test_vars = [_]ConfigVariable{ + .{ + .name = "enabled", + .config_key = "@enabled", + .var_type = .boolean, + .default = "Si", + .description = "Test", + }, + .{ + .name = "count", + .config_key = "@count", + .var_type = .integer, + .default = "10", + .description = "Test", + }, + }; + + const TestConfig = struct { + enabled: bool = true, + count: i32 = 10, + }; + + var config = TestConfig{}; + + const content = + \\# Comentario + \\@enabled: No + \\@count: 42 # inline comment + ; + + loadFromString(&test_vars, TestConfig, &config, content); + + try std.testing.expectEqual(false, config.enabled); + try std.testing.expectEqual(@as(i32, 42), config.count); +}