lib/windowing/src/wayland/seat/clipboard.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3 const native = @import("wayland");
4 const windowing = @import("../../root.zig");
5
6 const core = native.protocol.core;
7 const runtime = native.runtime;
8
9 pub const receive_timeout_ms: u32 = 1000;
10
11 pub const Error = error{
12 OutOfMemory,
13 CreateFailed,
14 SynchronizeFailed,
15 TransferFailed,
16 TransferTimedOut,
17 TransferTooLarge,
18 };
19
20 pub const Synchronizer = struct {
21 context: ?*anyopaque,
22 run: *const fn (context: ?*anyopaque) anyerror!void,
23
24 fn synchronize(self: Synchronizer) Error!void {
25 self.run(self.context) catch return error.SynchronizeFailed;
26 }
27 };
28
29 pub const Clipboard = struct {
30 allocator: std.mem.Allocator,
31 client: *runtime.Client,
32 received_text_byte_count: usize,
33 transfer_read: []u8,
34 source: windowing.clipboard.Source,
35 manager: ?u32 = null,
36 device: ?u32 = null,
37 source_id: ?u32 = null,
38 offers: OfferSet = .{},
39
40 pub fn init(
41 allocator: std.mem.Allocator,
42 client: *runtime.Client,
43 capacity: windowing.clipboard.Capacity,
44 ) std.mem.Allocator.Error!Clipboard {
45 std.debug.assert(capacity.received_text_byte_count > 0);
46 std.debug.assert(capacity.transfer_read_byte_count > 0);
47 const transfer_read = try allocator.alloc(u8, capacity.transfer_read_byte_count);
48 errdefer allocator.free(transfer_read);
49 const source = try windowing.clipboard.Source.init(allocator, capacity);
50 return .{
51 .allocator = allocator,
52 .client = client,
53 .received_text_byte_count = capacity.received_text_byte_count,
54 .transfer_read = transfer_read,
55 .source = source,
56 };
57 }
58
59 pub fn bindSeat(self: *Clipboard, manager: u32, seat: u32) !void {
60 self.resetDevice();
61 self.manager = manager;
62 var created: [1]u32 = undefined;
63 try self.client.request(
64 manager,
65 core.wl_data_device_manager.requests.get_data_device.opcode,
66 &.{ .{ .new_id = .fixed }, .{ .object = seat } },
67 &created,
68 );
69 self.device = created[0];
70 }
71
72 pub fn unbindSeat(self: *Clipboard) void {
73 self.resetDevice();
74 }
75
76 pub fn dispatch(self: *Clipboard, event: *runtime.RoutedView) !bool {
77 if (event.object_id == self.source_id) {
78 try self.dispatchSource(event, true);
79 return true;
80 }
81 if (event.object_id == self.device) {
82 try self.dispatchDevice(event, true);
83 return true;
84 }
85 if (self.offers.find(event.object_id)) |offer| {
86 try self.dispatchOffer(offer, event);
87 return true;
88 }
89 if (std.mem.eql(u8, event.interface.name, core.wl_data_device.metadata.name)) {
90 try self.dispatchDevice(event, false);
91 return true;
92 }
93 if (std.mem.eql(u8, event.interface.name, core.wl_data_source.metadata.name)) {
94 try self.dispatchSource(event, false);
95 return true;
96 }
97 return false;
98 }
99
100 pub fn setText(
101 self: *Clipboard,
102 text: []const u8,
103 serial: u32,
104 ) windowing.clipboard.PublishError!void {
105 try self.source.replace(text);
106 self.destroySource(false);
107 const manager = self.manager orelse return error.Unavailable;
108 const device = self.device orelse return error.Unavailable;
109 var created: [1]u32 = undefined;
110 self.client.request(
111 manager,
112 core.wl_data_device_manager.requests.create_data_source.opcode,
113 &.{.{ .new_id = .fixed }},
114 &created,
115 ) catch return error.Unexpected;
116 const source_id = created[0];
117 for (text_mime_preference) |mime| {
118 self.client.request(
119 source_id,
120 core.wl_data_source.requests.offer.opcode,
121 &.{.{ .string = mime }},
122 &.{},
123 ) catch {
124 self.destroyDetachedSource(source_id);
125 return error.Unexpected;
126 };
127 }
128 self.client.request(
129 device,
130 core.wl_data_device.requests.set_selection.opcode,
131 &.{ .{ .object = source_id }, .{ .uint = serial } },
132 &.{},
133 ) catch {
134 self.destroyDetachedSource(source_id);
135 return error.Unexpected;
136 };
137 self.source_id = source_id;
138 }
139
140 pub fn getTextAlloc(
141 self: *Clipboard,
142 output_allocator: std.mem.Allocator,
143 synchronizer: Synchronizer,
144 ) Error!?[]u8 {
145 const selection = self.offers.selection orelse return null;
146 const mime = selection.mimes.preferred() orelse return null;
147
148 const descriptors = transferPipe() catch return error.TransferFailed;
149 defer sys.fd.close(descriptors[0]);
150 var write_owned = true;
151 defer if (write_owned) sys.fd.close(descriptors[1]);
152
153 self.client.request(
154 selection.id,
155 core.wl_data_offer.requests.receive.opcode,
156 &.{ .{ .string = mime }, .{ .descriptor_owned = descriptors[1] } },
157 &.{},
158 ) catch return error.TransferFailed;
159 write_owned = false;
160 try synchronizer.synchronize();
161
162 return try readBoundedAlloc(
163 output_allocator,
164 descriptors[0],
165 receive_timeout_ms,
166 self.received_text_byte_count,
167 self.transfer_read,
168 );
169 }
170
171 pub fn deinit(self: *Clipboard) void {
172 self.resetDevice();
173 self.source.deinit();
174 self.allocator.free(self.transfer_read);
175 self.* = undefined;
176 }
177
178 fn resetDevice(self: *Clipboard) void {
179 self.destroySource(true);
180 if (self.offers.takePending()) |offer| self.destroyDetachedOffer(offer, true);
181 if (self.offers.takeSelection()) |offer| self.destroyDetachedOffer(offer, true);
182 const device = self.device orelse return;
183 if (self.client.object(device)) |object| {
184 if (object.version >= core.wl_data_device.requests.release.since) {
185 self.client.request(
186 device,
187 core.wl_data_device.requests.release.opcode,
188 &.{},
189 &.{},
190 ) catch {};
191 }
192 }
193 self.device = null;
194 self.manager = null;
195 }
196
197 fn destroySource(self: *Clipboard, clear: bool) void {
198 if (self.source_id) |source_id| self.destroyDetachedSource(source_id);
199 self.source_id = null;
200 if (clear) self.source.clear();
201 }
202
203 fn destroyDetachedSource(self: *Clipboard, source_id: u32) void {
204 self.client.request(
205 source_id,
206 core.wl_data_source.requests.destroy.opcode,
207 &.{},
208 &.{},
209 ) catch {};
210 }
211
212 fn introduceOffer(self: *Clipboard, id: u32) void {
213 if (self.offers.introduce(id)) |displaced| self.destroyDetachedOffer(displaced, true);
214 }
215
216 fn replaceSelection(self: *Clipboard, id: ?u32) void {
217 const transition = self.offers.replaceSelection(id);
218 if (transition.displaced) |offer| self.destroyDetachedOffer(offer, true);
219 if (transition.orphaned) |offer| self.destroyDetachedOffer(offer, true);
220 }
221
222 fn discardDragOffer(self: *Clipboard, id: ?u32) void {
223 if (self.offers.discardDrag(id)) |offer| self.destroyDetachedOffer(offer, true);
224 }
225
226 fn destroyDetachedOffer(self: *Clipboard, offer: Offer, send_destroy: bool) void {
227 if (send_destroy) {
228 self.client.request(
229 offer.id,
230 core.wl_data_offer.requests.destroy.opcode,
231 &.{},
232 &.{},
233 ) catch {};
234 }
235 }
236
237 fn dispatchDevice(self: *Clipboard, event: *runtime.RoutedView, active: bool) !void {
238 var decoder = try event.borrowedDecoder();
239 switch (event.metadata.opcode) {
240 core.wl_data_device.events.data_offer.opcode => {
241 const offer_id = (try decoder.newId()).raw;
242 try decoder.finish();
243 if (active) {
244 self.introduceOffer(offer_id);
245 } else {
246 self.client.request(
247 offer_id,
248 core.wl_data_offer.requests.destroy.opcode,
249 &.{},
250 &.{},
251 ) catch {};
252 }
253 },
254 core.wl_data_device.events.enter.opcode => {
255 _ = try decoder.unsigned();
256 _ = try decoder.object();
257 _ = try decoder.fixed();
258 _ = try decoder.fixed();
259 const offer_id = if (try decoder.optionalObject()) |value| value.raw else null;
260 try decoder.finish();
261 if (active) self.discardDragOffer(offer_id);
262 },
263 core.wl_data_device.events.leave.opcode,
264 core.wl_data_device.events.drop.opcode,
265 => try decoder.finish(),
266 core.wl_data_device.events.motion.opcode => {
267 _ = try decoder.unsigned();
268 _ = try decoder.fixed();
269 _ = try decoder.fixed();
270 try decoder.finish();
271 },
272 core.wl_data_device.events.selection.opcode => {
273 const offer_id = if (try decoder.optionalObject()) |value| value.raw else null;
274 try decoder.finish();
275 if (active) self.replaceSelection(offer_id);
276 },
277 else => return error.InvalidDataDeviceEvent,
278 }
279 }
280
281 fn dispatchOffer(_: *Clipboard, offer: *Offer, event: *runtime.RoutedView) !void {
282 var decoder = try event.borrowedDecoder();
283 switch (event.metadata.opcode) {
284 core.wl_data_offer.events.offer.opcode => {
285 const mime = try decoder.string();
286 try decoder.finish();
287 offer.mimes.add(mime);
288 },
289 core.wl_data_offer.events.source_actions.opcode,
290 core.wl_data_offer.events.action.opcode,
291 => {
292 _ = try decoder.unsigned();
293 try decoder.finish();
294 },
295 else => return error.InvalidDataOfferEvent,
296 }
297 }
298
299 fn dispatchSource(self: *Clipboard, event: *runtime.RoutedView, active: bool) !void {
300 var decoder = try event.borrowedDecoder();
301 switch (event.metadata.opcode) {
302 core.wl_data_source.events.target.opcode => {
303 _ = try decoder.optionalString();
304 try decoder.finish();
305 },
306 core.wl_data_source.events.send.opcode => {
307 const mime = try decoder.string();
308 _ = try decoder.descriptor();
309 try decoder.finish();
310 const descriptor = try event.takeDescriptor(0);
311 defer sys.fd.close(descriptor);
312 if (active and isTextMime(mime)) writeSource(descriptor, self.source.text());
313 },
314 core.wl_data_source.events.cancelled.opcode => {
315 try decoder.finish();
316 if (active) self.destroySource(true);
317 },
318 core.wl_data_source.events.dnd_drop_performed.opcode,
319 core.wl_data_source.events.dnd_finished.opcode,
320 => try decoder.finish(),
321 core.wl_data_source.events.action.opcode => {
322 _ = try decoder.unsigned();
323 try decoder.finish();
324 },
325 else => return error.InvalidDataSourceEvent,
326 }
327 }
328
329 pub fn status(self: *const Clipboard) windowing.clipboard.Status {
330 return self.source.status();
331 }
332 };
333
334 const Offer = struct {
335 id: u32,
336 mimes: MimeTypes = .{},
337 };
338
339 const SelectionTransition = struct {
340 displaced: ?Offer = null,
341 orphaned: ?Offer = null,
342 };
343
344 const MimeTypes = struct {
345 present: [text_mime_preference.len]bool = @splat(false),
346
347 fn add(self: *MimeTypes, mime: []const u8) void {
348 for (text_mime_preference, 0..) |candidate, index| {
349 if (std.mem.eql(u8, candidate, mime)) self.present[index] = true;
350 }
351 }
352
353 fn preferred(self: *const MimeTypes) ?[:0]const u8 {
354 for (text_mime_preference, self.present) |candidate, present| {
355 if (present) return candidate;
356 }
357 return null;
358 }
359 };
360
361 const OfferSet = struct {
362 pending: ?Offer = null,
363 selection: ?Offer = null,
364
365 fn find(self: *OfferSet, id: u32) ?*Offer {
366 if (self.pending) |*offer| if (offer.id == id) return offer;
367 if (self.selection) |*offer| if (offer.id == id) return offer;
368 return null;
369 }
370
371 fn introduce(self: *OfferSet, id: u32) ?Offer {
372 const pending = self.pending;
373 self.pending = .{ .id = id };
374 return pending;
375 }
376
377 fn replaceSelection(self: *OfferSet, id: ?u32) SelectionTransition {
378 if (id) |value| {
379 if (self.selection) |offer| {
380 if (offer.id == value) return .{};
381 }
382 if (self.pending) |offer| {
383 if (offer.id == value) {
384 const displaced = self.selection;
385 self.selection = offer;
386 self.pending = null;
387 return .{ .displaced = displaced };
388 }
389 }
390 }
391 const displaced = self.selection;
392 const orphaned = self.pending;
393 self.selection = null;
394 self.pending = null;
395 return .{ .displaced = displaced, .orphaned = orphaned };
396 }
397
398 fn discardDrag(self: *OfferSet, id: ?u32) ?Offer {
399 const value = id orelse return null;
400 if (self.selection) |offer| {
401 if (offer.id == value) return null;
402 }
403 const pending = self.pending orelse return null;
404 if (pending.id != value) return null;
405 self.pending = null;
406 return pending;
407 }
408
409 fn takePending(self: *OfferSet) ?Offer {
410 const offer = self.pending;
411 self.pending = null;
412 return offer;
413 }
414
415 fn takeSelection(self: *OfferSet) ?Offer {
416 const offer = self.selection;
417 self.selection = null;
418 return offer;
419 }
420 };
421
422 const text_mime_preference = [_][:0]const u8{
423 "text/plain;charset=utf-8",
424 "UTF8_STRING",
425 "text/plain",
426 };
427
428 fn isTextMime(mime: []const u8) bool {
429 for (text_mime_preference) |candidate| {
430 if (std.mem.eql(u8, candidate, mime)) return true;
431 }
432 return false;
433 }
434
435 fn writeSource(descriptor: sys.fd.Descriptor, bytes: []const u8) void {
436 var offset: usize = 0;
437 while (offset < bytes.len) {
438 const written = sys.fd.write(descriptor, bytes[offset..]) catch return;
439 if (written == 0) return;
440 offset += written;
441 }
442 }
443
444 fn readBoundedAlloc(
445 allocator: std.mem.Allocator,
446 descriptor: sys.fd.Descriptor,
447 timeout_ms: u32,
448 max_bytes: usize,
449 scratch: []u8,
450 ) Error![]u8 {
451 std.debug.assert(max_bytes > 0);
452 std.debug.assert(scratch.len > 0);
453 var bytes: std.ArrayList(u8) = .empty;
454 errdefer bytes.deinit(allocator);
455
456 const now = sys.time.nanoTimestamp();
457 const timeout_ns = @as(i128, timeout_ms) * std.time.ns_per_ms;
458 const deadline = now + timeout_ns;
459
460 while (true) {
461 const remaining = deadline - sys.time.nanoTimestamp();
462 if (remaining <= 0) return error.TransferTimedOut;
463 const rounded_ms = @divFloor(remaining + std.time.ns_per_ms - 1, std.time.ns_per_ms);
464 const poll_timeout: i32 = @intCast(@min(rounded_ms, std.math.maxInt(i32)));
465 var poll_fds = [_]sys.poll.Descriptor{.{
466 .fd = descriptor,
467 .events = sys.poll.Event.input,
468 .revents = 0,
469 }};
470 const ready = sys.poll.wait(&poll_fds, poll_timeout) catch return error.TransferFailed;
471 if (ready == 0) return error.TransferTimedOut;
472 if ((poll_fds[0].revents & sys.poll.Event.invalid) != 0) return error.TransferFailed;
473
474 while (true) {
475 const read_count = sys.fd.read(descriptor, scratch) catch |failure| switch (failure) {
476 error.WouldBlock => break,
477 else => return error.TransferFailed,
478 };
479 if (read_count == 0) return try bytes.toOwnedSlice(allocator);
480 if (read_count > max_bytes -| bytes.items.len) return error.TransferTooLarge;
481 try bytes.appendSlice(allocator, scratch[0..read_count]);
482 }
483 }
484 }
485
486 fn transferPipe() ![2]sys.fd.Descriptor {
487 const descriptors = try sys.fd.pipeWithOptions(.{ .close_on_exec = true });
488 errdefer sys.fd.close(descriptors[0]);
489 errdefer sys.fd.close(descriptors[1]);
490 try sys.fd.setNonBlocking(descriptors[0]);
491 return descriptors;
492 }
493
494 test "clipboard MIME preference is deterministic" {
495 var mimes: MimeTypes = .{};
496 mimes.add("text/plain");
497 mimes.add("application/octet-stream");
498 mimes.add("UTF8_STRING");
499 mimes.add("text/plain;charset=utf-8");
500 mimes.add("text/plain");
501 try std.testing.expectEqual([3]bool{ true, true, true }, mimes.present);
502 try std.testing.expectEqualStrings("text/plain;charset=utf-8", mimes.preferred().?);
503 }
504
505 test "clipboard offers follow pending selection and discarded drag roles" {
506 var offers: OfferSet = .{};
507 try std.testing.expect(offers.introduce(10) == null);
508 offers.find(10).?.mimes.add("text/plain");
509 const first = offers.replaceSelection(10);
510 try std.testing.expect(first.displaced == null);
511 try std.testing.expect(first.orphaned == null);
512 try std.testing.expect(offers.pending == null);
513 try std.testing.expectEqual(@as(u32, 10), offers.selection.?.id);
514
515 try std.testing.expect(offers.introduce(20) == null);
516 const second = offers.replaceSelection(20);
517 try std.testing.expectEqual(@as(u32, 10), second.displaced.?.id);
518 try std.testing.expect(second.orphaned == null);
519 try std.testing.expectEqual(@as(u32, 20), offers.selection.?.id);
520
521 try std.testing.expect(offers.introduce(30) == null);
522 try std.testing.expectEqual(@as(u32, 30), offers.discardDrag(30).?.id);
523 try std.testing.expect(offers.pending == null);
524 try std.testing.expectEqual(@as(u32, 20), offers.selection.?.id);
525
526 try std.testing.expect(offers.introduce(40) == null);
527 try std.testing.expectEqual(@as(u32, 40), offers.introduce(50).?.id);
528 const cleared = offers.replaceSelection(null);
529 try std.testing.expectEqual(@as(u32, 20), cleared.displaced.?.id);
530 try std.testing.expectEqual(@as(u32, 50), cleared.orphaned.?.id);
531 try std.testing.expect(offers.pending == null);
532 try std.testing.expect(offers.selection == null);
533 }
534
535 test "clipboard initialization owns exact read and publication storage" {
536 const capacity = try windowing.clipboard.Capacity.derive(.{
537 .received_text_byte_count = 7,
538 .published_text_byte_count = 5,
539 .transfer_read_byte_count = 3,
540 .retained_wait_event_count = 2,
541 });
542 const client: *runtime.Client = undefined;
543 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
544 try std.testing.expectError(
545 error.OutOfMemory,
546 Clipboard.init(failing.allocator(), client, capacity),
547 );
548
549 failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 });
550 try std.testing.expectError(
551 error.OutOfMemory,
552 Clipboard.init(failing.allocator(), client, capacity),
553 );
554
555 failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 2 });
556 var clipboard = try Clipboard.init(failing.allocator(), client, capacity);
557 defer clipboard.deinit();
558 try std.testing.expectEqual(@as(usize, 3), clipboard.transfer_read.len);
559 try std.testing.expectEqual(@as(usize, 7), clipboard.received_text_byte_count);
560 try std.testing.expectEqual(@as(usize, 5), clipboard.source.bytes.len);
561 try std.testing.expectEqual(@as(usize, 2), failing.allocations);
562 }
563
564 test "clipboard data source send transfers its descriptor and exact retained bytes" {
565 const capacity = try windowing.clipboard.Capacity.derive(.{
566 .received_text_byte_count = 7,
567 .published_text_byte_count = 16,
568 .transfer_read_byte_count = 3,
569 .retained_wait_event_count = 2,
570 });
571 const client: *runtime.Client = undefined;
572 var clipboard = try Clipboard.init(std.testing.allocator, client, capacity);
573 defer clipboard.deinit();
574 try clipboard.source.replace("exact λ");
575 clipboard.source_id = 77;
576
577 const descriptors = try transferPipe();
578 defer sys.fd.close(descriptors[0]);
579 var encoder = try native.protocol.value.Encoder.init(
580 std.testing.allocator,
581 native.protocol.value.default_encoder_capacity,
582 );
583 defer encoder.deinit();
584 try encoder.string(text_mime_preference[0]);
585 try encoder.descriptorBorrowed(descriptors[1]);
586 const encoded = try encoder.finish(&core.wl_data_source.events.send);
587 var routed_descriptors = [_]sys.fd.Descriptor{descriptors[1]};
588 var event: runtime.RoutedView = .{
589 .object_id = 77,
590 .interface = &core.wl_data_source.metadata,
591 .version = 3,
592 .metadata = encoded.metadata,
593 .payload = encoded.payload,
594 .descriptors = &routed_descriptors,
595 };
596 try std.testing.expect(try clipboard.dispatch(&event));
597 clipboard.source_id = null;
598 try std.testing.expectEqual(@as(sys.fd.Descriptor, -1), routed_descriptors[0]);
599
600 var received: [16]u8 = undefined;
601 const received_count = try sys.fd.read(descriptors[0], &received);
602 try std.testing.expectEqualStrings("exact λ", received[0..received_count]);
603 }
604
605 test "clipboard data source cancellation clears retained bytes" {
606 const capacity = try windowing.clipboard.Capacity.derive(.{
607 .received_text_byte_count = 7,
608 .published_text_byte_count = 16,
609 .transfer_read_byte_count = 3,
610 .retained_wait_event_count = 2,
611 });
612 const client: *runtime.Client = undefined;
613 var clipboard = try Clipboard.init(std.testing.allocator, client, capacity);
614 defer clipboard.deinit();
615 try clipboard.source.replace("retained");
616
617 var no_descriptors: [0]sys.fd.Descriptor = .{};
618 var event: runtime.RoutedView = .{
619 .object_id = 78,
620 .interface = &core.wl_data_source.metadata,
621 .version = 3,
622 .metadata = &core.wl_data_source.events.cancelled,
623 .payload = &.{},
624 .descriptors = &no_descriptors,
625 };
626 try clipboard.dispatchSource(&event, true);
627 try std.testing.expectEqualStrings("", clipboard.source.text());
628 }
629
630 test "clipboard transfer reader enforces its byte bound" {
631 const descriptors = try transferPipe();
632 defer sys.fd.close(descriptors[0]);
633 var write_open = true;
634 defer if (write_open) sys.fd.close(descriptors[1]);
635 try writeAll(descriptors[1], "large");
636 sys.fd.close(descriptors[1]);
637 write_open = false;
638 var scratch: [2]u8 = undefined;
639 try std.testing.expectError(
640 error.TransferTooLarge,
641 readBoundedAlloc(std.testing.allocator, descriptors[0], 100, 4, &scratch),
642 );
643 }
644
645 test "clipboard transfer reader reuses nondefault scratch across reads" {
646 const descriptors = try transferPipe();
647 defer sys.fd.close(descriptors[0]);
648 var write_open = true;
649 defer if (write_open) sys.fd.close(descriptors[1]);
650 try writeAll(descriptors[1], "hello");
651 sys.fd.close(descriptors[1]);
652 write_open = false;
653 var scratch: [2]u8 = undefined;
654 const text = try readBoundedAlloc(std.testing.allocator, descriptors[0], 100, 5, &scratch);
655 defer std.testing.allocator.free(text);
656 try std.testing.expectEqualStrings("hello", text);
657 }
658
659 test "clipboard transfer reader times out while a source stays silent" {
660 const descriptors = try transferPipe();
661 defer sys.fd.close(descriptors[0]);
662 defer sys.fd.close(descriptors[1]);
663 var scratch: [3]u8 = undefined;
664 try std.testing.expectError(
665 error.TransferTimedOut,
666 readBoundedAlloc(std.testing.allocator, descriptors[0], 5, 8, &scratch),
667 );
668 }
669
670 fn writeAll(descriptor: sys.fd.Descriptor, bytes: []const u8) !void {
671 var offset: usize = 0;
672 while (offset < bytes.len) offset += try sys.fd.write(descriptor, bytes[offset..]);
673 }