an experimental irc client
at main 16 kB view raw
1const std = @import("std"); 2const vaxis = @import("vaxis"); 3const irc = @import("irc.zig"); 4 5const mem = std.mem; 6 7const ColorState = enum { 8 ground, 9 fg, 10 bg, 11}; 12 13const LinkState = enum { 14 h, 15 t1, 16 t2, 17 p, 18 s, 19 colon, 20 slash, 21 consume, 22}; 23 24/// generate vaxis.Segments for the message content 25pub fn message(segments: *std.ArrayList(vaxis.Segment), user: *const irc.User, msg: irc.Message) !void { 26 var iter = msg.paramIterator(); 27 // skip the first param, this is the receiver of the message 28 _ = iter.next() orelse return error.InvalidMessage; 29 const content = iter.next() orelse return error.InvalidMessage; 30 31 var start: usize = 0; 32 var i: usize = 0; 33 var style: vaxis.Style = .{}; 34 while (i < content.len) : (i += 1) { 35 const b = content[i]; 36 switch (b) { 37 0x01 => { 38 if (i == 0 and 39 content.len > 7 and 40 mem.startsWith(u8, content[1..], "ACTION")) 41 { 42 style.italic = true; 43 const user_style: vaxis.Style = .{ 44 .fg = user.color, 45 .italic = true, 46 }; 47 try segments.append(.{ 48 .text = user.nick, 49 .style = user_style, 50 }); 51 i += 6; // "ACTION" 52 } else { 53 try segments.append(.{ 54 .text = content[start..i], 55 .style = style, 56 }); 57 } 58 start = i + 1; 59 }, 60 0x02 => { 61 if (i > start) { 62 try segments.append(.{ 63 .text = content[start..i], 64 .style = style, 65 }); 66 } 67 style.bold = !style.bold; 68 start = i + 1; 69 }, 70 0x03 => { 71 if (i > start) { 72 try segments.append(.{ 73 .text = content[start..i], 74 .style = style, 75 }); 76 } 77 i += 1; 78 var state: ColorState = .ground; 79 var fg_idx: ?u8 = null; 80 var bg_idx: ?u8 = null; 81 while (i < content.len) : (i += 1) { 82 const d = content[i]; 83 switch (state) { 84 .ground => { 85 switch (d) { 86 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 87 state = .fg; 88 fg_idx = d - '0'; 89 }, 90 else => { 91 style.fg = .default; 92 style.bg = .default; 93 start = i; 94 break; 95 }, 96 } 97 }, 98 .fg => { 99 switch (d) { 100 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 101 const fg = fg_idx orelse 0; 102 if (fg > 9) { 103 style.fg = irc.toVaxisColor(fg); 104 start = i; 105 break; 106 } else { 107 fg_idx = fg * 10 + (d - '0'); 108 } 109 }, 110 else => { 111 if (fg_idx) |fg| { 112 style.fg = irc.toVaxisColor(fg); 113 start = i; 114 } 115 if (d == ',') state = .bg else break; 116 }, 117 } 118 }, 119 .bg => { 120 switch (d) { 121 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 122 const bg = bg_idx orelse 0; 123 if (i - start == 2) { 124 style.bg = irc.toVaxisColor(bg); 125 start = i; 126 break; 127 } else { 128 bg_idx = bg * 10 + (d - '0'); 129 } 130 }, 131 else => { 132 if (bg_idx) |bg| { 133 style.bg = irc.toVaxisColor(bg); 134 start = i; 135 } 136 break; 137 }, 138 } 139 }, 140 } 141 } 142 }, 143 0x0F => { 144 if (i > start) { 145 try segments.append(.{ 146 .text = content[start..i], 147 .style = style, 148 }); 149 } 150 style = .{}; 151 start = i + 1; 152 }, 153 0x16 => { 154 if (i > start) { 155 try segments.append(.{ 156 .text = content[start..i], 157 .style = style, 158 }); 159 } 160 style.reverse = !style.reverse; 161 start = i + 1; 162 }, 163 0x1D => { 164 if (i > start) { 165 try segments.append(.{ 166 .text = content[start..i], 167 .style = style, 168 }); 169 } 170 style.italic = !style.italic; 171 start = i + 1; 172 }, 173 0x1E => { 174 if (i > start) { 175 try segments.append(.{ 176 .text = content[start..i], 177 .style = style, 178 }); 179 } 180 style.strikethrough = !style.strikethrough; 181 start = i + 1; 182 }, 183 0x1F => { 184 if (i > start) { 185 try segments.append(.{ 186 .text = content[start..i], 187 .style = style, 188 }); 189 } 190 191 style.ul_style = if (style.ul_style == .off) .single else .off; 192 start = i + 1; 193 }, 194 else => { 195 if (b == 'h') { 196 var state: LinkState = .h; 197 const h_start = i; 198 // consume until a space or EOF 199 i += 1; 200 while (i < content.len) : (i += 1) { 201 const b1 = content[i]; 202 switch (state) { 203 .h => { 204 if (b1 == 't') state = .t1 else break; 205 }, 206 .t1 => { 207 if (b1 == 't') state = .t2 else break; 208 }, 209 .t2 => { 210 if (b1 == 'p') state = .p else break; 211 }, 212 .p => { 213 if (b1 == 's') 214 state = .s 215 else if (b1 == ':') 216 state = .colon 217 else 218 break; 219 }, 220 .s => { 221 if (b1 == ':') state = .colon else break; 222 }, 223 .colon => { 224 if (b1 == '/') state = .slash else break; 225 }, 226 .slash => { 227 if (b1 == '/') { 228 state = .consume; 229 if (h_start > start) { 230 try segments.append(.{ 231 .text = content[start..h_start], 232 .style = style, 233 }); 234 } 235 start = h_start; 236 } else break; 237 }, 238 .consume => { 239 switch (b1) { 240 0x00...0x20, 0x7F => { 241 try segments.append(.{ 242 .text = content[h_start..i], 243 .style = .{ 244 .fg = .{ .index = 4 }, 245 }, 246 .link = .{ 247 .uri = content[h_start..i], 248 }, 249 }); 250 start = i; 251 // backup one 252 i -= 1; 253 break; 254 }, 255 else => { 256 if (i == content.len - 1) { 257 try segments.append(.{ 258 .text = content[h_start..], 259 .style = .{ 260 .fg = .{ .index = 4 }, 261 }, 262 .link = .{ 263 .uri = content[h_start..], 264 }, 265 }); 266 return; 267 } 268 }, 269 } 270 }, 271 } 272 } 273 } 274 }, 275 } 276 } 277 if (start < i and start < content.len) { 278 try segments.append(.{ 279 .text = content[start..], 280 .style = style, 281 }); 282 } 283} 284 285test "format.zig: no format" { 286 const user: irc.User = .{ .nick = "rockorager" }; 287 const msg: irc.Message = .{ .bytes = "PRIVMSG #comlink :foo" }; 288 289 var list = std.ArrayList(vaxis.Segment).init(std.testing.allocator); 290 defer list.deinit(); 291 try message(&list, &user, msg); 292 try std.testing.expectEqual(1, list.items.len); 293 const expected: vaxis.Segment = .{ .text = "foo" }; 294 try std.testing.expectEqualDeep(expected, list.items[0]); 295} 296 297test "format.zig: bold" { 298 const user: irc.User = .{ .nick = "rockorager" }; 299 const msg: irc.Message = .{ .bytes = "PRIVMSG #comlink :\x02foo\x02" }; 300 301 var list = std.ArrayList(vaxis.Segment).init(std.testing.allocator); 302 defer list.deinit(); 303 try message(&list, &user, msg); 304 try std.testing.expectEqual(1, list.items.len); 305 const expected: vaxis.Segment = .{ .text = "foo", .style = .{ .bold = true } }; 306 try std.testing.expectEqualDeep(expected, list.items[0]); 307} 308 309test "format.zig: italic" { 310 const user: irc.User = .{ .nick = "rockorager" }; 311 const msg: irc.Message = .{ .bytes = "PRIVMSG #comlink :\x1dfoo\x1d" }; 312 313 var list = std.ArrayList(vaxis.Segment).init(std.testing.allocator); 314 defer list.deinit(); 315 try message(&list, &user, msg); 316 try std.testing.expectEqual(1, list.items.len); 317 const expected: vaxis.Segment = .{ .text = "foo", .style = .{ .italic = true } }; 318 try std.testing.expectEqualDeep(expected, list.items[0]); 319} 320 321test "format.zig: strikethrough, reverse, underline" { 322 const user: irc.User = .{ .nick = "rockorager" }; 323 const msg: irc.Message = .{ 324 .bytes = "PRIVMSG #comlink :\x16foo\x16\x1Dbar\x1D\x1Ebaz\x1E\x1Ffoo\x1F", 325 }; 326 327 var list = std.ArrayList(vaxis.Segment).init(std.testing.allocator); 328 defer list.deinit(); 329 try message(&list, &user, msg); 330 const expected: []const vaxis.Segment = &.{ 331 .{ .text = "foo", .style = .{ .reverse = true } }, 332 .{ .text = "bar", .style = .{ .italic = true } }, 333 .{ .text = "baz", .style = .{ .strikethrough = true } }, 334 .{ .text = "foo", .style = .{ .ul_style = .single } }, 335 }; 336 try std.testing.expectEqual(expected.len, list.items.len); 337 for (expected, 0..) |seg, i| { 338 try std.testing.expectEqualDeep(seg, list.items[i]); 339 } 340} 341 342test "format.zig: format without closer" { 343 const user: irc.User = .{ .nick = "rockorager" }; 344 const msg: irc.Message = .{ 345 .bytes = "PRIVMSG #comlink :\x16foo\x16\x1Dbar\x1D\x1Ebaz\x1E\x1Ffoo", 346 }; 347 348 var list = std.ArrayList(vaxis.Segment).init(std.testing.allocator); 349 defer list.deinit(); 350 try message(&list, &user, msg); 351 const expected: []const vaxis.Segment = &.{ 352 .{ .text = "foo", .style = .{ .reverse = true } }, 353 .{ .text = "bar", .style = .{ .italic = true } }, 354 .{ .text = "baz", .style = .{ .strikethrough = true } }, 355 .{ .text = "foo", .style = .{ .ul_style = .single } }, 356 }; 357 try std.testing.expectEqual(expected.len, list.items.len); 358 for (expected, 0..) |seg, i| { 359 try std.testing.expectEqualDeep(seg, list.items[i]); 360 } 361} 362 363test "format.zig: hyperlink" { 364 const user: irc.User = .{ .nick = "rockorager" }; 365 const msg: irc.Message = .{ 366 .bytes = "PRIVMSG #comlink :https://example.org", 367 }; 368 369 var list = std.ArrayList(vaxis.Segment).init(std.testing.allocator); 370 defer list.deinit(); 371 try message(&list, &user, msg); 372 const expected: []const vaxis.Segment = &.{ 373 .{ 374 .text = "https://example.org", 375 .style = .{ .fg = .{ .index = 4 } }, 376 .link = .{ .uri = "https://example.org" }, 377 }, 378 }; 379 try std.testing.expectEqual(expected.len, list.items.len); 380 for (expected, 0..) |seg, i| { 381 try std.testing.expectEqualDeep(seg, list.items[i]); 382 } 383} 384 385test "format.zig: more than hyperlink" { 386 const user: irc.User = .{ .nick = "rockorager" }; 387 const msg: irc.Message = .{ 388 .bytes = "PRIVMSG #comlink :look https://example.org here", 389 }; 390 391 var list = std.ArrayList(vaxis.Segment).init(std.testing.allocator); 392 defer list.deinit(); 393 try message(&list, &user, msg); 394 const expected: []const vaxis.Segment = &.{ 395 .{ .text = "look " }, 396 .{ 397 .text = "https://example.org", 398 .style = .{ .fg = .{ .index = 4 } }, 399 .link = .{ .uri = "https://example.org" }, 400 }, 401 .{ .text = " here" }, 402 }; 403 try std.testing.expectEqual(expected.len, list.items.len); 404 for (expected, 0..) |seg, i| { 405 try std.testing.expectEqualDeep(seg, list.items[i]); 406 } 407}