//! What a click on text MEANS. The acme "look" (right click / Enter): expand //! the click to a file-ish word, then resolve it against the pane's directory. //! This is the one deliberately platform-divergent file — the divergence is a //! comptime switch on pardes.platform, used the way the stdlib switches on //! os.tag, so every platform's behavior sits in the same screenful: //! tty/gui — the word resolves through the real filesystem (realpath, //! open(O_DIRECTORY)); dirs open shells, files open file panes. //! web — there is no filesystem; only URLs mean anything, and a URL //! opens in a new tab (the open_link effect). const std = @import("std"); const linux = std.os.linux; const pardes = @import("pardes.zig"); extern "c" fn realpath(path: [*:0]const u8, resolved: [*]u8) ?[*:0]u8; /// file-ish word chars (acme isfilec): alnum + . - + / : @ _ ~ pub fn isFileChar(c: u8) bool { return std.ascii.isAlphanumeric(c) or switch (c) { '.', '-', '+', '/', ':', '@', '_', '~' => true, else => false, }; } /// peel a trailing :NNN (or :NNN:) line suffix: main.zig:100 -> {main.zig, 100} pub fn parsePathLine(tok: []const u8) struct { path: []const u8, line: usize } { var sep: usize = 0; while (sep < tok.len) : (sep += 1) { if (tok[sep] != ':') continue; var j = sep + 1; var line: usize = 0; while (j < tok.len and std.ascii.isDigit(tok[j])) : (j += 1) line = line * 10 + (tok[j] - '0'); if (j == sep + 1) continue; // no digits after ':' if (j < tok.len and tok[j] != ':') continue; // junk after the number return .{ .path = tok[0..sep], .line = line }; } return .{ .path = tok, .line = 0 }; } pub const Target = union(enum) { none, dir: []const u8, // resolved absolute path, in caller's buf file: struct { path: []const u8, line: usize }, image: struct { path: []const u8 }, url: []const u8, }; const image_exts = [_][]const u8{ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ppm", ".pgm", ".tga" }; pub fn isImagePath(path: []const u8) bool { for (image_exts) |ext| { if (std.ascii.endsWithIgnoreCase(path, ext)) return true; } return false; } /// Resolve a looked-at word against the pane's directory. `realbuf` must /// outlive the returned Target (dir/file/image paths point into it). pub fn resolve(word_raw: []const u8, cwd: []const u8, realbuf: *[4096]u8) Target { const trimmed = std.mem.trim(u8, word_raw, " \t\r\n"); const pl = parsePathLine(trimmed); const word = pl.path; if (word.len == 0) return .none; if (platform_has_fs) { var joinbuf: [2048]u8 = undefined; const joined: ?[:0]u8 = if (word[0] == '/') (std.fmt.bufPrintSentinel(&joinbuf, "{s}", .{word}, 0) catch null) else (std.fmt.bufPrintSentinel(&joinbuf, "{s}/{s}", .{ cwd, word }, 0) catch null); const jz = joined orelse return .none; const rp = realpath(jz.ptr, realbuf) orelse return .none; const resolved = std.mem.span(rp); if (isDir(rp)) return .{ .dir = resolved }; if (isImagePath(resolved)) return .{ .image = .{ .path = resolved } }; return .{ .file = .{ .path = resolved, .line = pl.line } }; } else { // web: no filesystem. A URL opens in a new tab; everything else is inert. if (std.mem.startsWith(u8, trimmed, "http://") or std.mem.startsWith(u8, trimmed, "https://")) return .{ .url = trimmed }; return .none; } } const platform_has_fs = switch (pardes.platform) { .tty, .gui => true, .web => false, }; /// true if `path` exists and is a directory (open(O_DIRECTORY), no stat needed) fn isDir(path: [*:0]const u8) bool { const rc = linux.open(path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true }, 0); const sfd: isize = @bitCast(rc); if (sfd < 0) return false; _ = linux.close(@intCast(sfd)); return true; } /// read a whole file (gpa-owned) — the look side of opening a file pane. /// Compiled out on web (no fs); dumps provide file content there instead. pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 { if (!platform_has_fs) return error.NoFilesystem; var pathbuf: [4096]u8 = undefined; const path_z = std.fmt.bufPrintSentinel(&pathbuf, "{s}", .{path}, 0) catch return error.PathTooLong; const rc = linux.open(path_z, .{ .ACCMODE = .RDONLY }, 0); const sfd: isize = @bitCast(rc); if (sfd < 0) return error.OpenFailed; const fd: i32 = @intCast(sfd); defer _ = linux.close(fd); var buf: std.ArrayList(u8) = .empty; errdefer buf.deinit(gpa); var chunk: [16384]u8 = undefined; while (true) { const r = linux.read(fd, &chunk, chunk.len); const n: isize = @bitCast(r); if (n < 0) { if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue; return error.ReadFailed; } if (n == 0) break; try buf.appendSlice(gpa, chunk[0..@intCast(n)]); } return buf.toOwnedSlice(gpa); }