diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index ef362ce78..03dece7c6 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -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; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index e25011ea4..9fa814e3e 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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. /// diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 1c6e796c3..3063bed40 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -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; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 4a6840d58..2f8766c56 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -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}); }, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 115db9c14..294019b83 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -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 {