src/gui/picker.zig
Ref: Size: 29.1 KiB History
//! Host/session opening policy, with a modal picker or automatic naming on
//! the focused target. Discovery jobs own IO; requests retain their destination
//! and attachment generation until creation and pane insertion finish.
const std = @import("std");
const client = @import("client");
const proto = @import("term").protocol;
const model = @import("workspace.zig");
const runtime = @import("runtime.zig");
const popover = @import("popover.zig");
const discovery = client.discovery;
pub const Level = enum { hosts, sessions, host_name, session_name, busy };
pub const Key = enum { up, down, enter, escape, backspace };
const Host = struct { label: []const u8, target: ?client.Target, reason: []const u8 = "" };
pub const Picker = struct {
pub const Mode = enum { insert, replace, quick };
alloc: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
rt: *runtime.Runtime,
origin: ?model.Attachment,
origin_tab: model.TabId,
pending: ?model.Pending,
mode: Mode = .insert,
ticket: discovery.Ticket,
next_generation: *u64,
level: Level = .hosts,
host_index: usize = 0,
hosts: std.ArrayList(Host) = .empty,
sessions: [proto.sessions_max][proto.session_name_max]u8 = undefined,
session_lens: [proto.sessions_max]u8 = @splat(0),
session_count: usize = 0,
presentation: popover.State,
job: ?*discovery.Job = null,
wake_ctx: ?*anyopaque,
wake: ?*const fn (?*anyopaque, discovery.Ticket) void,
catalogue: ?[]const u8 = null,
key_path: ?[]const u8,
width: u32,
height: u32,
metrics: model.Metrics,
closed: bool = false,
inserted: bool = false,
collisions: usize = 0,
pub fn init(alloc: std.mem.Allocator, rt: *runtime.Runtime, next_generation: *u64, key_path: ?[]const u8, width: u32, height: u32, metrics: model.Metrics, wake_ctx: ?*anyopaque, wake: ?*const fn (?*anyopaque, discovery.Ticket) void) !*Picker {
return initMode(alloc, rt, next_generation, key_path, width, height, metrics, wake_ctx, wake, .insert);
}
pub fn initMode(alloc: std.mem.Allocator, rt: *runtime.Runtime, next_generation: *u64, key_path: ?[]const u8, width: u32, height: u32, metrics: model.Metrics, wake_ctx: ?*anyopaque, wake: ?*const fn (?*anyopaque, discovery.Ticket) void, mode: Mode) !*Picker {
if (mode != .replace and rt.workspace.tab().pending == null) rt.workspace.arm(.beside);
const pending = rt.workspace.tab().pending;
const id = if (mode == .replace) rt.workspace.tab().focus else if (pending) |p| p.pane else null;
const pane = if (id) |p| rt.workspace.pane(p) orelse return error.MissingPane else null;
if (mode != .insert and pane == null) return error.MissingPane;
const origin: ?model.Attachment = if (pane) |p| .{ .pane = p.id, .generation = p.generation } else null;
var presentation = try popover.State.init(alloc, .{});
const self = alloc.create(Picker) catch |err| {
presentation.deinit();
return err;
};
self.* = .{ .alloc = alloc, .arena = std.heap.ArenaAllocator.init(alloc), .rt = rt, .origin = origin, .origin_tab = rt.workspace.active_tab_id, .pending = pending, .mode = mode, .ticket = .{ .generation = next_generation.*, .owner = if (origin) |v| v.pane else 0, .attachment_generation = if (origin) |v| v.generation else 0 }, .next_generation = next_generation, .key_path = key_path, .width = width, .height = height, .metrics = metrics, .wake_ctx = wake_ctx, .wake = wake, .presentation = presentation };
next_generation.* += 1;
errdefer self.deinit();
if (mode == .quick) {
// Check geometry before any request, and retain the exact target,
// including custom socket, key and SSH arguments.
_ = try self.preflight();
try self.includeTarget(pane.?.identity.target);
try self.setNoticeFmt("Opening on {s}... (Esc cancels)", .{self.host()});
try self.list();
return self;
}
const a = self.arena.allocator();
self.catalogue = client.hosts.statePath(a) catch null;
if (self.catalogue) |path| {
const loaded = client.hosts.load(a, path) catch |err| blk: {
self.setNotice(@errorName(err));
break :blk client.hosts.Hosts{};
};
for (loaded.lines.items) |line| {
const target = client.Target.fromSpec(a, try client.hosts.parse(line), key_path, client.quic_idle_ms_default, false) catch |err| {
try self.hosts.append(a, .{ .label = line, .target = null, .reason = @errorName(err) });
continue;
};
try self.hosts.append(a, .{ .label = line, .target = target });
}
}
if (pane) |p| try self.includeTarget(p.identity.target);
try self.show(.hosts);
return self;
}
pub fn includeTarget(self: *Picker, current: client.Target) !void {
const a = self.arena.allocator();
const label = switch (current) {
.sock => |path| try std.fmt.allocPrint(a, "--sock {s}", .{path}),
.quic => |q| try std.fmt.allocPrint(a, "quic://{s}", .{q.host_port}),
.hand => |h| h.host,
.via => "Current connection",
};
var found = false;
for (self.hosts.items) |*entry| if (std.mem.eql(u8, entry.label, label)) {
entry.target = try discovery.cloneTarget(a, current);
entry.reason = "";
found = true;
break;
};
if (!found) try self.hosts.append(a, .{ .label = try a.dupe(u8, label), .target = try discovery.cloneTarget(a, current) });
if (self.level == .hosts) try self.show(.hosts);
}
pub fn deinit(self: *Picker) void {
if (self.job != null) self.cancel();
self.presentation.deinit();
self.arena.deinit();
self.alloc.destroy(self);
}
pub fn noticeText(self: *const Picker) []const u8 {
return self.presentation.noticeText();
}
pub fn host(self: *const Picker) []const u8 {
return if (self.hosts.items.len > 0) self.hosts.items[self.host_index].label else "";
}
pub fn text(self: *Picker, bytes: []const u8) !void {
const accepted = self.presentation.append(bytes) catch |err| switch (err) {
error.TextTooLong => {
self.setNotice("Name is too long");
return;
},
else => return err,
};
if (accepted) self.clearNotice();
}
pub fn key(self: *Picker, pressed: Key) !void {
switch (pressed) {
.up => self.presentation.navigate(false),
.down => self.presentation.navigate(true),
.backspace => if (self.presentation.backspace()) self.clearNotice(),
.escape => try self.back(),
.enter => if (self.presentation.submit()) |value| try self.event(value),
}
}
pub fn pointerDown(self: *Picker, x: u32, y: u32) void {
self.presentation.pointerDown(self.layout(), x, y);
}
pub fn pointerUp(self: *Picker, x: u32, y: u32) !void {
if (self.presentation.pointerUp(self.layout(), x, y)) |value| try self.event(value);
}
pub fn cancelPress(self: *Picker) void {
self.presentation.cancelPress();
}
pub fn setDirection(self: *Picker, direction: model.Direction) void {
if (self.mode != .insert or (self.level != .hosts and self.level != .sessions) or !self.validOrigin()) return;
if (self.pending) |*pending| {
pending.direction = direction;
self.rt.workspace.tab().pending = pending.*;
}
}
pub fn cancel(self: *Picker) void {
self.clearNotice();
self.show(.hosts) catch {};
self.closed = self.mode == .quick;
if (self.closed) self.setNotice("Pane opening cancelled");
if (self.job) |job| {
job.cancel();
const result = self.finishJob();
self.ticket.generation = self.next_generation.*;
self.next_generation.* += 1;
if (result.may_have_created) {
self.show(.session_name) catch {};
self.outcomeNotice(result.phase == .created, "Esc refreshes sessions; no automatic retry.");
}
}
}
/// Pointer dismissal owns its complete gesture and tears down the modal
/// after cancelling work. Escape retains the established back/refresh
/// screen when an outcome is uncertain.
fn dismiss(self: *Picker) void {
var previous: [1024]u8 = undefined;
const old = self.noticeText();
const len = @min(old.len, previous.len);
@memcpy(previous[0..len], old[0..len]);
self.cancel();
if (self.noticeText().len == 0 and len != 0) self.setNotice(previous[0..len]);
self.closed = true;
}
fn back(self: *Picker) !void {
if (self.mode == .quick) return self.cancel();
self.clearNotice();
switch (self.level) {
.hosts => self.closed = true,
.sessions, .host_name => {
try self.show(.hosts);
},
.session_name => try self.list(),
.busy => self.cancel(),
}
}
fn choose(self: *Picker) !void {
self.clearNotice();
switch (self.level) {
.hosts => {
const selected = self.presentation.selected orelse return;
if (selected == self.hosts.items.len) {
try self.show(.host_name);
self.presentation.input.clearRetainingCapacity();
} else {
self.host_index = selected;
try self.list();
}
},
.sessions => {
const selected = self.presentation.selected orelse return;
if (selected == self.session_count) {
try self.show(.session_name);
self.presentation.input.clearRetainingCapacity();
} else try self.insert(self.sessions[selected][0..self.session_lens[selected]], false);
},
.host_name => {
const a = self.arena.allocator();
const spelling = self.presentation.input.items;
const spec = client.hosts.parse(spelling) catch |err| {
self.setNotice(client.hosts.reason(err));
return;
};
const target = client.Target.fromSpec(a, spec, self.key_path, client.quic_idle_ms_default, false) catch |err| {
self.setNotice(@errorName(err));
return;
};
const path = self.catalogue orelse {
self.setNotice("Host catalogue path is unavailable");
return;
};
_ = client.hosts.record(self.alloc, path, spelling) catch |err| {
self.setNotice(@errorName(err));
return;
};
for (self.hosts.items, 0..) |h, i| if (std.mem.eql(u8, h.label, spelling)) {
self.host_index = i;
try self.list();
return;
};
try self.hosts.append(a, .{ .label = try a.dupe(u8, spelling), .target = try discovery.cloneTarget(a, target) });
self.host_index = self.hosts.items.len - 1;
try self.list();
},
.session_name => try self.create(),
.busy => {},
}
}
fn create(self: *Picker) !void {
const name = self.presentation.input.items;
if (!proto.validSessionName(name)) {
self.setNotice("Choose a valid session name (no spaces)");
return;
}
const placement = self.preflight() catch |err| {
self.setNotice(@errorName(err));
self.closed = self.mode == .quick;
return;
};
try self.start(.{ .create = .{ .name = name, .cols = placement.cols, .rows = placement.rows } });
}
fn validOrigin(self: *Picker) bool {
if (self.rt.workspace.active_tab_id != self.origin_tab) return false;
if (self.origin) |origin| {
if (!self.rt.accepts(origin)) return false;
} else if (self.rt.workspace.tab().tree.root != null) return false;
return self.mode == .replace or std.meta.eql(self.rt.workspace.tab().pending, self.pending);
}
fn preflight(self: *Picker) !model.Placement {
if (!self.validOrigin()) return error.OriginChanged;
if (self.mode == .replace) {
const workspace_layout = self.rt.workspace.layout(self.width, self.height, self.metrics);
if (!workspace_layout.fits) return error.TooSmall;
return workspace_layout.get(self.origin.?.pane) orelse error.MissingPane;
}
return self.rt.workspace.preview(self.width, self.height, self.metrics);
}
fn insert(self: *Picker, name: []const u8, created: bool) !void {
if (!self.validOrigin()) {
self.insertionFailed(error.OriginChanged, created);
return;
}
const target = self.hosts.items[self.host_index].target orelse {
self.setNotice("Target is unavailable");
return;
};
if (self.mode == .replace) {
const workspace_layout = self.rt.workspace.layout(self.width, self.height, self.metrics);
self.rt.replace(self.origin.?.pane, target, name, workspace_layout.get(self.origin.?.pane).?, false) catch |err| {
self.insertionFailed(err, created);
return;
};
} else {
const focus = self.rt.workspace.tab().focus;
_ = self.rt.addWithPolicy(target, name, self.width, self.height, self.metrics, true) catch |err| {
self.insertionFailed(err, created);
return;
};
// Typing may have moved to a different pane during discovery.
if (self.mode == .quick and focus != self.origin.?.pane) if (focus) |id| {
_ = self.rt.workspace.focus(id);
};
}
self.inserted = true;
self.closed = true;
}
fn insertionFailed(self: *Picker, err: anyerror, created: bool) void {
if (created) self.outcomeNotice(true, "Pane insertion failed. Esc refreshes available sessions.") else self.setNotice(@errorName(err));
self.closed = self.mode == .quick;
}
fn list(self: *Picker) !void {
if (self.hosts.items[self.host_index].target == null) {
try self.show(.hosts);
self.setNotice(self.hosts.items[self.host_index].reason);
return;
}
try self.start(.list);
}
fn start(self: *Picker, operation: discovery.Operation) !void {
std.debug.assert(self.job == null);
self.ticket.generation = self.next_generation.*;
self.next_generation.* += 1;
self.job = discovery.Job.start(self.alloc, .{ .ticket = self.ticket, .target = self.hosts.items[self.host_index].target.?, .operation = operation, .use_poll_recipe = self.mode != .quick, .wake_ctx = self.wake_ctx, .wake = self.wake }) catch |err| {
self.setNotice(@errorName(err));
self.closed = self.mode == .quick;
return;
};
try self.show(.busy);
}
pub fn poll(self: *Picker) !bool {
const job = self.job orelse return false;
if (!job.done.load(.acquire)) return false;
const creating = job.opts.operation == .create;
try self.applyResult(self.finishJob(), creating);
return true;
}
fn finishJob(self: *Picker) discovery.Result {
const job = self.job.?;
const result = job.join();
job.stop();
self.job = null;
return result;
}
fn applyResult(self: *Picker, result: discovery.Result, creating: bool) !void {
if (!std.meta.eql(result.ticket, self.ticket) or !self.validOrigin()) {
try self.show(.hosts);
if (result.phase == .created) self.outcomeNotice(true, "Destination changed. Refresh sessions to choose it.") else if (result.may_have_created) self.outcomeNotice(false, "Destination changed. Refresh sessions before retrying.") else self.setNotice("The pane for this request changed");
self.closed = self.mode == .quick;
return;
}
switch (result.phase) {
.sessions => {
if (self.mode == .quick) {
var buf: [proto.session_name_max]u8 = undefined;
self.presentation.input.clearRetainingCapacity();
try self.presentation.input.appendSlice(self.alloc, client.nextFreeName(&buf, result.text()));
try self.create();
return;
}
self.session_count = 0;
var iter = proto.sessionsIter(result.text());
while (iter.next()) |name| {
if (self.session_count == self.sessions.len) break;
@memcpy(self.sessions[self.session_count][0..name.len], name);
self.session_lens[self.session_count] = @intCast(name.len);
self.session_count += 1;
}
try self.show(.sessions);
},
.created => {
try self.show(.session_name);
try self.insert(self.presentation.input.items, true);
},
.exists => {
// A definite collision is safe to retry. A lost reply is not:
// it may already have created the shell under this name.
if (self.mode == .quick and self.collisions < proto.sessions_max) {
self.collisions += 1;
try self.list();
} else {
try self.show(.session_name);
self.closed = self.mode == .quick;
self.setNotice(if (self.mode == .quick) "Session names keep changing; try again" else result.reasonText());
}
},
else => {
if (!creating) self.session_count = 0;
try self.show(if (creating) .session_name else .sessions);
if (result.may_have_created) self.outcomeNotice(false, "Esc refreshes sessions; no automatic retry.") else self.setNotice(result.reasonText());
self.closed = self.mode == .quick;
},
}
return;
}
fn outcomeNotice(self: *Picker, created: bool, suffix: []const u8) void {
const outcome = if (created) "Session created" else "Creation outcome unknown for";
var message: [1024]u8 = undefined;
const message_text = if (self.mode == .quick)
std.fmt.bufPrint(&message, "{s} '{s}' on {s}. Prefix Enter to check sessions.", .{ outcome, self.presentation.input.items, self.host() }) catch "Creation outcome is unknown"
else
std.fmt.bufPrint(&message, "{s} '{s}'. {s}", .{ outcome, self.presentation.input.items, suffix }) catch "Creation outcome is unknown";
self.setNotice(message_text);
}
pub fn layout(self: *Picker) popover.Layout {
return self.presentation.layout(self.width, self.height, .{ .cell_w = self.metrics.cell_w, .cell_h = self.metrics.cell_h });
}
fn event(self: *Picker, value: popover.Event) !void {
switch (value) {
.select => |index| {
self.presentation.selected = index;
try self.choose();
},
.submit => try self.choose(),
.dismiss => self.dismiss(),
}
}
/// Picker screens remain workflow state, but each screen has exactly one
/// owned presentation. Replacement completes before the screen changes.
pub fn show(self: *Picker, next: Level) !void {
var rows: std.ArrayListUnmanaged(popover.Row) = .empty;
defer rows.deinit(self.alloc);
switch (next) {
.hosts => {
for (self.hosts.items) |entry| try rows.append(self.alloc, .{ .label = entry.label });
try rows.append(self.alloc, .{ .label = "Add host..." });
},
.sessions => {
for (0..self.session_count) |i| try rows.append(self.alloc, .{ .label = self.sessions[i][0..self.session_lens[i]] });
try rows.append(self.alloc, .{ .label = "New session..." });
},
else => {},
}
const title = switch (next) {
.hosts => "Choose host",
.sessions => "Choose session",
.host_name => "Add host",
.session_name => "New session",
.busy => if (self.job != null and self.job.?.opts.operation == .create) "Creating session..." else "Loading sessions...",
};
const editing = next == .host_name or next == .session_name;
const hint = if (editing) "Enter confirms | Esc goes back" else if (next == .busy) "Esc cancels" else if (self.mode == .insert) "j/k choose | Enter selects | v below, b beside | Esc back" else "Up/Down or j/k choose | Enter selects | Esc goes back";
try self.presentation.replace(.{ .title = title, .context = if (next != .hosts and next != .host_name) self.host() else "", .notice = self.noticeText(), .hint = hint, .rows = rows.items, .editor = if (editing) .{ .limit = if (next == .session_name) proto.session_name_max else 1024 } else null });
self.level = next;
if (next == .hosts and self.host_index < self.hosts.items.len) self.presentation.selected = self.host_index;
}
fn clearNotice(self: *Picker) void {
if (self.noticeText().len == 0) return;
self.presentation.setNotice("");
}
fn setNoticeFmt(self: *Picker, comptime format: []const u8, args: anytype) !void {
var bytes: [1024]u8 = undefined;
self.setNotice(try std.fmt.bufPrint(&bytes, format, args));
}
fn setNotice(self: *Picker, text_: []const u8) void {
self.presentation.setNotice(text_);
}
};
test "picker validates fit before create and preserves remote outcome after origin or tab changes" {
const a = std.testing.allocator;
var rt = runtime.Runtime.init(a, .{});
defer rt.deinit();
const metrics: model.Metrics = .{ .cell_w = 8, .cell_h = 16 };
const id = try rt.add(.{ .via = "cat" }, "origin", 800, 600, metrics);
rt.workspace.arm(.beside);
var next: u64 = 2;
var picker: Picker = .{ .alloc = a, .arena = std.heap.ArenaAllocator.init(a), .rt = &rt, .origin = rt.get(id).?.key, .origin_tab = rt.workspace.active_tab_id, .pending = rt.workspace.tab().pending.?, .ticket = .{ .generation = 1, .owner = id, .attachment_generation = 1 }, .next_generation = &next, .key_path = null, .width = 1, .height = 1, .metrics = metrics, .wake = null, .wake_ctx = null, .presentation = try popover.State.init(a, .{}) };
defer picker.arena.deinit();
defer picker.presentation.deinit();
try picker.hosts.append(picker.arena.allocator(), .{ .label = "fixture", .target = .{ .via = "cat" } });
try picker.includeTarget(.{ .via = "local" });
try std.testing.expectEqualStrings("Current connection", picker.presentation.view().rows[1].label);
try picker.show(.session_name);
try picker.text("new-shell");
try picker.key(.enter);
try std.testing.expect(picker.job == null);
try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "TooSmall") != null);
try picker.text("\x01");
try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "TooSmall") != null);
picker.dismiss();
try std.testing.expect(picker.closed and std.mem.indexOf(u8, picker.noticeText(), "TooSmall") != null);
picker.closed = false;
try picker.show(.session_name);
try picker.show(.busy);
try picker.key(.backspace);
try std.testing.expectEqualStrings("new-shell", picker.presentation.input.items);
try picker.show(.session_name);
picker.session_count = 1;
@memcpy(picker.sessions[0][0..3], "old");
picker.session_lens[0] = 3;
try picker.show(.sessions);
try picker.applyResult(.{ .ticket = picker.ticket, .phase = .failed }, false);
try std.testing.expectEqual(@as(usize, 0), picker.session_count);
try std.testing.expectEqualStrings("New session...", picker.presentation.view().rows[0].label);
try picker.show(.session_name);
try std.testing.expectEqual(@as(usize, 1), rt.workspace.layout(800, 600, metrics).len);
picker.width = 800;
picker.height = 600;
const next_id = rt.workspace.next_pane_id;
_ = try picker.preflight();
try std.testing.expectEqual(next_id, rt.workspace.next_pane_id);
rt.workspace.active_tab_id += 1;
try picker.applyResult(.{ .ticket = picker.ticket, .phase = .created, .may_have_created = true }, true);
try std.testing.expect(!picker.inserted and !picker.closed);
try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "Session created 'new-shell'") != null);
rt.workspace.active_tab_id = picker.origin_tab;
rt.workspace.cancel();
try picker.applyResult(.{ .ticket = picker.ticket, .phase = .cancelled, .may_have_created = true }, true);
try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "outcome unknown") != null);
try std.testing.expectEqual(@as(usize, 1), rt.workspace.layout(800, 600, metrics).len);
rt.workspace.tab().pending = picker.pending;
const placement = rt.workspace.layout(800, 600, metrics).get(id).?;
const old_key = rt.get(id).?.key;
try rt.retry(id, placement);
try std.testing.expect(!rt.accepts(old_key));
try picker.applyResult(.{ .ticket = picker.ticket, .phase = .created, .may_have_created = true }, true);
try std.testing.expect(std.mem.indexOf(u8, picker.noticeText(), "Session created 'new-shell'") != null);
try std.testing.expectEqual(@as(usize, 1), rt.workspace.layout(800, 600, metrics).len);
}
test "quick creation retries collisions but retains unknown outcomes and its original destination" {
const a = std.testing.allocator;
var rt = runtime.Runtime.init(a, .{});
defer rt.deinit();
const metrics: model.Metrics = .{ .cell_w = 8, .cell_h = 16 };
const id = try rt.add(.{ .via = "cat" }, "origin", 1200, 900, metrics);
const neighbor = try rt.add(.{ .via = "cat" }, "neighbor", 1200, 900, metrics);
_ = rt.workspace.focus(id);
rt.workspace.arm(.stacked);
var next: u64 = 2;
var opening: Picker = .{ .alloc = a, .arena = std.heap.ArenaAllocator.init(a), .rt = &rt, .origin = rt.get(id).?.key, .origin_tab = rt.workspace.active_tab_id, .pending = rt.workspace.tab().pending, .mode = .quick, .ticket = .{ .generation = 1, .owner = id, .attachment_generation = 1 }, .next_generation = &next, .key_path = null, .width = 1200, .height = 900, .metrics = metrics, .wake = null, .wake_ctx = null, .presentation = try popover.State.init(a, .{}) };
defer opening.arena.deinit();
defer opening.presentation.deinit();
defer if (opening.job) |job| job.stop();
try opening.includeTarget(rt.workspace.pane(id).?.identity.target);
var result: discovery.Result = .{ .ticket = opening.ticket, .phase = .sessions };
const names = "0\n2\n";
@memcpy(result.bytes[0..names.len], names);
result.len = names.len;
try opening.applyResult(result, false);
try std.testing.expectEqualStrings("1", opening.job.?.opts.operation.create.name);
opening.job.?.stop();
opening.job = null;
try opening.applyResult(.{ .ticket = opening.ticket, .phase = .exists }, true);
try std.testing.expect(opening.job.?.opts.operation == .list and !opening.closed);
opening.job.?.stop();
opening.job = null;
const updated = "0\n1\n2\n";
@memcpy(result.bytes[0..updated.len], updated);
result.len = updated.len;
result.ticket = opening.ticket;
try opening.applyResult(result, false);
try std.testing.expectEqualStrings("3", opening.job.?.opts.operation.create.name);
opening.job.?.stop();
opening.job = null;
try opening.applyResult(.{ .ticket = opening.ticket, .phase = .failed, .may_have_created = true }, true);
try std.testing.expect(opening.closed and opening.job == null and !opening.inserted);
try std.testing.expect(std.mem.indexOf(u8, opening.noticeText(), "outcome unknown for '3'") != null);
opening.closed = false;
rt.workspace.active_tab_id += 1;
try opening.applyResult(.{ .ticket = opening.ticket, .phase = .created, .may_have_created = true }, true);
try std.testing.expect(opening.closed and !opening.inserted);
try std.testing.expect(std.mem.indexOf(u8, opening.noticeText(), "Session created '3'") != null);
rt.workspace.active_tab_id = opening.origin_tab;
opening.closed = false;
_ = rt.workspace.focus(neighbor);
try opening.applyResult(.{ .ticket = opening.ticket, .phase = .created, .may_have_created = true }, true);
try std.testing.expect(opening.closed and opening.inserted);
try std.testing.expectEqual(neighbor, rt.workspace.tab().focus.?);
const flat = rt.workspace.layout(1200, 900, metrics);
try std.testing.expectEqual(@as(usize, 3), flat.len);
const inserted = flat.items()[1];
try std.testing.expect(inserted.outer.y >= flat.get(id).?.outer.y + flat.get(id).?.outer.h);
try std.testing.expectEqualStrings("3", rt.workspace.pane(inserted.id).?.identity.session);
try std.testing.expect(rt.get(inserted.id).?.pump.opts.existing_only);
}