summaryrefslogtreecommitdiff
path: root/src/look.zig
blob: 67fbf8c58df4690bee0fe5c4c66774bdf92f3f0b (plain) (blame)
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);
}