diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-02 20:07:26 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 022773d94dad2775c806f863a374bd945d4dc989 (patch) | |
| tree | 588ac763f778a296168d7b82c4b0d2d2cd26b0ed /src/pardes.zig | |
| parent | 820ff337d156831d250ff0fcef5139fe69474e4e (diff) | |
| download | pardes-022773d94dad2775c806f863a374bd945d4dc989.tar.gz pardes-022773d94dad2775c806f863a374bd945d4dc989.zip | |
show New in every pane tag and enforce builtin-first Exec fallback
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 164 |
1 files changed, 153 insertions, 11 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 8cf8d2e2..bc56f966 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -98,6 +98,7 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" { try std.testing.expect(std.mem.indexOf(u8, initial_tag, " width ") != null); try std.testing.expect(std.mem.indexOf(u8, initial_tag, " height ") == null); try std.testing.expect(std.mem.indexOf(u8, initial_tag, "PdfFit") != null); + try std.testing.expect(std.mem.endsWith(u8, initial_tag, pane_tail)); p.native_images = true; var frame = std.heap.ArenaAllocator.init(gpa); @@ -583,6 +584,12 @@ test "feature-off .pdf argv opens an ordinary file pane" { const pane_tail = " " ++ config.pane_builtins_str; const file_pane_tail = " " ++ config.file_pane_builtins_str; +// Dumps persist the whole rendered tag. These were the two canonical tails +// before New joined every pane; restoreTail recognizes exactly these spellings +// as defaults so an old session upgrades, without mistaking an edited tail for +// chrome it may replace. +const legacy_pane_tail = " Del"; +const legacy_file_pane_tail = " Save Del"; // Builtins: executing the name (middle-click / Tab) runs it through the ONE // dispatcher (runBuiltin, reached from execute), no matter where the name @@ -633,6 +640,137 @@ test "startup config runs builtin lines in order and isolates bad lines" { }; } +test "pane-tag Exec prefers New and argument builtins before shell fallback" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .cols = 100, .rows = 30 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} // initial shell spawn/geometry + + const pane = p.panes[0].?; + try std.testing.expectEqualStrings(" New Del", Pardes.curTail(pane)); + const tag_x = p.rects[0].x + config.GUTTER; + const tag_y = p.rects[0].y; + + // A real middle-click on the canonical pane-tag word reaches Exec, which + // must consume New as a builtin before any write can reach the shell. + p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .press, .col = tag_x + 1, .row = tag_y } }); + p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .release, .col = tag_x + 1, .row = tag_y } }); + var saw_new = false; + while (p.nextEffect()) |effect| switch (effect) { + .new_file => |request| { + try std.testing.expectEqual(@as(u8, 0), request.pane); + try std.testing.expectEqual(pane.serial, request.serial); + saw_new = true; + }, + .write => return error.NewFellThroughToPty, + else => {}, + }; + try std.testing.expect(saw_new); + + // Argument-taking builtins use the same gesture path: a tag sweep is the + // complete command line, and Theme consumes its tail without a PTY write. + pane.tag_tail.clearRetainingCapacity(); + try pane.tag_tail.appendSlice(gpa, " Theme dark"); + pane.tag_init = true; + p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .press, .col = tag_x + 1, .row = tag_y } }); + p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .drag, .col = tag_x + 10, .row = tag_y } }); + p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .release, .col = tag_x + 10, .row = tag_y } }); + try std.testing.expectEqualStrings("dark", p.theme().name); + while (p.nextEffect()) |effect| switch (effect) { + .write => return error.ArgumentBuiltinFellThroughToPty, + else => {}, + }; + + // Fixed-name builtins remain exact: a tail does not turn New or Kill into + // a match (especially important for the destructive one). + try std.testing.expect(!p.executeBuiltinLine(0, "New argument")); + try std.testing.expect(!p.executeBuiltinLine(0, "Kill trailing-garbage")); + try std.testing.expect(!p.quit); + + // A word absent from the builtin vocabulary takes the fallback, including + // the terminating carriage return expected by the shell prompt. + pane.tag_tail.clearRetainingCapacity(); + try pane.tag_tail.appendSlice(gpa, " DefinitelyNotABuiltin"); + p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .press, .col = tag_x + 1, .row = tag_y } }); + p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .release, .col = tag_x + 1, .row = tag_y } }); + var sent: std.ArrayList(u8) = .empty; + defer sent.deinit(gpa); + while (p.nextEffect()) |effect| switch (effect) { + .write => |w| { + try std.testing.expectEqual(@as(u8, 0), w.pane); + try sent.appendSlice(gpa, w.bytes.slice()); + }, + .new_file => return error.UnknownCommandRanNew, + else => {}, + }; + try std.testing.expectEqualStrings("DefinitelyNotABuiltin\r", sent.items); +} + +test "legacy default tag tails upgrade while custom tails remain owned" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{}); + defer p.deinit(); + + const terminal = p.panes[0].?; + const terminal_old = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{ + try p.tagPrefix(terminal), + legacy_pane_tail, + }); + p.restoreTail(terminal, terminal_old); + try std.testing.expect(!terminal.tag_init); + try std.testing.expectEqualStrings(" New Del", Pardes.curTail(terminal)); + + const terminal_custom = try std.fmt.allocPrint(p.scratch.allocator(), "{s} Keep Del", .{ + try p.tagPrefix(terminal), + }); + p.restoreTail(terminal, terminal_custom); + try std.testing.expect(terminal.tag_init); + try std.testing.expectEqualStrings(" Keep Del", Pardes.curTail(terminal)); + + // A savable file has a distinct old default. Save remains first after the + // migration so the tag's established `:w<Tab>` route is unchanged. + const file = try p.hxOpenFileContent(""); + const file_old = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{ + try p.tagPrefix(file), + legacy_file_pane_tail, + }); + p.restoreTail(file, file_old); + try std.testing.expect(!file.tag_init); + try std.testing.expectEqualStrings(" Save New Del", Pardes.curTail(file)); + + const file_custom = try std.fmt.allocPrint(p.scratch.allocator(), "{s} Save Mine Del", .{ + try p.tagPrefix(file), + }); + p.restoreTail(file, file_custom); + try std.testing.expect(file.tag_init); + try std.testing.expectEqualStrings(" Save Mine Del", Pardes.curTail(file)); +} + +test "unknown Exec from an image writes to a terminal in the image directory" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{}); + defer p.deinit(); + while (p.nextEffect()) |_| {} + + p.setCwd(0, "/tmp/pardes-image-dir"); + const image_pane = try p.newDocPane(1); + image_pane.image = .{ .path = try gpa.dupe(u8, "/tmp/pardes-image-dir/pic.ppm") }; + try std.testing.expectEqualStrings("/tmp/pardes-image-dir", Pardes.paneDir(image_pane)); + try std.testing.expectEqual(@as(?usize, 0), p.execute(1, "echo image-fallback")); + + var sent: std.ArrayList(u8) = .empty; + defer sent.deinit(gpa); + while (p.nextEffect()) |effect| switch (effect) { + .write => |w| { + try std.testing.expectEqual(@as(u8, 0), w.pane); + try sent.appendSlice(gpa, w.bytes.slice()); + }, + .spawn => return error.ImageExecSpawnedDespiteMatchingTerminal, + else => {}, + }; + try std.testing.expectEqualStrings("echo image-fallback\r", sent.items); +} + test "New completes as an empty watched file in the calling column and focuses it" { const p = try Pardes.init(std.testing.allocator, .{ .shells = 3, .cols = 100, .rows = 30 }); defer p.deinit(); @@ -4751,6 +4889,7 @@ pub const Pardes = struct { if (pane.file) |f| return std.fs.path.dirname(f.path) orelse "/"; if (comptime pdf_enabled) if (pane.pdf) |pv| return std.fs.path.dirname(pv.path) orelse "/"; + if (pane.image) |iv| return std.fs.path.dirname(iv.path) orelse "/"; return pane.cwdSlice(); } @@ -8330,14 +8469,11 @@ pub const Pardes = struct { // Anything not in the builtin vocabulary is a command line typed at // a shell. Startup config calls executeBuiltinLine directly and never // reaches this fallback. - // terminal: run in itself. file pane: run in a shell in the file's - // directory (found by cwd, else freshly forked). - const dst = (if (pane.file) |f| - p.ttyForDir(std.fs.path.dirname(f.path) orelse "/") - else if (hasPdf(pane)) - p.ttyForDir(paneDir(pane)) - else - id) orelse return null; + // A terminal runs in itself. Every document kind — real/output file, + // image, or PDF — runs in a terminal for its directory (an existing + // one when possible, otherwise a freshly forked shell). In particular, + // never emit a PTY write addressed to an image's no-PTY pane slot. + const dst = (if (pane.isTerminal()) id else p.ttyForDir(paneDir(pane))) orelse return null; term_pane.padOutputBelowEdits(p, dst); p.emitWrite(dst, cmd); p.emitWrite(dst, "\r"); @@ -8760,13 +8896,19 @@ pub const Pardes = struct { return p; } - /// restore a custom tag tail from a saved tag: whatever follows the live - /// prefix, unless it's just the defaults + /// Restore a custom tag tail from a saved tag: whatever follows the live + /// prefix, unless it is this build's default or either exact legacy + /// default from before New appeared in every pane. Legacy defaults upgrade + /// to the live default; genuinely edited tails remain byte-for-byte owned. fn restoreTail(p: *Pardes, pane: *Pane, saved_tag: []const u8) void { const pfx = p.tagPrefix(pane) catch return; if (!std.mem.startsWith(u8, saved_tag, pfx)) return; const rest = saved_tag[pfx.len..]; - if (std.mem.eql(u8, rest, curTail(pane))) return; + const legacy = if (pane.file) |f| + if (output_pane.fileTraits(f.output).saves) legacy_file_pane_tail else legacy_pane_tail + else + legacy_pane_tail; + if (std.mem.eql(u8, rest, curTail(pane)) or std.mem.eql(u8, rest, legacy)) return; pane.tag_tail.appendSlice(p.gpa, rest) catch return; pane.tag_init = true; } |
