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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
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();
// 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 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/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.at.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/tty.zig").run(init, opts),
.gui => try @import("gui/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;
}
// 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 both gui.zig and
// builtins.zig and still needs its own line here.
test {
if (comptime pardes.platform == .gui) {
_ = @import("gui/gui.zig");
_ = @import("gui/fonts.zig");
}
}
|