summaryrefslogtreecommitdiff
path: root/build.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 14:11:05 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitf2c0c6f1aa90a567dc9d37c5eb8f53ece58cff88 (patch)
tree8d758a9da26c495df9076c8a473225087667ebbe /build.zig
parent4e224b7c7e67fafcbe7359fcf534048e254b701d (diff)
downloadpardes-f2c0c6f1aa90a567dc9d37c5eb8f53ece58cff88.tar.gz
pardes-f2c0c6f1aa90a567dc9d37c5eb8f53ece58cff88.zip
Terminal replies and malformed escape sequences never become typed text
Diffstat (limited to 'build.zig')
-rw-r--r--build.zig188
1 files changed, 186 insertions, 2 deletions
diff --git a/build.zig b/build.zig
index 79de8a81..aee8541a 100644
--- a/build.zig
+++ b/build.zig
@@ -682,11 +682,195 @@ pub fn build(b: *std.Build) void {
.tables_path = uucode_tables,
}).module("uucode");
- const vaxis_mod = b.dependency("vaxis", .{
+ const vaxis_dep = b.dependency("vaxis", .{
.target = target,
.optimize = optimize,
.external_uucode = true,
- }).module("vaxis");
+ });
+ const vaxis_mod = vaxis_dep.module("vaxis");
+ // vaxis's input parser, patched here at build time like mvzr so that a
+ // fresh fetch keeps it: no reply, split read or malformed sequence may
+ // reach the editor as typed text, and none may panic or end input.
+ // - A cursor position report that no F3 could have sent is no key: the
+ // startup width queries' `CSI 1;1R` arrived as two F3s, which a shell
+ // pane took for text.
+ // - ESC before a CSI or SS3 key (rxvt's Alt+arrow) is Alt on that key,
+ // not Alt+Escape followed by `[A` as text.
+ // - An X10 mouse report and a kitty graphics reply cut short by a read
+ // wait for their rest: the first became three bytes of text, the
+ // second sliced past its input.
+ // - A malformed color or clipboard reply is dropped instead of failing
+ // the parse, which stopped input; so is a byte no UTF-8 starts with.
+ // - An OSC reply ends at its first terminator: one ended by BEL ran on to
+ // the next sequence's ESC and left what lay between as text.
+ // - A device report's fields are sought in the sequence, not past it.
+ // A bump that moves an anchor stops the build.
+ {
+ const parser_path = vaxis_dep.path("src/Parser.zig").getPath(b);
+ var src = std.Io.Dir.cwd().readFileAlloc(io, parser_path, b.allocator, .limited(1 << 20)) catch @panic("read vaxis Parser.zig");
+ const edits = [_]struct { anchor: []const u8, with: []const u8, count: usize = 1 }{
+ .{
+ .anchor =
+ \\ 0x5F => return parseApc(input),
+ \\ else => {
+ ,
+ .with =
+ \\ 0x5F => return parseApc(input),
+ \\ // pardes's patch (its build.zig): ESC before a key's sequence is Alt on the key.
+ \\ 0x1B => if (input.len > 2 and (input[2] == '[' or input[2] == 'O')) {
+ \\ var inner = if (input[2] == '[') parseCsi(input[1..], &self.buf) else parseSs3(input[1..]);
+ \\ if (inner.n == 0) return inner;
+ \\ if (inner.event) |*event| switch (event.*) {
+ \\ .key_press, .key_release => |*key| key.mods.alt = true,
+ \\ else => {},
+ \\ };
+ \\ inner.n += 1;
+ \\ return inner;
+ \\ } else return .{ .event = .{ .key_press = .{ .codepoint = Key.escape, .mods = .{ .alt = true } } }, .n = 2 },
+ \\ else => {
+ ,
+ },
+ .{
+ .anchor = " const first_cp = iter.next() orelse return error.InvalidUTF8;\n",
+ .with = " const first_cp = iter.next() orelse return .{ .event = null, .n = 1 }; // pardes's patch (its build.zig)\n",
+ },
+ .{
+ .anchor = " n = std.unicode.utf8CodepointSequenceLength(first_cp) catch return error.InvalidUTF8;\n",
+ .with = " n = std.unicode.utf8CodepointSequenceLength(first_cp) catch return .{ .event = null, .n = 1 }; // pardes's patch (its build.zig)\n",
+ },
+ .{
+ .anchor =
+ \\ const sequence = input[0 .. end + 1 + 1];
+ \\
+ \\ switch (input[2]) {
+ ,
+ .with =
+ \\ // pardes's patch (its build.zig): an ESC that ends the input is half an ST.
+ \\ if (input.len < end + 2) return .{ .event = null, .n = 0 };
+ \\ const sequence = input[0 .. end + 1 + 1];
+ \\
+ \\ switch (input[2]) {
+ ,
+ },
+ .{
+ .anchor =
+ \\ const esc_result = skipUntilST(input);
+ \\ if (esc_result.n > 0) break :blk esc_result.n;
+ \\
+ \\ // No escape, could be BEL terminated
+ \\ const bel = std.mem.indexOfScalarPos(u8, input, 2, 0x07) orelse return .{
+ \\ .event = null,
+ \\ .n = 0,
+ \\ };
+ \\ bel_terminated = true;
+ \\ break :blk bel + 1;
+ ,
+ .with =
+ \\ // pardes's patch (its build.zig): whichever terminator comes first ends it.
+ \\ const esc = std.mem.indexOfScalarPos(u8, input, 2, 0x1b);
+ \\ const bel = std.mem.indexOfScalarPos(u8, input, 2, 0x07);
+ \\ if (bel != null and (esc == null or bel.? < esc.?)) {
+ \\ bel_terminated = true;
+ \\ break :blk bel.? + 1;
+ \\ }
+ \\ const esc_result = skipUntilST(input);
+ \\ if (esc_result.n > 0) break :blk esc_result.n;
+ \\ return .{ .event = null, .n = 0 };
+ ,
+ },
+ .{
+ .anchor = " const color = try Color.rgbFromSpec(color_spec);\n",
+ .with = " const color = Color.rgbFromSpec(color_spec) catch return null_event; // pardes's patch (its build.zig)\n",
+ .count = 2,
+ },
+ .{
+ .anchor =
+ \\ if (input[semicolon_idx + 1] != 'c') return null_event;
+ \\ const payload = if (bel_terminated)
+ \\ input[semicolon_idx + 3 .. sequence.len - 1]
+ \\ else
+ \\ input[semicolon_idx + 3 .. sequence.len - 2];
+ \\ const decoder = std.base64.standard.Decoder;
+ \\ const text = try paste_allocator.?.alloc(u8, try decoder.calcSizeForSlice(payload));
+ \\ try decoder.decode(text, payload);
+ ,
+ .with =
+ \\ // pardes's patch (its build.zig): a malformed reply is dropped.
+ \\ const body_end = if (bel_terminated) sequence.len - 1 else sequence.len - 2;
+ \\ if (semicolon_idx + 3 > body_end or input[semicolon_idx + 1] != 'c') return null_event;
+ \\ const payload = input[semicolon_idx + 3 .. body_end];
+ \\ const decoder = std.base64.standard.Decoder;
+ \\ const gpa = paste_allocator orelse return null_event;
+ \\ const text = gpa.alloc(u8, decoder.calcSizeForSlice(payload) catch return null_event) catch return null_event;
+ \\ decoder.decode(text, payload) catch {
+ \\ gpa.free(text);
+ \\ return null_event;
+ \\ };
+ ,
+ },
+ .{
+ .anchor =
+ \\ 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => {
+ \\
+ ,
+ .with =
+ \\ 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => {
+ \\ // pardes's patch (its build.zig): a cursor position report is no key.
+ \\ if (final == 'R' and cursorReport(sequence)) return null_event;
+ \\
+ ,
+ },
+ .{
+ .anchor = " std.debug.assert(sequence.len >= 4); // ESC [ ? c == 4 bytes\n",
+ .with = " if (sequence.len < 4) return null_event; // pardes's patch (its build.zig)\n",
+ },
+ .{
+ .anchor = "const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';')",
+ .with = "const delim_idx = std.mem.indexOfScalarPos(u8, sequence, 3, ';')",
+ .count = 2,
+ },
+ .{
+ .anchor = "input[delim_idx + 1 .. sequence.len - 2]",
+ .with = "sequence[@min(delim_idx + 1, sequence.len - 2) .. sequence.len - 2]",
+ },
+ .{
+ .anchor = " if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) {\n",
+ .with =
+ \\ // pardes's patch (its build.zig): an X10 report still arriving is incomplete, not text.
+ \\ if (input.len == 3 and input[2] == 'M' and full_input.len < 6) return .{ .event = null, .n = 0 };
+ \\ if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) {
+ \\
+ ,
+ },
+ };
+ for (edits) |edit| {
+ if (std.mem.count(u8, src, edit.anchor) != edit.count) std.debug.panic("vaxis's Parser.zig changed at `{s}`: redo its patch in build.zig", .{edit.anchor});
+ src = std.mem.replaceOwned(u8, b.allocator, src, edit.anchor, edit.with) catch @panic("OOM");
+ }
+ const cursor_report =
+ \\
+ \\/// pardes's patch (its build.zig): whether `CSI ... R` answers a cursor
+ \\/// position query. Only `CSI 1 ; mods R` with mods past 1 is a modified F3
+ \\/// (a bare F3 is `SS3 R`); a report has another row, or col 1 (mods 1).
+ \\fn cursorReport(sequence: []const u8) bool {
+ \\ const params = sequence[2 .. sequence.len - 1];
+ \\ if (params.len > 0 and params[0] == '?') return true;
+ \\ var fields = std.mem.splitScalar(u8, params, ';');
+ \\ const row = fields.first();
+ \\ const col = fields.next() orelse return false;
+ \\ if (fields.next() != null or row.len == 0 or col.len == 0) return false;
+ \\ for (params) |c| if (c != ';' and !std.ascii.isDigit(c)) return false;
+ \\ return !std.mem.eql(u8, row, "1") or std.mem.eql(u8, col, "1");
+ \\}
+ \\
+ ;
+ const files = b.addWriteFiles();
+ _ = files.add("Parser.zig", b.fmt("{s}{s}", .{ src, cursor_report }));
+ _ = files.addCopyFile(vaxis_dep.path("src/widgets/terminal/Parser.zig"), "widgets/terminal/Parser.zig");
+ // Suffixes: this leaves out both Parser.zig files, written above.
+ _ = files.addCopyDirectory(vaxis_dep.path("src"), "", .{ .exclude_extensions = &.{"Parser.zig"} });
+ vaxis_mod.root_source_file = files.getDirectory().path(b, "main.zig");
+ }
var ghostty_vt_for_snap: ?*std.Build.Module = null;
const ghostty_simd = !freestanding_core and (!target.result.os.tag.isDarwin() or b.graph.host.result.os.tag.isDarwin());