summaryrefslogtreecommitdiff
path: root/src/main.zig
blob: 4712169165f982c5c1a7200dcbb2431f60b9e6cc (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
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
const std = @import("std");
const builtin = @import("builtin");
const pardes = @import("pardes.zig");

const is_emscripten = builtin.os.tag == .emscripten;

extern "c" fn emscripten_console_error(utf8: [*:0]const u8) void;

// emscripten: std.debug's default threaded-io singleton doesn't run on wasm;
// fail it (init errors surface via emscripten_console_error below). Native
// values match the std defaults. Same shim as the prototype's replay.zig.
pub const std_options_debug_threaded_io: ?*std.Io.Threaded = if (is_emscripten)
    null
else
    std.Io.Threaded.global_single_threaded;
pub const std_options_debug_io: std.Io = if (is_emscripten)
    std.Io.failing
else
    std_options_debug_threaded_io.?.io();

// ghostty-vt's stream logs every mode it doesn't implement, and in the tty
// shell stderr IS the screen — the warning scribbles over the grid. macOS
// bash/readline sends one such mode (1034, meta-key) on every startup, so
// keep the stream scope to errors; linux shells just never tripped it.
pub const std_options: std.Options = .{
    .log_scope_levels = &.{.{ .scope = .stream, .level = .err }},
};

// A panic must restore the terminal (cooked mode, main screen, mouse off)
// before the trace prints, or it lands garbled in a raw alt screen. recover()
// no-ops unless the vaxis tty is live, so gui/tty share the handler.
pub const panic = if (is_emscripten) std.debug.FullPanic(std.debug.defaultPanic) else std.debug.FullPanic(struct {
    fn call(msg: []const u8, ret_addr: ?usize) noreturn {
        @import("vaxis").recover();
        std.debug.defaultPanic(msg, ret_addr);
    }
}.call);

// Fatal signals (SIGSEGV/SIGILL/SIGBUS/SIGFPE) bypass the panic handler and
// no defer/errdefer ever runs — hook std.debug's segfault path the same way
// so the terminal is restored before the trace prints.
pub const debug = if (is_emscripten) struct {} else struct {
    pub fn handleSegfault(addr: ?usize, name: []const u8, opt_ctx: anytype) noreturn {
        @import("vaxis").recover();
        return std.debug.defaultHandleSegfault(addr, name, opt_ctx);
    }
};

const help_text =
    \\Usage: pardes [options] [file|dir]
    \\
    \\With no arguments the shell pane starts in tty mode. A FILE argument
    \\(optionally file:LINE) opens it focused beside a terminal on the launch
    \\directory; a DIRECTORY argument becomes the cwd shells spawn in.
    \\
    \\Options:
    \\  --tty                  start as one shell pane already in tty mode
    \\  -n <count>             initial shell panes: 1 (default) or 3 (classic)
    \\  --tty-toggle <key>     use Ctrl-<key> to enter/leave tty mode
    \\  -l <dump.zon>          load a dump of another instance (see Dump)
    \\  -h, --help             show this help and exit
    \\
;

// The browser runtime calls a C main (exported below); everything else keeps
// the std.process.Init entry.
pub const main = if (is_emscripten) webMain else nativeMain;

comptime {
    if (is_emscripten) @export(&emscriptenMain, .{ .name = "main" });
}

fn emscriptenMain(argc: c_int, argv: [*]?[*:0]u8) callconv(.c) c_int {
    _ = argc;
    _ = argv;
    webMain() catch |err| {
        var buf: [128]u8 = undefined;
        const msg = std.fmt.bufPrintSentinel(&buf, "pardes web init failed: {t}", .{err}, 0) catch "pardes web init failed";
        emscripten_console_error(msg.ptr);
        return 1;
    };
    return 0;
}

// no argv in the browser: options stay default, state comes from the dump
fn webMain() !void {
    try @import("gui.zig").run(.{});
}

fn nativeMain(init: std.process.Init) !void {
    var opts: pardes.Options = .{};
    const arena = init.arena.allocator();
    const args = try init.minimal.args.toSlice(arena);
    if (args.len == 1) opts.tty_only = true; // bare `pardes` boots straight into tty mode
    var positional = false;
    var i: usize = 1;
    while (i < args.len) : (i += 1) {
        const a = args[i];
        if (std.mem.eql(u8, a, "--tty")) {
            opts.tty_only = true;
        } else if (std.mem.startsWith(u8, a, "--tty-toggle=")) {
            opts.tty_toggle = parseCtrlKey(a["--tty-toggle=".len..]) orelse return error.BadArgs;
        } else if (std.mem.eql(u8, a, "--tty-toggle")) {
            i += 1;
            if (i >= args.len) return error.BadArgs;
            opts.tty_toggle = parseCtrlKey(args[i]) orelse return error.BadArgs;
        } else if (std.mem.eql(u8, a, "-n")) {
            i += 1;
            if (i >= args.len) return error.BadArgs;
            opts.shells = std.fmt.parseInt(u8, args[i], 10) catch return error.BadArgs;
        } else if (std.mem.eql(u8, a, "-l")) {
            i += 1;
            if (i >= args.len) return error.BadArgs;
            opts.load_path = args[i];
        } else if (std.mem.eql(u8, a, "-h") or std.mem.eql(u8, a, "--help")) {
            try std.Io.File.stdout().writeStreamingAll(init.io, help_text);
            return;
        } else if (a.len > 0 and a[0] != '-' and !positional) {
            // one positional: a directory becomes the cwd shells spawn in
            // (chdir succeeds only on dirs); anything else resolves as a file
            positional = true;
            if (std.c.chdir(a.ptr) != 0) {
                var cwdbuf: [4096]u8 = undefined;
                const cwd = std.c.getcwd(&cwdbuf, cwdbuf.len) orelse return error.BadArgs;
                var realbuf: [4096]u8 = undefined;
                switch (@import("look.zig").resolve(a, std.mem.span(@as([*:0]u8, @ptrCast(cwd))), &realbuf)) {
                    .file => |t| {
                        opts.file = try arena.dupe(u8, t.path);
                        opts.file_line = t.line;
                    },
                    .image => |t| opts.file = try arena.dupe(u8, t.path),
                    else => return error.BadArgs,
                }
            }
        } else {
            return error.BadArgs;
        }
    }
    switch (pardes.platform) {
        .tty => try @import("tty.zig").run(init, opts),
        .gui => try @import("gui.zig").run(init, opts),
        .web => unreachable, // web enters through webMain, never here
    }
}

fn parseCtrlKey(raw: []const u8) ?u21 {
    var value = std.mem.trim(u8, raw, " \t\r\n");
    if (value.len >= 5 and std.ascii.eqlIgnoreCase(value[0..5], "ctrl-")) {
        value = value[5..];
    } else if (value.len >= 5 and std.ascii.eqlIgnoreCase(value[0..5], "ctrl+")) {
        value = value[5..];
    } else if (value.len == 2 and value[0] == '^') {
        value = value[1..];
    }
    if (value.len != 1) return null;
    const c = std.ascii.toLower(value[0]);
    if (c < 'a' or c > 'z' or c == 'c') return null;
    return c;
}