a73x

402a1681

feat: expose correlated selection replies to web

a73x   2026-08-18 12:27

Commit message
feat: expose correlated selection replies to web

src/client.zig
Old New
@@ -1335,6 +1335,7 @@ fn session(
1335 effect, 1335 effect,
1336 appendHostEffect, 1336 appendHostEffect,
1337 ), 1337 ),
1338 .reply => {},
1338 } 1339 }
1339 }, 1340 },
1340 .term_title => { 1341 .term_title => {
src/client_core.zig
Old New
@@ -18,31 +18,52 @@ pub const Effect = union(enum) {
18 bell, 18 bell,
19 }; 19 };
20 20
21 pub const Reply = union(enum) {
22 selection: proto.SelectionReply,
23 };
24
21 pub const Result = union(enum) { 25 pub const Result = union(enum) {
22 ignored, 26 ignored,
23 state: State, 27 state: State,
24 effect: Effect, 28 effect: Effect,
29 reply: Reply,
25 }; 30 };
26 31
27 pub const ClientCore = struct { 32 pub const ClientCore = struct {
28 terminal_modes: proto.TermModes = .{ .bracketed_paste = false }, 33 terminal_modes: proto.TermModes = .{ .bracketed_paste = false },
34 pending_selection_id: ?u32 = null,
29 35
30 /// Decode one daemon frame's semantic terminal state or event. Any 36 /// Decode one daemon frame's semantic terminal state or event. Any
31 /// borrowed clipboard bytes in the result remain valid only while 37 /// borrowed clipboard bytes or selection reply text in the result remain
32 /// `payload` remains valid and unchanged. 38 /// valid only while `payload` remains valid and unchanged.
33 pub fn receive(self: *ClientCore, msg_type: proto.MsgType, payload: []const u8) Result { 39 pub fn receive(self: *ClientCore, msg_type: proto.MsgType, payload: []const u8) Result {
34 return switch (msg_type) { 40 return switch (msg_type) {
35 .term_modes => self.receiveModes(payload), 41 .term_modes => self.receiveModes(payload),
36 .term_event => receiveEvent(payload), 42 .term_event => receiveEvent(payload),
43 .selection_reply => self.receiveSelectionReply(payload),
37 else => .ignored, 44 else => .ignored,
38 }; 45 };
39 } 46 }
40 47
48 /// Start (or replace) the single correlated selection operation.
49 pub fn beginSelection(self: *ClientCore, req: proto.SelectionReq) [proto.selection_req_len]u8 {
50 self.pending_selection_id = req.id;
51 return proto.encodeSelectionReq(req);
52 }
53
41 fn receiveModes(self: *ClientCore, payload: []const u8) Result { 54 fn receiveModes(self: *ClientCore, payload: []const u8) Result {
42 const modes = proto.decodeTermModes(payload) catch return .ignored; 55 const modes = proto.decodeTermModes(payload) catch return .ignored;
43 self.terminal_modes = modes; 56 self.terminal_modes = modes;
44 return .{ .state = .{ .terminal_modes = modes } }; 57 return .{ .state = .{ .terminal_modes = modes } };
45 } 58 }
59
60 fn receiveSelectionReply(self: *ClientCore, payload: []const u8) Result {
61 const reply = proto.decodeSelectionReply(payload) catch return .ignored;
62 const pending_id = self.pending_selection_id orelse return .ignored;
63 if (reply.id != pending_id) return .ignored;
64 self.pending_selection_id = null;
65 return .{ .reply = .{ .selection = reply } };
66 }
46 }; 67 };
47 68
48 fn receiveEvent(payload: []const u8) Result { 69 fn receiveEvent(payload: []const u8) Result {
@@ -254,6 +275,105 @@ test "client core ignores unknown message types" {
254 try expectIgnored(core.receive(@enumFromInt(0xa0), &payload)); 275 try expectIgnored(core.receive(@enumFromInt(0xa0), &payload));
255 } 276 }
256 277
278 test "client core begins selection with exact request bytes" {
279 var core = ClientCore{};
280 const req: proto.SelectionReq = .{
281 .id = 0x78563412,
282 .anchor = .{ .row = 0x44332211, .col = 0x6655 },
283 .active = .{ .row = 0xaa998877, .col = 0xccbb },
284 };
285
286 const encoded = core.beginSelection(req);
287 try std.testing.expectEqualSlices(u8, &.{
288 0x12, 0x34, 0x56, 0x78,
289 0x11, 0x22, 0x33, 0x44,
290 0x55, 0x66, 0x77, 0x88,
291 0x99, 0xaa, 0xbb, 0xcc,
292 }, &encoded);
293 try std.testing.expectEqualDeep(req, try proto.decodeSelectionReq(&encoded));
294 try std.testing.expectEqual(@as(?u32, req.id), core.pending_selection_id);
295 }
296
297 test "client core ignores stale selection reply then accepts matching reply once" {
298 var core = ClientCore{};
299 _ = core.beginSelection(.{
300 .id = 22,
301 .anchor = .{ .row = 1, .col = 2 },
302 .active = .{ .row = 3, .col = 4 },
303 });
304 const stale = [_]u8{ 21, 0, 0, 0, 0, 'n', 'o' };
305 const matching = [_]u8{ 22, 0, 0, 0, 0, 'o', 'k' };
306
307 try expectIgnored(core.receive(.selection_reply, &stale));
308 try std.testing.expectEqual(@as(?u32, 22), core.pending_selection_id);
309 try expectSelection(core.receive(.selection_reply, &matching), 22, .ok, "ok");
310 try std.testing.expectEqual(@as(?u32, null), core.pending_selection_id);
311 try expectIgnored(core.receive(.selection_reply, &matching));
312 }
313
314 test "client core latest selection begin replaces the older pending id" {
315 var core = ClientCore{};
316 _ = core.beginSelection(.{
317 .id = 7,
318 .anchor = .{ .row = 0, .col = 0 },
319 .active = .{ .row = 0, .col = 1 },
320 });
321 _ = core.beginSelection(.{
322 .id = 8,
323 .anchor = .{ .row = 2, .col = 3 },
324 .active = .{ .row = 4, .col = 5 },
325 });
326
327 try expectIgnored(core.receive(.selection_reply, &.{ 7, 0, 0, 0, 0, 'x' }));
328 try std.testing.expectEqual(@as(?u32, 8), core.pending_selection_id);
329 try expectSelection(core.receive(.selection_reply, &.{ 8, 0, 0, 0, 0, 'y' }), 8, .ok, "y");
330 }
331
332 test "client core malformed matching selection reply preserves pending request" {
333 var core = ClientCore{};
334 _ = core.beginSelection(.{
335 .id = 9,
336 .anchor = .{ .row = 0, .col = 0 },
337 .active = .{ .row = 0, .col = 0 },
338 });
339
340 try expectIgnored(core.receive(.selection_reply, &.{ 9, 0, 0, 0, 1, 'x' }));
341 try std.testing.expectEqual(@as(?u32, 9), core.pending_selection_id);
342 try expectSelection(core.receive(.selection_reply, &.{ 9, 0, 0, 0, 0 }), 9, .ok, "");
343 }
344
345 test "client core delivers every matching non-ok selection status with empty text" {
346 const statuses = [_]proto.SelectionStatus{ .invalid, .too_large, .unavailable };
347 for (statuses, 0..) |status, i| {
348 var core = ClientCore{};
349 const id: u32 = @intCast(100 + i);
350 _ = core.beginSelection(.{
351 .id = id,
352 .anchor = .{ .row = 0, .col = 0 },
353 .active = .{ .row = 0, .col = 0 },
354 });
355 var payload = [_]u8{ 0, 0, 0, 0, @intFromEnum(status) };
356 std.mem.writeInt(u32, payload[0..4], id, .little);
357
358 try expectSelection(core.receive(.selection_reply, &payload), id, status, "");
359 try std.testing.expectEqual(@as(?u32, null), core.pending_selection_id);
360 }
361 }
362
363 test "client core selection reply text borrows the frame payload" {
364 var core = ClientCore{};
365 _ = core.beginSelection(.{
366 .id = 1,
367 .anchor = .{ .row = 0, .col = 0 },
368 .active = .{ .row = 0, .col = 0 },
369 });
370 var payload = [_]u8{ 1, 0, 0, 0, 0, 'h', 'i' };
371
372 const result = core.receive(.selection_reply, &payload);
373 payload[5] = 'H';
374 try expectSelection(result, 1, .ok, "Hi");
375 }
376
257 fn expectIgnored(result: Result) !void { 377 fn expectIgnored(result: Result) !void {
258 switch (result) { 378 switch (result) {
259 .ignored => {}, 379 .ignored => {},
@@ -269,3 +389,21 @@ fn expectModes(result: Result, expected: bool) !void {
269 else => return error.ExpectedModes, 389 else => return error.ExpectedModes,
270 } 390 }
271 } 391 }
392
393 fn expectSelection(
394 result: Result,
395 id: u32,
396 status: proto.SelectionStatus,
397 text: []const u8,
398 ) !void {
399 switch (result) {
400 .reply => |reply| switch (reply) {
401 .selection => |selection| {
402 try std.testing.expectEqual(id, selection.id);
403 try std.testing.expectEqual(status, selection.status);
404 try std.testing.expectEqualStrings(text, selection.text);
405 },
406 },
407 else => return error.ExpectedSelection,
408 }
409 }
src/wasm_core.zig
Old New
@@ -58,6 +58,9 @@ const Core = struct {
58 /// Borrows from input_buf. It is valid only until the host next stages 58 /// Borrows from input_buf. It is valid only until the host next stages
59 /// a frame, exactly like ClientCore's payload-borrowing result. 59 /// a frame, exactly like ClientCore's payload-borrowing result.
60 clipboard: client_core.ClipboardSet = .{ .target = 0, .base64 = &.{} }, 60 clipboard: client_core.ClipboardSet = .{ .target = 0, .base64 = &.{} },
61 /// Borrows from input_buf. The text is valid only until the host next
62 /// stages or writes input, or starts another selection request.
63 selection: proto.SelectionReply = .{ .id = 0, .status = .unavailable, .text = &.{} },
61 /// Grid the readout buffers are sized for; follows rep.grid. 64 /// Grid the readout buffers are sized for; follows rep.grid.
62 cols: u16, 65 cols: u16,
63 rows: u16, 66 rows: u16,
@@ -79,12 +82,9 @@ const Core = struct {
79 var core: ?*Core = null; 82 var core: ?*Core = null;
80 83
81 /// Bytes in (frame payloads, paste bytes) cross through this staging 84 /// Bytes in (frame payloads, paste bytes) cross through this staging
82 /// buffer: one memcpy from JS, no malloc protocol to get wrong. 256 KiB — 85 /// buffer: one memcpy from JS, no malloc protocol to get wrong. It must fit
83 /// a full snapshot payload must fit in ONE frame; verify.js asserts a 86 /// both a full snapshot and the protocol's largest selection reply.
84 /// real snapshot at the default scrollback does. The hub's own inbound 87 var input_buf: [@max(256 * 1024, proto.selection_reply_prefix_len + proto.selection_text_max)]u8 = undefined;
85 /// bound is far smaller (64 KiB) because browser->hub messages are keys
86 /// and pastes; daemon->browser frames ride hub->browser with no bound.
87 var input_buf: [256 * 1024]u8 = undefined;
88 88
89 /// Variable-length results out (encoded keys, attach payloads, dumps). 89 /// Variable-length results out (encoded keys, attach payloads, dumps).
90 var output_buf: [64 * 1024]u8 = undefined; 90 var output_buf: [64 * 1024]u8 = undefined;
@@ -95,6 +95,7 @@ const ClientAction = enum(i32) {
95 terminal_modes = 1, 95 terminal_modes = 1,
96 clipboard = 2, 96 clipboard = 2,
97 bell = 3, 97 bell = 3,
98 selection = 4,
98 }; 99 };
99 100
100 // --------------------------------------------------------------------- 101 // ---------------------------------------------------------------------
@@ -250,11 +251,13 @@ export fn mux_apply_frame(msg_type: u32, len: u32) i32 {
250 } 251 }
251 252
252 /// Decode one staged daemon frame through the same semantic core used by 253 /// Decode one staged daemon frame through the same semantic core used by
253 /// the native client. Clipboard bytes remain borrowed from input_buf until 254 /// the native client. Clipboard and selection bytes remain borrowed from
254 /// the host stages the next frame; getters never copy or allocate them. 255 /// input_buf until the host stages or writes the next input; getters never
256 /// copy or allocate them.
255 export fn mux_client_frame(msg_type: u32, len: u32) i32 { 257 export fn mux_client_frame(msg_type: u32, len: u32) i32 {
256 const c = core orelse return -1; 258 const c = core orelse return -1;
257 c.clipboard = .{ .target = 0, .base64 = &.{} }; 259 c.clipboard = .{ .target = 0, .base64 = &.{} };
260 c.selection = .{ .id = 0, .status = .unavailable, .text = &.{} };
258 if (len > input_buf.len) return -2; 261 if (len > input_buf.len) return -2;
259 if (msg_type > 0xff) return @intFromEnum(ClientAction.ignored); 262 if (msg_type > 0xff) return @intFromEnum(ClientAction.ignored);
260 263
@@ -273,6 +276,12 @@ export fn mux_client_frame(msg_type: u32, len: u32) i32 {
273 }, 276 },
274 .bell => @intFromEnum(ClientAction.bell), 277 .bell => @intFromEnum(ClientAction.bell),
275 }, 278 },
279 .reply => |reply| switch (reply) {
280 .selection => |selection| blk: {
281 c.selection = selection;
282 break :blk @intFromEnum(ClientAction.selection);
283 },
284 },
276 }; 285 };
277 } 286 }
278 287
@@ -297,6 +306,51 @@ export fn mux_clipboard_len() u32 {
297 return @intCast(c.clipboard.base64.len); 306 return @intCast(c.clipboard.base64.len);
298 } 307 }
299 308
309 /// Begin a correlated selection request and write its exact protocol payload
310 /// to output_buf. Invalid u16 columns do not disturb an existing pending id.
311 export fn mux_selection_request(
312 id: u32,
313 anchor_row: u32,
314 anchor_col: u32,
315 active_row: u32,
316 active_col: u32,
317 ) i32 {
318 output_len = 0;
319 const c = core orelse return -1;
320 if (anchor_col > std.math.maxInt(u16) or active_col > std.math.maxInt(u16)) return -3;
321
322 const payload = c.client.beginSelection(.{
323 .id = id,
324 .anchor = .{ .row = anchor_row, .col = @intCast(anchor_col) },
325 .active = .{ .row = active_row, .col = @intCast(active_col) },
326 });
327 c.selection = .{ .id = 0, .status = .unavailable, .text = &.{} };
328 @memcpy(output_buf[0..payload.len], &payload);
329 output_len = payload.len;
330 return @intCast(payload.len);
331 }
332
333 export fn mux_selection_id() u32 {
334 const c = core orelse return 0;
335 return c.selection.id;
336 }
337
338 export fn mux_selection_status() u32 {
339 const c = core orelse return @intFromEnum(proto.SelectionStatus.unavailable);
340 return @intFromEnum(c.selection.status);
341 }
342
343 export fn mux_selection_ptr() [*]const u8 {
344 const c = core orelse return &input_buf;
345 if (c.selection.text.len == 0) return &input_buf;
346 return c.selection.text.ptr;
347 }
348
349 export fn mux_selection_len() u32 {
350 const c = core orelse return 0;
351 return @intCast(c.selection.text.len);
352 }
353
300 /// Force a full repaint on the next mux_read_viewport (scroll-mode exit, 354 /// Force a full repaint on the next mux_read_viewport (scroll-mode exit,
301 /// a canvas the host lost, first paint after tab restore). 355 /// a canvas the host lost, first paint after tab restore).
302 export fn mux_mark_all_dirty() void { 356 export fn mux_mark_all_dirty() void {
web/verify.js
Old New
@@ -24,6 +24,7 @@ const clientAction = Object.freeze({
24 terminalModes: 1, 24 terminalModes: 1,
25 clipboard: 2, 25 clipboard: 2,
26 bell: 3, 26 bell: 3,
27 selection: 4,
27 }); 28 });
28 29
29 function check(name, got, want) { 30 function check(name, got, want) {
@@ -817,6 +818,110 @@ async function main() {
817 check('wide type clears clipboard len', e.mux_clipboard_len(), 0); 818 check('wide type clears clipboard len', e.mux_clipboard_len(), 0);
818 check('wide type clears clipboard target', e.mux_clipboard_target(), 0); 819 check('wide type clears clipboard target', e.mux_clipboard_target(), 0);
819 820
821 const selectionReply = (id, status, text = '') => {
822 const body = Buffer.from(text, 'utf8');
823 const reply = Buffer.alloc(5 + body.length);
824 reply.writeUInt32LE(id, 0);
825 reply[4] = status;
826 body.copy(reply, 5);
827 return reply;
828 };
829 const selectionText = () => Buffer.from(mem().subarray(
830 e.mux_selection_ptr(),
831 e.mux_selection_ptr() + e.mux_selection_len(),
832 ));
833 const firstSelectionId = 0x78563412;
834 check('selection request len', e.mux_selection_request(firstSelectionId, 0x44332211, 0x6655, 0xaa998877, 0xccbb), 16);
835 check('selection request output len', e.mux_output_len(), 16);
836 check(
837 'selection request golden bytes',
838 outBytes().toString('hex'),
839 '12345678112233445566778899aabbcc',
840 );
841 check('selection invalid anchor col', e.mux_selection_request(30, 1, 65536, 2, 3), -3);
842 check('selection invalid anchor col clears output', e.mux_output_len(), 0);
843 check('selection invalid active col', e.mux_selection_request(30, 1, 2, 3, 65536), -3);
844 check('selection invalid active col clears output', e.mux_output_len(), 0);
845 check(
846 'selection invalid columns preserve pending id',
847 e.mux_client_frame(0x90, stage(selectionReply(firstSelectionId, 0, 'preserved'))),
848 clientAction.selection,
849 );
850
851 const supersededSelectionId = 0x01020304;
852 check('selection superseded request', e.mux_selection_request(supersededSelectionId, 5, 6, 7, 8), 16);
853 const latestSelectionId = 0x10203040;
854 check('selection latest request replaces pending', e.mux_selection_request(latestSelectionId, 9, 10, 11, 12), 16);
855 check(
856 'selection request survives enlarged input staging capacity',
857 outBytes().toString('hex'),
858 '40302010090000000a000b0000000c00',
859 );
860 check(
861 'selection stale reply ignored',
862 e.mux_client_frame(0x90, stage(selectionReply(supersededSelectionId, 0, 'stale'))),
863 clientAction.ignored,
864 );
865 check('selection stale reply leaves getters empty', e.mux_selection_len(), 0);
866 check(
867 'selection malformed matching reply ignored',
868 e.mux_client_frame(0x90, stage(selectionReply(latestSelectionId, 1, 'bad'))),
869 clientAction.ignored,
870 );
871 check(
872 'selection valid after malformed matching reply',
873 e.mux_client_frame(0x90, stage(selectionReply(latestSelectionId, 0, 'selected 漢'))),
874 clientAction.selection,
875 );
876 check('selection getter id', e.mux_selection_id(), latestSelectionId);
877 check('selection getter status', e.mux_selection_status(), 0);
878 check('selection getter len', e.mux_selection_len(), Buffer.byteLength('selected 漢'));
879 check('selection getter borrowed text', selectionText().toString('utf8'), 'selected 漢');
880 check(
881 'selection repeated reply after consume ignored',
882 e.mux_client_frame(0x90, stage(selectionReply(latestSelectionId, 0, 'again'))),
883 clientAction.ignored,
884 );
885 check('selection repeated reply clears getter', e.mux_selection_len(), 0);
886
887 for (const [name, status] of [['invalid', 1], ['too large', 2], ['unavailable', 3]]) {
888 const id = 100 + status;
889 check(`selection ${name} request`, e.mux_selection_request(id, 0, 0, 0, 0), 16);
890 check(
891 `selection ${name} action`,
892 e.mux_client_frame(0x90, stage(selectionReply(id, status))),
893 clientAction.selection,
894 );
895 check(`selection ${name} id`, e.mux_selection_id(), id);
896 check(`selection ${name} status`, e.mux_selection_status(), status);
897 check(`selection ${name} empty text`, e.mux_selection_len(), 0);
898 }
899
900 const populateSelection = (id) => {
901 check(`selection ${id} request`, e.mux_selection_request(id, 0, 0, 0, 0), 16);
902 check(
903 `selection ${id} reply`,
904 e.mux_client_frame(0x90, stage(selectionReply(id, 0, 'x'))),
905 clientAction.selection,
906 );
907 };
908 populateSelection(201);
909 check('bell after selection', e.mux_client_frame(0x8f, stage(Buffer.from([1]))), clientAction.bell);
910 check('bell clears selection getter', e.mux_selection_len(), 0);
911 check('empty selection pointer is input buffer', e.mux_selection_ptr(), e.mux_input_ptr());
912 populateSelection(202);
913 check('ignored after selection', e.mux_client_frame(0x40, 0), clientAction.ignored);
914 check('ignored clears selection getter', e.mux_selection_len(), 0);
915 populateSelection(203);
916 check('oversize after selection', e.mux_client_frame(0x90, e.mux_input_cap() + 1), -2);
917 check('oversize clears selection getter', e.mux_selection_len(), 0);
918 populateSelection(204);
919 check('wide type after selection', e.mux_client_frame(0x100, 0), clientAction.ignored);
920 check('wide type clears selection getter', e.mux_selection_len(), 0);
921 populateSelection(205);
922 check('valid new request after selection', e.mux_selection_request(206, 1, 2, 3, 4), 16);
923 check('valid new request clears selection getter', e.mux_selection_len(), 0);
924
820 // --- attach payload before any state: quotes (0,0); wall spelling 1x1 --- 925 // --- attach payload before any state: quotes (0,0); wall spelling 1x1 ---
821 check('attach len', e.mux_attach_payload(1, 1, 0), 20); 926 check('attach len', e.mux_attach_payload(1, 1, 0), 20);
822 let att = outBytes(); 927 let att = outBytes();
@@ -952,10 +1057,9 @@ async function main() {
952 check('dump starts', outBytes().toString('utf8').startsWith('漢字'), true); 1057 check('dump starts', outBytes().toString('utf8').startsWith('漢字'), true);
953 1058
954 // --- a real-sized snapshot fits the staging buffer --- 1059 // --- a real-sized snapshot fits the staging buffer ---
955 // 100x30 grid fully painted with styled cells is well under 256K; the 1060 // The staging cap is pinned to the largest selection reply: its five-byte
956 // pin here is the CAP ITSELF: input_cap must hold the biggest payload 1061 // correlation/status prefix plus the protocol's one-MiB text maximum.
957 // the daemon sends in one frame for the wall's grids. 1062 check('input cap', e.mux_input_cap(), 1024 * 1024 + 5);
958 check('input cap', e.mux_input_cap(), 256 * 1024);
959 1063
960 // --- the shell's ACTUAL call list, read out of mux.js --- 1064 // --- the shell's ACTUAL call list, read out of mux.js ---
961 // Everything above pins exports this file happens to name. This pins 1065 // Everything above pins exports this file happens to name. This pins
@@ -1197,12 +1301,24 @@ async function main() {
1197 check('clipboard before lifecycle reset', e.mux_client_frame(0x8f, stage(clipboard)), clientAction.clipboard); 1301 check('clipboard before lifecycle reset', e.mux_client_frame(0x8f, stage(clipboard)), clientAction.clipboard);
1198 check('clipboard populated before lifecycle reset', e.mux_clipboard_len(), 4); 1302 check('clipboard populated before lifecycle reset', e.mux_clipboard_len(), 4);
1199 check('modes populated before lifecycle reset', e.mux_bracketed_paste(), 1); 1303 check('modes populated before lifecycle reset', e.mux_bracketed_paste(), 1);
1304 check('selection before lifecycle reset request', e.mux_selection_request(301, 0, 0, 0, 0), 16);
1305 check('selection before lifecycle reset reply', e.mux_client_frame(0x90, stage(selectionReply(301, 0, 'reset'))), clientAction.selection);
1306 check('selection populated before lifecycle reset', e.mux_selection_len(), 5);
1200 e.mux_deinit(); 1307 e.mux_deinit();
1201 check('apply after deinit', e.mux_apply_frame(0x81, 0), -1); 1308 check('apply after deinit', e.mux_apply_frame(0x81, 0), -1);
1202 check('client frame after deinit', e.mux_client_frame(0x8d, 0), -1); 1309 check('client frame after deinit', e.mux_client_frame(0x8d, 0), -1);
1310 check('selection id after deinit', e.mux_selection_id(), 0);
1311 check('selection status after deinit', e.mux_selection_status(), 3);
1312 check('selection len after deinit', e.mux_selection_len(), 0);
1313 check('selection ptr after deinit is input buffer', e.mux_selection_ptr(), e.mux_input_ptr());
1314 check('selection request after deinit', e.mux_selection_request(302, 0, 0, 0, 0), -1);
1315 check('selection request error clears output', e.mux_output_len(), 0);
1203 check('re-init', e.mux_init(80, 24), 0); 1316 check('re-init', e.mux_init(80, 24), 0);
1204 check('re-init resets bracketed paste', e.mux_bracketed_paste(), 0); 1317 check('re-init resets bracketed paste', e.mux_bracketed_paste(), 0);
1205 check('re-init resets clipboard', e.mux_clipboard_len(), 0); 1318 check('re-init resets clipboard', e.mux_clipboard_len(), 0);
1319 check('deinit and re-init reset selection id', e.mux_selection_id(), 0);
1320 check('deinit and re-init reset selection status', e.mux_selection_status(), 3);
1321 check('deinit and re-init reset selection len', e.mux_selection_len(), 0);
1206 e.mux_deinit(); 1322 e.mux_deinit();
1207 1323
1208 console.log(`verify: ${passed} passed, ${failed} failed`); 1324 console.log(`verify: ${passed} passed, ${failed} failed`);