diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-18 14:21:38 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:45:30 -0300 |
| commit | 382abe3dafb872b3e6c9792a9cc0abcf6267e180 (patch) | |
| tree | e30f47d911561db9b02e714287a1658822c77a09 /src/file_watch.zig | |
| parent | c24a9e40215bc30c68c9db7675f1c6a07d9bbae3 (diff) | |
| download | pardes-382abe3dafb872b3e6c9792a9cc0abcf6267e180.tar.gz pardes-382abe3dafb872b3e6c9792a9cc0abcf6267e180.zip | |
file_watch + builtins + config: richer watch semantics, new builtins, config docs
Diffstat (limited to 'src/file_watch.zig')
| -rw-r--r-- | src/file_watch.zig | 158 |
1 files changed, 147 insertions, 11 deletions
diff --git a/src/file_watch.zig b/src/file_watch.zig index b616d481..a5347844 100644 --- a/src/file_watch.zig +++ b/src/file_watch.zig @@ -42,29 +42,30 @@ pub const Watch = struct { generation: Generation, }; -pub const Table = [pardes.MAX_PANES]?Watch; +/// The final slot is the one non-pane watched payload: the active ThemeFile. +/// Sharing the table also shares directory descriptors correctly when a user +/// happens to open that .zon as an ordinary document. +pub const theme_slot = pardes.MAX_PANES; +pub const Table = [pardes.MAX_PANES + 1]?Watch; -/// Mark or unmark one pane pathname. Panes in the same directory share an -/// inotify descriptor, so removing one pane drops the kernel mark only after -/// its last user disappears. -pub fn watchPane( +fn watchPath( fd: c_int, watches: *Table, - id: u8, + slot: usize, path: ?[]const u8, serial: u32, generation: Generation, ) void { if (comptime builtin.os.tag != .linux) return; if (fd < 0) return; - if (watches[id]) |old| { + if (watches[slot]) |old| { var shared = false; for (watches, 0..) |other, i| { const candidate = other orelse continue; - if (i != id and candidate.wd == old.wd) shared = true; + if (i != slot and candidate.wd == old.wd) shared = true; } if (!shared) _ = libc.inotify_rm_watch(fd, old.wd); - watches[id] = null; + watches[slot] = null; } const watched_path = path orelse return; const dir = std.fs.path.dirname(watched_path) orelse "."; @@ -77,7 +78,22 @@ pub fn watchPane( const mask = linux.IN.CLOSE_WRITE | linux.IN.MOVED_TO | linux.IN.CREATE | linux.IN.ONLYDIR; const wd = libc.inotify_add_watch(fd, dir_buf[0..dir.len :0], mask); if (wd < 0) return; - watches[id] = .{ .wd = wd, .serial = serial, .generation = generation }; + watches[slot] = .{ .wd = wd, .serial = serial, .generation = generation }; +} + +/// Mark or unmark one pane pathname. Panes in the same directory share an +/// inotify descriptor, so removing one pane drops the kernel mark only after +/// its last user disappears. +pub fn watchPane( + fd: c_int, + watches: *Table, + id: u8, + path: ?[]const u8, + serial: u32, + generation: Generation, +) void { + std.debug.assert(id < pardes.MAX_PANES); + watchPath(fd, watches, id, path, serial, generation); } /// Reconcile one marked pane. A true result requests an immediate second pass: @@ -178,12 +194,73 @@ pub fn reloadChanged( watches: *Table, ) bool { var retry = false; - for (watches, 0..) |slot, id| { + for (watches[0..pardes.MAX_PANES], 0..) |slot, id| { if (slot != null) retry = reloadPane(core, io, gpa, watches, id, true) or retry; } + if (watches[theme_slot] != null) + retry = reloadTheme(core, gpa, watches, true) or retry; return retry; } +/// Stop the old theme watch, synchronously load the requested snapshot, then +/// install its parent-directory mark and reconcile once to close the read -> +/// watch race. Returns true only when that reconciliation wants another pass. +pub fn applyThemeEffect( + core: *pardes.Pardes, + gpa: std.mem.Allocator, + fd: c_int, + watches: *Table, + generation: u32, + on: bool, + animate: bool, +) bool { + watchPath(fd, watches, theme_slot, null, 0, .{ .text = 0 }); + if (!on) return false; + const request = core.themeFileRequest(generation) orelse return false; + const bytes = look.readFile(gpa, request.path) catch |err| { + core.failThemeFile(generation, err); + return false; + }; + defer gpa.free(bytes); + if (!core.loadThemeFile(generation, bytes, animate)) return false; + const hash = std.hash.Wyhash.hash(0, bytes); + watchPath(fd, watches, theme_slot, request.path, generation, .{ .text = hash }); + if (watches[theme_slot] == null) return false; + return reloadTheme(core, gpa, watches, false); +} + +/// Re-read the one active theme file after a coalesced directory edge. The +/// generation advances only after a valid parse, so a malformed save never +/// replaces the last good colors or blesses bad bytes as the new baseline. +pub fn reloadTheme( + core: *pardes.Pardes, + gpa: std.mem.Allocator, + watches: *Table, + announce: bool, +) bool { + const watched = watches[theme_slot] orelse return false; + const request = core.themeFileRequest(watched.serial) orelse return false; + const bytes = look.readFile(gpa, request.path) catch |err| { + core.failThemeFile(watched.serial, err); + return false; + }; + defer gpa.free(bytes); + const hash = std.hash.Wyhash.hash(0, bytes); + switch (watched.generation) { + .text => |accepted| if (accepted == hash) return false, + .pdf => unreachable, + } + if (!core.loadThemeFile(watched.serial, bytes, true)) return false; + const live = if (watches[theme_slot]) |*entry| entry else return false; + if (live.serial != watched.serial) return false; + live.generation = .{ .text = hash }; + if (announce) { + var msg_buf: [256]u8 = undefined; + core.setMessage(request.pane, message.stamp(&msg_buf, "reloaded theme", request.path)); + } + return false; +} + pub fn identify(io: std.Io, path: []const u8) !Identity { const stat = try std.Io.Dir.cwd().statFile(io, path, .{}); if (stat.kind != .file) return error.NotFile; @@ -216,3 +293,62 @@ test "file identity changes for in-place and rename-over writes" { try std.testing.expect(after.inode != replaced.inode); try std.testing.expect(!after.eql(replaced)); } + +test "theme watch reloads valid ZON and keeps the last theme across a bad save" { + if (comptime builtin.os.tag != .linux or pardes.platform == .web) return; + const io = std.testing.io; + const gpa = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var base_buf: [std.fs.max_path_bytes]u8 = undefined; + const base_len = try tmp.dir.realPath(io, &base_buf); + const path = try std.fs.path.join(gpa, &.{ base_buf[0..base_len], "mine.zon" }); + defer gpa.free(path); + + var first = pardes.themes[0]; + first.name = "watched"; + first.bg = .{ 1, 2, 3 }; + var first_zon: std.Io.Writer.Allocating = .init(gpa); + defer first_zon.deinit(); + try std.zon.stringify.serialize(first, .{ .whitespace = true }, &first_zon.writer); + try tmp.dir.writeFile(io, .{ .sub_path = "mine.zon", .data = first_zon.written() }); + + const core = try pardes.Pardes.init(gpa, .{ .tty_only = true }); + defer core.deinit(); + while (core.nextEffect()) |_| {} + core.requestThemeFile(0, path); + const request = while (core.nextEffect()) |effect| switch (effect) { + .theme_file => |theme_file| break theme_file, + else => {}, + } else return error.MissingThemeFileEffect; + + const fd = libc.inotify_init1(linux.IN.CLOEXEC | linux.IN.NONBLOCK); + if (fd < 0) return error.InotifyInitFailed; + defer _ = libc.close(fd); + var watches: Table = @splat(null); + defer _ = applyThemeEffect(core, gpa, fd, &watches, 0, false, false); + try std.testing.expect(!applyThemeEffect( + core, + gpa, + fd, + &watches, + request.generation, + true, + false, + )); + try std.testing.expectEqual(@as(?[3]u8, .{ 1, 2, 3 }), core.theme().bg); + try std.testing.expect(watches[theme_slot] != null); + + var second = first; + second.bg = .{ 9, 8, 7 }; + var second_zon: std.Io.Writer.Allocating = .init(gpa); + defer second_zon.deinit(); + try std.zon.stringify.serialize(second, .{ .whitespace = true }, &second_zon.writer); + try tmp.dir.writeFile(io, .{ .sub_path = "mine.zon", .data = second_zon.written() }); + try std.testing.expect(!reloadTheme(core, gpa, &watches, false)); + try std.testing.expectEqual(@as(?[3]u8, .{ 9, 8, 7 }), core.theme().bg); + + try tmp.dir.writeFile(io, .{ .sub_path = "mine.zon", .data = ".{ .name = " }); + try std.testing.expect(!reloadTheme(core, gpa, &watches, false)); + try std.testing.expectEqual(@as(?[3]u8, .{ 9, 8, 7 }), core.theme().bg); +} |
