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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
//! 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 builtin = @import("builtin");
const libc = std.c;
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 fd = libc.open(path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true });
if (fd < 0) return false;
_ = libc.close(fd);
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 fd = libc.open(path_z, .{ .ACCMODE = .RDONLY });
if (fd < 0) return error.OpenFailed;
defer _ = libc.close(fd);
var buf: std.ArrayList(u8) = .empty;
errdefer buf.deinit(gpa);
var chunk: [16384]u8 = undefined;
while (true) {
const n = libc.read(fd, &chunk, chunk.len);
if (n < 0) {
if (libc.errno(n) == .INTR) continue;
return error.ReadFailed;
}
if (n == 0) break;
try buf.appendSlice(gpa, chunk[0..@intCast(n)]);
}
return buf.toOwnedSlice(gpa);
}
// ---- shell cwd: what directory a pane's looks resolve against ----
// macOS has no /proc; libproc's proc_pidinfo(PROC_PIDVNODEPATHINFO) yields the
// cwd vnode path. Not in std.c — layout from xnu's sys/proc_info.h.
const vnode_info_path = extern struct {
vi: [152]u8 align(8), // struct vnode_info: vinfo_stat + type + pad + fsid
path: [1024]u8, // MAXPATHLEN
};
const proc_vnodepathinfo = extern struct {
cdir: vnode_info_path,
rdir: vnode_info_path,
};
const PROC_PIDVNODEPATHINFO: c_int = 9;
extern "c" fn proc_pidinfo(pid: c_int, flavor: c_int, arg: u64, buffer: *anyopaque, buffersize: c_int) c_int;
/// Live cwd of a shell process (pane tags, look resolution). linux reads
/// /proc/<pid>/cwd, darwin asks libproc; other POSIX systems have no cheap
/// answer — return null and panes keep their spawn-time cwd (callers already
/// tolerate failure: dead shells have no cwd either).
pub fn shellCwd(pid: libc.pid_t, buf: *[1024]u8) ?[]const u8 {
switch (builtin.os.tag) {
.linux => {
var pbuf: [64]u8 = undefined;
const path = std.fmt.bufPrintSentinel(&pbuf, "/proc/{d}/cwd", .{pid}, 0) catch return null;
const n = libc.readlink(path, buf, buf.len);
if (n <= 0) return null;
return buf[0..@intCast(n)];
},
.macos, .ios, .tvos, .watchos, .visionos => {
var info: proc_vnodepathinfo = undefined;
const n = proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &info, @sizeOf(proc_vnodepathinfo));
if (n < @as(c_int, @sizeOf(proc_vnodepathinfo))) return null;
const path = std.mem.sliceTo(&info.cdir.path, 0);
if (path.len == 0) return null;
@memcpy(buf[0..path.len], path);
return buf[0..path.len];
},
else => return null,
}
}
|