summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-30 22:36:34 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit21f6eae107e9d9ead756923c65340b41fcef7249 (patch)
tree31697a80c1ac51120c288516e039c43c09b983c7
parentcc173a821bd6fd84fa6e7c2f272b7e049c7a1aaa (diff)
downloadpardes-21f6eae107e9d9ead756923c65340b41fcef7249.tar.gz
pardes-21f6eae107e9d9ead756923c65340b41fcef7249.zip
openLink: hand URLs to the desktop opener (xdg-open / open) via double fork
-rw-r--r--next-next-steps.txt5
-rw-r--r--src/gui.zig2
-rw-r--r--src/look.zig43
-rw-r--r--src/tty.zig2
4 files changed, 41 insertions, 11 deletions
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;