Merge pull request #148 from mitchellh/prompt-redraw

Semantic Prompt Redraw
This commit is contained in:
Mitchell Hashimoto
2023-05-31 16:34:58 -07:00
committed by GitHub
5 changed files with 216 additions and 3 deletions

View File

@ -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;

View File

@ -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.
///

View File

@ -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;

View File

@ -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});
},

View File

@ -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 {