an experimental irc client
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}