terminal: start implementing proper semantic prompt behaviors

This commit is contained in:
Mitchell Hashimoto
2026-01-24 12:10:17 -08:00
parent 7a69e2bf86
commit 24bf642bdc
2 changed files with 77 additions and 2 deletions

View File

@@ -17,6 +17,7 @@ const charsets = @import("charsets.zig");
const csi = @import("csi.zig");
const hyperlink = @import("hyperlink.zig");
const kitty = @import("kitty.zig");
const osc = @import("osc.zig");
const point = @import("point.zig");
const sgr = @import("sgr.zig");
const Tabstops = @import("Tabstops.zig");
@@ -1058,6 +1059,65 @@ pub fn setProtectedMode(self: *Terminal, mode: ansi.ProtectedMode) void {
}
}
/// Perform a semantic prompt command.
///
/// If there is an error, we do our best to get the terminal into
/// some coherent state, since callers typically can't handle errors
/// (since they're sending sequences via the pty).
pub fn semanticPrompt(
self: *Terminal,
cmd: osc.Command.SemanticPrompt,
) !void {
switch (cmd.action) {
.fresh_line => try self.semanticPromptFreshLine(),
.fresh_line_new_prompt => {
// "First do a fresh-line."
try self.semanticPromptFreshLine();
// "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)."
// TODO
// The "aid" and "cl" options are also valid for this
// command but we don't yet handle these in any meaningful way.
},
else => {},
}
}
fn semanticPromptSet(
self: *Terminal,
mode: pagepkg.Cell.SemanticContent,
) void {
// We always reset this when we mode change. The caller can set it
// again after if they care.
self.screens.active.cursor.semantic_content_clear_eol = false;
// Update our mode
self.screens.active.cursor.semantic_content = mode;
}
// OSC 133;L
fn semanticPromptFreshLine(self: *Terminal) !void {
const left_margin = if (self.screens.active.cursor.x < self.scrolling_region.left)
0
else
self.scrolling_region.left;
// Spec: "If the cursor is the initial column (left, assuming
// left-to-right writing), do nothing" This specification is very under
// specified. We are taking the liberty to assume that in a left/right
// margin context, if the cursor is outside of the left margin, we treat
// it as being at the left margin for the purposes of this command.
// This is arbitrary. If someone has a better reasonable idea we can
// apply it.
if (self.screens.active.cursor.x == left_margin) return;
self.carriageReturn();
try self.index();
}
/// The semantic prompt type. This is used when tracking a line type and
/// requires integration with the shell. By default, we mark a line as "none"
/// meaning we don't know what type it is.

View File

@@ -153,7 +153,7 @@ pub const Handler = struct {
.full_reset => self.terminal.fullReset(),
.start_hyperlink => try self.terminal.screens.active.startHyperlink(value.uri, value.id),
.end_hyperlink => self.terminal.screens.active.endHyperlink(),
.semantic_prompt => self.semanticPrompt(value),
.semantic_prompt => try self.semanticPrompt(value),
.mouse_shape => self.terminal.mouse_shape = value,
.color_operation => try self.colorOperation(value.op, &value.requests),
.kitty_color_report => try self.kittyColorOperation(value),
@@ -212,7 +212,9 @@ pub const Handler = struct {
fn semanticPrompt(
self: *Handler,
cmd: Action.SemanticPrompt,
) void {
) !void {
try self.terminal.semanticPrompt(cmd);
switch (cmd.action) {
.fresh_line_new_prompt => {
const kind = cmd.readOption(.prompt_kind) orelse .initial;
@@ -905,3 +907,16 @@ test "palette dirty flag set on color change" {
try s.nextSlice("\x1b]21;1=rgb:00/ff/00\x1b\\");
try testing.expect(t.flags.dirty.palette);
}
test "semantic prompt fresh line" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
var s: Stream = .initAlloc(testing.allocator, .init(&t));
defer s.deinit();
try s.nextSlice("Hello");
try s.nextSlice("\x1b]133;L\x07");
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
}