an experimental irc client

wip: initial vxfw compilation

+1267 -693
-1
build.zig
··· 34 34 const vaxis_dep = b.dependency("vaxis", .{ 35 35 .target = target, 36 36 .optimize = optimize, 37 - .libxev = false, 38 37 }); 39 38 40 39 const zeit_dep = b.dependency("zeit", .{
+2 -2
build.zig.zon
··· 7 7 .hash = "1220affeb3fe37ef09411b5a213b5fdf9bb6568e9913bade204694648983a8b2776d", 8 8 }, 9 9 .vaxis = .{ 10 - .url = "git+https://github.com/rockorager/libvaxis?ref=v0.5.1#2ab3b46e89bced844b60601c7ab9961420d15994", 11 - .hash = "1220de23a3240e503397ea579de4fd85db422f537e10036ef74717c50164475813ce", 10 + .url = "git+https://github.com/rockorager/libvaxis#0eaf6226b2dd58720c5954d3646d6782e0c063f5", 11 + .hash = "12208b6363d1bff963081ee4cba5c8be9f782e89ed7604e5ceab61999b1a7980f791", 12 12 }, 13 13 .zeit = .{ 14 14 .url = "git+https://github.com/rockorager/zeit?ref=main#d943bc4bfe9e18490460dfdd64f48e997065eba8",
+721 -646
src/app.zig
··· 11 11 const irc = comlink.irc; 12 12 const lua = comlink.lua; 13 13 const mem = std.mem; 14 + const vxfw = vaxis.vxfw; 14 15 15 16 const assert = std.debug.assert; 16 17 18 + const Allocator = std.mem.Allocator; 17 19 const Base64Encoder = std.base64.standard.Encoder; 18 20 const Bind = comlink.Bind; 19 21 const Completer = comlink.Completer; ··· 65 67 env: std.process.EnvMap, 66 68 /// Local timezone 67 69 tz: zeit.TimeZone, 68 - /// Instance of vaxis 69 - vx: vaxis.Vaxis, 70 - /// The tty we are talking to 71 - tty: vaxis.Tty, 72 70 73 71 state: State = .{}, 74 72 ··· 80 78 81 79 paste_buffer: std.ArrayList(u8), 82 80 81 + lua: *Lua, 82 + 83 + write_queue: comlink.WriteQueue, 84 + write_thread: std.Thread, 85 + 83 86 /// initialize vaxis, lua state 84 - pub fn init(alloc: std.mem.Allocator) !App { 85 - const vx = try vaxis.init(alloc, .{}); 86 - const env = try std.process.getEnvMap(alloc); 87 - var app: App = .{ 88 - .alloc = alloc, 89 - .clients = std.ArrayList(*irc.Client).init(alloc), 90 - .env = env, 91 - .vx = vx, 92 - .tty = try vaxis.Tty.init(), 93 - .binds = try std.ArrayList(Bind).initCapacity(alloc, 16), 94 - .paste_buffer = std.ArrayList(u8).init(alloc), 95 - .tz = try zeit.local(alloc, &env), 87 + pub fn init(self: *App, gpa: std.mem.Allocator) !void { 88 + self.* = .{ 89 + .alloc = gpa, 90 + .clients = std.ArrayList(*irc.Client).init(gpa), 91 + .env = try std.process.getEnvMap(gpa), 92 + .binds = try std.ArrayList(Bind).initCapacity(gpa, 16), 93 + .paste_buffer = std.ArrayList(u8).init(gpa), 94 + .tz = try zeit.local(gpa, null), 95 + .lua = undefined, 96 + .write_queue = .{}, 97 + .write_thread = undefined, 96 98 }; 97 99 98 - try app.binds.append(.{ 100 + self.lua = try Lua.init(&self.alloc); 101 + self.write_thread = try std.Thread.spawn(.{}, writeLoop, .{ self.alloc, &self.write_queue }); 102 + 103 + try lua.init(self); 104 + 105 + try self.binds.append(.{ 99 106 .key = .{ 100 107 .codepoint = 'c', 101 108 .mods = .{ .ctrl = true }, 102 109 }, 103 110 .command = .quit, 104 111 }); 105 - try app.binds.append(.{ 112 + try self.binds.append(.{ 106 113 .key = .{ 107 114 .codepoint = vaxis.Key.up, 108 115 .mods = .{ .alt = true }, 109 116 }, 110 117 .command = .@"prev-channel", 111 118 }); 112 - try app.binds.append(.{ 119 + try self.binds.append(.{ 113 120 .key = .{ 114 121 .codepoint = vaxis.Key.down, 115 122 .mods = .{ .alt = true }, 116 123 }, 117 124 .command = .@"next-channel", 118 125 }); 119 - try app.binds.append(.{ 126 + try self.binds.append(.{ 120 127 .key = .{ 121 128 .codepoint = 'l', 122 129 .mods = .{ .ctrl = true }, ··· 125 132 }); 126 133 127 134 // Get our system tls certs 128 - try app.bundle.rescan(alloc); 129 - 130 - return app; 135 + try self.bundle.rescan(gpa); 131 136 } 132 137 133 138 /// close the application. This closes the TUI, disconnects clients, and cleans ··· 135 140 pub fn deinit(self: *App) void { 136 141 if (self.deinited) return; 137 142 self.deinited = true; 143 + // Push a join command to the write thread 144 + self.write_queue.push(.join); 138 145 139 146 // clean up clients 140 147 { ··· 153 160 } 154 161 155 162 self.bundle.deinit(self.alloc); 156 - self.vx.deinit(self.alloc, self.tty.anyWriter()); 157 - self.tty.deinit(); 158 163 159 164 if (self.completer) |*completer| completer.deinit(); 160 165 self.binds.deinit(); 161 166 self.paste_buffer.deinit(); 162 167 self.tz.deinit(); 168 + 169 + // Join the write thread 170 + self.write_thread.join(); 163 171 self.env.deinit(); 172 + self.lua.deinit(); 164 173 } 165 174 166 - pub fn run(self: *App, lua_state: *Lua) !void { 167 - const writer = self.tty.anyWriter(); 175 + pub fn widget(self: *App) vxfw.Widget { 176 + return .{ 177 + .userdata = self, 178 + .captureHandler = App.typeErasedCaptureHandler, 179 + .eventHandler = App.typeErasedEventHandler, 180 + .drawFn = App.typeErasedDrawFn, 181 + }; 182 + } 168 183 169 - var loop: comlink.EventLoop = .{ .vaxis = &self.vx, .tty = &self.tty }; 170 - try loop.init(); 171 - try loop.start(); 172 - defer loop.stop(); 173 - 174 - try self.vx.enterAltScreen(writer); 175 - try self.vx.queryTerminal(writer, 1 * std.time.ns_per_s); 176 - try self.vx.setMouseMode(writer, true); 177 - try self.vx.setBracketedPaste(writer, true); 178 - 179 - // start our write thread 180 - var write_queue: comlink.WriteQueue = .{}; 181 - const write_thread = try std.Thread.spawn(.{}, writeLoop, .{ self.alloc, &write_queue }); 182 - defer { 183 - write_queue.push(.join); 184 - write_thread.join(); 184 + fn typeErasedCaptureHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 185 + // const self: *App = @ptrCast(@alignCast(ptr)); 186 + _ = ptr; 187 + switch (event) { 188 + .key_press => |key| { 189 + if (key.matches('c', .{ .ctrl = true })) { 190 + ctx.quit = true; 191 + } 192 + }, 193 + else => {}, 185 194 } 186 - 187 - // initialize lua state 188 - try lua.init(self, lua_state, &loop); 195 + } 189 196 190 - var input = TextInput.init(self.alloc, &self.vx.unicode); 191 - defer input.deinit(); 192 - 193 - var last_frame: i64 = std.time.milliTimestamp(); 194 - loop: while (!self.should_quit) { 195 - var redraw: bool = false; 196 - std.time.sleep(8 * std.time.ns_per_ms); 197 - if (self.state.messages.pending_scroll != 0) { 198 - redraw = true; 199 - if (self.state.messages.pending_scroll > 0) { 200 - self.state.messages.pending_scroll -= 1; 201 - self.state.messages.scroll_offset += 1; 202 - } else { 203 - self.state.messages.pending_scroll += 1; 204 - self.state.messages.scroll_offset -|= 1; 197 + fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 198 + const self: *App = @ptrCast(@alignCast(ptr)); 199 + switch (event) { 200 + .init => try ctx.tick(8, self.widget()), 201 + .key_press => |key| { 202 + if (key.matches('c', .{ .ctrl = true })) { 203 + ctx.quit = true; 205 204 } 206 - } 207 - while (loop.tryEvent()) |event| { 208 - redraw = true; 209 - switch (event) { 210 - .redraw => {}, 211 - .key_press => |key| { 212 - if (self.state.paste.showDialog()) { 213 - if (key.matches(vaxis.Key.escape, .{})) { 214 - self.state.paste.has_newline = false; 215 - self.paste_buffer.clearAndFree(); 216 - } 217 - break; 218 - } 219 - if (self.state.paste.pasting) { 220 - if (key.matches(vaxis.Key.enter, .{})) { 221 - self.state.paste.has_newline = true; 222 - try self.paste_buffer.append('\n'); 223 - continue :loop; 224 - } 225 - const text = key.text orelse continue :loop; 226 - try self.paste_buffer.appendSlice(text); 227 - continue; 228 - } 229 - for (self.binds.items) |bind| { 230 - if (key.matches(bind.key.codepoint, bind.key.mods)) { 231 - switch (bind.command) { 232 - .quit => self.should_quit = true, 233 - .@"next-channel" => self.nextChannel(), 234 - .@"prev-channel" => self.prevChannel(), 235 - .redraw => self.vx.queueRefresh(), 236 - .lua_function => |ref| try lua.execFn(lua_state, ref), 237 - else => {}, 238 - } 239 - break; 240 - } 241 - } else if (key.matches(vaxis.Key.tab, .{})) { 242 - // if we already have a completion word, then we are 243 - // cycling through the options 244 - if (self.completer) |*completer| { 245 - const line = completer.next(); 246 - input.clearRetainingCapacity(); 247 - try input.insertSliceAtCursor(line); 248 - } else { 249 - var completion_buf: [irc.maximum_message_size]u8 = undefined; 250 - const content = input.sliceToCursor(&completion_buf); 251 - self.completer = try Completer.init(self.alloc, content); 252 - } 253 - } else if (key.matches(vaxis.Key.tab, .{ .shift = true })) { 254 - if (self.completer) |*completer| { 255 - const line = completer.prev(); 256 - input.clearRetainingCapacity(); 257 - try input.insertSliceAtCursor(line); 258 - } 259 - } else if (key.matches(vaxis.Key.enter, .{})) { 260 - const buffer = self.selectedBuffer() orelse @panic("no buffer"); 261 - const content = try input.toOwnedSlice(); 262 - if (content.len == 0) continue; 263 - defer self.alloc.free(content); 264 - if (content[0] == '/') 265 - self.handleCommand(lua_state, buffer, content) catch |err| { 266 - log.err("couldn't handle command: {}", .{err}); 267 - } 268 - else { 269 - switch (buffer) { 270 - .channel => |channel| { 271 - var buf: [1024]u8 = undefined; 272 - const msg = try std.fmt.bufPrint( 273 - &buf, 274 - "PRIVMSG {s} :{s}\r\n", 275 - .{ 276 - channel.name, 277 - content, 278 - }, 279 - ); 280 - try channel.client.queueWrite(msg); 281 - }, 282 - .client => log.err("can't send message to client", .{}), 283 - } 284 - } 285 - if (self.completer != null) { 286 - self.completer.?.deinit(); 287 - self.completer = null; 288 - } 289 - } else if (key.matches(vaxis.Key.page_up, .{})) { 290 - self.state.messages.scroll_offset +|= 3; 291 - } else if (key.matches(vaxis.Key.page_down, .{})) { 292 - self.state.messages.scroll_offset -|= 3; 293 - } else if (key.matches(vaxis.Key.home, .{})) { 294 - self.state.messages.scroll_offset = 0; 295 - } else { 296 - if (self.completer != null and !key.isModifier()) { 297 - self.completer.?.deinit(); 298 - self.completer = null; 299 - } 300 - log.debug("{}", .{key}); 301 - try input.update(.{ .key_press = key }); 302 - } 303 - }, 304 - .paste_start => self.state.paste.pasting = true, 305 - .paste_end => { 306 - self.state.paste.pasting = false; 307 - if (self.state.paste.has_newline) { 308 - log.warn("NEWLINE", .{}); 309 - } else { 310 - try input.insertSliceAtCursor(self.paste_buffer.items); 311 - defer self.paste_buffer.clearAndFree(); 312 - } 313 - }, 314 - .focus_out => self.state.mouse = null, 315 - .mouse => |mouse| { 316 - self.state.mouse = mouse; 317 - }, 318 - .winsize => |ws| try self.vx.resize(self.alloc, writer, ws), 319 - .connect => |cfg| { 320 - const client = try self.alloc.create(irc.Client); 321 - client.* = try irc.Client.init(self.alloc, self, &write_queue, cfg); 322 - client.thread = try std.Thread.spawn(.{}, irc.Client.readLoop, .{ client, &loop }); 323 - try self.clients.append(client); 324 - }, 325 - .irc => |irc_event| { 326 - const msg: irc.Message = .{ .bytes = irc_event.msg.slice() }; 327 - const client = irc_event.client; 328 - defer irc_event.msg.deinit(); 329 - switch (msg.command()) { 330 - .unknown => {}, 331 - .CAP => { 332 - // syntax: <client> <ACK/NACK> :caps 333 - var iter = msg.paramIterator(); 334 - _ = iter.next() orelse continue; // client 335 - const ack_or_nak = iter.next() orelse continue; 336 - const caps = iter.next() orelse continue; 337 - var cap_iter = mem.splitScalar(u8, caps, ' '); 338 - while (cap_iter.next()) |cap| { 339 - if (mem.eql(u8, ack_or_nak, "ACK")) { 340 - client.ack(cap); 341 - if (mem.eql(u8, cap, "sasl")) 342 - try client.queueWrite("AUTHENTICATE PLAIN\r\n"); 343 - } else if (mem.eql(u8, ack_or_nak, "NAK")) { 344 - log.debug("CAP not supported {s}", .{cap}); 345 - } 346 - } 347 - }, 348 - .AUTHENTICATE => { 349 - var iter = msg.paramIterator(); 350 - while (iter.next()) |param| { 351 - // A '+' is the continuuation to send our 352 - // AUTHENTICATE info 353 - if (!mem.eql(u8, param, "+")) continue; 354 - var buf: [4096]u8 = undefined; 355 - const config = client.config; 356 - const sasl = try std.fmt.bufPrint( 357 - &buf, 358 - "{s}\x00{s}\x00{s}", 359 - .{ config.user, config.nick, config.password }, 360 - ); 205 + }, 206 + .tick => { 207 + for (self.clients.items) |client| { 208 + client.drainFifo(); 209 + } 210 + try ctx.tick(8, self.widget()); 211 + }, 212 + else => {}, 213 + } 214 + } 361 215 362 - // Create a buffer big enough for the base64 encoded string 363 - const b64_buf = try self.alloc.alloc(u8, Base64Encoder.calcSize(sasl.len)); 364 - defer self.alloc.free(b64_buf); 365 - const encoded = Base64Encoder.encode(b64_buf, sasl); 366 - // Make our message 367 - const auth = try std.fmt.bufPrint( 368 - &buf, 369 - "AUTHENTICATE {s}\r\n", 370 - .{encoded}, 371 - ); 372 - try client.queueWrite(auth); 373 - if (config.network_id) |id| { 374 - const bind = try std.fmt.bufPrint( 375 - &buf, 376 - "BOUNCER BIND {s}\r\n", 377 - .{id}, 378 - ); 379 - try client.queueWrite(bind); 380 - } 381 - try client.queueWrite("CAP END\r\n"); 382 - } 383 - }, 384 - .RPL_WELCOME => { 385 - const now = try zeit.instant(.{}); 386 - var now_buf: [30]u8 = undefined; 387 - const now_fmt = try now.time().bufPrint(&now_buf, .rfc3339); 216 + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 217 + const self: *App = @ptrCast(@alignCast(ptr)); 388 218 389 - const past = try now.subtract(.{ .days = 7 }); 390 - var past_buf: [30]u8 = undefined; 391 - const past_fmt = try past.time().bufPrint(&past_buf, .rfc3339); 219 + var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); 220 + _ = &children; 392 221 393 - var buf: [128]u8 = undefined; 394 - const targets = try std.fmt.bufPrint( 395 - &buf, 396 - "CHATHISTORY TARGETS timestamp={s} timestamp={s} 50\r\n", 397 - .{ now_fmt, past_fmt }, 398 - ); 399 - try client.queueWrite(targets); 400 - // on_connect callback 401 - try lua.onConnect(lua_state, client); 402 - }, 403 - .RPL_YOURHOST => {}, 404 - .RPL_CREATED => {}, 405 - .RPL_MYINFO => {}, 406 - .RPL_ISUPPORT => { 407 - // syntax: <client> <token>[ <token>] :are supported 408 - var iter = msg.paramIterator(); 409 - _ = iter.next() orelse continue; // client 410 - while (iter.next()) |token| { 411 - if (mem.eql(u8, token, "WHOX")) 412 - client.supports.whox = true 413 - else if (mem.startsWith(u8, token, "PREFIX")) { 414 - const prefix = blk: { 415 - const idx = mem.indexOfScalar(u8, token, ')') orelse 416 - // default is "@+" 417 - break :blk try self.alloc.dupe(u8, "@+"); 418 - break :blk try self.alloc.dupe(u8, token[idx + 1 ..]); 419 - }; 420 - client.supports.prefix = prefix; 421 - } 422 - } 423 - }, 424 - .RPL_LOGGEDIN => {}, 425 - .RPL_TOPIC => { 426 - // syntax: <client> <channel> :<topic> 427 - var iter = msg.paramIterator(); 428 - _ = iter.next() orelse continue :loop; // client ("*") 429 - const channel_name = iter.next() orelse continue :loop; // channel 430 - const topic = iter.next() orelse continue :loop; // topic 222 + const text: vxfw.Text = .{ .text = "hey" }; 223 + _ = text; 224 + return .{ 225 + .size = ctx.max.size(), 226 + .widget = self.widget(), 227 + .buffer = &.{}, 228 + .children = children.items, 229 + }; 230 + } 431 231 432 - var channel = try client.getOrCreateChannel(channel_name); 433 - if (channel.topic) |old_topic| { 434 - self.alloc.free(old_topic); 435 - } 436 - channel.topic = try self.alloc.dupe(u8, topic); 437 - }, 438 - .RPL_SASLSUCCESS => {}, 439 - .RPL_WHOREPLY => { 440 - // syntax: <client> <channel> <username> <host> <server> <nick> <flags> :<hopcount> <real name> 441 - var iter = msg.paramIterator(); 442 - _ = iter.next() orelse continue :loop; // client 443 - const channel_name = iter.next() orelse continue :loop; // channel 444 - if (mem.eql(u8, channel_name, "*")) continue; 445 - _ = iter.next() orelse continue :loop; // username 446 - _ = iter.next() orelse continue :loop; // host 447 - _ = iter.next() orelse continue :loop; // server 448 - const nick = iter.next() orelse continue :loop; // nick 449 - const flags = iter.next() orelse continue :loop; // flags 232 + // pub fn run(self: *App, lua_state: *Lua) !void { 233 + // const writer = self.tty.anyWriter(); 234 + // 235 + // var loop: comlink.EventLoop = .{ .vaxis = &self.vx, .tty = &self.tty }; 236 + // try loop.init(); 237 + // try loop.start(); 238 + // defer loop.stop(); 239 + // 240 + // try self.vx.enterAltScreen(writer); 241 + // try self.vx.queryTerminal(writer, 1 * std.time.ns_per_s); 242 + // try self.vx.setMouseMode(writer, true); 243 + // try self.vx.setBracketedPaste(writer, true); 244 + // 245 + // // start our write thread 246 + // var write_queue: comlink.WriteQueue = .{}; 247 + // const write_thread = try std.Thread.spawn(.{}, writeLoop, .{ self.alloc, &write_queue }); 248 + // defer { 249 + // write_queue.push(.join); 250 + // write_thread.join(); 251 + // } 252 + // 253 + // // initialize lua state 254 + // try lua.init(self, lua_state, &loop); 255 + // 256 + // var input = TextInput.init(self.alloc, &self.vx.unicode); 257 + // defer input.deinit(); 258 + // 259 + // var last_frame: i64 = std.time.milliTimestamp(); 260 + // loop: while (!self.should_quit) { 261 + // var redraw: bool = false; 262 + // std.time.sleep(8 * std.time.ns_per_ms); 263 + // if (self.state.messages.pending_scroll != 0) { 264 + // redraw = true; 265 + // if (self.state.messages.pending_scroll > 0) { 266 + // self.state.messages.pending_scroll -= 1; 267 + // self.state.messages.scroll_offset += 1; 268 + // } else { 269 + // self.state.messages.pending_scroll += 1; 270 + // self.state.messages.scroll_offset -|= 1; 271 + // } 272 + // } 273 + // while (loop.tryEvent()) |event| { 274 + // redraw = true; 275 + // switch (event) { 276 + // .redraw => {}, 277 + // .key_press => |key| { 278 + // if (self.state.paste.showDialog()) { 279 + // if (key.matches(vaxis.Key.escape, .{})) { 280 + // self.state.paste.has_newline = false; 281 + // self.paste_buffer.clearAndFree(); 282 + // } 283 + // break; 284 + // } 285 + // if (self.state.paste.pasting) { 286 + // if (key.matches(vaxis.Key.enter, .{})) { 287 + // self.state.paste.has_newline = true; 288 + // try self.paste_buffer.append('\n'); 289 + // continue :loop; 290 + // } 291 + // const text = key.text orelse continue :loop; 292 + // try self.paste_buffer.appendSlice(text); 293 + // continue; 294 + // } 295 + // for (self.binds.items) |bind| { 296 + // if (key.matches(bind.key.codepoint, bind.key.mods)) { 297 + // switch (bind.command) { 298 + // .quit => self.should_quit = true, 299 + // .@"next-channel" => self.nextChannel(), 300 + // .@"prev-channel" => self.prevChannel(), 301 + // .redraw => self.vx.queueRefresh(), 302 + // .lua_function => |ref| try lua.execFn(lua_state, ref), 303 + // else => {}, 304 + // } 305 + // break; 306 + // } 307 + // } else if (key.matches(vaxis.Key.tab, .{})) { 308 + // // if we already have a completion word, then we are 309 + // // cycling through the options 310 + // if (self.completer) |*completer| { 311 + // const line = completer.next(); 312 + // input.clearRetainingCapacity(); 313 + // try input.insertSliceAtCursor(line); 314 + // } else { 315 + // var completion_buf: [irc.maximum_message_size]u8 = undefined; 316 + // const content = input.sliceToCursor(&completion_buf); 317 + // self.completer = try Completer.init(self.alloc, content); 318 + // } 319 + // } else if (key.matches(vaxis.Key.tab, .{ .shift = true })) { 320 + // if (self.completer) |*completer| { 321 + // const line = completer.prev(); 322 + // input.clearRetainingCapacity(); 323 + // try input.insertSliceAtCursor(line); 324 + // } 325 + // } else if (key.matches(vaxis.Key.enter, .{})) { 326 + // const buffer = self.selectedBuffer() orelse @panic("no buffer"); 327 + // const content = try input.toOwnedSlice(); 328 + // if (content.len == 0) continue; 329 + // defer self.alloc.free(content); 330 + // if (content[0] == '/') 331 + // self.handleCommand(lua_state, buffer, content) catch |err| { 332 + // log.err("couldn't handle command: {}", .{err}); 333 + // } 334 + // else { 335 + // switch (buffer) { 336 + // .channel => |channel| { 337 + // var buf: [1024]u8 = undefined; 338 + // const msg = try std.fmt.bufPrint( 339 + // &buf, 340 + // "PRIVMSG {s} :{s}\r\n", 341 + // .{ 342 + // channel.name, 343 + // content, 344 + // }, 345 + // ); 346 + // try channel.client.queueWrite(msg); 347 + // }, 348 + // .client => log.err("can't send message to client", .{}), 349 + // } 350 + // } 351 + // if (self.completer != null) { 352 + // self.completer.?.deinit(); 353 + // self.completer = null; 354 + // } 355 + // } else if (key.matches(vaxis.Key.page_up, .{})) { 356 + // self.state.messages.scroll_offset +|= 3; 357 + // } else if (key.matches(vaxis.Key.page_down, .{})) { 358 + // self.state.messages.scroll_offset -|= 3; 359 + // } else if (key.matches(vaxis.Key.home, .{})) { 360 + // self.state.messages.scroll_offset = 0; 361 + // } else { 362 + // if (self.completer != null and !key.isModifier()) { 363 + // self.completer.?.deinit(); 364 + // self.completer = null; 365 + // } 366 + // log.debug("{}", .{key}); 367 + // try input.update(.{ .key_press = key }); 368 + // } 369 + // }, 370 + // .paste_start => self.state.paste.pasting = true, 371 + // .paste_end => { 372 + // self.state.paste.pasting = false; 373 + // if (self.state.paste.has_newline) { 374 + // log.warn("NEWLINE", .{}); 375 + // } else { 376 + // try input.insertSliceAtCursor(self.paste_buffer.items); 377 + // defer self.paste_buffer.clearAndFree(); 378 + // } 379 + // }, 380 + // .focus_out => self.state.mouse = null, 381 + // .mouse => |mouse| { 382 + // self.state.mouse = mouse; 383 + // }, 384 + // .winsize => |ws| try self.vx.resize(self.alloc, writer, ws), 385 + // .connect => |cfg| { 386 + // const client = try self.alloc.create(irc.Client); 387 + // client.* = try irc.Client.init(self.alloc, self, &write_queue, cfg); 388 + // client.thread = try std.Thread.spawn(.{}, irc.Client.readLoop, .{ client, &loop }); 389 + // try self.clients.append(client); 390 + // }, 391 + // .irc => |irc_event| { 392 + // const msg: irc.Message = .{ .bytes = irc_event.msg.slice() }; 393 + // const client = irc_event.client; 394 + // defer irc_event.msg.deinit(); 395 + // switch (msg.command()) { 396 + // .unknown => {}, 397 + // .CAP => { 398 + // // syntax: <client> <ACK/NACK> :caps 399 + // var iter = msg.paramIterator(); 400 + // _ = iter.next() orelse continue; // client 401 + // const ack_or_nak = iter.next() orelse continue; 402 + // const caps = iter.next() orelse continue; 403 + // var cap_iter = mem.splitScalar(u8, caps, ' '); 404 + // while (cap_iter.next()) |cap| { 405 + // if (mem.eql(u8, ack_or_nak, "ACK")) { 406 + // client.ack(cap); 407 + // if (mem.eql(u8, cap, "sasl")) 408 + // try client.queueWrite("AUTHENTICATE PLAIN\r\n"); 409 + // } else if (mem.eql(u8, ack_or_nak, "NAK")) { 410 + // log.debug("CAP not supported {s}", .{cap}); 411 + // } 412 + // } 413 + // }, 414 + // .AUTHENTICATE => { 415 + // var iter = msg.paramIterator(); 416 + // while (iter.next()) |param| { 417 + // // A '+' is the continuuation to send our 418 + // // AUTHENTICATE info 419 + // if (!mem.eql(u8, param, "+")) continue; 420 + // var buf: [4096]u8 = undefined; 421 + // const config = client.config; 422 + // const sasl = try std.fmt.bufPrint( 423 + // &buf, 424 + // "{s}\x00{s}\x00{s}", 425 + // .{ config.user, config.nick, config.password }, 426 + // ); 427 + // 428 + // // Create a buffer big enough for the base64 encoded string 429 + // const b64_buf = try self.alloc.alloc(u8, Base64Encoder.calcSize(sasl.len)); 430 + // defer self.alloc.free(b64_buf); 431 + // const encoded = Base64Encoder.encode(b64_buf, sasl); 432 + // // Make our message 433 + // const auth = try std.fmt.bufPrint( 434 + // &buf, 435 + // "AUTHENTICATE {s}\r\n", 436 + // .{encoded}, 437 + // ); 438 + // try client.queueWrite(auth); 439 + // if (config.network_id) |id| { 440 + // const bind = try std.fmt.bufPrint( 441 + // &buf, 442 + // "BOUNCER BIND {s}\r\n", 443 + // .{id}, 444 + // ); 445 + // try client.queueWrite(bind); 446 + // } 447 + // try client.queueWrite("CAP END\r\n"); 448 + // } 449 + // }, 450 + // .RPL_WELCOME => { 451 + // const now = try zeit.instant(.{}); 452 + // var now_buf: [30]u8 = undefined; 453 + // const now_fmt = try now.time().bufPrint(&now_buf, .rfc3339); 454 + // 455 + // const past = try now.subtract(.{ .days = 7 }); 456 + // var past_buf: [30]u8 = undefined; 457 + // const past_fmt = try past.time().bufPrint(&past_buf, .rfc3339); 458 + // 459 + // var buf: [128]u8 = undefined; 460 + // const targets = try std.fmt.bufPrint( 461 + // &buf, 462 + // "CHATHISTORY TARGETS timestamp={s} timestamp={s} 50\r\n", 463 + // .{ now_fmt, past_fmt }, 464 + // ); 465 + // try client.queueWrite(targets); 466 + // // on_connect callback 467 + // try lua.onConnect(lua_state, client); 468 + // }, 469 + // .RPL_YOURHOST => {}, 470 + // .RPL_CREATED => {}, 471 + // .RPL_MYINFO => {}, 472 + // .RPL_ISUPPORT => { 473 + // // syntax: <client> <token>[ <token>] :are supported 474 + // var iter = msg.paramIterator(); 475 + // _ = iter.next() orelse continue; // client 476 + // while (iter.next()) |token| { 477 + // if (mem.eql(u8, token, "WHOX")) 478 + // client.supports.whox = true 479 + // else if (mem.startsWith(u8, token, "PREFIX")) { 480 + // const prefix = blk: { 481 + // const idx = mem.indexOfScalar(u8, token, ')') orelse 482 + // // default is "@+" 483 + // break :blk try self.alloc.dupe(u8, "@+"); 484 + // break :blk try self.alloc.dupe(u8, token[idx + 1 ..]); 485 + // }; 486 + // client.supports.prefix = prefix; 487 + // } 488 + // } 489 + // }, 490 + // .RPL_LOGGEDIN => {}, 491 + // .RPL_TOPIC => { 492 + // // syntax: <client> <channel> :<topic> 493 + // var iter = msg.paramIterator(); 494 + // _ = iter.next() orelse continue :loop; // client ("*") 495 + // const channel_name = iter.next() orelse continue :loop; // channel 496 + // const topic = iter.next() orelse continue :loop; // topic 497 + // 498 + // var channel = try client.getOrCreateChannel(channel_name); 499 + // if (channel.topic) |old_topic| { 500 + // self.alloc.free(old_topic); 501 + // } 502 + // channel.topic = try self.alloc.dupe(u8, topic); 503 + // }, 504 + // .RPL_SASLSUCCESS => {}, 505 + // .RPL_WHOREPLY => { 506 + // // syntax: <client> <channel> <username> <host> <server> <nick> <flags> :<hopcount> <real name> 507 + // var iter = msg.paramIterator(); 508 + // _ = iter.next() orelse continue :loop; // client 509 + // const channel_name = iter.next() orelse continue :loop; // channel 510 + // if (mem.eql(u8, channel_name, "*")) continue; 511 + // _ = iter.next() orelse continue :loop; // username 512 + // _ = iter.next() orelse continue :loop; // host 513 + // _ = iter.next() orelse continue :loop; // server 514 + // const nick = iter.next() orelse continue :loop; // nick 515 + // const flags = iter.next() orelse continue :loop; // flags 516 + // 517 + // const user_ptr = try client.getOrCreateUser(nick); 518 + // if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true; 519 + // var channel = try client.getOrCreateChannel(channel_name); 520 + // 521 + // const prefix = for (flags) |c| { 522 + // if (std.mem.indexOfScalar(u8, client.supports.prefix, c)) |_| { 523 + // break c; 524 + // } 525 + // } else ' '; 526 + // 527 + // try channel.addMember(user_ptr, .{ .prefix = prefix }); 528 + // }, 529 + // .RPL_WHOSPCRPL => { 530 + // // syntax: <client> <channel> <nick> <flags> :<realname> 531 + // var iter = msg.paramIterator(); 532 + // _ = iter.next() orelse continue; 533 + // const channel_name = iter.next() orelse continue; // channel 534 + // const nick = iter.next() orelse continue; 535 + // const flags = iter.next() orelse continue; 536 + // 537 + // const user_ptr = try client.getOrCreateUser(nick); 538 + // if (iter.next()) |real_name| { 539 + // if (user_ptr.real_name) |old_name| { 540 + // self.alloc.free(old_name); 541 + // } 542 + // user_ptr.real_name = try self.alloc.dupe(u8, real_name); 543 + // } 544 + // if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true; 545 + // var channel = try client.getOrCreateChannel(channel_name); 546 + // 547 + // const prefix = for (flags) |c| { 548 + // if (std.mem.indexOfScalar(u8, client.supports.prefix, c)) |_| { 549 + // break c; 550 + // } 551 + // } else ' '; 552 + // 553 + // try channel.addMember(user_ptr, .{ .prefix = prefix }); 554 + // }, 555 + // .RPL_ENDOFWHO => { 556 + // // syntax: <client> <mask> :End of WHO list 557 + // var iter = msg.paramIterator(); 558 + // _ = iter.next() orelse continue :loop; // client 559 + // const channel_name = iter.next() orelse continue :loop; // channel 560 + // if (mem.eql(u8, channel_name, "*")) continue; 561 + // var channel = try client.getOrCreateChannel(channel_name); 562 + // channel.in_flight.who = false; 563 + // }, 564 + // .RPL_NAMREPLY => { 565 + // // syntax: <client> <symbol> <channel> :[<prefix>]<nick>{ [<prefix>]<nick>} 566 + // var iter = msg.paramIterator(); 567 + // _ = iter.next() orelse continue; // client 568 + // _ = iter.next() orelse continue; // symbol 569 + // const channel_name = iter.next() orelse continue; // channel 570 + // const names = iter.next() orelse continue; 571 + // var channel = try client.getOrCreateChannel(channel_name); 572 + // var name_iter = std.mem.splitScalar(u8, names, ' '); 573 + // while (name_iter.next()) |name| { 574 + // const nick, const prefix = for (client.supports.prefix) |ch| { 575 + // if (name[0] == ch) { 576 + // break .{ name[1..], name[0] }; 577 + // } 578 + // } else .{ name, ' ' }; 579 + // 580 + // if (prefix != ' ') { 581 + // log.debug("HAS PREFIX {s}", .{name}); 582 + // } 583 + // 584 + // const user_ptr = try client.getOrCreateUser(nick); 585 + // 586 + // try channel.addMember(user_ptr, .{ .prefix = prefix, .sort = false }); 587 + // } 588 + // 589 + // channel.sortMembers(); 590 + // }, 591 + // .RPL_ENDOFNAMES => { 592 + // // syntax: <client> <channel> :End of /NAMES list 593 + // var iter = msg.paramIterator(); 594 + // _ = iter.next() orelse continue; // client 595 + // const channel_name = iter.next() orelse continue; // channel 596 + // var channel = try client.getOrCreateChannel(channel_name); 597 + // channel.in_flight.names = false; 598 + // }, 599 + // .BOUNCER => { 600 + // var iter = msg.paramIterator(); 601 + // while (iter.next()) |param| { 602 + // if (mem.eql(u8, param, "NETWORK")) { 603 + // const id = iter.next() orelse continue; 604 + // const attr = iter.next() orelse continue; 605 + // // check if we already have this network 606 + // for (self.clients.items, 0..) |cl, i| { 607 + // if (cl.config.network_id) |net_id| { 608 + // if (mem.eql(u8, net_id, id)) { 609 + // if (mem.eql(u8, attr, "*")) { 610 + // // * means the network was 611 + // // deleted 612 + // cl.deinit(); 613 + // _ = self.clients.swapRemove(i); 614 + // } 615 + // continue :loop; 616 + // } 617 + // } 618 + // } 619 + // 620 + // var cfg = client.config; 621 + // cfg.network_id = try self.alloc.dupe(u8, id); 622 + // 623 + // var attr_iter = std.mem.splitScalar(u8, attr, ';'); 624 + // while (attr_iter.next()) |kv| { 625 + // const n = std.mem.indexOfScalar(u8, kv, '=') orelse continue; 626 + // const key = kv[0..n]; 627 + // if (mem.eql(u8, key, "name")) 628 + // cfg.name = try self.alloc.dupe(u8, kv[n + 1 ..]) 629 + // else if (mem.eql(u8, key, "nickname")) 630 + // cfg.network_nick = try self.alloc.dupe(u8, kv[n + 1 ..]); 631 + // } 632 + // loop.postEvent(.{ .connect = cfg }); 633 + // } 634 + // } 635 + // }, 636 + // .AWAY => { 637 + // const src = msg.source() orelse continue :loop; 638 + // var iter = msg.paramIterator(); 639 + // const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len; 640 + // const user = try client.getOrCreateUser(src[0..n]); 641 + // // If there are any params, the user is away. Otherwise 642 + // // they are back. 643 + // user.away = if (iter.next()) |_| true else false; 644 + // }, 645 + // .BATCH => { 646 + // var iter = msg.paramIterator(); 647 + // const tag = iter.next() orelse continue; 648 + // switch (tag[0]) { 649 + // '+' => { 650 + // const batch_type = iter.next() orelse continue; 651 + // if (mem.eql(u8, batch_type, "chathistory")) { 652 + // const target = iter.next() orelse continue; 653 + // var channel = try client.getOrCreateChannel(target); 654 + // channel.at_oldest = true; 655 + // const duped_tag = try self.alloc.dupe(u8, tag[1..]); 656 + // try client.batches.put(duped_tag, channel); 657 + // } 658 + // }, 659 + // '-' => { 660 + // const key = client.batches.getKey(tag[1..]) orelse continue; 661 + // var chan = client.batches.get(key) orelse @panic("key should exist here"); 662 + // chan.history_requested = false; 663 + // _ = client.batches.remove(key); 664 + // self.alloc.free(key); 665 + // }, 666 + // else => {}, 667 + // } 668 + // }, 669 + // .CHATHISTORY => { 670 + // var iter = msg.paramIterator(); 671 + // const should_targets = iter.next() orelse continue; 672 + // if (!mem.eql(u8, should_targets, "TARGETS")) continue; 673 + // const target = iter.next() orelse continue; 674 + // // we only add direct messages, not more channels 675 + // assert(target.len > 0); 676 + // if (target[0] == '#') continue; 677 + // 678 + // var channel = try client.getOrCreateChannel(target); 679 + // const user_ptr = try client.getOrCreateUser(target); 680 + // const me_ptr = try client.getOrCreateUser(client.nickname()); 681 + // try channel.addMember(user_ptr, .{}); 682 + // try channel.addMember(me_ptr, .{}); 683 + // // we set who_requested so we don't try to request 684 + // // who on DMs 685 + // channel.who_requested = true; 686 + // var buf: [128]u8 = undefined; 687 + // const mark_read = try std.fmt.bufPrint( 688 + // &buf, 689 + // "MARKREAD {s}\r\n", 690 + // .{channel.name}, 691 + // ); 692 + // try client.queueWrite(mark_read); 693 + // try client.requestHistory(.after, channel); 694 + // }, 695 + // .JOIN => { 696 + // // get the user 697 + // const src = msg.source() orelse continue :loop; 698 + // const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len; 699 + // const user = try client.getOrCreateUser(src[0..n]); 700 + // 701 + // // get the channel 702 + // var iter = msg.paramIterator(); 703 + // const target = iter.next() orelse continue; 704 + // var channel = try client.getOrCreateChannel(target); 705 + // 706 + // // If it's our nick, we request chat history 707 + // if (mem.eql(u8, user.nick, client.nickname())) { 708 + // try client.requestHistory(.after, channel); 709 + // if (self.explicit_join) { 710 + // self.selectChannelName(client, target); 711 + // self.explicit_join = false; 712 + // } 713 + // } else try channel.addMember(user, .{}); 714 + // }, 715 + // .MARKREAD => { 716 + // var iter = msg.paramIterator(); 717 + // const target = iter.next() orelse continue; 718 + // const timestamp = iter.next() orelse continue; 719 + // const equal = std.mem.indexOfScalar(u8, timestamp, '=') orelse continue; 720 + // const last_read = zeit.instant(.{ 721 + // .source = .{ 722 + // .iso8601 = timestamp[equal + 1 ..], 723 + // }, 724 + // }) catch |err| { 725 + // log.err("couldn't convert timestamp: {}", .{err}); 726 + // continue; 727 + // }; 728 + // var channel = try client.getOrCreateChannel(target); 729 + // channel.last_read = last_read.unixTimestamp(); 730 + // const last_msg = channel.messages.getLastOrNull() orelse continue; 731 + // const time = last_msg.time() orelse continue; 732 + // if (time.unixTimestamp() > channel.last_read) 733 + // channel.has_unread = true 734 + // else 735 + // channel.has_unread = false; 736 + // }, 737 + // .PART => { 738 + // // get the user 739 + // const src = msg.source() orelse continue :loop; 740 + // const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len; 741 + // const user = try client.getOrCreateUser(src[0..n]); 742 + // 743 + // // get the channel 744 + // var iter = msg.paramIterator(); 745 + // const target = iter.next() orelse continue; 746 + // 747 + // if (mem.eql(u8, user.nick, client.nickname())) { 748 + // for (client.channels.items, 0..) |channel, i| { 749 + // if (!mem.eql(u8, channel.name, target)) continue; 750 + // var chan = client.channels.orderedRemove(i); 751 + // self.state.buffers.selected_idx -|= 1; 752 + // chan.deinit(self.alloc); 753 + // break; 754 + // } 755 + // } else { 756 + // const channel = try client.getOrCreateChannel(target); 757 + // channel.removeMember(user); 758 + // } 759 + // }, 760 + // .PRIVMSG, .NOTICE => { 761 + // // syntax: <target> :<message> 762 + // const msg2: irc.Message = .{ 763 + // .bytes = try self.alloc.dupe(u8, msg.bytes), 764 + // }; 765 + // var iter = msg2.paramIterator(); 766 + // const target = blk: { 767 + // const tgt = iter.next() orelse continue; 768 + // if (mem.eql(u8, tgt, client.nickname())) { 769 + // // If the target is us, it likely has our 770 + // // hostname in it. 771 + // const source = msg2.source() orelse continue; 772 + // const n = mem.indexOfScalar(u8, source, '!') orelse source.len; 773 + // break :blk source[0..n]; 774 + // } else break :blk tgt; 775 + // }; 776 + // 777 + // // We handle batches separately. When we encounter a 778 + // // PRIVMSG from a batch, we use the original target 779 + // // from the batch start. We also never notify from a 780 + // // batched message. Batched messages also require 781 + // // sorting 782 + // var tag_iter = msg2.tagIterator(); 783 + // while (tag_iter.next()) |tag| { 784 + // if (mem.eql(u8, tag.key, "batch")) { 785 + // const entry = client.batches.getEntry(tag.value) orelse @panic("TODO"); 786 + // var channel = entry.value_ptr.*; 787 + // try channel.messages.append(msg2); 788 + // std.sort.insertion(irc.Message, channel.messages.items, {}, irc.Message.compareTime); 789 + // channel.at_oldest = false; 790 + // const time = msg2.time() orelse continue; 791 + // if (time.unixTimestamp() > channel.last_read) { 792 + // channel.has_unread = true; 793 + // const content = iter.next() orelse continue; 794 + // if (std.mem.indexOf(u8, content, client.nickname())) |_| { 795 + // channel.has_unread_highlight = true; 796 + // } 797 + // } 798 + // break; 799 + // } 800 + // } else { 801 + // // standard handling 802 + // var channel = try client.getOrCreateChannel(target); 803 + // try channel.messages.append(msg2); 804 + // const content = iter.next() orelse continue; 805 + // var has_highlight = false; 806 + // { 807 + // const sender: []const u8 = blk: { 808 + // const src = msg2.source() orelse break :blk ""; 809 + // const l = std.mem.indexOfScalar(u8, src, '!') orelse 810 + // std.mem.indexOfScalar(u8, src, '@') orelse 811 + // src.len; 812 + // break :blk src[0..l]; 813 + // }; 814 + // try lua.onMessage(lua_state, client, channel.name, sender, content); 815 + // } 816 + // if (std.mem.indexOf(u8, content, client.nickname())) |_| { 817 + // var buf: [64]u8 = undefined; 818 + // const title_or_err = if (msg2.source()) |source| 819 + // std.fmt.bufPrint(&buf, "{s} - {s}", .{ channel.name, source }) 820 + // else 821 + // std.fmt.bufPrint(&buf, "{s}", .{channel.name}); 822 + // const title = title_or_err catch title: { 823 + // const len = @min(buf.len, channel.name.len); 824 + // @memcpy(buf[0..len], channel.name[0..len]); 825 + // break :title buf[0..len]; 826 + // }; 827 + // try self.vx.notify(writer, title, content); 828 + // has_highlight = true; 829 + // } 830 + // const time = msg2.time() orelse continue; 831 + // if (time.unixTimestamp() > channel.last_read) { 832 + // channel.has_unread_highlight = has_highlight; 833 + // channel.has_unread = true; 834 + // } 835 + // } 836 + // 837 + // // If we get a message from the current user mark the channel as 838 + // // read, since they must have just sent the message. 839 + // const sender: []const u8 = blk: { 840 + // const src = msg2.source() orelse break :blk ""; 841 + // const l = std.mem.indexOfScalar(u8, src, '!') orelse 842 + // std.mem.indexOfScalar(u8, src, '@') orelse 843 + // src.len; 844 + // break :blk src[0..l]; 845 + // }; 846 + // if (std.mem.eql(u8, sender, client.nickname())) { 847 + // self.markSelectedChannelRead(); 848 + // } 849 + // }, 850 + // } 851 + // }, 852 + // } 853 + // } 854 + // 855 + // if (redraw) { 856 + // try self.draw(&input); 857 + // last_frame = std.time.milliTimestamp(); 858 + // } 859 + // } 860 + // } 450 861 451 - const user_ptr = try client.getOrCreateUser(nick); 452 - if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true; 453 - var channel = try client.getOrCreateChannel(channel_name); 862 + pub fn connect(self: *App, cfg: irc.Client.Config) !void { 863 + const client = try self.alloc.create(irc.Client); 864 + client.* = try irc.Client.init(self.alloc, self, &self.write_queue, cfg); 865 + client.thread = try std.Thread.spawn(.{}, irc.Client.readLoop, .{client}); 866 + try self.clients.append(client); 867 + } 454 868 455 - const prefix = for (flags) |c| { 456 - if (std.mem.indexOfScalar(u8, client.supports.prefix, c)) |_| { 457 - break c; 458 - } 459 - } else ' '; 460 - 461 - try channel.addMember(user_ptr, .{ .prefix = prefix }); 462 - }, 463 - .RPL_WHOSPCRPL => { 464 - // syntax: <client> <channel> <nick> <flags> :<realname> 465 - var iter = msg.paramIterator(); 466 - _ = iter.next() orelse continue; 467 - const channel_name = iter.next() orelse continue; // channel 468 - const nick = iter.next() orelse continue; 469 - const flags = iter.next() orelse continue; 470 - 471 - const user_ptr = try client.getOrCreateUser(nick); 472 - if (iter.next()) |real_name| { 473 - if (user_ptr.real_name) |old_name| { 474 - self.alloc.free(old_name); 475 - } 476 - user_ptr.real_name = try self.alloc.dupe(u8, real_name); 477 - } 478 - if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true; 479 - var channel = try client.getOrCreateChannel(channel_name); 480 - 481 - const prefix = for (flags) |c| { 482 - if (std.mem.indexOfScalar(u8, client.supports.prefix, c)) |_| { 483 - break c; 484 - } 485 - } else ' '; 486 - 487 - try channel.addMember(user_ptr, .{ .prefix = prefix }); 488 - }, 489 - .RPL_ENDOFWHO => { 490 - // syntax: <client> <mask> :End of WHO list 491 - var iter = msg.paramIterator(); 492 - _ = iter.next() orelse continue :loop; // client 493 - const channel_name = iter.next() orelse continue :loop; // channel 494 - if (mem.eql(u8, channel_name, "*")) continue; 495 - var channel = try client.getOrCreateChannel(channel_name); 496 - channel.in_flight.who = false; 497 - }, 498 - .RPL_NAMREPLY => { 499 - // syntax: <client> <symbol> <channel> :[<prefix>]<nick>{ [<prefix>]<nick>} 500 - var iter = msg.paramIterator(); 501 - _ = iter.next() orelse continue; // client 502 - _ = iter.next() orelse continue; // symbol 503 - const channel_name = iter.next() orelse continue; // channel 504 - const names = iter.next() orelse continue; 505 - var channel = try client.getOrCreateChannel(channel_name); 506 - var name_iter = std.mem.splitScalar(u8, names, ' '); 507 - while (name_iter.next()) |name| { 508 - const nick, const prefix = for (client.supports.prefix) |ch| { 509 - if (name[0] == ch) { 510 - break .{ name[1..], name[0] }; 511 - } 512 - } else .{ name, ' ' }; 513 - 514 - if (prefix != ' ') { 515 - log.debug("HAS PREFIX {s}", .{name}); 516 - } 517 - 518 - const user_ptr = try client.getOrCreateUser(nick); 519 - 520 - try channel.addMember(user_ptr, .{ .prefix = prefix, .sort = false }); 521 - } 522 - 523 - channel.sortMembers(); 524 - }, 525 - .RPL_ENDOFNAMES => { 526 - // syntax: <client> <channel> :End of /NAMES list 527 - var iter = msg.paramIterator(); 528 - _ = iter.next() orelse continue; // client 529 - const channel_name = iter.next() orelse continue; // channel 530 - var channel = try client.getOrCreateChannel(channel_name); 531 - channel.in_flight.names = false; 532 - }, 533 - .BOUNCER => { 534 - var iter = msg.paramIterator(); 535 - while (iter.next()) |param| { 536 - if (mem.eql(u8, param, "NETWORK")) { 537 - const id = iter.next() orelse continue; 538 - const attr = iter.next() orelse continue; 539 - // check if we already have this network 540 - for (self.clients.items, 0..) |cl, i| { 541 - if (cl.config.network_id) |net_id| { 542 - if (mem.eql(u8, net_id, id)) { 543 - if (mem.eql(u8, attr, "*")) { 544 - // * means the network was 545 - // deleted 546 - cl.deinit(); 547 - _ = self.clients.swapRemove(i); 548 - } 549 - continue :loop; 550 - } 551 - } 552 - } 553 - 554 - var cfg = client.config; 555 - cfg.network_id = try self.alloc.dupe(u8, id); 556 - 557 - var attr_iter = std.mem.splitScalar(u8, attr, ';'); 558 - while (attr_iter.next()) |kv| { 559 - const n = std.mem.indexOfScalar(u8, kv, '=') orelse continue; 560 - const key = kv[0..n]; 561 - if (mem.eql(u8, key, "name")) 562 - cfg.name = try self.alloc.dupe(u8, kv[n + 1 ..]) 563 - else if (mem.eql(u8, key, "nickname")) 564 - cfg.network_nick = try self.alloc.dupe(u8, kv[n + 1 ..]); 565 - } 566 - loop.postEvent(.{ .connect = cfg }); 567 - } 568 - } 569 - }, 570 - .AWAY => { 571 - const src = msg.source() orelse continue :loop; 572 - var iter = msg.paramIterator(); 573 - const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len; 574 - const user = try client.getOrCreateUser(src[0..n]); 575 - // If there are any params, the user is away. Otherwise 576 - // they are back. 577 - user.away = if (iter.next()) |_| true else false; 578 - }, 579 - .BATCH => { 580 - var iter = msg.paramIterator(); 581 - const tag = iter.next() orelse continue; 582 - switch (tag[0]) { 583 - '+' => { 584 - const batch_type = iter.next() orelse continue; 585 - if (mem.eql(u8, batch_type, "chathistory")) { 586 - const target = iter.next() orelse continue; 587 - var channel = try client.getOrCreateChannel(target); 588 - channel.at_oldest = true; 589 - const duped_tag = try self.alloc.dupe(u8, tag[1..]); 590 - try client.batches.put(duped_tag, channel); 591 - } 592 - }, 593 - '-' => { 594 - const key = client.batches.getKey(tag[1..]) orelse continue; 595 - var chan = client.batches.get(key) orelse @panic("key should exist here"); 596 - chan.history_requested = false; 597 - _ = client.batches.remove(key); 598 - self.alloc.free(key); 599 - }, 600 - else => {}, 601 - } 602 - }, 603 - .CHATHISTORY => { 604 - var iter = msg.paramIterator(); 605 - const should_targets = iter.next() orelse continue; 606 - if (!mem.eql(u8, should_targets, "TARGETS")) continue; 607 - const target = iter.next() orelse continue; 608 - // we only add direct messages, not more channels 609 - assert(target.len > 0); 610 - if (target[0] == '#') continue; 611 - 612 - var channel = try client.getOrCreateChannel(target); 613 - const user_ptr = try client.getOrCreateUser(target); 614 - const me_ptr = try client.getOrCreateUser(client.nickname()); 615 - try channel.addMember(user_ptr, .{}); 616 - try channel.addMember(me_ptr, .{}); 617 - // we set who_requested so we don't try to request 618 - // who on DMs 619 - channel.who_requested = true; 620 - var buf: [128]u8 = undefined; 621 - const mark_read = try std.fmt.bufPrint( 622 - &buf, 623 - "MARKREAD {s}\r\n", 624 - .{channel.name}, 625 - ); 626 - try client.queueWrite(mark_read); 627 - try client.requestHistory(.after, channel); 628 - }, 629 - .JOIN => { 630 - // get the user 631 - const src = msg.source() orelse continue :loop; 632 - const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len; 633 - const user = try client.getOrCreateUser(src[0..n]); 634 - 635 - // get the channel 636 - var iter = msg.paramIterator(); 637 - const target = iter.next() orelse continue; 638 - var channel = try client.getOrCreateChannel(target); 639 - 640 - // If it's our nick, we request chat history 641 - if (mem.eql(u8, user.nick, client.nickname())) { 642 - try client.requestHistory(.after, channel); 643 - if (self.explicit_join) { 644 - self.selectChannelName(client, target); 645 - self.explicit_join = false; 646 - } 647 - } else try channel.addMember(user, .{}); 648 - }, 649 - .MARKREAD => { 650 - var iter = msg.paramIterator(); 651 - const target = iter.next() orelse continue; 652 - const timestamp = iter.next() orelse continue; 653 - const equal = std.mem.indexOfScalar(u8, timestamp, '=') orelse continue; 654 - const last_read = zeit.instant(.{ 655 - .source = .{ 656 - .iso8601 = timestamp[equal + 1 ..], 657 - }, 658 - }) catch |err| { 659 - log.err("couldn't convert timestamp: {}", .{err}); 660 - continue; 661 - }; 662 - var channel = try client.getOrCreateChannel(target); 663 - channel.last_read = last_read.unixTimestamp(); 664 - const last_msg = channel.messages.getLastOrNull() orelse continue; 665 - const time = last_msg.time() orelse continue; 666 - if (time.unixTimestamp() > channel.last_read) 667 - channel.has_unread = true 668 - else 669 - channel.has_unread = false; 670 - }, 671 - .PART => { 672 - // get the user 673 - const src = msg.source() orelse continue :loop; 674 - const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len; 675 - const user = try client.getOrCreateUser(src[0..n]); 676 - 677 - // get the channel 678 - var iter = msg.paramIterator(); 679 - const target = iter.next() orelse continue; 680 - 681 - if (mem.eql(u8, user.nick, client.nickname())) { 682 - for (client.channels.items, 0..) |channel, i| { 683 - if (!mem.eql(u8, channel.name, target)) continue; 684 - var chan = client.channels.orderedRemove(i); 685 - self.state.buffers.selected_idx -|= 1; 686 - chan.deinit(self.alloc); 687 - break; 688 - } 689 - } else { 690 - const channel = try client.getOrCreateChannel(target); 691 - channel.removeMember(user); 692 - } 693 - }, 694 - .PRIVMSG, .NOTICE => { 695 - // syntax: <target> :<message> 696 - const msg2: irc.Message = .{ 697 - .bytes = try self.alloc.dupe(u8, msg.bytes), 698 - }; 699 - var iter = msg2.paramIterator(); 700 - const target = blk: { 701 - const tgt = iter.next() orelse continue; 702 - if (mem.eql(u8, tgt, client.nickname())) { 703 - // If the target is us, it likely has our 704 - // hostname in it. 705 - const source = msg2.source() orelse continue; 706 - const n = mem.indexOfScalar(u8, source, '!') orelse source.len; 707 - break :blk source[0..n]; 708 - } else break :blk tgt; 709 - }; 710 - 711 - // We handle batches separately. When we encounter a 712 - // PRIVMSG from a batch, we use the original target 713 - // from the batch start. We also never notify from a 714 - // batched message. Batched messages also require 715 - // sorting 716 - var tag_iter = msg2.tagIterator(); 717 - while (tag_iter.next()) |tag| { 718 - if (mem.eql(u8, tag.key, "batch")) { 719 - const entry = client.batches.getEntry(tag.value) orelse @panic("TODO"); 720 - var channel = entry.value_ptr.*; 721 - try channel.messages.append(msg2); 722 - std.sort.insertion(irc.Message, channel.messages.items, {}, irc.Message.compareTime); 723 - channel.at_oldest = false; 724 - const time = msg2.time() orelse continue; 725 - if (time.unixTimestamp() > channel.last_read) { 726 - channel.has_unread = true; 727 - const content = iter.next() orelse continue; 728 - if (std.mem.indexOf(u8, content, client.nickname())) |_| { 729 - channel.has_unread_highlight = true; 730 - } 731 - } 732 - break; 733 - } 734 - } else { 735 - // standard handling 736 - var channel = try client.getOrCreateChannel(target); 737 - try channel.messages.append(msg2); 738 - const content = iter.next() orelse continue; 739 - var has_highlight = false; 740 - { 741 - const sender: []const u8 = blk: { 742 - const src = msg2.source() orelse break :blk ""; 743 - const l = std.mem.indexOfScalar(u8, src, '!') orelse 744 - std.mem.indexOfScalar(u8, src, '@') orelse 745 - src.len; 746 - break :blk src[0..l]; 747 - }; 748 - try lua.onMessage(lua_state, client, channel.name, sender, content); 749 - } 750 - if (std.mem.indexOf(u8, content, client.nickname())) |_| { 751 - var buf: [64]u8 = undefined; 752 - const title_or_err = if (msg2.source()) |source| 753 - std.fmt.bufPrint(&buf, "{s} - {s}", .{ channel.name, source }) 754 - else 755 - std.fmt.bufPrint(&buf, "{s}", .{channel.name}); 756 - const title = title_or_err catch title: { 757 - const len = @min(buf.len, channel.name.len); 758 - @memcpy(buf[0..len], channel.name[0..len]); 759 - break :title buf[0..len]; 760 - }; 761 - try self.vx.notify(writer, title, content); 762 - has_highlight = true; 763 - } 764 - const time = msg2.time() orelse continue; 765 - if (time.unixTimestamp() > channel.last_read) { 766 - channel.has_unread_highlight = has_highlight; 767 - channel.has_unread = true; 768 - } 769 - } 770 - 771 - // If we get a message from the current user mark the channel as 772 - // read, since they must have just sent the message. 773 - const sender: []const u8 = blk: { 774 - const src = msg2.source() orelse break :blk ""; 775 - const l = std.mem.indexOfScalar(u8, src, '!') orelse 776 - std.mem.indexOfScalar(u8, src, '@') orelse 777 - src.len; 778 - break :blk src[0..l]; 779 - }; 780 - if (std.mem.eql(u8, sender, client.nickname())) { 781 - self.markSelectedChannelRead(); 782 - } 783 - }, 784 - } 785 - }, 786 - } 787 - } 788 - 789 - if (redraw) { 790 - try self.draw(&input); 791 - last_frame = std.time.milliTimestamp(); 792 - } 793 - } 794 - } 795 869 pub fn nextChannel(self: *App) void { 796 870 // When leaving a channel we mark it as read, so we make sure that's done 797 871 // before we change to the new channel. ··· 968 1042 return client.queueWrite(msg); 969 1043 } 970 1044 }, 971 - .redraw => self.vx.queueRefresh(), 1045 + .redraw => {}, 1046 + // .redraw => self.vx.queueRefresh(), 972 1047 .version => { 973 1048 if (channel == null) return error.InvalidCommand; 974 1049 const msg = try std.fmt.bufPrint( ··· 1978 2053 } 1979 2054 } 1980 2055 1981 - fn markSelectedChannelRead(self: *App) void { 2056 + pub fn markSelectedChannelRead(self: *App) void { 1982 2057 const buffer = self.selectedBuffer() orelse return; 1983 2058 1984 2059 switch (buffer) {
+497 -5
src/irc.zig
··· 1 1 const std = @import("std"); 2 2 const comlink = @import("comlink.zig"); 3 + const lua = @import("lua.zig"); 3 4 const tls = @import("tls"); 4 5 const vaxis = @import("vaxis"); 5 6 const zeit = @import("zeit"); 6 7 const bytepool = @import("pool.zig"); 7 8 8 9 const testing = std.testing; 10 + const mem = std.mem; 9 11 10 12 const Allocator = std.mem.Allocator; 13 + const Base64Encoder = std.base64.standard.Encoder; 11 14 pub const MessagePool = bytepool.BytePool(max_raw_msg_size * 4); 12 15 pub const Slice = MessagePool.Slice; 13 16 ··· 25 28 client: *Client, 26 29 channel: *Channel, 27 30 }; 31 + 32 + pub const Event = comlink.IrcEvent; 28 33 29 34 pub const Command = enum { 30 35 RPL_WELCOME, // 001 ··· 495 500 496 501 thread: ?std.Thread = null, 497 502 498 - pub fn init(alloc: std.mem.Allocator, app: *comlink.App, wq: *comlink.WriteQueue, cfg: Config) !Client { 503 + redraw: std.atomic.Value(bool), 504 + fifo: std.fifo.LinearFifo(Event, .Dynamic), 505 + fifo_mutex: std.Thread.Mutex, 506 + 507 + pub fn init( 508 + alloc: std.mem.Allocator, 509 + app: *comlink.App, 510 + wq: *comlink.WriteQueue, 511 + cfg: Config, 512 + ) !Client { 499 513 return .{ 500 514 .alloc = alloc, 501 515 .app = app, ··· 506 520 .users = std.StringHashMap(*User).init(alloc), 507 521 .batches = std.StringHashMap(*Channel).init(alloc), 508 522 .write_queue = wq, 523 + .redraw = std.atomic.Value(bool).init(false), 524 + .fifo = std.fifo.LinearFifo(Event, .Dynamic).init(alloc), 525 + .fifo_mutex = .{}, 509 526 }; 510 527 } 511 528 ··· 544 561 self.alloc.free(key.*); 545 562 } 546 563 batches.deinit(); 564 + self.fifo.deinit(); 565 + } 566 + 567 + pub fn drainFifo(self: *Client) void { 568 + self.fifo_mutex.lock(); 569 + defer self.fifo_mutex.unlock(); 570 + while (self.fifo.readItem()) |item| { 571 + self.handleEvent(item) catch |err| { 572 + log.err("error: {}", .{err}); 573 + }; 574 + } 575 + } 576 + 577 + pub fn handleEvent(self: *Client, event: Event) !void { 578 + const msg: Message = .{ .bytes = event.msg.slice() }; 579 + const client = event.client; 580 + defer event.msg.deinit(); 581 + switch (msg.command()) { 582 + .unknown => {}, 583 + .CAP => { 584 + // syntax: <client> <ACK/NACK> :caps 585 + var iter = msg.paramIterator(); 586 + _ = iter.next() orelse return; // client 587 + const ack_or_nak = iter.next() orelse return; 588 + const caps = iter.next() orelse return; 589 + var cap_iter = mem.splitScalar(u8, caps, ' '); 590 + while (cap_iter.next()) |cap| { 591 + if (mem.eql(u8, ack_or_nak, "ACK")) { 592 + client.ack(cap); 593 + if (mem.eql(u8, cap, "sasl")) 594 + try client.queueWrite("AUTHENTICATE PLAIN\r\n"); 595 + } else if (mem.eql(u8, ack_or_nak, "NAK")) { 596 + log.debug("CAP not supported {s}", .{cap}); 597 + } 598 + } 599 + }, 600 + .AUTHENTICATE => { 601 + var iter = msg.paramIterator(); 602 + while (iter.next()) |param| { 603 + // A '+' is the continuuation to send our 604 + // AUTHENTICATE info 605 + if (!mem.eql(u8, param, "+")) continue; 606 + var buf: [4096]u8 = undefined; 607 + const config = client.config; 608 + const sasl = try std.fmt.bufPrint( 609 + &buf, 610 + "{s}\x00{s}\x00{s}", 611 + .{ config.user, config.nick, config.password }, 612 + ); 613 + 614 + // Create a buffer big enough for the base64 encoded string 615 + const b64_buf = try self.alloc.alloc(u8, Base64Encoder.calcSize(sasl.len)); 616 + defer self.alloc.free(b64_buf); 617 + const encoded = Base64Encoder.encode(b64_buf, sasl); 618 + // Make our message 619 + const auth = try std.fmt.bufPrint( 620 + &buf, 621 + "AUTHENTICATE {s}\r\n", 622 + .{encoded}, 623 + ); 624 + try client.queueWrite(auth); 625 + if (config.network_id) |id| { 626 + const bind = try std.fmt.bufPrint( 627 + &buf, 628 + "BOUNCER BIND {s}\r\n", 629 + .{id}, 630 + ); 631 + try client.queueWrite(bind); 632 + } 633 + try client.queueWrite("CAP END\r\n"); 634 + } 635 + }, 636 + .RPL_WELCOME => { 637 + const now = try zeit.instant(.{}); 638 + var now_buf: [30]u8 = undefined; 639 + const now_fmt = try now.time().bufPrint(&now_buf, .rfc3339); 640 + 641 + const past = try now.subtract(.{ .days = 7 }); 642 + var past_buf: [30]u8 = undefined; 643 + const past_fmt = try past.time().bufPrint(&past_buf, .rfc3339); 644 + 645 + var buf: [128]u8 = undefined; 646 + const targets = try std.fmt.bufPrint( 647 + &buf, 648 + "CHATHISTORY TARGETS timestamp={s} timestamp={s} 50\r\n", 649 + .{ now_fmt, past_fmt }, 650 + ); 651 + try client.queueWrite(targets); 652 + // on_connect callback 653 + try lua.onConnect(self.app.lua, client); 654 + }, 655 + .RPL_YOURHOST => {}, 656 + .RPL_CREATED => {}, 657 + .RPL_MYINFO => {}, 658 + .RPL_ISUPPORT => { 659 + // syntax: <client> <token>[ <token>] :are supported 660 + var iter = msg.paramIterator(); 661 + _ = iter.next() orelse return; // client 662 + while (iter.next()) |token| { 663 + if (mem.eql(u8, token, "WHOX")) 664 + client.supports.whox = true 665 + else if (mem.startsWith(u8, token, "PREFIX")) { 666 + const prefix = blk: { 667 + const idx = mem.indexOfScalar(u8, token, ')') orelse 668 + // default is "@+" 669 + break :blk try self.alloc.dupe(u8, "@+"); 670 + break :blk try self.alloc.dupe(u8, token[idx + 1 ..]); 671 + }; 672 + client.supports.prefix = prefix; 673 + } 674 + } 675 + }, 676 + .RPL_LOGGEDIN => {}, 677 + .RPL_TOPIC => { 678 + // syntax: <client> <channel> :<topic> 679 + var iter = msg.paramIterator(); 680 + _ = iter.next() orelse return; // client ("*") 681 + const channel_name = iter.next() orelse return; // channel 682 + const topic = iter.next() orelse return; // topic 683 + 684 + var channel = try client.getOrCreateChannel(channel_name); 685 + if (channel.topic) |old_topic| { 686 + self.alloc.free(old_topic); 687 + } 688 + channel.topic = try self.alloc.dupe(u8, topic); 689 + }, 690 + .RPL_SASLSUCCESS => {}, 691 + .RPL_WHOREPLY => { 692 + // syntax: <client> <channel> <username> <host> <server> <nick> <flags> :<hopcount> <real name> 693 + var iter = msg.paramIterator(); 694 + _ = iter.next() orelse return; // client 695 + const channel_name = iter.next() orelse return; // channel 696 + if (mem.eql(u8, channel_name, "*")) return; 697 + _ = iter.next() orelse return; // username 698 + _ = iter.next() orelse return; // host 699 + _ = iter.next() orelse return; // server 700 + const nick = iter.next() orelse return; // nick 701 + const flags = iter.next() orelse return; // flags 702 + 703 + const user_ptr = try client.getOrCreateUser(nick); 704 + if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true; 705 + var channel = try client.getOrCreateChannel(channel_name); 706 + 707 + const prefix = for (flags) |c| { 708 + if (std.mem.indexOfScalar(u8, client.supports.prefix, c)) |_| { 709 + break c; 710 + } 711 + } else ' '; 712 + 713 + try channel.addMember(user_ptr, .{ .prefix = prefix }); 714 + }, 715 + .RPL_WHOSPCRPL => { 716 + // syntax: <client> <channel> <nick> <flags> :<realname> 717 + var iter = msg.paramIterator(); 718 + _ = iter.next() orelse return; 719 + const channel_name = iter.next() orelse return; // channel 720 + const nick = iter.next() orelse return; 721 + const flags = iter.next() orelse return; 722 + 723 + const user_ptr = try client.getOrCreateUser(nick); 724 + if (iter.next()) |real_name| { 725 + if (user_ptr.real_name) |old_name| { 726 + self.alloc.free(old_name); 727 + } 728 + user_ptr.real_name = try self.alloc.dupe(u8, real_name); 729 + } 730 + if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true; 731 + var channel = try client.getOrCreateChannel(channel_name); 732 + 733 + const prefix = for (flags) |c| { 734 + if (std.mem.indexOfScalar(u8, client.supports.prefix, c)) |_| { 735 + break c; 736 + } 737 + } else ' '; 738 + 739 + try channel.addMember(user_ptr, .{ .prefix = prefix }); 740 + }, 741 + .RPL_ENDOFWHO => { 742 + // syntax: <client> <mask> :End of WHO list 743 + var iter = msg.paramIterator(); 744 + _ = iter.next() orelse return; // client 745 + const channel_name = iter.next() orelse return; // channel 746 + if (mem.eql(u8, channel_name, "*")) return; 747 + var channel = try client.getOrCreateChannel(channel_name); 748 + channel.in_flight.who = false; 749 + }, 750 + .RPL_NAMREPLY => { 751 + // syntax: <client> <symbol> <channel> :[<prefix>]<nick>{ [<prefix>]<nick>} 752 + var iter = msg.paramIterator(); 753 + _ = iter.next() orelse return; // client 754 + _ = iter.next() orelse return; // symbol 755 + const channel_name = iter.next() orelse return; // channel 756 + const names = iter.next() orelse return; 757 + var channel = try client.getOrCreateChannel(channel_name); 758 + var name_iter = std.mem.splitScalar(u8, names, ' '); 759 + while (name_iter.next()) |name| { 760 + const nick, const prefix = for (client.supports.prefix) |ch| { 761 + if (name[0] == ch) { 762 + break .{ name[1..], name[0] }; 763 + } 764 + } else .{ name, ' ' }; 765 + 766 + if (prefix != ' ') { 767 + log.debug("HAS PREFIX {s}", .{name}); 768 + } 769 + 770 + const user_ptr = try client.getOrCreateUser(nick); 771 + 772 + try channel.addMember(user_ptr, .{ .prefix = prefix, .sort = false }); 773 + } 774 + 775 + channel.sortMembers(); 776 + }, 777 + .RPL_ENDOFNAMES => { 778 + // syntax: <client> <channel> :End of /NAMES list 779 + var iter = msg.paramIterator(); 780 + _ = iter.next() orelse return; // client 781 + const channel_name = iter.next() orelse return; // channel 782 + var channel = try client.getOrCreateChannel(channel_name); 783 + channel.in_flight.names = false; 784 + }, 785 + .BOUNCER => { 786 + var iter = msg.paramIterator(); 787 + while (iter.next()) |param| { 788 + if (mem.eql(u8, param, "NETWORK")) { 789 + const id = iter.next() orelse continue; 790 + const attr = iter.next() orelse continue; 791 + // check if we already have this network 792 + for (self.app.clients.items, 0..) |cl, i| { 793 + if (cl.config.network_id) |net_id| { 794 + if (mem.eql(u8, net_id, id)) { 795 + if (mem.eql(u8, attr, "*")) { 796 + // * means the network was 797 + // deleted 798 + cl.deinit(); 799 + _ = self.app.clients.swapRemove(i); 800 + } 801 + return; 802 + } 803 + } 804 + } 805 + 806 + var cfg = client.config; 807 + cfg.network_id = try self.alloc.dupe(u8, id); 808 + 809 + var attr_iter = std.mem.splitScalar(u8, attr, ';'); 810 + while (attr_iter.next()) |kv| { 811 + const n = std.mem.indexOfScalar(u8, kv, '=') orelse continue; 812 + const key = kv[0..n]; 813 + if (mem.eql(u8, key, "name")) 814 + cfg.name = try self.alloc.dupe(u8, kv[n + 1 ..]) 815 + else if (mem.eql(u8, key, "nickname")) 816 + cfg.network_nick = try self.alloc.dupe(u8, kv[n + 1 ..]); 817 + } 818 + try self.app.connect(cfg); 819 + } 820 + } 821 + }, 822 + .AWAY => { 823 + const src = msg.source() orelse return; 824 + var iter = msg.paramIterator(); 825 + const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len; 826 + const user = try client.getOrCreateUser(src[0..n]); 827 + // If there are any params, the user is away. Otherwise 828 + // they are back. 829 + user.away = if (iter.next()) |_| true else false; 830 + }, 831 + .BATCH => { 832 + var iter = msg.paramIterator(); 833 + const tag = iter.next() orelse return; 834 + switch (tag[0]) { 835 + '+' => { 836 + const batch_type = iter.next() orelse return; 837 + if (mem.eql(u8, batch_type, "chathistory")) { 838 + const target = iter.next() orelse return; 839 + var channel = try client.getOrCreateChannel(target); 840 + channel.at_oldest = true; 841 + const duped_tag = try self.alloc.dupe(u8, tag[1..]); 842 + try client.batches.put(duped_tag, channel); 843 + } 844 + }, 845 + '-' => { 846 + const key = client.batches.getKey(tag[1..]) orelse return; 847 + var chan = client.batches.get(key) orelse @panic("key should exist here"); 848 + chan.history_requested = false; 849 + _ = client.batches.remove(key); 850 + self.alloc.free(key); 851 + }, 852 + else => {}, 853 + } 854 + }, 855 + .CHATHISTORY => { 856 + var iter = msg.paramIterator(); 857 + const should_targets = iter.next() orelse return; 858 + if (!mem.eql(u8, should_targets, "TARGETS")) return; 859 + const target = iter.next() orelse return; 860 + // we only add direct messages, not more channels 861 + assert(target.len > 0); 862 + if (target[0] == '#') return; 863 + 864 + var channel = try client.getOrCreateChannel(target); 865 + const user_ptr = try client.getOrCreateUser(target); 866 + const me_ptr = try client.getOrCreateUser(client.nickname()); 867 + try channel.addMember(user_ptr, .{}); 868 + try channel.addMember(me_ptr, .{}); 869 + // we set who_requested so we don't try to request 870 + // who on DMs 871 + channel.who_requested = true; 872 + var buf: [128]u8 = undefined; 873 + const mark_read = try std.fmt.bufPrint( 874 + &buf, 875 + "MARKREAD {s}\r\n", 876 + .{channel.name}, 877 + ); 878 + try client.queueWrite(mark_read); 879 + try client.requestHistory(.after, channel); 880 + }, 881 + .JOIN => { 882 + // get the user 883 + const src = msg.source() orelse return; 884 + const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len; 885 + const user = try client.getOrCreateUser(src[0..n]); 886 + 887 + // get the channel 888 + var iter = msg.paramIterator(); 889 + const target = iter.next() orelse return; 890 + var channel = try client.getOrCreateChannel(target); 891 + 892 + // If it's our nick, we request chat history 893 + if (mem.eql(u8, user.nick, client.nickname())) { 894 + try client.requestHistory(.after, channel); 895 + if (self.app.explicit_join) { 896 + self.app.selectChannelName(client, target); 897 + self.app.explicit_join = false; 898 + } 899 + } else try channel.addMember(user, .{}); 900 + }, 901 + .MARKREAD => { 902 + var iter = msg.paramIterator(); 903 + const target = iter.next() orelse return; 904 + const timestamp = iter.next() orelse return; 905 + const equal = std.mem.indexOfScalar(u8, timestamp, '=') orelse return; 906 + const last_read = zeit.instant(.{ 907 + .source = .{ 908 + .iso8601 = timestamp[equal + 1 ..], 909 + }, 910 + }) catch |err| { 911 + log.err("couldn't convert timestamp: {}", .{err}); 912 + return; 913 + }; 914 + var channel = try client.getOrCreateChannel(target); 915 + channel.last_read = last_read.unixTimestamp(); 916 + const last_msg = channel.messages.getLastOrNull() orelse return; 917 + const time = last_msg.time() orelse return; 918 + if (time.unixTimestamp() > channel.last_read) 919 + channel.has_unread = true 920 + else 921 + channel.has_unread = false; 922 + }, 923 + .PART => { 924 + // get the user 925 + const src = msg.source() orelse return; 926 + const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len; 927 + const user = try client.getOrCreateUser(src[0..n]); 928 + 929 + // get the channel 930 + var iter = msg.paramIterator(); 931 + const target = iter.next() orelse return; 932 + 933 + if (mem.eql(u8, user.nick, client.nickname())) { 934 + for (client.channels.items, 0..) |channel, i| { 935 + if (!mem.eql(u8, channel.name, target)) continue; 936 + var chan = client.channels.orderedRemove(i); 937 + self.app.state.buffers.selected_idx -|= 1; 938 + chan.deinit(self.app.alloc); 939 + break; 940 + } 941 + } else { 942 + const channel = try client.getOrCreateChannel(target); 943 + channel.removeMember(user); 944 + } 945 + }, 946 + .PRIVMSG, .NOTICE => { 947 + // syntax: <target> :<message> 948 + const msg2: Message = .{ 949 + .bytes = try self.app.alloc.dupe(u8, msg.bytes), 950 + }; 951 + var iter = msg2.paramIterator(); 952 + const target = blk: { 953 + const tgt = iter.next() orelse return; 954 + if (mem.eql(u8, tgt, client.nickname())) { 955 + // If the target is us, it likely has our 956 + // hostname in it. 957 + const source = msg2.source() orelse return; 958 + const n = mem.indexOfScalar(u8, source, '!') orelse source.len; 959 + break :blk source[0..n]; 960 + } else break :blk tgt; 961 + }; 962 + 963 + // We handle batches separately. When we encounter a 964 + // PRIVMSG from a batch, we use the original target 965 + // from the batch start. We also never notify from a 966 + // batched message. Batched messages also require 967 + // sorting 968 + var tag_iter = msg2.tagIterator(); 969 + while (tag_iter.next()) |tag| { 970 + if (mem.eql(u8, tag.key, "batch")) { 971 + const entry = client.batches.getEntry(tag.value) orelse @panic("TODO"); 972 + var channel = entry.value_ptr.*; 973 + try channel.messages.append(msg2); 974 + std.sort.insertion(Message, channel.messages.items, {}, Message.compareTime); 975 + channel.at_oldest = false; 976 + const time = msg2.time() orelse continue; 977 + if (time.unixTimestamp() > channel.last_read) { 978 + channel.has_unread = true; 979 + const content = iter.next() orelse continue; 980 + if (std.mem.indexOf(u8, content, client.nickname())) |_| { 981 + channel.has_unread_highlight = true; 982 + } 983 + } 984 + break; 985 + } 986 + } else { 987 + // standard handling 988 + var channel = try client.getOrCreateChannel(target); 989 + try channel.messages.append(msg2); 990 + const content = iter.next() orelse return; 991 + var has_highlight = false; 992 + { 993 + const sender: []const u8 = blk: { 994 + const src = msg2.source() orelse break :blk ""; 995 + const l = std.mem.indexOfScalar(u8, src, '!') orelse 996 + std.mem.indexOfScalar(u8, src, '@') orelse 997 + src.len; 998 + break :blk src[0..l]; 999 + }; 1000 + try lua.onMessage(self.app.lua, client, channel.name, sender, content); 1001 + } 1002 + if (std.mem.indexOf(u8, content, client.nickname())) |_| { 1003 + var buf: [64]u8 = undefined; 1004 + const title_or_err = if (msg2.source()) |source| 1005 + std.fmt.bufPrint(&buf, "{s} - {s}", .{ channel.name, source }) 1006 + else 1007 + std.fmt.bufPrint(&buf, "{s}", .{channel.name}); 1008 + const title = title_or_err catch title: { 1009 + const len = @min(buf.len, channel.name.len); 1010 + @memcpy(buf[0..len], channel.name[0..len]); 1011 + break :title buf[0..len]; 1012 + }; 1013 + _ = title; 1014 + // TODO: fix this 1015 + // try self.vx.notify(writer, title, content); 1016 + has_highlight = true; 1017 + } 1018 + const time = msg2.time() orelse return; 1019 + if (time.unixTimestamp() > channel.last_read) { 1020 + channel.has_unread_highlight = has_highlight; 1021 + channel.has_unread = true; 1022 + } 1023 + } 1024 + 1025 + // If we get a message from the current user mark the channel as 1026 + // read, since they must have just sent the message. 1027 + const sender: []const u8 = blk: { 1028 + const src = msg2.source() orelse break :blk ""; 1029 + const l = std.mem.indexOfScalar(u8, src, '!') orelse 1030 + std.mem.indexOfScalar(u8, src, '@') orelse 1031 + src.len; 1032 + break :blk src[0..l]; 1033 + }; 1034 + if (std.mem.eql(u8, sender, client.nickname())) { 1035 + self.app.markSelectedChannelRead(); 1036 + } 1037 + }, 1038 + } 547 1039 } 548 1040 549 1041 pub fn nickname(self: *Client) []const u8 { ··· 569 1061 } 570 1062 } 571 1063 572 - pub fn readLoop(self: *Client, loop: *comlink.EventLoop) !void { 1064 + pub fn readLoop(self: *Client) !void { 573 1065 var delay: u64 = 1 * std.time.ns_per_s; 574 1066 575 1067 while (!self.should_close) { ··· 624 1116 if (now - last_msg > keep_alive + max_rt) { 625 1117 // reconnect?? 626 1118 self.status = .disconnected; 627 - loop.postEvent(.redraw); 1119 + self.redraw.store(true, .unordered); 628 1120 break; 629 1121 } 630 1122 if (now - last_msg > keep_alive) { ··· 637 1129 if (self.should_close) return; 638 1130 if (n == 0) { 639 1131 self.status = .disconnected; 640 - loop.postEvent(.redraw); 1132 + self.redraw.store(true, .unordered); 641 1133 break; 642 1134 } 643 1135 last_msg = std.time.milliTimestamp(); ··· 649 1141 @memcpy(buffer.slice(), buf[i..idx]); 650 1142 assert(std.mem.eql(u8, buf[idx .. idx + 2], "\r\n")); 651 1143 log.debug("[<-{s}] {s}", .{ self.config.name orelse self.config.server, buffer.slice() }); 652 - loop.postEvent(.{ .irc = .{ .client = self, .msg = buffer } }); 1144 + try self.fifo.writeItem(.{ .client = self, .msg = buffer }); 653 1145 } 654 1146 if (i != n) { 655 1147 // we had a part of a line read. Copy it to the beginning of the
+19 -20
src/lua.zig
··· 5 5 6 6 const irc = comlink.irc; 7 7 const App = comlink.App; 8 - const EventLoop = comlink.EventLoop; 9 8 const Lua = ziglua.Lua; 10 9 11 10 const assert = std.debug.assert; ··· 16 15 /// global key for the app userdata pointer in the registry 17 16 const app_key = "comlink.app"; 18 17 19 - /// global key for the loop userdata pointer 20 - const loop_key = "comlink.loop"; 21 - 22 18 /// active client key. This gets replaced with the client context during callbacks 23 19 const client_key = "comlink.client"; 24 20 25 - pub fn init(app: *App, lua: *Lua, loop: *comlink.EventLoop) !void { 21 + pub fn init(app: *App) !void { 22 + const lua = app.lua; 26 23 // load standard libraries 27 24 lua.openLibs(); 28 25 ··· 65 62 // keep a reference to our app in the lua state 66 63 lua.pushLightUserdata(app); // [userdata] 67 64 lua.setField(registry_index, app_key); // [] 68 - // keep a reference to our loop in the lua state 69 - lua.pushLightUserdata(loop); // [userdata] 70 - lua.setField(registry_index, loop_key); // [] 71 65 72 66 // load config 73 67 var buf: [std.posix.PATH_MAX]u8 = undefined; ··· 98 92 return app; 99 93 } 100 94 101 - /// retrieves the *Loop lightuserdata from the registry index 102 - fn getLoop(lua: *Lua) *EventLoop { 103 - const lua_type = lua.getField(registry_index, loop_key); // [userdata] 104 - assert(lua_type == .light_userdata); // set by comlink as a lightuserdata 105 - const loop = lua.toUserdata(comlink.EventLoop, -1) catch unreachable; // already asserted 106 - // as lightuserdata 107 - return loop; 108 - } 95 + // /// retrieves the *Loop lightuserdata from the registry index 96 + // fn getLoop(lua: *Lua) *EventLoop { 97 + // const lua_type = lua.getField(registry_index, loop_key); // [userdata] 98 + // assert(lua_type == .light_userdata); // set by comlink as a lightuserdata 99 + // const loop = lua.toUserdata(comlink.EventLoop, -1) catch unreachable; // already asserted 100 + // // as lightuserdata 101 + // return loop; 102 + // } 109 103 110 104 fn getClient(lua: *Lua) *irc.Client { 111 105 const lua_type = lua.getField(registry_index, client_key); // [userdata] ··· 354 348 .port = port, 355 349 }; 356 350 357 - const loop = getLoop(lua); // [] 358 - loop.postEvent(.{ .connect = cfg }); 351 + const app = getApp(lua); 352 + app.connect(cfg) catch { 353 + lua.raiseErrorStr("couldn't connect", .{}); 354 + }; 359 355 360 356 // put the table back on the stack 361 357 Client.getTable(lua, table_ref); // [table] ··· 374 370 lua.argCheck(lua.isString(1), 1, "expected a string"); // [string, string] 375 371 lua.argCheck(lua.isString(2), 2, "expected a string"); // [string, string] 376 372 const app = getApp(lua); 373 + _ = app; // autofix 377 374 const title = lua.toString(1) catch { // [string, string] 378 375 lua.raiseErrorStr("couldn't write notification", .{}); 379 376 }; 377 + _ = title; // autofix 380 378 const body = lua.toString(2) catch { // [string, string] 381 379 lua.raiseErrorStr("couldn't write notification", .{}); 382 380 }; 381 + _ = body; // autofix 383 382 lua.pop(2); // [] 384 - app.vx.notify(app.tty.anyWriter(), title, body) catch 385 - lua.raiseErrorStr("couldn't write notification", .{}); 383 + // app.vx.notify(app.tty.anyWriter(), title, body) catch 384 + // lua.raiseErrorStr("couldn't write notification", .{}); 386 385 return 0; 387 386 } 388 387
+28 -19
src/main.zig
··· 22 22 23 23 /// Called after receiving a terminating signal 24 24 fn cleanUp(sig: c_int) callconv(.C) void { 25 - if (vaxis.Tty.global_tty) |gty| { 25 + if (vaxis.tty.global_tty) |gty| { 26 26 const reset: []const u8 = vaxis.ctlseqs.csi_u_pop ++ 27 27 vaxis.ctlseqs.mouse_reset ++ 28 28 vaxis.ctlseqs.bp_reset ++ ··· 79 79 comlink.Command.user_commands = std.StringHashMap(i32).init(alloc); 80 80 defer comlink.Command.user_commands.deinit(); 81 81 82 - const lua = try Lua.init(&alloc); 83 - defer lua.deinit(); 82 + var app = try vaxis.vxfw.App.init(gpa.allocator()); 83 + defer app.deinit(); 84 + 85 + // const lua = try Lua.init(&alloc); 86 + // defer lua.deinit(); 87 + 88 + // var app = try comlink.App.init(alloc); 89 + // defer app.deinit(); 90 + 91 + var comlink_app: comlink.App = undefined; 92 + try comlink_app.init(gpa.allocator()); 93 + defer comlink_app.deinit(); 84 94 85 - var app = try comlink.App.init(alloc); 86 - defer app.deinit(); 95 + try app.run(comlink_app.widget(), .{}); 87 96 88 - app.run(lua) catch |err| { 89 - switch (err) { 90 - // ziglua errors 91 - error.LuaError => { 92 - const msg = lua.toString(-1) catch ""; 93 - const duped = alloc.dupe(u8, msg) catch ""; 94 - app.deinit(); 95 - defer alloc.free(duped); 96 - log.err("{s}", .{duped}); 97 - return err; 98 - }, 99 - else => return err, 100 - } 101 - }; 97 + // app.run(lua) catch |err| { 98 + // switch (err) { 99 + // // ziglua errors 100 + // error.LuaError => { 101 + // const msg = lua.toString(-1) catch ""; 102 + // const duped = alloc.dupe(u8, msg) catch ""; 103 + // app.deinit(); 104 + // defer alloc.free(duped); 105 + // log.err("{s}", .{duped}); 106 + // return err; 107 + // }, 108 + // else => return err, 109 + // } 110 + // }; 102 111 } 103 112 104 113 fn argMatch(maybe_short: ?[]const u8, maybe_long: ?[]const u8, arg: [:0]const u8) bool {