From e2b84a8af4cc19ad5816e6bb4d8a295b14a1cfee Mon Sep 17 00:00:00 2001 From: Vinesh Benny Date: Sat, 11 Jul 2026 11:18:59 -0400 Subject: [PATCH 1/6] added socket listening to reload doc --- src/Context.zig | 27 ++++++++ src/config/Config.zig | 15 +++++ src/services/SocketServer.zig | 122 ++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 src/services/SocketServer.zig diff --git a/src/Context.zig b/src/Context.zig index d204f44d..106e566f 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -8,6 +8,7 @@ const DocumentHandler = @import("handlers/DocumentHandler.zig"); const Cache = @import("./Cache.zig"); const ReloadIndicatorTimer = @import("services/ReloadIndicatorTimer.zig"); const History = @import("services/History.zig"); +const SocketServer = @import("services/SocketServer.zig"); pub const panic = vaxis.panic_handler; @@ -37,6 +38,8 @@ pub const Context = struct { current_page: ?vaxis.Image, watcher: ?fzwatch.Watcher, watcher_thread: ?std.Thread, + socket_server: ?SocketServer, + socket_thread: ?std.Thread, config: *Config, current_mode: Mode, history: History, @@ -68,6 +71,10 @@ pub const Context = struct { watcher = try fzwatch.Watcher.init(allocator); if (watcher) |*w| try w.addFile(path); } + var socket_server: ?SocketServer = null; + if (config.socket.enabled) { + socket_server = SocketServer.init(allocator, config) catch null; + } const vx = try vaxis.init(allocator, .{}); const buf = try allocator.alloc(u8, 4096); @@ -87,6 +94,8 @@ pub const Context = struct { .watcher = watcher, .mouse = null, .watcher_thread = null, + .socket_server = socket_server, + .socket_thread = null, .config = config, .current_mode = undefined, .history = history, @@ -110,6 +119,10 @@ pub const Context = struct { if (self.watcher_thread) |thread| thread.join(); w.deinit(); } + if (self.socket_server) |*s| { + s.deinit(); + if (self.socket_thread) |t| t.join(); + } if (self.page_info_text.len > 0) self.allocator.free(self.page_info_text); @@ -138,6 +151,15 @@ pub const Context = struct { try watcher.start(.{ .latency = self.config.file_monitor.latency }); } + fn socketWorker(server: *SocketServer, loop: *vaxis.Loop(Event)) !void { + try server.listen(loop, socketCallback); + } + + fn socketCallback(context: ?*anyopaque) void { + const loop = @as(*vaxis.Loop(Event), @ptrCast(@alignCast(context.?))); + loop.postEvent(Event.file_changed); + } + pub fn run(self: *Self) !void { self.current_mode = .{ .view = ViewMode.init(self) }; @@ -169,6 +191,11 @@ pub const Context = struct { } } } + if (self.config.socket.enabled) { + if (self.socket_server) |*s| { + self.socket_thread = try std.Thread.spawn(.{}, socketWorker, .{ s, &loop }); + } + } while (!self.should_quit) { loop.pollEvent(); diff --git a/src/config/Config.zig b/src/config/Config.zig index c35f828f..a2db79ef 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -78,6 +78,19 @@ pub const FileMonitor = struct { } }; +pub const Socket = struct { + enabled: bool = false, + + pub fn parse(val: std.json.Value, allocator: std.mem.Allocator) Socket { + var socket = Socket{}; + if (val != .object) return socket; + + socket.enabled = parseType(bool, val.object, "enabled", allocator, socket.enabled); + + return socket; + } +}; + pub const General = struct { colorize: bool = false, white: i32 = 0x000000, @@ -227,6 +240,7 @@ arena: std.heap.ArenaAllocator, key_map: KeyMap = .{}, file_monitor: FileMonitor = .{}, +socket: Socket = .{}, general: General = .{}, status_bar: StatusBar = .{}, cache: Cache = .{}, @@ -271,6 +285,7 @@ pub fn init(allocator: std.mem.Allocator) Self { if (parsed.value.object.get("KeyMap")) |key_map| self.key_map = KeyMap.parse(key_map, arena_allocator); if (parsed.value.object.get("FileMonitor")) |file_monitor| self.file_monitor = FileMonitor.parse(file_monitor, arena_allocator); + if (parsed.value.object.get("Socket")) |socket| self.socket = Socket.parse(socket, arena_allocator); if (parsed.value.object.get("General")) |general| self.general = General.parse(general, arena_allocator); if (parsed.value.object.get("StatusBar")) |status_bar| self.status_bar = StatusBar.parse(status_bar, arena_allocator); if (parsed.value.object.get("Cache")) |cache| self.cache = Cache.parse(cache, arena_allocator); diff --git a/src/services/SocketServer.zig b/src/services/SocketServer.zig new file mode 100644 index 00000000..b79f7d35 --- /dev/null +++ b/src/services/SocketServer.zig @@ -0,0 +1,122 @@ +const Self = @This(); +const std = @import("std"); +const Config = @import("../config/Config.zig"); + +allocator: std.mem.Allocator, +config: *Config, +path: []u8, +server: ?std.net.Server, +should_stop: std.atomic.Value(bool), + +pub fn init(allocator: std.mem.Allocator, config: *Config) !Self { + const runtime_dir = std.process.getEnvVarOwned(allocator, "XDG_RUNTIME_DIR") catch + try allocator.dupe(u8, "/tmp"); + defer allocator.free(runtime_dir); + + const path = try std.fmt.allocPrint(allocator, "{s}/fancy-cat.sock", .{runtime_dir}); + errdefer allocator.free(path); + + // Remove a stale socket file + std.fs.deleteFileAbsolute(path) catch {}; + + const address = try std.net.Address.initUnix(path); + const server = try address.listen(.{ .reuse_address = true }); + + return .{ + .allocator = allocator, + .config = config, + .path = path, + .server = server, + .should_stop = std.atomic.Value(bool).init(false), + }; +} + +pub fn deinit(self: *Self) void { + self.should_stop.store(true, .seq_cst); + if (self.server) |*s| s.deinit(); + std.fs.deleteFileAbsolute(self.path) catch {}; + self.allocator.free(self.path); +} + +pub fn listen( + self: *Self, + context: ?*anyopaque, + callback: *const fn (context: ?*anyopaque) void, +) !void { + while (!self.should_stop.load(.seq_cst)) { + const conn = self.server.?.accept() catch { + if (self.should_stop.load(.seq_cst)) return; + continue; + }; + defer conn.stream.close(); + + var buf: [64]u8 = undefined; + _ = conn.stream.read(&buf) catch continue; + + callback(context); + } +} + +/// Following is for polling at 200ms + +// pub fn init(allocator: std.mem.Allocator, config: *Config) !Self { +// const runtime_dir = std.process.getEnvVarOwned(allocator, "XDG_RUNTIME_DIR") catch +// try allocator.dupe(u8, "/tmp"); +// defer allocator.free(runtime_dir); +// +// const path = try std.fmt.allocPrint(allocator, "{s}/fancy-cat.sock", .{runtime_dir}); +// errdefer allocator.free(path); +// +// std.fs.deleteFileAbsolute(path) catch {}; +// +// const address = try std.net.Address.initUnix(path); +// var server = try address.listen(.{ .reuse_address = true }); +// errdefer server.deinit(); +// +// // Make the listening socket non-blocking so accept() returns +// // immediately +// const flags = try posix.fcntl(server.stream.handle, posix.F.GETFL, 0); +// _ = try posix.fcntl( +// server.stream.handle, +// posix.F.SETFL, +// flags | @as(u32, @bitCast(posix.O{ .NONBLOCK = true })), +// ); +// +// return .{ +// .allocator = allocator, +// .config = config, +// .path = path, +// .server = server, +// .should_stop = std.atomic.Value(bool).init(false), +// }; +// } + +// pub fn listen( +// self: *Self, +// context: ?*anyopaque, +// callback: *const fn (context: ?*anyopaque) void, +// ) !void { +// const timeout_ms = 200; +// +// while (!self.should_stop.load(.seq_cst)) { +// var poll_fds = [_]posix.pollfd{ +// .{ .fd = self.server.?.stream.handle, .events = posix.POLL.IN, .revents = 0 }, +// }; +// +// const n = posix.poll(&poll_fds, timeout_ms) catch continue; +// if (n == 0) continue; // timed out, loop back and re-check should_stop +// +// if (poll_fds[0].revents & posix.POLL.IN != 0) { +// const conn = self.server.?.accept() catch |err| switch (err) { +// error.WouldBlock => continue, +// else => continue, +// }; +// defer conn.stream.close(); +// +// var buf: [64]u8 = undefined; +// _ = conn.stream.read(&buf) catch {}; +// +// callback(context); +// } +// } +// } From eba4a6fdc5cc137875476c425d1094f20bf033e0 Mon Sep 17 00:00:00 2001 From: Vinesh Benny Date: Sat, 11 Jul 2026 11:21:11 -0400 Subject: [PATCH 2/6] Removing extra slash --- src/services/SocketServer.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/SocketServer.zig b/src/services/SocketServer.zig index b79f7d35..e5cbef65 100644 --- a/src/services/SocketServer.zig +++ b/src/services/SocketServer.zig @@ -57,7 +57,7 @@ pub fn listen( } } -/// Following is for polling at 200ms +// Following is for polling at 200ms // pub fn init(allocator: std.mem.Allocator, config: *Config) !Self { // const runtime_dir = std.process.getEnvVarOwned(allocator, "XDG_RUNTIME_DIR") catch From f56e7dd57d6a15abc8b62120495e7baf58b0897b Mon Sep 17 00:00:00 2001 From: Vinesh Benny Date: Sat, 11 Jul 2026 11:25:58 -0400 Subject: [PATCH 3/6] removing unnecessary commented code --- src/services/SocketServer.zig | 64 ----------------------------------- 1 file changed, 64 deletions(-) diff --git a/src/services/SocketServer.zig b/src/services/SocketServer.zig index e5cbef65..18f43a88 100644 --- a/src/services/SocketServer.zig +++ b/src/services/SocketServer.zig @@ -56,67 +56,3 @@ pub fn listen( callback(context); } } - -// Following is for polling at 200ms - -// pub fn init(allocator: std.mem.Allocator, config: *Config) !Self { -// const runtime_dir = std.process.getEnvVarOwned(allocator, "XDG_RUNTIME_DIR") catch -// try allocator.dupe(u8, "/tmp"); -// defer allocator.free(runtime_dir); -// -// const path = try std.fmt.allocPrint(allocator, "{s}/fancy-cat.sock", .{runtime_dir}); -// errdefer allocator.free(path); -// -// std.fs.deleteFileAbsolute(path) catch {}; -// -// const address = try std.net.Address.initUnix(path); -// var server = try address.listen(.{ .reuse_address = true }); -// errdefer server.deinit(); -// -// // Make the listening socket non-blocking so accept() returns -// // immediately -// const flags = try posix.fcntl(server.stream.handle, posix.F.GETFL, 0); -// _ = try posix.fcntl( -// server.stream.handle, -// posix.F.SETFL, -// flags | @as(u32, @bitCast(posix.O{ .NONBLOCK = true })), -// ); -// -// return .{ -// .allocator = allocator, -// .config = config, -// .path = path, -// .server = server, -// .should_stop = std.atomic.Value(bool).init(false), -// }; -// } - -// pub fn listen( -// self: *Self, -// context: ?*anyopaque, -// callback: *const fn (context: ?*anyopaque) void, -// ) !void { -// const timeout_ms = 200; -// -// while (!self.should_stop.load(.seq_cst)) { -// var poll_fds = [_]posix.pollfd{ -// .{ .fd = self.server.?.stream.handle, .events = posix.POLL.IN, .revents = 0 }, -// }; -// -// const n = posix.poll(&poll_fds, timeout_ms) catch continue; -// if (n == 0) continue; // timed out, loop back and re-check should_stop -// -// if (poll_fds[0].revents & posix.POLL.IN != 0) { -// const conn = self.server.?.accept() catch |err| switch (err) { -// error.WouldBlock => continue, -// else => continue, -// }; -// defer conn.stream.close(); -// -// var buf: [64]u8 = undefined; -// _ = conn.stream.read(&buf) catch {}; -// -// callback(context); -// } -// } -// } From f752a290833d97c1cf2c3b4df0650fd18574da85 Mon Sep 17 00:00:00 2001 From: Vinesh Benny Date: Sat, 11 Jul 2026 11:32:19 -0400 Subject: [PATCH 4/6] parse config.json for SocketServer instead of Socket --- src/Context.zig | 4 ++-- src/config/Config.zig | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Context.zig b/src/Context.zig index 106e566f..f72ea135 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -72,7 +72,7 @@ pub const Context = struct { if (watcher) |*w| try w.addFile(path); } var socket_server: ?SocketServer = null; - if (config.socket.enabled) { + if (config.socket_server.enabled) { socket_server = SocketServer.init(allocator, config) catch null; } @@ -191,7 +191,7 @@ pub const Context = struct { } } } - if (self.config.socket.enabled) { + if (self.config.socket_server.enabled) { if (self.socket_server) |*s| { self.socket_thread = try std.Thread.spawn(.{}, socketWorker, .{ s, &loop }); } diff --git a/src/config/Config.zig b/src/config/Config.zig index a2db79ef..c09d3d29 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -78,16 +78,16 @@ pub const FileMonitor = struct { } }; -pub const Socket = struct { +pub const SocketServer = struct { enabled: bool = false, - pub fn parse(val: std.json.Value, allocator: std.mem.Allocator) Socket { - var socket = Socket{}; - if (val != .object) return socket; + pub fn parse(val: std.json.Value, allocator: std.mem.Allocator) SocketServer { + var socket_server = SocketServer{}; + if (val != .object) return socket_server; - socket.enabled = parseType(bool, val.object, "enabled", allocator, socket.enabled); + socket_server.enabled = parseType(bool, val.object, "enabled", allocator, socket_server.enabled); - return socket; + return socket_server; } }; @@ -240,7 +240,7 @@ arena: std.heap.ArenaAllocator, key_map: KeyMap = .{}, file_monitor: FileMonitor = .{}, -socket: Socket = .{}, +socket_server: SocketServer = .{}, general: General = .{}, status_bar: StatusBar = .{}, cache: Cache = .{}, @@ -285,7 +285,7 @@ pub fn init(allocator: std.mem.Allocator) Self { if (parsed.value.object.get("KeyMap")) |key_map| self.key_map = KeyMap.parse(key_map, arena_allocator); if (parsed.value.object.get("FileMonitor")) |file_monitor| self.file_monitor = FileMonitor.parse(file_monitor, arena_allocator); - if (parsed.value.object.get("Socket")) |socket| self.socket = Socket.parse(socket, arena_allocator); + if (parsed.value.object.get("SocketServerServer")) |socket_server| self.socket_server = SocketServer.parse(socket_server, arena_allocator); if (parsed.value.object.get("General")) |general| self.general = General.parse(general, arena_allocator); if (parsed.value.object.get("StatusBar")) |status_bar| self.status_bar = StatusBar.parse(status_bar, arena_allocator); if (parsed.value.object.get("Cache")) |cache| self.cache = Cache.parse(cache, arena_allocator); From e3000d98f877f129891031921af17d63b90cb413 Mon Sep 17 00:00:00 2001 From: Vinesh Benny Date: Sat, 11 Jul 2026 11:57:47 -0400 Subject: [PATCH 5/6] Removed "Server" duplication --- src/config/Config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index c09d3d29..8ab968be 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -285,7 +285,7 @@ pub fn init(allocator: std.mem.Allocator) Self { if (parsed.value.object.get("KeyMap")) |key_map| self.key_map = KeyMap.parse(key_map, arena_allocator); if (parsed.value.object.get("FileMonitor")) |file_monitor| self.file_monitor = FileMonitor.parse(file_monitor, arena_allocator); - if (parsed.value.object.get("SocketServerServer")) |socket_server| self.socket_server = SocketServer.parse(socket_server, arena_allocator); + if (parsed.value.object.get("SocketServer")) |socket_server| self.socket_server = SocketServer.parse(socket_server, arena_allocator); if (parsed.value.object.get("General")) |general| self.general = General.parse(general, arena_allocator); if (parsed.value.object.get("StatusBar")) |status_bar| self.status_bar = StatusBar.parse(status_bar, arena_allocator); if (parsed.value.object.get("Cache")) |cache| self.cache = Cache.parse(cache, arena_allocator); From 21675e4cbaec96e3d0b36f776a07c5ddfe97a089 Mon Sep 17 00:00:00 2001 From: Vinesh Benny Date: Fri, 17 Jul 2026 20:43:39 -0400 Subject: [PATCH 6/6] made server and path null so double free can't happen --- src/services/SocketServer.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/SocketServer.zig b/src/services/SocketServer.zig index 18f43a88..8d54b558 100644 --- a/src/services/SocketServer.zig +++ b/src/services/SocketServer.zig @@ -33,9 +33,13 @@ pub fn init(allocator: std.mem.Allocator, config: *Config) !Self { pub fn deinit(self: *Self) void { self.should_stop.store(true, .seq_cst); - if (self.server) |*s| s.deinit(); + if (self.server) |*s| { + s.deinit(); + self.server = null; + } std.fs.deleteFileAbsolute(self.path) catch {}; self.allocator.free(self.path); + self.path = &.{}; } pub fn listen(