terminal: Screen tracks semantic content seen

This commit is contained in:
Mitchell Hashimoto
2026-01-26 09:39:44 -08:00
parent fd016fdb2a
commit 07dce38cc5
2 changed files with 58 additions and 40 deletions

View File

@@ -77,6 +77,9 @@ else
/// Dirty flags for the renderer.
dirty: Dirty = .{},
/// Packed flags for the screen, internal state.
flags: Flags = .{},
/// See Terminal.Dirty. This behaves the same way.
pub const Dirty = packed struct {
/// Set when the selection is set or unset, regardless of if the
@@ -88,6 +91,17 @@ pub const Dirty = packed struct {
hyperlink_hover: bool = false,
};
/// A set of internal state that we pack for memory size.
pub const Flags = packed struct {
/// This is flipped to true when any sort of semantic content is
/// seen. In particular, this is set to true only when a `prompt` type
/// is ever set on our cursor.
///
/// This is used to optimize away semantic content operations if we know
/// we've never seen them.
semantic_content: bool = false,
};
/// The cursor position and style.
pub const Cursor = struct {
// The x/y position within the active area.
@@ -152,39 +166,6 @@ pub const Cursor = struct {
alloc.destroy(link);
}
}
/// Modify the semantic content type of the cursor. This should
/// be preferred over setting it manually since it handles all the
/// proper accounting.
pub fn setSemanticContent(self: *Cursor, t: union(enum) {
prompt: osc.semantic_prompt.PromptKind,
output,
input: enum { clear_explicit, clear_eol },
}) void {
switch (t) {
.output => {
self.semantic_content = .output;
self.semantic_content_clear_eol = false;
},
.input => |clear| {
self.semantic_content = .input;
self.semantic_content_clear_eol = switch (clear) {
.clear_explicit => false,
.clear_eol => true,
};
},
.prompt => |kind| {
self.semantic_content = .prompt;
self.semantic_content_clear_eol = false;
self.page_row.semantic_prompt2 = switch (kind) {
.initial, .right => .prompt,
.continuation, .secondary => .prompt_continuation,
};
},
}
}
};
/// Saved cursor state.
@@ -397,6 +378,7 @@ pub fn reset(self: *Screen) void {
self.charset = .{};
self.kitty_keyboard = .{};
self.protected_mode = .off;
self.flags = .{};
self.clearSelection();
}
@@ -2363,6 +2345,42 @@ pub fn cursorSetHyperlink(self: *Screen) PageList.IncreaseCapacityError!void {
}
}
/// Modify the semantic content type of the cursor. This should
/// be preferred over setting it manually since it handles all the
/// proper accounting.
pub fn cursorSetSemanticContent(self: *Screen, t: union(enum) {
prompt: osc.semantic_prompt.PromptKind,
output,
input: enum { clear_explicit, clear_eol },
}) void {
const cursor = &self.cursor;
switch (t) {
.output => {
cursor.semantic_content = .output;
cursor.semantic_content_clear_eol = false;
},
.input => |clear| {
cursor.semantic_content = .input;
cursor.semantic_content_clear_eol = switch (clear) {
.clear_explicit => false,
.clear_eol => true,
};
},
.prompt => |kind| {
self.flags.semantic_content = true;
cursor.semantic_content = .prompt;
cursor.semantic_content_clear_eol = false;
cursor.page_row.semantic_prompt2 = switch (kind) {
.initial, .right => .prompt,
.continuation, .secondary => .prompt_continuation,
};
},
}
}
/// Set the selection to the given selection. If this is a tracked selection
/// then the screen will take ownership of the selection. If this is untracked
/// then the screen will convert it to tracked internally. This will automatically
@@ -3874,7 +3892,7 @@ test "Screen eraseRows active partial" {
}
}
test "Screen: clearPrompt" {
test "Screen: clearPrompt single line prompt" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@@ -1077,7 +1077,7 @@ 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)."
self.screens.active.cursor.setSemanticContent(.{
self.screens.active.cursorSetSemanticContent(.{
.prompt = cmd.readOption(.prompt_kind) orelse .initial,
});
@@ -1109,7 +1109,7 @@ 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).
self.screens.active.cursor.setSemanticContent(.{
self.screens.active.cursorSetSemanticContent(.{
.prompt = cmd.readOption(.prompt_kind) orelse .initial,
});
},
@@ -1117,21 +1117,21 @@ pub fn semanticPrompt(
.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.setSemanticContent(.{
self.screens.active.cursorSetSemanticContent(.{
.input = .clear_explicit,
});
},
.end_prompt_start_input_terminate_eol => {
// End of prompt and start of user input, terminated by end-of-line.
self.screens.active.cursor.setSemanticContent(.{
self.screens.active.cursorSetSemanticContent(.{
.input = .clear_eol,
});
},
.end_input_start_output => {
// "End of input, and start of output."
self.screens.active.cursor.setSemanticContent(.output);
self.screens.active.cursorSetSemanticContent(.output);
},
.end_command => {
@@ -1139,7 +1139,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.setSemanticContent(.output);
self.screens.active.cursorSetSemanticContent(.output);
},
}
}