Merge pull request #146 from mitchellh/semantic-prompt

Semantic Prompt (OSC 133) Integration
This commit is contained in:
Mitchell Hashimoto
2023-05-27 16:53:49 -07:00
committed by GitHub
6 changed files with 184 additions and 10 deletions

View File

@ -529,18 +529,18 @@ pub fn deinit(self: *Surface) void {
/// close process, which should ultimately deinitialize this surface.
pub fn close(self: *Surface) void {
const process_alive = process_alive: {
// Inform close() if it should hold open the surface or not. If the child
// exited, we don't want to
var process_alive = !self.child_exited;
// If the child has exited then our process is certainly not alive.
// We check this first to avoid the locking overhead below.
if (self.child_exited) break :process_alive false;
// However, if we are configured to not hold open surfaces explicitly,
// just tell close to not hold them open by saying there are no alive
// processes
if (!self.config.confirm_close_surface) {
process_alive = false;
}
// If we are configured to not hold open surfaces explicitly, just
// always say there is nothing alive.
if (!self.config.confirm_close_surface) break :process_alive false;
break :process_alive process_alive;
// We have to talk to the terminal.
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
break :process_alive !self.io.terminal.cursorIsAtPrompt();
};
self.rt_surface.close(process_alive);

View File

@ -136,7 +136,32 @@ pub const RowHeader = struct {
/// True if any cell in this row has a grapheme associated with it.
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.
@ -276,6 +301,16 @@ pub const Row = struct {
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.
pub fn header(self: Row) RowHeader {
return self.storage[0].header;

View File

@ -31,6 +31,17 @@ pub const ScreenType = enum {
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
/// the current screen is. The backup screen is the opposite of the active
/// screen.
@ -1386,6 +1397,51 @@ pub fn setScrollingRegion(self: *Terminal, top: usize, bottom: usize) void {
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 {
log.warn("semantic_prompt: {}", .{p});
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
pub fn fullReset(self: *Terminal) void {
self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = true });
@ -2117,3 +2173,49 @@ test "Terminal: insertBlanks more than size" {
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());
}

View File

@ -276,6 +276,7 @@ pub const Parser = struct {
self.command = .{ .end_of_command = .{} };
self.complete = true;
},
else => self.state = .invalid,
},

View File

@ -471,6 +471,30 @@ pub fn Stream(comptime Handler: type) type {
} 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_input => {
if (@hasDecl(T, "endOfInput")) {
try self.handler.endOfInput();
} 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"))
try self.handler.oscUnimplemented(cmd)
else

View File

@ -1235,4 +1235,16 @@ const StreamHandler = struct {
),
}, .{ .forever = {} });
}
pub fn promptStart(self: *StreamHandler) !void {
self.terminal.markSemanticPrompt(.prompt);
}
pub fn promptEnd(self: *StreamHandler) !void {
self.terminal.markSemanticPrompt(.input);
}
pub fn endOfInput(self: *StreamHandler) !void {
self.terminal.markSemanticPrompt(.command);
}
};