diff options
Diffstat (limited to 'src/macos.zig')
| -rw-r--r-- | src/macos.zig | 82 |
1 files changed, 78 insertions, 4 deletions
diff --git a/src/macos.zig b/src/macos.zig index 3c1458a3..38d2c961 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -172,6 +172,10 @@ const scene_frame_hz: u32 = 60; /// Keep the float-valued time and noise frame precise, then repeat after a /// little over an hour. None of the effects has state across this boundary. const scene_frame_wrap: u32 = 4096 * scene_frame_hz; +/// FileWatcher.swift keys sources by an opaque u8. Pane ids occupy 0..15; +/// the next value is the one process-global ThemeFile source. +const theme_watch_pane: u8 = @intCast(pardes.MAX_PANES); +const watch_slot_count = pardes.MAX_PANES + 1; // ---------------------------------------------------------------- state @@ -201,9 +205,9 @@ const WatchedFile = struct { }; const FileWatches = struct { - entries: [pardes.MAX_PANES]?WatchedFile = @splat(null), - generations: [pardes.MAX_PANES]u32 = @splat(0), - dirty: [pardes.MAX_PANES]bool = @splat(false), + entries: [watch_slot_count]?WatchedFile = @splat(null), + generations: [watch_slot_count]u32 = @splat(0), + dirty: [watch_slot_count]bool = @splat(false), fn nextGeneration(watches: *FileWatches, pane: u8) u32 { watches.generations[pane] +%= 1; @@ -617,6 +621,7 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void { const found = user_config.load(io, config_arena.allocator(), env); opts.startup_config = found.bytes; opts.startup_config_path = found.path; + opts.config_dir = found.dir; } pardes.image.start(io, allocs.image); @@ -729,6 +734,10 @@ export fn pardes_deinit() void { const generation = st.file_watches.stop(st.gpa, id); hostWatchFile(id, generation, null, 0); }; + if (st.file_watches.entries[theme_watch_pane] != null) { + const generation = st.file_watches.stop(st.gpa, theme_watch_pane); + hostWatchFile(theme_watch_pane, generation, null, 0); + } // Every reader is joined here, before anything it touches is freed. The // runtime joins its tasks on exit, so a reader left parked in read(2) would // hang the process instead of the app quitting. @@ -932,6 +941,57 @@ fn setFileWatch(st: *State, pane: u8, on: bool) void { _ = reloadWatchedFile(st, pane, false); } +fn setThemeFileWatch(st: *State, request_generation: u32, on: bool, animate: bool) void { + const stopped = st.file_watches.stop(st.gpa, theme_watch_pane); + hostWatchFile(theme_watch_pane, stopped, null, 0); + if (!on) return; + const request = st.core.themeFileRequest(request_generation) orelse return; + const bytes = look.readFile(st.gpa, request.path) catch |err| { + st.core.failThemeFile(request_generation, err); + return; + }; + defer st.gpa.free(bytes); + if (!st.core.loadThemeFile(request_generation, bytes, animate)) return; + const callback_generation = st.file_watches.replace( + st.gpa, + theme_watch_pane, + request.path, + request_generation, + .{ .text = std.hash.Wyhash.hash(0, bytes) }, + ) catch return; + const watched = st.file_watches.entries[theme_watch_pane].?; + hostWatchFile(theme_watch_pane, callback_generation, watched.path.ptr, watched.path.len); + // Read-before-watch has the same rename-over gap as document panes. One + // immediate reconciliation after Swift activates the source closes it. + _ = reloadWatchedTheme(st, false); +} + +fn reloadWatchedTheme(st: *State, announce: bool) bool { + const watched = if (st.file_watches.entries[theme_watch_pane]) |*entry| entry else return false; + const request = st.core.themeFileRequest(watched.serial) orelse return false; + if (!std.mem.eql(u8, watched.path, request.path)) return false; + const bytes = look.readFile(st.gpa, watched.path) catch |err| { + st.core.failThemeFile(watched.serial, err); + return false; + }; + defer st.gpa.free(bytes); + const hash = std.hash.Wyhash.hash(0, bytes); + switch (watched.generation_on_disk) { + .text => |accepted| if (accepted == hash) return false, + .pdf => unreachable, + } + if (!st.core.loadThemeFile(watched.serial, bytes, true)) return false; + const live = if (st.file_watches.entries[theme_watch_pane]) |*entry| entry else return false; + if (live.serial != watched.serial) return false; + live.generation_on_disk = .{ .text = hash }; + live.retries_left = 0; + if (announce) { + var mbuf: [256]u8 = undefined; + st.core.setMessage(request.pane, message.stamp(&mbuf, "reloaded theme", request.path)); + } + return true; +} + /// Read and apply on the main thread. Swift only says that this path or its /// parent changed. Text hashes an exact bounded snapshot; PDFs may be much /// larger than that bound and MuPDF reopens the path itself, so they compare a @@ -1027,7 +1087,7 @@ fn reloadWatchedFile(st: *State, pane: u8, announce: bool) bool { /// IO and state mutation stay in pardes_tick with pty/nested messages. export fn pardes_watch_changed(pane: u8, generation: u32) void { const st = &(state orelse return); - if (pane >= pardes.MAX_PANES) return; + if (pane >= watch_slot_count) return; if (st.file_watches.notify(pane, generation)) wake(st); } @@ -1083,6 +1143,9 @@ export fn pardes_tick() bool { if (!st.file_watches.takeDirty(id)) continue; if (reloadWatchedFile(st, id, true)) changed = true; } + if (st.file_watches.takeDirty(theme_watch_pane)) { + if (reloadWatchedTheme(st, true)) changed = true; + } refreshCwds(st); if (drainEffects(st, true)) changed = true; return changed; @@ -1748,6 +1811,16 @@ fn drainEffects(st: *State, threads_ok: bool) bool { _ = libc.close(fd); core.setLastDump(path); }, + .dump_themes => |request| { + const config_dir = core.opts.config_dir orelse continue; + const out_dir = user_config.dumpThemes(st.io, st.gpa, config_dir, pardes.themes) catch |err| { + core.reportError(request.pane, "dump themes", err); + continue; + }; + defer st.gpa.free(out_dir); + var mbuf: [256]u8 = undefined; + core.setMessage(request.pane, message.stamp(&mbuf, "dumped themes", out_dir)); + }, .set_clipboard => { const cb = st.runtime.set_clipboard orelse continue; const y = core.yank orelse continue; @@ -1771,6 +1844,7 @@ fn drainEffects(st: *State, threads_ok: bool) bool { // dot-Tab for the rest of the session does nothing at all. .lsp => |q| core.update(.{ .lsp_resp = .{ .id = q.id, .rows = "" } }), .watch => |watch| setFileWatch(st, watch.pane, watch.on), + .theme_file => |request| setThemeFileWatch(st, request.generation, request.on, threads_ok), .pipe => {}, .quit => {}, } |
