mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
Merge pull request #148 from mitchellh/prompt-redraw
Semantic Prompt Redraw
This commit is contained in:
@ -2027,6 +2027,7 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void {
|
||||
try self.scroll(.{ .delta = 1 });
|
||||
}
|
||||
new_row = self.getRow(.{ .active = y });
|
||||
new_row.setSemanticPrompt(old_row.getSemanticPrompt());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2112,6 +2113,8 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void {
|
||||
}
|
||||
|
||||
const row = self.getRow(.{ .active = y });
|
||||
row.setSemanticPrompt(old_row.getSemanticPrompt());
|
||||
|
||||
fastmem.copy(
|
||||
StorageCell,
|
||||
row.storage[1..],
|
||||
@ -2125,6 +2128,7 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void {
|
||||
// Slow path: the row is wrapped or doesn't fit so we have to
|
||||
// wrap ourselves. In this case, we basically just "print and wrap"
|
||||
var row = self.getRow(.{ .active = y });
|
||||
row.setSemanticPrompt(old_row.getSemanticPrompt());
|
||||
var x: usize = 0;
|
||||
var cur_old_row = old_row;
|
||||
var cur_old_row_wrapped = old_row_wrapped;
|
||||
@ -2145,6 +2149,7 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void {
|
||||
}
|
||||
|
||||
row = self.getRow(.{ .active = y });
|
||||
row.setSemanticPrompt(cur_old_row.getSemanticPrompt());
|
||||
}
|
||||
|
||||
// If our cursor is on this char, then set the new cursor.
|
||||
@ -4450,6 +4455,53 @@ test "Screen: resize more cols trailing background colors" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: resize more cols no reflow preserves semantic prompt" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
defer s.deinit();
|
||||
const str = "1ABCD\n2EFGH\n3IJKL";
|
||||
try s.testWriteString(str);
|
||||
|
||||
// Set one of the rows to be a prompt
|
||||
{
|
||||
const row = s.getRow(.{ .active = 1 });
|
||||
row.setSemanticPrompt(.prompt);
|
||||
}
|
||||
|
||||
const cursor = s.cursor;
|
||||
try s.resize(3, 10);
|
||||
|
||||
// Cursor should not move
|
||||
try testing.expectEqual(cursor, s.cursor);
|
||||
|
||||
{
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings(str, contents);
|
||||
}
|
||||
{
|
||||
var contents = try s.testString(alloc, .screen);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings(str, contents);
|
||||
}
|
||||
|
||||
// Our one row should still be a semantic prompt, the others should not.
|
||||
{
|
||||
const row = s.getRow(.{ .active = 0 });
|
||||
try testing.expect(row.getSemanticPrompt() == .unknown);
|
||||
}
|
||||
{
|
||||
const row = s.getRow(.{ .active = 1 });
|
||||
try testing.expect(row.getSemanticPrompt() == .prompt);
|
||||
}
|
||||
{
|
||||
const row = s.getRow(.{ .active = 2 });
|
||||
try testing.expect(row.getSemanticPrompt() == .unknown);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: resize more cols grapheme map" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@ -4973,6 +5025,55 @@ test "Screen: resize less cols with graphemes" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: resize less cols no reflow preserves semantic prompt" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
defer s.deinit();
|
||||
const str = "1AB\n2EF\n3IJ";
|
||||
try s.testWriteString(str);
|
||||
|
||||
// Set one of the rows to be a prompt
|
||||
{
|
||||
const row = s.getRow(.{ .active = 1 });
|
||||
row.setSemanticPrompt(.prompt);
|
||||
}
|
||||
|
||||
s.cursor.x = 0;
|
||||
s.cursor.y = 0;
|
||||
const cursor = s.cursor;
|
||||
try s.resize(3, 3);
|
||||
|
||||
// Cursor should not move
|
||||
try testing.expectEqual(cursor, s.cursor);
|
||||
|
||||
{
|
||||
var contents = try s.testString(alloc, .viewport);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings(str, contents);
|
||||
}
|
||||
{
|
||||
var contents = try s.testString(alloc, .screen);
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings(str, contents);
|
||||
}
|
||||
|
||||
// Our one row should still be a semantic prompt, the others should not.
|
||||
{
|
||||
const row = s.getRow(.{ .active = 0 });
|
||||
try testing.expect(row.getSemanticPrompt() == .unknown);
|
||||
}
|
||||
{
|
||||
const row = s.getRow(.{ .active = 1 });
|
||||
try testing.expect(row.getSemanticPrompt() == .prompt);
|
||||
}
|
||||
{
|
||||
const row = s.getRow(.{ .active = 2 });
|
||||
try testing.expect(row.getSemanticPrompt() == .unknown);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: resize less cols with reflow but row space" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
@ -94,6 +94,12 @@ modes: packed struct {
|
||||
|
||||
bracketed_paste: bool = false, // 2004
|
||||
|
||||
// This isn't a mode, this is set by OSC 133 using the "A" event.
|
||||
// If this is true, it tells us that the shell supports redrawing
|
||||
// the prompt and that when we resize, if the cursor is at a prompt,
|
||||
// then we should clear the screen below and allow the shell to redraw.
|
||||
shell_redraws_prompt: bool = false,
|
||||
|
||||
test {
|
||||
// We have this here so that we explicitly fail when we change the
|
||||
// size of modes. The size of modes is NOT particularly important,
|
||||
@ -312,6 +318,7 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) !
|
||||
|
||||
// If we're making the screen smaller, dealloc the unused items.
|
||||
if (self.active_screen == .primary) {
|
||||
self.clearPromptForResize();
|
||||
try self.screen.resize(rows, cols);
|
||||
try self.secondary_screen.resizeWithoutReflow(rows, cols);
|
||||
} else {
|
||||
@ -330,6 +337,58 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) !
|
||||
};
|
||||
}
|
||||
|
||||
/// If modes.shell_redraws_prompt is true and we're on the primary screen,
|
||||
/// then this will clear the screen from the cursor down if the cursor is
|
||||
/// on a prompt in order to allow the shell to redraw the prompt.
|
||||
fn clearPromptForResize(self: *Terminal) void {
|
||||
assert(self.active_screen == .primary);
|
||||
|
||||
if (!self.modes.shell_redraws_prompt) return;
|
||||
|
||||
// We need to find the first y that is a prompt. If we find any line
|
||||
// that is NOT a prompt (or input -- which is part of a prompt) then
|
||||
// we are not at a prompt and we can exit this function.
|
||||
const prompt_y: usize = prompt_y: {
|
||||
// Keep track of the found value, because we want to find the START
|
||||
var found: ?usize = null;
|
||||
|
||||
// Search from the cursor up
|
||||
var y: usize = 0;
|
||||
while (y <= self.screen.cursor.y) : (y += 1) {
|
||||
const real_y = self.screen.cursor.y - y;
|
||||
const row = self.screen.getRow(.{ .active = real_y });
|
||||
switch (row.getSemanticPrompt()) {
|
||||
// If we're at a prompt or input area, then we are at a prompt.
|
||||
// We mark our found value and continue because the prompt
|
||||
// may be multi-line.
|
||||
.prompt,
|
||||
.input,
|
||||
=> found = real_y,
|
||||
|
||||
// If we have command output, then we're most certainly not
|
||||
// at a prompt. Break out of the loop.
|
||||
.command => break,
|
||||
|
||||
// If we don't know, we keep searching.
|
||||
.unknown => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (found) |found_y| break :prompt_y found_y;
|
||||
return;
|
||||
};
|
||||
assert(prompt_y < self.rows);
|
||||
|
||||
// We want to clear all the lines from prompt_y downwards because
|
||||
// the shell will redraw the prompt.
|
||||
for (prompt_y..self.rows) |y| {
|
||||
const row = self.screen.getRow(.{ .active = y });
|
||||
row.setWrapped(false);
|
||||
row.setDirty(true);
|
||||
row.clear(.{});
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the current string value of the terminal. Newlines are
|
||||
/// encoded as "\n". This omits any formatting such as fg/bg.
|
||||
///
|
||||
|
@ -26,6 +26,7 @@ pub const Command = union(enum) {
|
||||
/// not all shells will send the prompt end code.
|
||||
prompt_start: struct {
|
||||
aid: ?[]const u8 = null,
|
||||
redraw: bool = true,
|
||||
},
|
||||
|
||||
/// End of prompt and start of user input, terminated by a OSC "133;C"
|
||||
@ -345,6 +346,29 @@ pub const Parser = struct {
|
||||
.prompt_start => |*v| v.aid = value,
|
||||
else => {},
|
||||
}
|
||||
} else if (mem.eql(u8, self.temp_state.key, "redraw")) {
|
||||
// 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.
|
||||
switch (self.command) {
|
||||
.prompt_start => |*v| {
|
||||
const valid = if (value.len == 1) valid: {
|
||||
switch (value[0]) {
|
||||
'0' => v.redraw = false,
|
||||
'1' => v.redraw = true,
|
||||
else => break :valid false,
|
||||
}
|
||||
|
||||
break :valid true;
|
||||
} else false;
|
||||
|
||||
if (!valid) {
|
||||
log.info("OSC 133 A invalid redraw value: {s}", .{value});
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
} else log.info("unknown semantic prompts option: {s}", .{self.temp_state.key});
|
||||
}
|
||||
|
||||
@ -416,6 +440,7 @@ test "OSC: prompt_start" {
|
||||
const cmd = p.end().?;
|
||||
try testing.expect(cmd == .prompt_start);
|
||||
try testing.expect(cmd.prompt_start.aid == null);
|
||||
try testing.expect(cmd.prompt_start.redraw);
|
||||
}
|
||||
|
||||
test "OSC: prompt_start with single option" {
|
||||
@ -431,6 +456,32 @@ test "OSC: prompt_start with single option" {
|
||||
try testing.expectEqualStrings("14", cmd.prompt_start.aid.?);
|
||||
}
|
||||
|
||||
test "OSC: prompt_start with redraw disabled" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "133;A;redraw=0";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end().?;
|
||||
try testing.expect(cmd == .prompt_start);
|
||||
try testing.expect(!cmd.prompt_start.redraw);
|
||||
}
|
||||
|
||||
test "OSC: prompt_start with redraw invalid value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "133;A;redraw=42";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end().?;
|
||||
try testing.expect(cmd == .prompt_start);
|
||||
try testing.expect(cmd.prompt_start.redraw);
|
||||
}
|
||||
|
||||
test "OSC: end_of_command no exit code" {
|
||||
const testing = std.testing;
|
||||
|
||||
|
@ -471,9 +471,9 @@ pub fn Stream(comptime Handler: type) type {
|
||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
.prompt_start => {
|
||||
.prompt_start => |v| {
|
||||
if (@hasDecl(T, "promptStart")) {
|
||||
try self.handler.promptStart();
|
||||
try self.handler.promptStart(v.aid, v.redraw);
|
||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
|
@ -1236,8 +1236,10 @@ const StreamHandler = struct {
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
pub fn promptStart(self: *StreamHandler) !void {
|
||||
pub fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) !void {
|
||||
_ = aid;
|
||||
self.terminal.markSemanticPrompt(.prompt);
|
||||
self.terminal.modes.shell_redraws_prompt = redraw;
|
||||
}
|
||||
|
||||
pub fn promptEnd(self: *StreamHandler) !void {
|
||||
|
Reference in New Issue
Block a user