diff options
| -rw-r--r-- | src/Terminal.zig | 21 | ||||
| -rw-r--r-- | src/pardes.zig | 18 | ||||
| -rw-r--r-- | test/panes.zig | 28 |
3 files changed, 60 insertions, 7 deletions
diff --git a/src/Terminal.zig b/src/Terminal.zig index 4808298e..32595d25 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -1013,6 +1013,27 @@ pub fn onAlternateScreen(pane: *const Pane) bool { return state.vt.screens.active_key == .alternate; } +/// Whether a new terminal's greeting may be typed now: only at the shell's +/// first prompt (pardes's own marks: no command started; a prompt drawn +/// again for a resize is the same first prompt), with nothing +/// typed at it and no program on the alternate screen. Any later moment is `.never`, for good: something has run or +/// been typed first, and an `ls` then would go into it. A shell pardes +/// cannot mark has no first prompt to find, and is never greeted. +pub fn greeting(pane: *const Pane) enum { wait, now, never } { + if (comptime !enabled) return .never; + if (pane.fs.unmarked) return .never; + // Text typed into the edit buffer that stands in for the prompt row. + if (pane.ovl) |o| if (o.text.len > 0) return .never; + const state = pane.terminal orelse return .wait; + const marks = &state.stream.handler; + // (Not `declined`: bash's first prompt comes after a D of its own.) + if (marks.started > 0) return .never; + // At a prompt's input (ghostty's semantic marks, pardes's or the + // shell's own), before any command pardes's marks saw run. + if (!promptInputReady(pane)) return .wait; + return if (promptInputEmpty(pane)) .now else .never; +} + pub fn promptInputReady(pane: *const Pane) bool { if (comptime !enabled) return false; const state = pane.terminal orelse return false; diff --git a/src/pardes.zig b/src/pardes.zig index 3a574dc9..de4d6abc 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -6686,11 +6686,19 @@ pub const Pardes = struct { if (pane.file) |*file| file.syntax_dirty = true; } panes.Terminal.releasePendingCommandIfReady(p, id, pane); - if (pane.greet and pane.isTerminal() and p.resize_count > 0 and panes.Terminal.promptInputReady(pane)) { - p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = pane.cols, .rows = pane.rows } }); - p.emit(.{ .write = .{ .pane = @intCast(id), .bytes = .from("ls\r") } }); - pane.greet = false; - } + // The greeting `ls` of a new terminal in a directory is typed at + // its shell's FIRST prompt, or never: typed later it would land + // in whatever runs then, or in the middle of a line someone + // typed (panes.Terminal.greeting). + if (pane.greet and pane.isTerminal() and p.resize_count > 0) switch (panes.Terminal.greeting(pane)) { + .wait => {}, + .never => pane.greet = false, + .now => { + p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = pane.cols, .rows = pane.rows } }); + p.emit(.{ .write = .{ .pane = @intCast(id), .bytes = .from("ls\r") } }); + pane.greet = false; + }, + }; } } diff --git a/test/panes.zig b/test/panes.zig index 1ca33053..c1ae1666 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -3466,14 +3466,14 @@ const TerminalTests = struct { .write => return error.GreetedBeforePrompt, else => {}, }; - p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;A\x07prompt$ " } }); + p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;A;aid=pardes\x07prompt$ " } }); try std.testing.expect(!promptInputReady(pane)); while (p.nextEffect()) |effect| switch (effect) { .write => return error.GreetedDuringPrompt, else => {}, }; - p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;B\x07" } }); + p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;B;aid=pardes\x07" } }); try std.testing.expect(promptInputReady(pane)); var greeted = false; while (p.nextEffect()) |effect| switch (effect) { @@ -3484,6 +3484,30 @@ const TerminalTests = struct { try std.testing.expect(!pane.greet); } + test "the greeting is dropped, never typed later, once something ran or was typed first" { + if (pardes.platform == .web) return; + for ([_][]const u8{ + // A command ran before the first prompt was seen at rest. + "\x1b]133;A;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07\x1b]133;C;aid=pardes\x07out\r\n\x1b]133;D;0;aid=pardes\x07\x1b]133;A;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07", + // Someone typed at the first prompt. + "\x1b]133;A;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07vim", + }) |output| { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + const pane = p.panes[0].?; + while (p.nextEffect()) |_| {} + p.update(.{ .resize = .{ .cols = 80, .rows = 24 } }); + p.update(.{ .output = .{ .pane = 0, .bytes = output } }); + // and a later, quiet prompt changes nothing + p.update(.{ .output = .{ .pane = 0, .bytes = "\r\n\x1b]133;A;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07" } }); + while (p.nextEffect()) |effect| switch (effect) { + .write => |write| if (std.mem.eql(u8, write.bytes.slice(), "ls\r")) return error.GreetedLate, + else => {}, + }; + try std.testing.expect(!pane.greet); + } + } + test "fresh-shell commands preserve order and wait for OSC 133 B" { if (pardes.platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); |
