1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
//! 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);
}
|