const std = @import("std"); const builtin = @import("builtin"); const pardes = @import("pardes.zig"); const nested = @import("nested.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(); // Every std.log call in the process — ours and every dependency's — funnels // through this one function. ghostty-vt narrates whatever it does not // implement in the bytes a child writes to its pty (`debug(stream)`, // `warning(stream): ignoring unimplemented CSI p`, `debug(kitty_gfx)`; opening // yazi is worth several lines before it has drawn anything), and in the tty // shell stderr IS the screen — those land on top of the rendered grid, and in // the gui/web shells on the console. So drop the libraries at every level: an // `err` painted over the UI is no better than a debug one. Only pardes' own // scopes get through, because their messages carry detail the error returns // don't (gui's SDL_GetError strings, dump's zon parse diagnostic) — .default // is NOT one of them, ghostty and uucode both log unscoped. Set PARDES_LOG to // get the real logger back: `PARDES_LOG=1 pardes 2>/tmp/pardes.log`. pub const std_options: std.Options = .{ .logFn = logFn }; fn logFn( comptime level: std.log.Level, comptime scope: @EnumLiteral(), comptime format: []const u8, args: anytype, ) void { if (scope != .gui and scope != .dump and std.c.getenv("PARDES_LOG") == null) return; std.log.defaultLog(level, scope, format, args); } // 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 alone, filling the window; a DIRECTORY \\argument becomes the cwd shells spawn in. \\ \\Options: \\ --tty start as one shell pane already in tty mode \\ -n initial shell panes: 1 (default) or 3 (classic). \\ A FILE argument boots just that file instead. \\ --tty-toggle use Ctrl- to enter/leave tty mode \\ -l load a dump of another instance (see Dump) \\ --nested run a full session even inside another pardes. \\ Without it, a pardes started inside a pardes \\ hands its FILE argument to the outer one. This \\ session will not serve its own children either. \\ -h, --help show this help and exit \\ ; const nested_text = \\pardes: this shell is already inside pardes, and a pardes inside a pardes \\is spicy. Name a file or a directory and the outer session opens it, or \\pass --nested if you really want a second one in here. \\ ; // 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/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); // bare `pardes` boots straight into tty mode — and `pardes --nested` still // counts as bare, because --nested says something about the session's // relationship to its parent and nothing about its layout if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--nested"))) opts.tty_only = true; // Kept RAW until every flag is parsed: classifying it means chdir'ing into // a directory and recording nothing, and the nested client below still // needs the word itself to resolve. var positional: ?[:0]const u8 = null; 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, "--nested")) { opts.nested = true; } 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 == null) { positional = a; } else { return error.BadArgs; } } // Started INSIDE another pardes: hand it the file and get out of the way // rather than stacking a second full-screen UI inside one of its panes. // The word is resolved here rather than sent raw because the outer // instance resolves against ITS panes' directories, which are not ours. // A word naming nothing on disk sends nothing and falls through to the // classification below, which already refuses it — no second UI either way. if (!opts.nested) if (nested.outer()) |outer_pid| { const word = positional orelse { try std.Io.File.stderr().writeStreamingAll(init.io, nested_text); std.process.exit(1); }; var cwdbuf: [4096]u8 = undefined; const cwd = std.c.getcwd(&cwdbuf, cwdbuf.len) orelse return error.BadArgs; var realbuf: [4096]u8 = undefined; const sent = switch (@import("look.zig").resolve(word, std.mem.span(@as([*:0]u8, @ptrCast(cwd))), &realbuf)) { .dir => |d| nested.sendLook(outer_pid, d, 0), .file => |t| nested.sendLook(outer_pid, t.path, t.at.line), .image => |t| nested.sendLook(outer_pid, t.path, 0), // an unreachable outer instance (an older build, a stale socket // path) is not worth failing a launch over: run normally instead else => false, }; if (sent) return; }; if (positional) |a| { // a directory becomes the cwd shells spawn in (chdir succeeds only on // dirs); anything else resolves as a file 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.at.line; }, .image => |t| opts.file = try arena.dupe(u8, t.path), else => return error.BadArgs, } } } // Native shells opt into the user config; the sans-IO core and web keep // Options' null default. Read it before entering either frontend so every // builtin has run before that frontend can render its first frame. const found = @import("user_config.zig").load(init.io, arena, init.environ_map); opts.startup_config = found.bytes; opts.startup_config_path = found.path; switch (pardes.platform) { .tty => try @import("tty/tty.zig").run(init, opts), .gui => try @import("gui/gui.zig").run(init, opts), // Both library shells are entered by their host through a flat C ABI, // and never link this file at all: the browser through src/web.zig, // the macOS app through src/macos.zig. .web, .macos => unreachable, } } 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; } // The shells are imported inside main(), which a test build never analyses — // so their inline tests need naming here to exist at all. gui.zig only // compiles when it IS the shell (it @cImports SDL), hence the comptime gate; // under -Dplatform=tty this block analyses to nothing. Naming a file gets THAT // file's tests and no further: fonts.zig is imported by gui.zig, builtins.zig // and the macOS host, and still needs its own line here. test { _ = @import("user_config.zig"); _ = @import("allocators.zig"); if (comptime pardes.platform == .gui) _ = @import("gui/gui.zig"); if (comptime pardes.font_picker) _ = @import("fonts.zig"); }