summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 15:26:17 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit830e936da4aca7ea802a6a862fa40c0086defd2f (patch)
tree8f2b76129f35fc27263dc975cfb61ac8b4024ea2 /src
parentd0b4e65eeaf71c8215cda347e7546f3abc07b25b (diff)
downloadpardes-830e936da4aca7ea802a6a862fa40c0086defd2f.tar.gz
pardes-830e936da4aca7ea802a6a862fa40c0086defd2f.zip
A command of several lines keeps its pane's tag to one row, and the tag never panics on a longer prefix
A two-line exec made a command pane whose tag prefix held the line's newline, so the tag ran to a second row and tagTintRange sliced its first row past its end: panic. acme runs a multi-line selection as one command, and so does pardes; the tag now shows the line's first row, cut at a character's start, and '...', tagTintRange stays inside the row it is given, and the '% line' echo turns the line's newlines into the terminal's, so it does not staircase. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/body_layer.zig5
-rw-r--r--src/exec.zig11
-rw-r--r--src/pardes.zig27
-rw-r--r--src/tagline.zig9
4 files changed, 46 insertions, 6 deletions
diff --git a/src/body_layer.zig b/src/body_layer.zig
index c8e229c4..42b9c082 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -674,7 +674,8 @@ pub fn paintHeaders(p: *Pardes) !void {
fn tagTintRange(pane: *const Pane, tag: []const u8, prefix_len: usize) ?struct { lo: usize, hi: usize } {
if (pane.isTerminal()) {
- var words = std.mem.tokenizeAny(u8, tag[prefix_len..], " \t\r\n");
+ // `tag` is the first row; a prefix longer than it ran to a second.
+ var words = std.mem.tokenizeAny(u8, tag[@min(prefix_len, tag.len)..], " \t\r\n");
while (words.next()) |word| {
if (!std.mem.eql(u8, word, "Tty") and !std.mem.startsWith(u8, word, "Tty+")) continue;
const lo = @intFromPtr(word.ptr) - @intFromPtr(tag.ptr);
@@ -687,7 +688,7 @@ fn tagTintRange(pane: *const Pane, tag: []const u8, prefix_len: usize) ?struct {
if (pane.prompt == .name) pane.input.own orelse file.path else file.path
else if (pane.image) |state| blk: {
// Image tags put renderer controls before the path.
- start = std.mem.lastIndexOf(u8, tag[0..prefix_len], state.path) orelse return null;
+ start = std.mem.lastIndexOf(u8, tag[0..@min(prefix_len, tag.len)], state.path) orelse return null;
break :blk state.path;
} else if (comptime pdf_enabled) blk: {
break :blk if (pane.pdf) |state| state.path else return null;
diff --git a/src/exec.zig b/src/exec.zig
index 1b82d2a8..84a0ad5f 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -534,9 +534,14 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
// shown, then a soft reset (DECSTR) for the rest.
if (panes.Terminal.onAlternateScreen(pane)) panes.Terminal.feedOutput(p, pane, "\x1b[?1049l");
panes.Terminal.feedOutput(p, pane, "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l\x1b[?25h\x1b[!p");
- var buf: [command_max + 8]u8 = undefined;
- const lead = if (panes.Terminal.gridCursor(pane).x != 0) "\r\n" else "";
- panes.Terminal.feedOutput(p, pane, std.fmt.bufPrint(&buf, "{s}% {s}\r\n", .{ lead, line }) catch "%\r\n");
+ // The line as it runs, its newlines the terminal's, so a multi-line
+ // command does not staircase.
+ var buf: [2 * command_max + 8]u8 = undefined;
+ var w = std.Io.Writer.fixed(&buf);
+ w.writeAll(if (panes.Terminal.gridCursor(pane).x != 0) "\r\n% " else "% ") catch {};
+ for (line) |c| (if (c == '\n') w.writeAll("\r\n") else w.writeByte(c)) catch {};
+ w.writeAll("\r\n") catch {};
+ panes.Terminal.feedOutput(p, pane, w.buffered());
p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } });
noteRun(p, pane, "run", line);
return id;
diff --git a/src/pardes.zig b/src/pardes.zig
index 9821839f..80a20a0d 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1361,6 +1361,33 @@ test "Exec in a terminal whose tty is taken runs as a command pane instead of ty
try std.testing.expect(spawned);
}
+test "a command of several lines keeps its tag one row, and its echo does not staircase" {
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const doc = try p.newDocPane(1);
+ doc.image = .{ .path = try gpa.dupe(u8, "/tmp/pic.ppm") };
+ const two = "def greet(name):\n return name\n\nprint(greet(\"x\"))\n";
+ const dst = exec.execute(p, 1, two) orelse return error.NoCommandPane;
+ while (p.nextEffect()) |_| {}
+ const pane = p.panes[dst].?;
+ const prefix = try tagline.tagPrefix(p, pane);
+ try std.testing.expect(std.mem.indexOfScalar(u8, prefix, '\n') == null);
+ try std.testing.expect(std.mem.indexOf(u8, prefix, "(def greet(name):...) running") != null);
+ // Rendered without a panic, however the tag wraps.
+ var frame = std.heap.ArenaAllocator.init(gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ p.update(.{ .exited = .{ .pane = @intCast(dst), .status = 0 } });
+ _ = exec.execute(p, 1, two);
+ while (p.nextEffect()) |_| {}
+ _ = try p.render(frame.allocator());
+ const body = try panes.Terminal.screenTextAlloc(pane, gpa);
+ defer gpa.free(body);
+ try std.testing.expect(std.mem.indexOf(u8, body, "% def greet(name):\n return name") != null);
+}
+
test "a spawn is for the pane it was made for, not whatever took the slot since" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true });
diff --git a/src/tagline.zig b/src/tagline.zig
index 14bf5677..28dffa06 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -112,8 +112,15 @@ pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 {
return std.fmt.allocPrint(arena, "{s} [{d}/{d}]", .{ pv.path, pv.page + 1, pv.page_count });
if (pane.image) |*state| return panes.Image.tagPrefix(arena, state);
// A command pane: its directory, the line it runs, and how that went.
+ // Its first line only, cut to 48 bytes at a character's start: a tag's
+ // first row is where the prefix ends, and a line that ran over to a
+ // second (acme runs a multi-line selection as one command) would move
+ // it (body_layer.zig tagTintRange).
if (pane.command) |line| {
- const shown = line[0..@min(line.len, 48)];
+ const first = line[0 .. std.mem.indexOfAny(u8, line, "\r\n") orelse line.len];
+ var cut = @min(first.len, 48);
+ while (cut < first.len and cut > 0 and first[cut] & 0xC0 == 0x80) cut -= 1;
+ const shown = first[0..cut];
const more = if (shown.len < line.len) "..." else "";
var idbuf: [48]u8 = undefined;
const id = exec.replId(&idbuf, pane);