terminal: add semantic_prompt2 to Row to track prompt state

This commit is contained in:
Mitchell Hashimoto
2026-01-24 20:03:36 -08:00
parent 84cfb9de1c
commit a80b3f34c0
3 changed files with 147 additions and 19 deletions

View File

@@ -1077,12 +1077,10 @@ pub fn semanticPrompt(
// "Subsequent text (until a OSC "133;B" or OSC "133;I" command)
// is a prompt string (as if followed by OSC 133;P;k=i\007)."
// Implementation note: we don't yet differentiate between
// the prompt types (k=) because it isn't of value to us
// currently. This may change in the future.
self.screens.active.cursor.semantic_content = .prompt;
self.screens.active.cursor.semantic_content_clear_eol = false;
self.semanticPromptSet(
.prompt,
cmd.readOption(.prompt_kind) orelse .initial,
);
// This is a kitty-specific flag that notes that the shell
// is capable of redraw.
@@ -1112,29 +1110,27 @@ pub fn semanticPrompt(
// The k (kind) option specifies the type of prompt:
// regular primary prompt (k=i or default),
// right-side prompts (k=r), or prompts for continuation lines (k=c or k=s).
// As noted above, we don't currently utilize the prompt type.
self.screens.active.cursor.semantic_content = .prompt;
self.screens.active.cursor.semantic_content_clear_eol = false;
self.semanticPromptSet(
.prompt,
cmd.readOption(.prompt_kind) orelse .initial,
);
},
.end_prompt_start_input => {
// End of prompt and start of user input, terminated by a OSC
// "133;C" or another prompt (OSC "133;P").
self.screens.active.cursor.semantic_content = .input;
self.screens.active.cursor.semantic_content_clear_eol = false;
self.semanticPromptSet(.input, .initial);
},
.end_prompt_start_input_terminate_eol => {
// End of prompt and start of user input, terminated by end-of-line.
self.semanticPromptSet(.input);
self.semanticPromptSet(.input, .initial);
self.screens.active.cursor.semantic_content_clear_eol = true;
},
.end_input_start_output => {
// "End of input, and start of output."
self.screens.active.cursor.semantic_content = .output;
self.screens.active.cursor.semantic_content_clear_eol = false;
self.semanticPromptSet(.output, .initial);
},
.end_command => {
@@ -1142,8 +1138,7 @@ pub fn semanticPrompt(
// anything. Other terminals appear to do nothing here. I think
// its reasonable at this point to reset our semantic content
// state but the spec doesn't really say what to do.
self.screens.active.cursor.semantic_content = .output;
self.screens.active.cursor.semantic_content_clear_eol = false;
self.semanticPromptSet(.output, .initial);
},
}
}
@@ -1151,6 +1146,7 @@ pub fn semanticPrompt(
fn semanticPromptSet(
self: *Terminal,
mode: pagepkg.Cell.SemanticContent,
kind: osc.semantic_prompt.PromptKind,
) void {
// We always reset this when we mode change. The caller can set it
// again after if they care.
@@ -1158,6 +1154,20 @@ fn semanticPromptSet(
// Update our mode
self.screens.active.cursor.semantic_content = mode;
// We only need to update our row marker for prompt types. We
// use a switch in case new modes are introduced so the compiler
// can force us to handle them.
switch (mode) {
.input, .output => return,
.prompt => {},
}
// Last prompt type wins
self.screens.active.cursor.page_row.semantic_prompt2 = switch (kind) {
.initial, .right => .prompt,
.continuation, .secondary => .prompt_continuation,
};
}
// OSC 133;L
@@ -1304,7 +1314,11 @@ pub fn index(self: *Terminal) !void {
// Unset pending wrap state
self.screens.active.cursor.pending_wrap = false;
// Always reset any semantic content clear-eol state
// Always reset any semantic content clear-eol state.
//
// The specification is not clear what "end-of-line" means. If we
// discover that there are more scenarios we should be unsetting
// this we should document and test it.
if (self.screens.active.cursor.semantic_content_clear_eol) {
@branchHint(.unlikely);
self.screens.active.cursor.semantic_content = .output;
@@ -11314,6 +11328,97 @@ test "Terminal: eraseDisplay complete preserves cursor" {
try testing.expect(t.screens.active.cursor.style_id != style.default_id);
}
test "Terminal: semantic prompt" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Prompt
try t.semanticPrompt(.init(.fresh_line_new_prompt));
for ("hello") |c| try t.print(c);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x);
{
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
.x = t.screens.active.cursor.x - 1,
.y = t.screens.active.cursor.y,
} }).?;
const cell = list_cell.cell;
try testing.expectEqual(.prompt, cell.semantic_content);
const row = list_cell.row;
try testing.expectEqual(.prompt, row.semantic_prompt2);
}
// Start input but end it on EOL
try t.semanticPrompt(.init(.end_prompt_start_input_terminate_eol));
t.carriageReturn();
try t.linefeed();
// Write some output
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
for ("world") |c| try t.print(c);
{
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
.x = t.screens.active.cursor.x - 1,
.y = t.screens.active.cursor.y,
} }).?;
const cell = list_cell.cell;
try testing.expectEqual(.output, cell.semantic_content);
const row = list_cell.row;
try testing.expectEqual(.no_prompt, row.semantic_prompt2);
}
}
test "Terminal: semantic prompt continuations" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Prompt
try t.semanticPrompt(.init(.fresh_line_new_prompt));
for ("hello") |c| try t.print(c);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x);
{
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
.x = t.screens.active.cursor.x - 1,
.y = t.screens.active.cursor.y,
} }).?;
const cell = list_cell.cell;
try testing.expectEqual(.prompt, cell.semantic_content);
const row = list_cell.row;
try testing.expectEqual(.prompt, row.semantic_prompt2);
}
// Start input but end it on EOL
t.carriageReturn();
try t.linefeed();
try t.semanticPrompt(.{
.action = .prompt_start,
.options_unvalidated = "k=c",
});
// Write some output
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
for ("world") |c| try t.print(c);
{
const list_cell = t.screens.active.pages.getCell(.{ .active = .{
.x = t.screens.active.cursor.x - 1,
.y = t.screens.active.cursor.y,
} }).?;
const cell = list_cell.cell;
try testing.expectEqual(.prompt, cell.semantic_content);
const row = list_cell.row;
try testing.expectEqual(.prompt_continuation, row.semantic_prompt2);
}
}
test "Terminal: cursorIsAtPrompt" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 3, .rows = 2 });

View File

@@ -17,6 +17,7 @@ const parsers = @import("osc/parsers.zig");
const encoding = @import("osc/encoding.zig");
pub const color = parsers.color;
pub const semantic_prompt = parsers.semantic_prompt;
const log = std.log.scoped(.osc);

View File

@@ -1902,6 +1902,17 @@ pub const Row = packed struct(u64) {
/// running program, or "unknown" if it was never set.
semantic_prompt: SemanticPrompt = .unknown,
/// The semantic prompt state for this row.
///
/// This is ONLY meant to note if there are ANY cells in this
/// row that are part of a prompt. This is an optimization for more
/// efficiently implementing jump-to-prompt operations.
///
/// This may contain false positives but never false negatives. If
/// this is set, you should still check individual cells to see if they
/// have prompt semantics.
semantic_prompt2: SemanticPrompt2 = .no_prompt,
/// True if this row contains a virtual placeholder for the Kitty
/// graphics protocol. (U+10EEEE)
// Note: We keep this as memory-using even if the kitty graphics
@@ -1922,7 +1933,18 @@ pub const Row = packed struct(u64) {
/// screen.
dirty: bool = false,
_padding: u22 = 0,
_padding: u20 = 0,
/// The semantic prompt state of the row. See `semantic_prompt`.
pub const SemanticPrompt2 = enum(u2) {
/// No prompt cells in this row.
no_prompt = 0,
/// Prompt cells exist in this row.
prompt = 1,
/// Prompt cells exist in this row that had k=c set (continuation)
/// line. This is used as a way to
prompt_continuation = 2,
};
/// Semantic prompt type.
pub const SemanticPrompt = enum(u3) {