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