an experimental irc client
at main 28 kB view raw
1const std = @import("std"); 2const builtin = @import("builtin"); 3const comlink = @import("comlink.zig"); 4const vaxis = @import("vaxis"); 5const zeit = @import("zeit"); 6const ziglua = @import("ziglua"); 7const Scrollbar = @import("Scrollbar.zig"); 8const main = @import("main.zig"); 9const format = @import("format.zig"); 10 11const irc = comlink.irc; 12const lua = comlink.lua; 13const mem = std.mem; 14const vxfw = vaxis.vxfw; 15 16const assert = std.debug.assert; 17 18const Allocator = std.mem.Allocator; 19const Base64Encoder = std.base64.standard.Encoder; 20const Bind = comlink.Bind; 21const Completer = comlink.Completer; 22const Event = comlink.Event; 23const Lua = ziglua.Lua; 24const TextInput = vaxis.widgets.TextInput; 25const WriteRequest = comlink.WriteRequest; 26 27const log = std.log.scoped(.app); 28 29const State = struct { 30 buffers: struct { 31 count: usize = 0, 32 width: u16 = 16, 33 } = .{}, 34 paste: struct { 35 pasting: bool = false, 36 has_newline: bool = false, 37 38 fn showDialog(self: @This()) bool { 39 return !self.pasting and self.has_newline; 40 } 41 } = .{}, 42}; 43 44pub const App = struct { 45 config: comlink.Config, 46 explicit_join: bool, 47 alloc: std.mem.Allocator, 48 /// System certificate bundle 49 bundle: std.crypto.Certificate.Bundle, 50 /// List of all configured clients 51 clients: std.ArrayList(*irc.Client), 52 /// if we have already called deinit 53 deinited: bool, 54 /// Process environment 55 env: std.process.EnvMap, 56 /// Local timezone 57 tz: zeit.TimeZone, 58 59 state: State, 60 61 completer: ?Completer, 62 63 binds: std.ArrayList(Bind), 64 65 paste_buffer: std.ArrayList(u8), 66 67 lua: *Lua, 68 69 write_queue: comlink.WriteQueue, 70 write_thread: std.Thread, 71 72 view: vxfw.SplitView, 73 buffer_list: vxfw.ListView, 74 unicode: *const vaxis.Unicode, 75 76 title_buf: [128]u8, 77 78 // Only valid during an event handler 79 ctx: ?*vxfw.EventContext, 80 last_height: u16, 81 82 /// Whether the application has focus or not 83 has_focus: bool, 84 85 fg: ?[3]u8, 86 bg: ?[3]u8, 87 yellow: ?[3]u8, 88 89 const default_rhs: vxfw.Text = .{ .text = "TODO: update this text" }; 90 91 /// initialize vaxis, lua state 92 pub fn init(self: *App, gpa: std.mem.Allocator, unicode: *const vaxis.Unicode) !void { 93 self.* = .{ 94 .alloc = gpa, 95 .config = .{}, 96 .state = .{}, 97 .clients = std.ArrayList(*irc.Client).init(gpa), 98 .env = try std.process.getEnvMap(gpa), 99 .binds = try std.ArrayList(Bind).initCapacity(gpa, 16), 100 .paste_buffer = std.ArrayList(u8).init(gpa), 101 .tz = try zeit.local(gpa, null), 102 .lua = undefined, 103 .write_queue = .{}, 104 .write_thread = undefined, 105 .view = .{ 106 .width = self.state.buffers.width, 107 .lhs = self.buffer_list.widget(), 108 .rhs = default_rhs.widget(), 109 }, 110 .explicit_join = false, 111 .bundle = .{}, 112 .deinited = false, 113 .completer = null, 114 .buffer_list = .{ 115 .children = .{ 116 .builder = .{ 117 .userdata = self, 118 .buildFn = App.bufferBuilderFn, 119 }, 120 }, 121 .draw_cursor = false, 122 }, 123 .unicode = unicode, 124 .title_buf = undefined, 125 .ctx = null, 126 .last_height = 0, 127 .has_focus = true, 128 .fg = null, 129 .bg = null, 130 .yellow = null, 131 }; 132 errdefer self.deinit(); 133 134 self.lua = try Lua.init(self.alloc); 135 self.write_thread = try std.Thread.spawn(.{}, writeLoop, .{ self.alloc, &self.write_queue }); 136 137 try lua.init(self); 138 139 try self.binds.append(.{ 140 .key = .{ .codepoint = 'c', .mods = .{ .ctrl = true } }, 141 .command = .quit, 142 }); 143 try self.binds.append(.{ 144 .key = .{ .codepoint = vaxis.Key.up, .mods = .{ .alt = true } }, 145 .command = .@"prev-channel", 146 }); 147 try self.binds.append(.{ 148 .key = .{ .codepoint = vaxis.Key.down, .mods = .{ .alt = true } }, 149 .command = .@"next-channel", 150 }); 151 try self.binds.append(.{ 152 .key = .{ .codepoint = 'l', .mods = .{ .ctrl = true } }, 153 .command = .redraw, 154 }); 155 156 // Get our system tls certs 157 try self.bundle.rescan(gpa); 158 } 159 160 /// close the application. This closes the TUI, disconnects clients, and cleans 161 /// up all resources 162 pub fn deinit(self: *App) void { 163 if (self.deinited) return; 164 self.deinited = true; 165 // Push a join command to the write thread 166 self.write_queue.push(.join); 167 168 // clean up clients 169 { 170 // Loop first to close connections. This will help us close faster by getting the 171 // threads exited 172 for (self.clients.items) |client| { 173 client.close(); 174 } 175 for (self.clients.items) |client| { 176 client.deinit(); 177 self.alloc.destroy(client); 178 } 179 self.clients.deinit(); 180 } 181 182 self.bundle.deinit(self.alloc); 183 184 if (self.completer) |*completer| completer.deinit(); 185 self.binds.deinit(); 186 self.paste_buffer.deinit(); 187 self.tz.deinit(); 188 189 // Join the write thread 190 self.write_thread.join(); 191 self.env.deinit(); 192 self.lua.deinit(); 193 } 194 195 pub fn widget(self: *App) vxfw.Widget { 196 return .{ 197 .userdata = self, 198 .captureHandler = App.typeErasedCaptureHandler, 199 .eventHandler = App.typeErasedEventHandler, 200 .drawFn = App.typeErasedDrawFn, 201 }; 202 } 203 204 fn typeErasedCaptureHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 205 const self: *App = @ptrCast(@alignCast(ptr)); 206 // Rewrite the ctx pointer every frame. We don't actually need to do this with the current 207 // vxfw runtime, because the context pointer is always valid. But for safe keeping, we will 208 // do it this way. 209 // 210 // In general, this is bad practice. But we need to be able to access this from lua 211 // callbacks 212 self.ctx = ctx; 213 switch (event) { 214 .color_scheme => { 215 // On a color scheme event, we request the colors again 216 try ctx.queryColor(.fg); 217 try ctx.queryColor(.bg); 218 try ctx.queryColor(.{ .index = 3 }); 219 }, 220 .color_report => |color| { 221 switch (color.kind) { 222 .fg => self.fg = color.value, 223 .bg => self.bg = color.value, 224 .index => |index| { 225 switch (index) { 226 3 => self.yellow = color.value, 227 else => {}, 228 } 229 }, 230 .cursor => {}, 231 } 232 if (self.fg != null and self.bg != null) { 233 for (self.clients.items) |client| { 234 client.text_field.style.bg = self.blendBg(10); 235 for (client.channels.items) |channel| { 236 channel.text_field.style.bg = self.blendBg(10); 237 } 238 } 239 } 240 ctx.redraw = true; 241 }, 242 .key_press => |key| { 243 if (self.state.paste.pasting) { 244 ctx.consume_event = true; 245 // Always ignore enter key 246 if (key.codepoint == vaxis.Key.enter) return; 247 if (key.text) |text| { 248 try self.paste_buffer.appendSlice(text); 249 } 250 return; 251 } 252 if (key.matches('c', .{ .ctrl = true })) { 253 ctx.quit = true; 254 } 255 for (self.binds.items) |bind| { 256 if (key.matches(bind.key.codepoint, bind.key.mods)) { 257 switch (bind.command) { 258 .quit => ctx.quit = true, 259 .@"next-channel" => self.nextChannel(), 260 .@"prev-channel" => self.prevChannel(), 261 .redraw => try ctx.queueRefresh(), 262 .lua_function => |ref| try lua.execFn(self.lua, ref), 263 else => {}, 264 } 265 return ctx.consumeAndRedraw(); 266 } 267 } 268 }, 269 .paste_start => self.state.paste.pasting = true, 270 .paste_end => { 271 self.state.paste.pasting = false; 272 if (std.mem.indexOfScalar(u8, self.paste_buffer.items, '\n')) |_| { 273 log.debug("paste had line ending", .{}); 274 return; 275 } 276 defer self.paste_buffer.clearRetainingCapacity(); 277 if (self.selectedBuffer()) |buffer| { 278 switch (buffer) { 279 .client => {}, 280 .channel => |channel| { 281 try channel.text_field.insertSliceAtCursor(self.paste_buffer.items); 282 return ctx.consumeAndRedraw(); 283 }, 284 } 285 } 286 }, 287 .focus_out => self.has_focus = false, 288 289 .focus_in => { 290 if (self.config.markread_on_focus) { 291 if (self.selectedBuffer()) |buffer| { 292 switch (buffer) { 293 .client => {}, 294 .channel => |channel| { 295 channel.last_read_indicator = channel.last_read; 296 }, 297 } 298 } 299 } 300 self.has_focus = true; 301 ctx.redraw = true; 302 }, 303 304 else => {}, 305 } 306 } 307 308 fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 309 const self: *App = @ptrCast(@alignCast(ptr)); 310 self.ctx = ctx; 311 switch (event) { 312 .init => { 313 const title = try std.fmt.bufPrint(&self.title_buf, "comlink", .{}); 314 try ctx.setTitle(title); 315 try ctx.tick(8, self.widget()); 316 try ctx.queryColor(.fg); 317 try ctx.queryColor(.bg); 318 try ctx.queryColor(.{ .index = 3 }); 319 if (self.clients.items.len > 0) { 320 try ctx.requestFocus(self.clients.items[0].text_field.widget()); 321 } 322 }, 323 .tick => { 324 for (self.clients.items) |client| { 325 if (client.status.load(.unordered) == .disconnected and 326 client.retry_delay_s == 0) 327 { 328 ctx.redraw = true; 329 try irc.Client.retryTickHandler(client, ctx, .tick); 330 } 331 client.drainFifo(ctx); 332 client.checkTypingStatus(ctx); 333 } 334 try ctx.tick(8, self.widget()); 335 }, 336 else => {}, 337 } 338 } 339 340 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 341 const self: *App = @ptrCast(@alignCast(ptr)); 342 const max = ctx.max.size(); 343 self.last_height = max.height; 344 if (self.selectedBuffer()) |buffer| { 345 switch (buffer) { 346 .client => |client| self.view.rhs = client.view(), 347 .channel => |channel| self.view.rhs = channel.view.widget(), 348 } 349 } else self.view.rhs = default_rhs.widget(); 350 351 var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); 352 353 // UI is a tree of splits 354 // │ │ │ │ 355 // │ │ │ │ 356 // │ buffers │ buffer content │ members │ 357 // │ │ │ │ 358 // │ │ │ │ 359 // │ │ │ │ 360 // │ │ │ │ 361 362 const sub: vxfw.SubSurface = .{ 363 .origin = .{ .col = 0, .row = 0 }, 364 .surface = try self.view.widget().draw(ctx), 365 }; 366 try children.append(sub); 367 368 for (self.clients.items) |client| { 369 if (client.list_modal.is_shown) { 370 const padding: u16 = 8; 371 const modal_ctx = ctx.withConstraints(ctx.min, .{ 372 .width = max.width -| padding * 2, 373 .height = max.height -| padding, 374 }); 375 const border: vxfw.Border = .{ .child = client.list_modal.widget() }; 376 try children.append(.{ 377 .origin = .{ .row = padding / 2, .col = padding }, 378 .surface = try border.draw(modal_ctx), 379 }); 380 break; 381 } 382 } 383 384 return .{ 385 .size = ctx.max.size(), 386 .widget = self.widget(), 387 .buffer = &.{}, 388 .children = children.items, 389 }; 390 } 391 392 fn bufferBuilderFn(ptr: *const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget { 393 const self: *const App = @ptrCast(@alignCast(ptr)); 394 var i: usize = 0; 395 for (self.clients.items) |client| { 396 if (i == idx) return client.nameWidget(i == cursor); 397 i += 1; 398 for (client.channels.items) |channel| { 399 if (i == idx) return channel.nameWidget(i == cursor); 400 i += 1; 401 } 402 } 403 return null; 404 } 405 406 pub fn connect(self: *App, cfg: irc.Client.Config) !void { 407 const client = try self.alloc.create(irc.Client); 408 try client.init(self.alloc, self, &self.write_queue, cfg); 409 try self.clients.append(client); 410 } 411 412 pub fn nextChannel(self: *App) void { 413 if (self.ctx) |ctx| { 414 self.buffer_list.nextItem(ctx); 415 if (self.selectedBuffer()) |buffer| { 416 switch (buffer) { 417 .client => |client| { 418 ctx.requestFocus(client.text_field.widget()) catch {}; 419 }, 420 .channel => |channel| { 421 ctx.requestFocus(channel.text_field.widget()) catch {}; 422 }, 423 } 424 } 425 } 426 } 427 428 pub fn prevChannel(self: *App) void { 429 if (self.ctx) |ctx| { 430 self.buffer_list.prevItem(ctx); 431 if (self.selectedBuffer()) |buffer| { 432 switch (buffer) { 433 .client => |client| { 434 ctx.requestFocus(client.text_field.widget()) catch {}; 435 }, 436 .channel => |channel| { 437 ctx.requestFocus(channel.text_field.widget()) catch {}; 438 }, 439 } 440 } 441 } 442 } 443 444 pub fn selectChannelName(self: *App, cl: *irc.Client, name: []const u8) void { 445 var i: usize = 0; 446 for (self.clients.items) |client| { 447 i += 1; 448 for (client.channels.items) |channel| { 449 if (cl == client) { 450 if (std.mem.eql(u8, name, channel.name)) { 451 self.selectBuffer(.{ .channel = channel }); 452 } 453 } 454 i += 1; 455 } 456 } 457 } 458 459 /// Blend fg and bg, otherwise return index 8. amt will be clamped to [0,100]. amt will be 460 /// interpreted as percentage of fg to blend into bg 461 pub fn blendBg(self: *App, amt: u8) vaxis.Color { 462 const bg = self.bg orelse return .{ .index = 8 }; 463 const fg = self.fg orelse return .{ .index = 8 }; 464 // Clamp to (0,100) 465 if (amt == 0) return .{ .rgb = bg }; 466 if (amt >= 100) return .{ .rgb = fg }; 467 468 const fg_r: u16 = std.math.mulWide(u8, fg[0], amt); 469 const fg_g: u16 = std.math.mulWide(u8, fg[1], amt); 470 const fg_b: u16 = std.math.mulWide(u8, fg[2], amt); 471 472 const bg_multiplier: u8 = 100 - amt; 473 const bg_r: u16 = std.math.mulWide(u8, bg[0], bg_multiplier); 474 const bg_g: u16 = std.math.mulWide(u8, bg[1], bg_multiplier); 475 const bg_b: u16 = std.math.mulWide(u8, bg[2], bg_multiplier); 476 477 return .{ 478 .rgb = .{ 479 @intCast((fg_r + bg_r) / 100), 480 @intCast((fg_g + bg_g) / 100), 481 @intCast((fg_b + bg_b) / 100), 482 }, 483 }; 484 } 485 486 /// Blend fg and bg, otherwise return index 8. amt will be clamped to [0,100]. amt will be 487 /// interpreted as percentage of fg to blend into bg 488 pub fn blendYellow(self: *App, amt: u8) vaxis.Color { 489 const bg = self.bg orelse return .{ .index = 3 }; 490 const yellow = self.yellow orelse return .{ .index = 3 }; 491 // Clamp to (0,100) 492 if (amt == 0) return .{ .rgb = bg }; 493 if (amt >= 100) return .{ .rgb = yellow }; 494 495 const yellow_r: u16 = std.math.mulWide(u8, yellow[0], amt); 496 const yellow_g: u16 = std.math.mulWide(u8, yellow[1], amt); 497 const yellow_b: u16 = std.math.mulWide(u8, yellow[2], amt); 498 499 const bg_multiplier: u8 = 100 - amt; 500 const bg_r: u16 = std.math.mulWide(u8, bg[0], bg_multiplier); 501 const bg_g: u16 = std.math.mulWide(u8, bg[1], bg_multiplier); 502 const bg_b: u16 = std.math.mulWide(u8, bg[2], bg_multiplier); 503 504 return .{ 505 .rgb = .{ 506 @intCast((yellow_r + bg_r) / 100), 507 @intCast((yellow_g + bg_g) / 100), 508 @intCast((yellow_b + bg_b) / 100), 509 }, 510 }; 511 } 512 513 /// handle a command 514 pub fn handleCommand(self: *App, buffer: irc.Buffer, cmd: []const u8) !void { 515 const lua_state = self.lua; 516 const command: comlink.Command = blk: { 517 const start: u1 = if (cmd[0] == '/') 1 else 0; 518 const end = mem.indexOfScalar(u8, cmd, ' ') orelse cmd.len; 519 if (comlink.Command.fromString(cmd[start..end])) |internal| 520 break :blk internal; 521 if (comlink.Command.user_commands.get(cmd[start..end])) |ref| { 522 const str = if (end == cmd.len) "" else std.mem.trim(u8, cmd[end..], " "); 523 return lua.execUserCommand(lua_state, str, ref); 524 } 525 return error.UnknownCommand; 526 }; 527 var buf: [1024]u8 = undefined; 528 const client: *irc.Client = switch (buffer) { 529 .client => |client| client, 530 .channel => |channel| channel.client, 531 }; 532 const channel: ?*irc.Channel = switch (buffer) { 533 .client => null, 534 .channel => |channel| channel, 535 }; 536 switch (command) { 537 .quote => { 538 const start = mem.indexOfScalar(u8, cmd, ' ') orelse return error.InvalidCommand; 539 const msg = try std.fmt.bufPrint( 540 &buf, 541 "{s}\r\n", 542 .{cmd[start + 1 ..]}, 543 ); 544 return client.queueWrite(msg); 545 }, 546 .join => { 547 const start = std.mem.indexOfScalar(u8, cmd, ' ') orelse return error.InvalidCommand; 548 const chan_name = cmd[start + 1 ..]; 549 for (client.channels.items) |chan| { 550 if (std.mem.eql(u8, chan.name, chan_name)) { 551 client.app.selectBuffer(.{ .channel = chan }); 552 return; 553 } 554 } 555 const msg = try std.fmt.bufPrint( 556 &buf, 557 "JOIN {s}\r\n", 558 .{ 559 chan_name, 560 }, 561 ); 562 563 // Check 564 // Ensure buffer exists 565 self.explicit_join = true; 566 return client.queueWrite(msg); 567 }, 568 .list => { 569 client.list_modal.expecting_response = true; 570 return client.queueWrite("LIST\r\n"); 571 }, 572 .me => { 573 if (channel == null) return error.InvalidCommand; 574 const msg = try std.fmt.bufPrint( 575 &buf, 576 "PRIVMSG {s} :\x01ACTION {s}\x01\r\n", 577 .{ 578 channel.?.name, 579 cmd[4..], 580 }, 581 ); 582 return client.queueWrite(msg); 583 }, 584 .msg => { 585 //syntax: /msg <nick> <msg> 586 const s = std.mem.indexOfScalar(u8, cmd, ' ') orelse return error.InvalidCommand; 587 const e = std.mem.indexOfScalarPos(u8, cmd, s + 1, ' ') orelse return error.InvalidCommand; 588 const msg = try std.fmt.bufPrint( 589 &buf, 590 "PRIVMSG {s} :{s}\r\n", 591 .{ 592 cmd[s + 1 .. e], 593 cmd[e + 1 ..], 594 }, 595 ); 596 return client.queueWrite(msg); 597 }, 598 .query => { 599 const s = std.mem.indexOfScalar(u8, cmd, ' ') orelse return error.InvalidCommand; 600 const e = std.mem.indexOfScalarPos(u8, cmd, s + 1, ' ') orelse cmd.len; 601 if (cmd[s + 1] == '#') return error.InvalidCommand; 602 603 const ch = try client.getOrCreateChannel(cmd[s + 1 .. e]); 604 try client.requestHistory(.after, ch); 605 self.selectChannelName(client, ch.name); 606 //handle sending the message 607 if (cmd.len - e > 1) { 608 const msg = try std.fmt.bufPrint( 609 &buf, 610 "PRIVMSG {s} :{s}\r\n", 611 .{ 612 cmd[s + 1 .. e], 613 cmd[e + 1 ..], 614 }, 615 ); 616 return client.queueWrite(msg); 617 } 618 }, 619 .names => { 620 if (channel == null) return error.InvalidCommand; 621 const msg = try std.fmt.bufPrint(&buf, "NAMES {s}\r\n", .{channel.?.name}); 622 return client.queueWrite(msg); 623 }, 624 .@"next-channel" => self.nextChannel(), 625 .@"prev-channel" => self.prevChannel(), 626 .quit => { 627 if (self.ctx) |ctx| ctx.quit = true; 628 }, 629 .who => { 630 if (channel == null) return error.InvalidCommand; 631 const msg = try std.fmt.bufPrint( 632 &buf, 633 "WHO {s}\r\n", 634 .{ 635 channel.?.name, 636 }, 637 ); 638 return client.queueWrite(msg); 639 }, 640 .part, .close => { 641 if (channel == null) return error.InvalidCommand; 642 var it = std.mem.tokenizeScalar(u8, cmd, ' '); 643 644 // Skip command 645 _ = it.next(); 646 const target = it.next() orelse channel.?.name; 647 648 if (target[0] != '#') { 649 for (client.channels.items, 0..) |search, i| { 650 if (!mem.eql(u8, search.name, target)) continue; 651 client.app.prevChannel(); 652 var chan = client.channels.orderedRemove(i); 653 chan.deinit(self.alloc); 654 self.alloc.destroy(chan); 655 break; 656 } 657 } else { 658 const msg = try std.fmt.bufPrint( 659 &buf, 660 "PART {s}\r\n", 661 .{ 662 target, 663 }, 664 ); 665 return client.queueWrite(msg); 666 } 667 }, 668 .redraw => {}, 669 // .redraw => self.vx.queueRefresh(), 670 .version => { 671 if (channel == null) return error.InvalidCommand; 672 const msg = try std.fmt.bufPrint( 673 &buf, 674 "NOTICE {s} :\x01VERSION comlink {s}\x01\r\n", 675 .{ 676 channel.?.name, 677 main.version, 678 }, 679 ); 680 return client.queueWrite(msg); 681 }, 682 .lua_function => {}, // we don't handle these from the text-input 683 } 684 } 685 686 pub fn selectedBuffer(self: *App) ?irc.Buffer { 687 var i: usize = 0; 688 for (self.clients.items) |client| { 689 if (i == self.buffer_list.cursor) return .{ .client = client }; 690 i += 1; 691 for (client.channels.items) |channel| { 692 if (i == self.buffer_list.cursor) return .{ .channel = channel }; 693 i += 1; 694 } 695 } 696 return null; 697 } 698 699 pub fn selectBuffer(self: *App, buffer: irc.Buffer) void { 700 var i: u32 = 0; 701 switch (buffer) { 702 .client => |target| { 703 for (self.clients.items) |client| { 704 if (client == target) { 705 if (self.ctx) |ctx| { 706 ctx.requestFocus(client.text_field.widget()) catch {}; 707 } 708 self.buffer_list.cursor = i; 709 self.buffer_list.ensureScroll(); 710 return; 711 } 712 i += 1; 713 for (client.channels.items) |_| i += 1; 714 } 715 }, 716 .channel => |target| { 717 for (self.clients.items) |client| { 718 i += 1; 719 for (client.channels.items) |channel| { 720 if (channel == target) { 721 self.buffer_list.cursor = i; 722 self.buffer_list.ensureScroll(); 723 channel.doSelect(); 724 if (self.ctx) |ctx| { 725 ctx.requestFocus(channel.text_field.widget()) catch {}; 726 } 727 return; 728 } 729 i += 1; 730 } 731 } 732 }, 733 } 734 } 735}; 736 737/// this loop is run in a separate thread and handles writes to all clients. 738/// Message content is deallocated when the write request is completed 739fn writeLoop(alloc: std.mem.Allocator, queue: *comlink.WriteQueue) !void { 740 log.debug("starting write thread", .{}); 741 while (true) { 742 const req = queue.pop(); 743 switch (req) { 744 .write => |w| { 745 try w.client.write(w.msg); 746 alloc.free(w.msg); 747 }, 748 .join => { 749 while (queue.tryPop()) |r| { 750 switch (r) { 751 .write => |w| alloc.free(w.msg), 752 else => {}, 753 } 754 } 755 return; 756 }, 757 } 758 } 759}