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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
|
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 <count> initial shell panes: 1 (default) or 3 (classic).
\\ A FILE argument boots just that file instead.
\\ --tty-toggle <key> use Ctrl-<key> to enter/leave tty mode
\\ -l <dump.zon> 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.
opts.startup_config = @import("user_config.zig").load(init.io, arena, init.environ_map);
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 both gui.zig and
// builtins.zig and still needs its own line here.
test {
_ = @import("user_config.zig");
if (comptime pardes.platform == .gui) {
_ = @import("gui/gui.zig");
_ = @import("gui/fonts.zig");
}
}
|