From 21f6eae107e9d9ead756923c65340b41fcef7249 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 30 Jul 2026 22:36:34 -0300 Subject: openLink: hand URLs to the desktop opener (xdg-open / open) via double fork --- next-next-steps.txt | 5 ----- src/gui.zig | 2 +- src/look.zig | 43 +++++++++++++++++++++++++++++++++++++++---- src/tty.zig | 2 +- 4 files changed, 41 insertions(+), 11 deletions(-) delete mode 100644 next-next-steps.txt diff --git a/next-next-steps.txt b/next-next-steps.txt deleted file mode 100644 index 412e4dd1..00000000 --- a/next-next-steps.txt +++ /dev/null @@ -1,5 +0,0 @@ -- in normal mode in a output pane pressing Esc should call a Del on that pane. -- add SPC-f-f builtin Find that will work the same way as calling fd (you should implement it like / for search on text files) n/N semantics should just work, if they don't you need to refactor -- add window navigation builtins: SPC-w-h SPC-w-j SPC-w-k SPC-w-l, and SPC-w-t (toggle terminal, will toggle the focus between the latest file pane and the latest termianal pane) -- when opening stuff, like yazi or something some debug text (probably from vaxis?) is printed - this should be hidden. You need to find this and fix it. -- by default the images will have all that image-specific builtins hidden from the tagline, and the main top tagline will not have the Crt and Colors builtins as well; \ No newline at end of file diff --git a/src/gui.zig b/src/gui.zig index 25abe905..4fa5d20e 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -2105,7 +2105,7 @@ fn drainEffects( _ = posix.system.ioctl(pt.fd, TIOCSWINSZ, @intFromPtr(&ws)); } }, - .open_link => {}, // native: look opens panes instead; nothing to launch + .open_link => |url| look.openLink(url.slice()), // desktop browser .save_file => |sf| { const pane = core.panes[sf.pane] orelse continue; const f = pane.file orelse continue; diff --git a/src/look.zig b/src/look.zig index a3d6ab30..556b8ca7 100644 --- a/src/look.zig +++ b/src/look.zig @@ -17,6 +17,38 @@ const embedded_sources = if (pardes.platform == .web) @import("embedded_sources" }; extern "c" fn realpath(path: [*:0]const u8, resolved: [*]u8) ?[*:0]u8; +extern "c" fn fork() c_int; +extern "c" fn execv(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int; +extern "c" fn _exit(status: c_int) noreturn; + +// absolute opener path per OS: execv must not search PATH (no allocation +// between fork and exec), same rule as the shell spawn. +// ponytail: hardcoded path; a distro that puts xdg-open elsewhere (nix) needs +// a PATH search in the child, which is not fork-safe here. +const opener_path: ?[*:0]const u8 = switch (builtin.os.tag) { + .linux => "/usr/bin/xdg-open", + .macos => "/usr/bin/open", + else => null, +}; + +/// Hand a URL to the desktop — the native half of the web backend's +/// window.open. Double fork: the opener is reparented to init, so the one +/// child we DO wait for exits immediately and nothing is left to reap. +pub fn openLink(url: []const u8) void { + const opener = opener_path orelse return; + var buf: [1024]u8 = undefined; + const url_z = std.fmt.bufPrintSentinel(&buf, "{s}", .{url}, 0) catch return; + const pid = fork(); + if (pid < 0) return; + if (pid == 0) { + if (fork() == 0) { + const argv: [3:null]?[*:0]const u8 = .{ opener, url_z.ptr, null }; + _ = execv(opener, &argv); + } + _exit(0); + } + _ = libc.waitpid(pid, null, 0); +} /// file-ish word chars (acme isfilec): alnum + . - + / : @ _ ~ pub fn isFileChar(c: u8) bool { @@ -90,6 +122,11 @@ pub fn resolve(word_raw: []const u8, cwd: []const u8, realbuf: *[4096]u8) Target } else return .{ .pane = .{ .id = id, .line = pl.line, .col = pl.col } }; } + // a URL is a URL everywhere: no filesystem can answer it, so it leaves the + // app (browser tab on web, xdg-open/open on the desktop). + if (std.mem.startsWith(u8, trimmed, "http://") or std.mem.startsWith(u8, trimmed, "https://")) + return .{ .url = trimmed }; + if (platform_has_fs) { var joinbuf: [2048]u8 = undefined; const joined: ?[:0]u8 = if (word[0] == '/') @@ -103,10 +140,8 @@ pub fn resolve(word_raw: []const u8, cwd: []const u8, realbuf: *[4096]u8) Target if (isImagePath(resolved)) return .{ .image = .{ .path = resolved } }; return .{ .file = .{ .path = resolved, .line = pl.line, .col = pl.col } }; } else { - // web: URLs leave the app; tracked Zig sources resolve inside the - // build-generated, read-only source filesystem. - if (std.mem.startsWith(u8, trimmed, "http://") or std.mem.startsWith(u8, trimmed, "https://")) - return .{ .url = trimmed }; + // web: tracked Zig sources resolve inside the build-generated, + // read-only source filesystem. if (resolveEmbedded(word, cwd, realbuf)) |source| return .{ .file = .{ .path = source.path, .line = pl.line, .col = pl.col } }; return .none; diff --git a/src/tty.zig b/src/tty.zig index 6b43acd3..0bb7ef63 100644 --- a/src/tty.zig +++ b/src/tty.zig @@ -388,7 +388,7 @@ fn drainEffects( _ = posix.system.ioctl(pt.file.handle, TIOCSWINSZ, @intFromPtr(&ws)); } }, - .open_link => {}, // tty: no link handling (look opens panes instead) + .open_link => |url| look.openLink(url.slice()), // desktop browser .save_file => |sf| { const pane = core.panes[sf.pane] orelse continue; const f = pane.file orelse continue; -- cgit v1.3