mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
terminal: track semantic prompt metadata per row
This commit is contained in:
@ -136,7 +136,32 @@ pub const RowHeader = struct {
|
|||||||
|
|
||||||
/// True if any cell in this row has a grapheme associated with it.
|
/// True if any cell in this row has a grapheme associated with it.
|
||||||
grapheme: bool = false,
|
grapheme: bool = false,
|
||||||
|
|
||||||
|
/// True if this row is an active prompt (awaiting input). This is
|
||||||
|
/// set to false when the semantic prompt events (OSC 133) are received.
|
||||||
|
/// There are scenarios where the shell may never send this event, so
|
||||||
|
/// in order to reliably test prompt status, you need to iterate
|
||||||
|
/// backwards from the cursor to check the current line status going
|
||||||
|
/// back.
|
||||||
|
semantic_prompt: SemanticPrompt = .unknown,
|
||||||
} = .{},
|
} = .{},
|
||||||
|
|
||||||
|
/// Semantic prompt type.
|
||||||
|
pub const SemanticPrompt = enum(u3) {
|
||||||
|
/// Unknown, the running application didn't tell us for this line.
|
||||||
|
unknown = 0,
|
||||||
|
|
||||||
|
/// This is a prompt line, meaning it only contains the shell prompt.
|
||||||
|
/// For poorly behaving shells, this may also be the input.
|
||||||
|
prompt = 1,
|
||||||
|
|
||||||
|
/// This line contains the input area. We don't currently track
|
||||||
|
/// where this actually is in the line, so we just assume it is somewhere.
|
||||||
|
input = 2,
|
||||||
|
|
||||||
|
/// This line is the start of command output.
|
||||||
|
command = 3,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Cell is a single cell within the screen.
|
/// Cell is a single cell within the screen.
|
||||||
@ -276,6 +301,16 @@ pub const Row = struct {
|
|||||||
return self.storage[0].header.flags.dirty;
|
return self.storage[0].header.flags.dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the semantic prompt state for this row.
|
||||||
|
pub fn setSemanticPrompt(self: Row, p: RowHeader.SemanticPrompt) void {
|
||||||
|
self.storage[0].header.flags.semantic_prompt = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the semantic prompt state for this row.
|
||||||
|
pub fn getSemanticPrompt(self: Row) RowHeader.SemanticPrompt {
|
||||||
|
return self.storage[0].header.flags.semantic_prompt;
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve the header for this row.
|
/// Retrieve the header for this row.
|
||||||
pub fn header(self: Row) RowHeader {
|
pub fn header(self: Row) RowHeader {
|
||||||
return self.storage[0].header;
|
return self.storage[0].header;
|
||||||
|
@ -31,6 +31,17 @@ pub const ScreenType = enum {
|
|||||||
alternate,
|
alternate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// See: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
|
||||||
|
pub const SemanticPrompt = enum {
|
||||||
|
prompt,
|
||||||
|
input,
|
||||||
|
command,
|
||||||
|
};
|
||||||
|
|
||||||
/// Screen is the current screen state. The "active_screen" field says what
|
/// Screen is the current screen state. The "active_screen" field says what
|
||||||
/// the current screen is. The backup screen is the opposite of the active
|
/// the current screen is. The backup screen is the opposite of the active
|
||||||
/// screen.
|
/// screen.
|
||||||
@ -1386,6 +1397,50 @@ pub fn setScrollingRegion(self: *Terminal, top: usize, bottom: usize) void {
|
|||||||
self.setCursorPos(1, 1);
|
self.setCursorPos(1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark the current semantic prompt information. Current escape sequences
|
||||||
|
/// (OSC 133) only allow setting this for wherever the current active cursor
|
||||||
|
/// is located.
|
||||||
|
pub fn markSemanticPrompt(self: *Terminal, p: SemanticPrompt) void {
|
||||||
|
const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
|
||||||
|
row.setSemanticPrompt(switch (p) {
|
||||||
|
.prompt => .prompt,
|
||||||
|
.input => .input,
|
||||||
|
.command => .command,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the cursor is currently at a prompt. Another way to look
|
||||||
|
/// at this is it returns false if the shell is currently outputing something.
|
||||||
|
/// This requires shell integration (semantic prompt integration).
|
||||||
|
///
|
||||||
|
/// If the shell integration doesn't exist, this will always return false.
|
||||||
|
pub fn cursorIsAtPrompt(self: *Terminal) bool {
|
||||||
|
// If we're on the secondary screen, we're never at a prompt.
|
||||||
|
if (self.active_screen == .alternate) return false;
|
||||||
|
|
||||||
|
var y: usize = 0;
|
||||||
|
while (y <= self.screen.cursor.y) : (y += 1) {
|
||||||
|
// We want to go bottom up
|
||||||
|
const bottom_y = self.screen.cursor.y - y;
|
||||||
|
const row = self.screen.getRow(.{ .active = bottom_y });
|
||||||
|
switch (row.getSemanticPrompt()) {
|
||||||
|
// If we're at a prompt or input area, then we are at a prompt.
|
||||||
|
.prompt,
|
||||||
|
.input,
|
||||||
|
=> return true,
|
||||||
|
|
||||||
|
// If we have command output, then we're most certainly not
|
||||||
|
// at a prompt.
|
||||||
|
.command => return false,
|
||||||
|
|
||||||
|
// If we don't know, we keep searching.
|
||||||
|
.unknown => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Full reset
|
/// Full reset
|
||||||
pub fn fullReset(self: *Terminal) void {
|
pub fn fullReset(self: *Terminal) void {
|
||||||
self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = true });
|
self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = true });
|
||||||
@ -2117,3 +2172,49 @@ test "Terminal: insertBlanks more than size" {
|
|||||||
try testing.expectEqualStrings("", str);
|
try testing.expectEqualStrings("", str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Terminal: cursorIsAtPrompt" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, 3, 2);
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
try testing.expect(!t.cursorIsAtPrompt());
|
||||||
|
t.markSemanticPrompt(.prompt);
|
||||||
|
try testing.expect(t.cursorIsAtPrompt());
|
||||||
|
|
||||||
|
// Input is also a prompt
|
||||||
|
t.markSemanticPrompt(.input);
|
||||||
|
try testing.expect(t.cursorIsAtPrompt());
|
||||||
|
|
||||||
|
// Newline -- we expect we're still at a prompt if we received
|
||||||
|
// prompt stuff before.
|
||||||
|
try t.linefeed();
|
||||||
|
try testing.expect(t.cursorIsAtPrompt());
|
||||||
|
|
||||||
|
// But once we say we're starting output, we're not a prompt
|
||||||
|
t.markSemanticPrompt(.command);
|
||||||
|
try testing.expect(!t.cursorIsAtPrompt());
|
||||||
|
try t.linefeed();
|
||||||
|
try testing.expect(!t.cursorIsAtPrompt());
|
||||||
|
|
||||||
|
// Until we know we're at a prompt again
|
||||||
|
try t.linefeed();
|
||||||
|
t.markSemanticPrompt(.prompt);
|
||||||
|
try testing.expect(t.cursorIsAtPrompt());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: cursorIsAtPrompt alternate screen" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, 3, 2);
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
try testing.expect(!t.cursorIsAtPrompt());
|
||||||
|
t.markSemanticPrompt(.prompt);
|
||||||
|
try testing.expect(t.cursorIsAtPrompt());
|
||||||
|
|
||||||
|
// Secondary screen is never a prompt
|
||||||
|
t.alternateScreen(.{});
|
||||||
|
try testing.expect(!t.cursorIsAtPrompt());
|
||||||
|
t.markSemanticPrompt(.prompt);
|
||||||
|
try testing.expect(!t.cursorIsAtPrompt());
|
||||||
|
}
|
||||||
|
@ -276,6 +276,7 @@ pub const Parser = struct {
|
|||||||
self.command = .{ .end_of_command = .{} };
|
self.command = .{ .end_of_command = .{} };
|
||||||
self.complete = true;
|
self.complete = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -471,6 +471,24 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.prompt_start => {
|
||||||
|
if (@hasDecl(T, "promptStart")) {
|
||||||
|
try self.handler.promptStart();
|
||||||
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
|
},
|
||||||
|
|
||||||
|
.prompt_end => {
|
||||||
|
if (@hasDecl(T, "promptEnd")) {
|
||||||
|
try self.handler.promptEnd();
|
||||||
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
|
},
|
||||||
|
|
||||||
|
.end_of_command => |end| {
|
||||||
|
if (@hasDecl(T, "endOfCommand")) {
|
||||||
|
try self.handler.endOfCommand(end.exit_code);
|
||||||
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
|
},
|
||||||
|
|
||||||
else => if (@hasDecl(T, "oscUnimplemented"))
|
else => if (@hasDecl(T, "oscUnimplemented"))
|
||||||
try self.handler.oscUnimplemented(cmd)
|
try self.handler.oscUnimplemented(cmd)
|
||||||
else
|
else
|
||||||
|
Reference in New Issue
Block a user