mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 03:25:50 +00:00
terminal: add redraw=last for bash for OSC133
This commit is contained in:
@@ -4295,7 +4295,7 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void {
|
||||
// This flag is only set if we've seen at least one semantic prompt
|
||||
// OSC sequence. If we've never seen that sequence, we can't possibly
|
||||
// move the cursor so we can fast path out of here.
|
||||
if (!t.flags.shell_redraws_prompt) return;
|
||||
if (!t.screens.active.flags.semantic_content) return;
|
||||
|
||||
// Get our path
|
||||
const from = t.screens.active.cursor.page_pin.*;
|
||||
|
||||
@@ -201,7 +201,7 @@ function __ghostty_precmd() {
|
||||
# Marks. We need to do fresh line (A) at the beginning of the prompt
|
||||
# since if the cursor is not at the beginning of a line, the terminal
|
||||
# will emit a newline.
|
||||
PS1='\[\e]133;A\a\]'$PS1'\[\e]133;B\a\]'
|
||||
PS1='\[\e]133;A;redraw=last\a\]'$PS1'\[\e]133;B\a\]'
|
||||
PS2='\[\e]133;A;k=s\a\]'$PS2'\[\e]133;B\a\]'
|
||||
|
||||
# Bash doesn't redraw the leading lines in a multiline prompt so
|
||||
@@ -240,7 +240,7 @@ function __ghostty_precmd() {
|
||||
fi
|
||||
|
||||
# Fresh line and start of prompt.
|
||||
builtin printf "\e]133;A;aid=%s\a" "$BASHPID"
|
||||
builtin printf "\e]133;A;redraw=last;aid=%s\a" "$BASHPID"
|
||||
_ghostty_executing=0
|
||||
}
|
||||
|
||||
|
||||
@@ -1608,11 +1608,11 @@ pub const Resize = struct {
|
||||
/// lost from the top of the scrollback.
|
||||
reflow: bool = true,
|
||||
|
||||
/// Set this to true to enable prompt redraw on resize. This signals
|
||||
/// Set this to enable prompt redraw on resize. This signals
|
||||
/// that the running program can redraw the prompt if the cursor is
|
||||
/// currently at a prompt. This detects OSC133 prompts lines and clears
|
||||
/// them.
|
||||
prompt_redraw: bool = false,
|
||||
/// them. If set to `.last`, only the most recent prompt line is cleared.
|
||||
prompt_redraw: osc.semantic_prompt.Redraw = .false,
|
||||
};
|
||||
|
||||
/// Resize the screen. The rows or cols can be bigger or smaller.
|
||||
@@ -1682,32 +1682,47 @@ pub inline fn resize(
|
||||
|
||||
// If our cursor is on a prompt line, then we clear the prompt so
|
||||
// the shell can redraw it. This works with OSC133 semantic prompts.
|
||||
if (opts.prompt_redraw and
|
||||
if (opts.prompt_redraw != .false and
|
||||
self.cursor.page_row.semantic_prompt != .none)
|
||||
prompt: {
|
||||
const start = start: {
|
||||
var it = self.cursor.page_pin.promptIterator(
|
||||
.left_up,
|
||||
null,
|
||||
);
|
||||
break :start it.next() orelse {
|
||||
// This should never happen because promptIterator should always
|
||||
// find a prompt if we already verified our row is some kind of
|
||||
// prompt.
|
||||
log.warn("cursor on prompt line but promptIterator found no prompt", .{});
|
||||
break :prompt;
|
||||
};
|
||||
};
|
||||
switch (opts.prompt_redraw) {
|
||||
.false => unreachable,
|
||||
|
||||
// Clear cells from our start down. We replace it with spaces,
|
||||
// and do not physically erase the rows (eraseRows) because the
|
||||
// shell is going to expect this space to be available.
|
||||
var it = start.rowIterator(.right_down, null);
|
||||
while (it.next()) |pin| {
|
||||
const page = &pin.node.data;
|
||||
const row = pin.rowAndCell().row;
|
||||
const cells = page.getCells(row);
|
||||
self.clearCells(page, row, cells);
|
||||
// For `.last`, only clear the current line where the cursor is.
|
||||
// For `.true`, clear all prompt lines starting from the beginning.
|
||||
.last => {
|
||||
const page = &self.cursor.page_pin.node.data;
|
||||
const row = self.cursor.page_row;
|
||||
const cells = page.getCells(row);
|
||||
self.clearCells(page, row, cells);
|
||||
},
|
||||
|
||||
.true => {
|
||||
const start = start: {
|
||||
var it = self.cursor.page_pin.promptIterator(
|
||||
.left_up,
|
||||
null,
|
||||
);
|
||||
break :start it.next() orelse {
|
||||
// This should never happen because promptIterator should always
|
||||
// find a prompt if we already verified our row is some kind of
|
||||
// prompt.
|
||||
log.warn("cursor on prompt line but promptIterator found no prompt", .{});
|
||||
break :prompt;
|
||||
};
|
||||
};
|
||||
|
||||
// Clear cells from our start down. We replace it with spaces,
|
||||
// and do not physically erase the rows (eraseRows) because the
|
||||
// shell is going to expect this space to be available.
|
||||
var it = start.rowIterator(.right_down, null);
|
||||
while (it.next()) |pin| {
|
||||
const page = &pin.node.data;
|
||||
const row = pin.rowAndCell().row;
|
||||
const cells = page.getCells(row);
|
||||
self.clearCells(page, row, cells);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7191,7 +7206,7 @@ test "Screen: resize more cols with cursor at prompt" {
|
||||
try s.resize(.{
|
||||
.cols = 20,
|
||||
.rows = 3,
|
||||
.prompt_redraw = true,
|
||||
.prompt_redraw = .true,
|
||||
});
|
||||
|
||||
// Cursor should not move
|
||||
@@ -7232,7 +7247,7 @@ test "Screen: resize more cols with cursor not at prompt" {
|
||||
try s.resize(.{
|
||||
.cols = 20,
|
||||
.rows = 3,
|
||||
.prompt_redraw = true,
|
||||
.prompt_redraw = .true,
|
||||
});
|
||||
|
||||
// Cursor should not move
|
||||
@@ -7247,6 +7262,88 @@ test "Screen: resize more cols with cursor not at prompt" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: resize with prompt_redraw last clears only one line" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 10, .rows = 4, .max_scrollback = 5 });
|
||||
defer s.deinit();
|
||||
|
||||
// zig fmt: off
|
||||
try s.testWriteString("ABCDE\n");
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_eol });
|
||||
try s.testWriteString("hello\n");
|
||||
try s.testWriteString("world");
|
||||
// zig fmt: on
|
||||
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
const expected = "ABCDE\n> hello\nworld";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
|
||||
// Move cursor back to the prompt line (row 1)
|
||||
s.cursorAbsolute(7, 1);
|
||||
|
||||
try s.resize(.{
|
||||
.cols = 20,
|
||||
.rows = 4,
|
||||
.prompt_redraw = .last,
|
||||
});
|
||||
|
||||
// With .last, only the first prompt line ("> ") should be cleared,
|
||||
// but subsequent input lines ("hello", "world") remain
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
const expected = "ABCDE\n\nworld";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: resize with prompt_redraw last multiline prompt clears only last line" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 5 });
|
||||
defer s.deinit();
|
||||
|
||||
// Create a 3-line prompt: 1 initial + 2 continuation lines
|
||||
// zig fmt: off
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("line1\n");
|
||||
s.cursorSetSemanticContent(.{ .prompt = .continuation });
|
||||
try s.testWriteString("line2\n");
|
||||
s.cursorSetSemanticContent(.{ .prompt = .continuation });
|
||||
try s.testWriteString("line3");
|
||||
// zig fmt: on
|
||||
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
const expected = "line1\nline2\nline3";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
|
||||
// Cursor is at end of line3 (the last continuation line)
|
||||
try s.resize(.{
|
||||
.cols = 30,
|
||||
.rows = 5,
|
||||
.prompt_redraw = .last,
|
||||
});
|
||||
|
||||
// With .last, only line3 (where cursor is) should be cleared
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
const expected = "line1\nline2";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: select untracked" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -83,7 +83,7 @@ flags: packed struct {
|
||||
// This supports a Kitty extension where programs using semantic
|
||||
// prompts (OSC133) can annotate their new prompts with `redraw=0` to
|
||||
// disable clearing the prompt on resize.
|
||||
shell_redraws_prompt: bool = true,
|
||||
shell_redraws_prompt: osc.semantic_prompt.Redraw = .true,
|
||||
|
||||
// This is set via ESC[4;2m. Any other modify key mode just sets
|
||||
// this to false and we act in mode 1 by default.
|
||||
|
||||
@@ -49,10 +49,8 @@ pub const Option = enum {
|
||||
err,
|
||||
|
||||
// https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
// Kitty supports a "redraw" option for prompt_start. I can't find
|
||||
// this documented anywhere but can see in the code that this is used
|
||||
// by shell environments to tell the terminal that the shell will NOT
|
||||
// redraw the prompt so we should attempt to resize it.
|
||||
// Kitty supports a "redraw" option for prompt_start. This is extended
|
||||
// by Ghostty with the "last" option. See Redraw the type for more details.
|
||||
redraw,
|
||||
|
||||
// Use a special key instead of arrow keys to move the cursor on
|
||||
@@ -82,7 +80,7 @@ pub const Option = enum {
|
||||
.cl => Click,
|
||||
.prompt_kind => PromptKind,
|
||||
.err => []const u8,
|
||||
.redraw => bool,
|
||||
.redraw => Redraw,
|
||||
.special_key => bool,
|
||||
.click_events => bool,
|
||||
.exit_code => i32,
|
||||
@@ -170,7 +168,15 @@ pub const Option = enum {
|
||||
.cl => std.meta.stringToEnum(Click, value),
|
||||
.prompt_kind => if (value.len == 1) PromptKind.init(value[0]) else null,
|
||||
.err => value,
|
||||
.redraw, .special_key, .click_events => if (value.len == 1) switch (value[0]) {
|
||||
.redraw => if (std.mem.eql(u8, value, "0"))
|
||||
.false
|
||||
else if (std.mem.eql(u8, value, "1"))
|
||||
.true
|
||||
else if (std.mem.eql(u8, value, "last"))
|
||||
.last
|
||||
else
|
||||
null,
|
||||
.special_key, .click_events => if (value.len == 1) switch (value[0]) {
|
||||
'0' => false,
|
||||
'1' => true,
|
||||
else => null,
|
||||
@@ -209,6 +215,29 @@ pub const PromptKind = enum {
|
||||
}
|
||||
};
|
||||
|
||||
/// The values for the `redraw` extension to OSC133. This was
|
||||
/// started by Kitty[1] and extended by Ghostty (the "last" option).
|
||||
///
|
||||
/// [1]: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
pub const Redraw = enum(u2) {
|
||||
/// The shell supports redrawing the full prompt and all continuations.
|
||||
/// This is the default value, it does not need to be explicitly set
|
||||
/// unless it is to reset a prior other value.
|
||||
true,
|
||||
|
||||
/// The shell does NOT support redrawing. In this case, Ghostty will NOT
|
||||
/// clear any prompt lines on resize.
|
||||
false,
|
||||
|
||||
/// The shell supports redrawing only the LAST line of the prompt.
|
||||
/// Ghostty will only clear the last line of the prompt on resize.
|
||||
///
|
||||
/// This is specifically introduced because Bash only redraws the last
|
||||
/// line. It is literally the only shell that does this and it does this
|
||||
/// because its bad and they should feel bad. Don't be like Bash.
|
||||
last,
|
||||
};
|
||||
|
||||
/// Parse OSC 133, semantic prompts
|
||||
pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand {
|
||||
const writer = parser.writer orelse {
|
||||
@@ -514,7 +543,7 @@ test "OSC 133: fresh_line_new_prompt with redraw=0" {
|
||||
const cmd = p.end(null).?.*;
|
||||
try testing.expect(cmd == .semantic_prompt);
|
||||
try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
|
||||
try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == false);
|
||||
try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == .false);
|
||||
}
|
||||
|
||||
test "OSC 133: fresh_line_new_prompt with redraw=1" {
|
||||
@@ -528,7 +557,7 @@ test "OSC 133: fresh_line_new_prompt with redraw=1" {
|
||||
const cmd = p.end(null).?.*;
|
||||
try testing.expect(cmd == .semantic_prompt);
|
||||
try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
|
||||
try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == true);
|
||||
try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == .true);
|
||||
}
|
||||
|
||||
test "OSC 133: fresh_line_new_prompt with invalid redraw" {
|
||||
@@ -871,8 +900,9 @@ test "Option.read err" {
|
||||
|
||||
test "Option.read redraw" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(Option.redraw.read("redraw=1").? == true);
|
||||
try testing.expect(Option.redraw.read("redraw=0").? == false);
|
||||
try testing.expect(Option.redraw.read("redraw=1").? == .true);
|
||||
try testing.expect(Option.redraw.read("redraw=0").? == .false);
|
||||
try testing.expect(Option.redraw.read("redraw=last").? == .last);
|
||||
try testing.expect(Option.redraw.read("redraw=2") == null);
|
||||
try testing.expect(Option.redraw.read("redraw=10") == null);
|
||||
try testing.expect(Option.redraw.read("redraw=") == null);
|
||||
|
||||
@@ -904,7 +904,7 @@ test "semantic prompt fresh line new prompt" {
|
||||
// Test with redraw option
|
||||
try s.nextSlice("prompt$ ");
|
||||
try s.nextSlice("\x1b]133;A;redraw=1\x07");
|
||||
try testing.expect(t.flags.shell_redraws_prompt);
|
||||
try testing.expect(t.flags.shell_redraws_prompt == .true);
|
||||
}
|
||||
|
||||
test "semantic prompt end of input, then start output" {
|
||||
|
||||
Reference in New Issue
Block a user