Add terminal escape hatch panel
This commit is contained in:
+86
-6
@@ -142,6 +142,10 @@ pub const Client = struct {
|
||||
if (std.mem.eql(u8, line, "job_status")) return self.openStaticJobRow("job-status", "job_status:idle");
|
||||
if (std.mem.eql(u8, line, "job_cancel")) return self.openStaticJobRow("job-status", "job_cancel:no_running_job");
|
||||
if (std.mem.startsWith(u8, line, "job_open_selected ")) return self.openSelectedJobLocation(line[18..]);
|
||||
if (std.mem.startsWith(u8, line, "terminal_run ")) return self.openTerminalRun(line[13..]);
|
||||
if (std.mem.eql(u8, line, "terminal_status")) return self.openStaticJobRow("terminal-status", "terminal_status:idle");
|
||||
if (std.mem.eql(u8, line, "terminal_cancel")) return self.openStaticJobRow("terminal-status", "terminal_cancel:no_running_terminal");
|
||||
if (std.mem.eql(u8, line, "terminal_exit")) return self.closeTerminalPanel();
|
||||
if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
|
||||
@@ -374,23 +378,34 @@ pub const Client = struct {
|
||||
|
||||
fn openJobRun(self: *Client, cwd_and_command: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
const first_space = std.mem.indexOfScalar(u8, cwd_and_command, ' ') orelse return Error.ProtocolRejected;
|
||||
const cwd = cwd_and_command[0..first_space];
|
||||
const command_line = std.mem.trim(u8, cwd_and_command[first_space + 1 ..], " ");
|
||||
if (command_line.len == 0) return Error.ProtocolRejected;
|
||||
const split = splitCwdAndCommand(cwd_and_command) orelse return Error.ProtocolRejected;
|
||||
var argv = std.ArrayList([]const u8).empty;
|
||||
defer argv.deinit(self.allocator);
|
||||
var parts = std.mem.splitScalar(u8, command_line, ' ');
|
||||
var parts = std.mem.splitScalar(u8, split.command, ' ');
|
||||
while (parts.next()) |part| {
|
||||
if (part.len == 0) continue;
|
||||
try argv.append(self.allocator, part);
|
||||
}
|
||||
const rows = job_mod.runRowsAlloc(self.allocator, io, cwd, argv.items) catch return Error.ProtocolRejected;
|
||||
const rows = job_mod.runRowsAlloc(self.allocator, io, split.cwd, argv.items) catch return Error.ProtocolRejected;
|
||||
defer freeOwnedRows(self.allocator, rows);
|
||||
try self.session.openListPanel("job-output", rows);
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openTerminalRun(self: *Client, cwd_and_command: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
const split = splitCwdAndCommand(cwd_and_command) orelse return Error.ProtocolRejected;
|
||||
const rows = job_mod.shellRowsAlloc(self.allocator, io, split.cwd, split.command) catch return Error.ProtocolRejected;
|
||||
defer freeOwnedRows(self.allocator, rows);
|
||||
try self.session.openListPanel("terminal-output", rows);
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn closeTerminalPanel(self: *Client) !void {
|
||||
try self.session.closePanel();
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openStaticJobRow(self: *Client, title: []const u8, row: []const u8) !void {
|
||||
try self.session.openListPanel(title, &.{row});
|
||||
self.message = null;
|
||||
@@ -636,6 +651,14 @@ fn freeOwnedRows(allocator: std.mem.Allocator, rows: []const []const u8) void {
|
||||
allocator.free(rows);
|
||||
}
|
||||
|
||||
fn splitCwdAndCommand(cwd_and_command: []const u8) ?struct { cwd: []const u8, command: []const u8 } {
|
||||
const first_space = std.mem.indexOfScalar(u8, cwd_and_command, ' ') orelse return null;
|
||||
const cwd = cwd_and_command[0..first_space];
|
||||
const command = std.mem.trim(u8, cwd_and_command[first_space + 1 ..], " ");
|
||||
if (cwd.len == 0 or command.len == 0) return null;
|
||||
return .{ .cwd = cwd, .command = command };
|
||||
}
|
||||
|
||||
test "regular: scripted narrow terminal trace edits saves exits and replays saved bytes" {
|
||||
const trace =
|
||||
\\open abc
|
||||
@@ -1379,3 +1402,60 @@ test "adversarial: bad job command is visible and failed job open does not corru
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||
}
|
||||
|
||||
test "regular: terminal escape hatch runs shell command exits and returns editor" {
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 72, .height = 6 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open safe_editor");
|
||||
try client.handleTraceLine("terminal_run . printf terminal_ok");
|
||||
{
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "terminal-output") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "terminal:status:exit_0") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "stdout:terminal_ok") != null);
|
||||
}
|
||||
|
||||
try client.handleTraceLine("terminal_exit");
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "terminal-output") == null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "safe_editor") != null);
|
||||
}
|
||||
|
||||
test "regular: terminal status and cancel are honest foreground lifecycle rows" {
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 64, .height = 5 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("terminal_status");
|
||||
{
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "terminal-status") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "terminal_status:idle") != null);
|
||||
}
|
||||
|
||||
try client.handleTraceLine("terminal_cancel");
|
||||
{
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "terminal_cancel:no_running_terminal") != null);
|
||||
}
|
||||
}
|
||||
|
||||
test "adversarial: terminal command rejection does not corrupt editor" {
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 72, .height = 5 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open safe");
|
||||
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("terminal_run . "));
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||
|
||||
try client.handleTraceLine("terminal_run . definitely-not-a-mim-command");
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "terminal:status:exit_127") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "stderr:") != null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user