tangled
alpha
login
or
join now
rockorager.dev
/
comlink
2
fork
atom
an experimental irc client
2
fork
atom
overview
issues
pulls
pipelines
ui: implement member list clicking
rockorager.dev
1 year ago
5dbcd379
a0959a52
+262
-715
3 changed files
expand all
collapse all
unified
split
src
app.zig
completer.zig
irc.zig
+42
-658
src/app.zig
···
27
27
const log = std.log.scoped(.app);
28
28
29
29
const State = struct {
30
30
-
mouse: ?vaxis.Mouse = null,
31
31
-
members: struct {
32
32
-
scroll_offset: usize = 0,
33
33
-
width: u16 = 16,
34
34
-
resizing: bool = false,
35
35
-
} = .{},
36
36
-
messages: struct {
37
37
-
scroll_offset: usize = 0,
38
38
-
pending_scroll: isize = 0,
39
39
-
} = .{},
40
30
buffers: struct {
41
41
-
scroll_offset: usize = 0,
42
31
count: usize = 0,
43
43
-
selected_idx: usize = 0,
44
32
width: u16 = 16,
45
45
-
resizing: bool = false,
46
33
} = .{},
47
34
paste: struct {
48
35
pasting: bool = false,
···
88
75
unicode: *const vaxis.Unicode,
89
76
90
77
title_buf: [128]u8,
78
78
+
79
79
+
// Only valid during an event handler
80
80
+
ctx: ?*vxfw.EventContext,
81
81
+
last_height: u16,
91
82
92
83
const default_rhs: vxfw.Text = .{ .text = "TODO: update this text" };
93
84
···
125
116
},
126
117
.unicode = unicode,
127
118
.title_buf = undefined,
119
119
+
.ctx = null,
120
120
+
.last_height = 0,
128
121
};
129
122
130
123
self.lua = try Lua.init(&self.alloc);
···
198
191
}
199
192
200
193
fn typeErasedCaptureHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
201
201
-
// const self: *App = @ptrCast(@alignCast(ptr));
202
202
-
_ = ptr;
194
194
+
const self: *App = @ptrCast(@alignCast(ptr));
195
195
+
// Rewrite the ctx pointer every frame. We don't actually need to do this with the current
196
196
+
// vxfw runtime, because the context pointer is always valid. But for safe keeping, we will
197
197
+
// do it this way.
198
198
+
//
199
199
+
// In general, this is bad practice. But we need to be able to access this from lua
200
200
+
// callbacks
201
201
+
self.ctx = ctx;
203
202
switch (event) {
204
203
.key_press => |key| {
205
204
if (key.matches('c', .{ .ctrl = true })) {
···
212
211
213
212
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
214
213
const self: *App = @ptrCast(@alignCast(ptr));
214
214
+
self.ctx = ctx;
215
215
switch (event) {
216
216
.init => {
217
217
const title = try std.fmt.bufPrint(&self.title_buf, "comlink", .{});
···
222
222
if (key.matches('c', .{ .ctrl = true })) {
223
223
ctx.quit = true;
224
224
}
225
225
+
for (self.binds.items) |bind| {
226
226
+
if (key.matches(bind.key.codepoint, bind.key.mods)) {
227
227
+
switch (bind.command) {
228
228
+
.quit => self.should_quit = true,
229
229
+
.@"next-channel" => self.nextChannel(),
230
230
+
.@"prev-channel" => self.prevChannel(),
231
231
+
// .redraw => self.vx.queueRefresh(),
232
232
+
.lua_function => |ref| try lua.execFn(self.lua, ref),
233
233
+
else => {},
234
234
+
}
235
235
+
return ctx.consumeAndRedraw();
236
236
+
}
237
237
+
}
225
238
},
226
239
.tick => {
227
240
for (self.clients.items) |client| {
···
241
254
242
255
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
243
256
const self: *App = @ptrCast(@alignCast(ptr));
257
257
+
const max = ctx.max.size();
258
258
+
self.last_height = max.height;
244
259
if (self.selectedBuffer()) |buffer| {
245
260
switch (buffer) {
246
261
.client => |client| self.view.rhs = client.view(),
···
249
264
} else self.view.rhs = default_rhs.widget();
250
265
251
266
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena);
252
252
-
_ = &children;
253
267
254
268
// UI is a tree of splits
255
269
// │ │ │ │
···
318
332
return text.draw(ctx);
319
333
}
320
334
321
321
-
// pub fn run(self: *App, lua_state: *Lua) !void {
322
322
-
// const writer = self.tty.anyWriter();
323
323
-
//
324
324
-
// var loop: comlink.EventLoop = .{ .vaxis = &self.vx, .tty = &self.tty };
325
325
-
// try loop.init();
326
326
-
// try loop.start();
327
327
-
// defer loop.stop();
328
328
-
//
329
329
-
// try self.vx.enterAltScreen(writer);
330
330
-
// try self.vx.queryTerminal(writer, 1 * std.time.ns_per_s);
331
331
-
// try self.vx.setMouseMode(writer, true);
332
332
-
// try self.vx.setBracketedPaste(writer, true);
333
333
-
//
334
334
-
// // start our write thread
335
335
-
// var write_queue: comlink.WriteQueue = .{};
336
336
-
// const write_thread = try std.Thread.spawn(.{}, writeLoop, .{ self.alloc, &write_queue });
337
337
-
// defer {
338
338
-
// write_queue.push(.join);
339
339
-
// write_thread.join();
340
340
-
// }
341
341
-
//
342
342
-
// // initialize lua state
343
343
-
// try lua.init(self, lua_state, &loop);
344
344
-
//
345
345
-
// var input = TextInput.init(self.alloc, &self.vx.unicode);
346
346
-
// defer input.deinit();
347
347
-
//
348
348
-
// var last_frame: i64 = std.time.milliTimestamp();
349
349
-
// loop: while (!self.should_quit) {
350
350
-
// var redraw: bool = false;
351
351
-
// std.time.sleep(8 * std.time.ns_per_ms);
352
352
-
// if (self.state.messages.pending_scroll != 0) {
353
353
-
// redraw = true;
354
354
-
// if (self.state.messages.pending_scroll > 0) {
355
355
-
// self.state.messages.pending_scroll -= 1;
356
356
-
// self.state.messages.scroll_offset += 1;
357
357
-
// } else {
358
358
-
// self.state.messages.pending_scroll += 1;
359
359
-
// self.state.messages.scroll_offset -|= 1;
360
360
-
// }
361
361
-
// }
362
362
-
// while (loop.tryEvent()) |event| {
363
363
-
// redraw = true;
364
364
-
// switch (event) {
365
365
-
// .redraw => {},
366
366
-
// .key_press => |key| {
367
367
-
// if (self.state.paste.showDialog()) {
368
368
-
// if (key.matches(vaxis.Key.escape, .{})) {
369
369
-
// self.state.paste.has_newline = false;
370
370
-
// self.paste_buffer.clearAndFree();
371
371
-
// }
372
372
-
// break;
373
373
-
// }
374
374
-
// if (self.state.paste.pasting) {
375
375
-
// if (key.matches(vaxis.Key.enter, .{})) {
376
376
-
// self.state.paste.has_newline = true;
377
377
-
// try self.paste_buffer.append('\n');
378
378
-
// continue :loop;
379
379
-
// }
380
380
-
// const text = key.text orelse continue :loop;
381
381
-
// try self.paste_buffer.appendSlice(text);
382
382
-
// continue;
383
383
-
// }
384
384
-
// for (self.binds.items) |bind| {
385
385
-
// if (key.matches(bind.key.codepoint, bind.key.mods)) {
386
386
-
// switch (bind.command) {
387
387
-
// .quit => self.should_quit = true,
388
388
-
// .@"next-channel" => self.nextChannel(),
389
389
-
// .@"prev-channel" => self.prevChannel(),
390
390
-
// .redraw => self.vx.queueRefresh(),
391
391
-
// .lua_function => |ref| try lua.execFn(lua_state, ref),
392
392
-
// else => {},
393
393
-
// }
394
394
-
// break;
395
395
-
// }
396
396
-
// } else if (key.matches(vaxis.Key.tab, .{})) {
397
397
-
// // if we already have a completion word, then we are
398
398
-
// // cycling through the options
399
399
-
// if (self.completer) |*completer| {
400
400
-
// const line = completer.next();
401
401
-
// input.clearRetainingCapacity();
402
402
-
// try input.insertSliceAtCursor(line);
403
403
-
// } else {
404
404
-
// var completion_buf: [irc.maximum_message_size]u8 = undefined;
405
405
-
// const content = input.sliceToCursor(&completion_buf);
406
406
-
// self.completer = try Completer.init(self.alloc, content);
407
407
-
// }
408
408
-
// } else if (key.matches(vaxis.Key.tab, .{ .shift = true })) {
409
409
-
// if (self.completer) |*completer| {
410
410
-
// const line = completer.prev();
411
411
-
// input.clearRetainingCapacity();
412
412
-
// try input.insertSliceAtCursor(line);
413
413
-
// }
414
414
-
// } else if (key.matches(vaxis.Key.enter, .{})) {
415
415
-
// const buffer = self.selectedBuffer() orelse @panic("no buffer");
416
416
-
// const content = try input.toOwnedSlice();
417
417
-
// if (content.len == 0) continue;
418
418
-
// defer self.alloc.free(content);
419
419
-
// if (content[0] == '/')
420
420
-
// self.handleCommand(lua_state, buffer, content) catch |err| {
421
421
-
// log.err("couldn't handle command: {}", .{err});
422
422
-
// }
423
423
-
// else {
424
424
-
// switch (buffer) {
425
425
-
// .channel => |channel| {
426
426
-
// var buf: [1024]u8 = undefined;
427
427
-
// const msg = try std.fmt.bufPrint(
428
428
-
// &buf,
429
429
-
// "PRIVMSG {s} :{s}\r\n",
430
430
-
// .{
431
431
-
// channel.name,
432
432
-
// content,
433
433
-
// },
434
434
-
// );
435
435
-
// try channel.client.queueWrite(msg);
436
436
-
// },
437
437
-
// .client => log.err("can't send message to client", .{}),
438
438
-
// }
439
439
-
// }
440
440
-
// if (self.completer != null) {
441
441
-
// self.completer.?.deinit();
442
442
-
// self.completer = null;
443
443
-
// }
444
444
-
// } else if (key.matches(vaxis.Key.page_up, .{})) {
445
445
-
// self.state.messages.scroll_offset +|= 3;
446
446
-
// } else if (key.matches(vaxis.Key.page_down, .{})) {
447
447
-
// self.state.messages.scroll_offset -|= 3;
448
448
-
// } else if (key.matches(vaxis.Key.home, .{})) {
449
449
-
// self.state.messages.scroll_offset = 0;
450
450
-
// } else {
451
451
-
// if (self.completer != null and !key.isModifier()) {
452
452
-
// self.completer.?.deinit();
453
453
-
// self.completer = null;
454
454
-
// }
455
455
-
// log.debug("{}", .{key});
456
456
-
// try input.update(.{ .key_press = key });
457
457
-
// }
458
458
-
// },
459
459
-
// .paste_start => self.state.paste.pasting = true,
460
460
-
// .paste_end => {
461
461
-
// self.state.paste.pasting = false;
462
462
-
// if (self.state.paste.has_newline) {
463
463
-
// log.warn("NEWLINE", .{});
464
464
-
// } else {
465
465
-
// try input.insertSliceAtCursor(self.paste_buffer.items);
466
466
-
// defer self.paste_buffer.clearAndFree();
467
467
-
// }
468
468
-
// },
469
469
-
// .focus_out => self.state.mouse = null,
470
470
-
// .mouse => |mouse| {
471
471
-
// self.state.mouse = mouse;
472
472
-
// },
473
473
-
// .winsize => |ws| try self.vx.resize(self.alloc, writer, ws),
474
474
-
// .connect => |cfg| {
475
475
-
// const client = try self.alloc.create(irc.Client);
476
476
-
// client.* = try irc.Client.init(self.alloc, self, &write_queue, cfg);
477
477
-
// client.thread = try std.Thread.spawn(.{}, irc.Client.readLoop, .{ client, &loop });
478
478
-
// try self.clients.append(client);
479
479
-
// },
480
480
-
// .irc => |irc_event| {
481
481
-
// const msg: irc.Message = .{ .bytes = irc_event.msg.slice() };
482
482
-
// const client = irc_event.client;
483
483
-
// defer irc_event.msg.deinit();
484
484
-
// switch (msg.command()) {
485
485
-
// .unknown => {},
486
486
-
// .CAP => {
487
487
-
// // syntax: <client> <ACK/NACK> :caps
488
488
-
// var iter = msg.paramIterator();
489
489
-
// _ = iter.next() orelse continue; // client
490
490
-
// const ack_or_nak = iter.next() orelse continue;
491
491
-
// const caps = iter.next() orelse continue;
492
492
-
// var cap_iter = mem.splitScalar(u8, caps, ' ');
493
493
-
// while (cap_iter.next()) |cap| {
494
494
-
// if (mem.eql(u8, ack_or_nak, "ACK")) {
495
495
-
// client.ack(cap);
496
496
-
// if (mem.eql(u8, cap, "sasl"))
497
497
-
// try client.queueWrite("AUTHENTICATE PLAIN\r\n");
498
498
-
// } else if (mem.eql(u8, ack_or_nak, "NAK")) {
499
499
-
// log.debug("CAP not supported {s}", .{cap});
500
500
-
// }
501
501
-
// }
502
502
-
// },
503
503
-
// .AUTHENTICATE => {
504
504
-
// var iter = msg.paramIterator();
505
505
-
// while (iter.next()) |param| {
506
506
-
// // A '+' is the continuuation to send our
507
507
-
// // AUTHENTICATE info
508
508
-
// if (!mem.eql(u8, param, "+")) continue;
509
509
-
// var buf: [4096]u8 = undefined;
510
510
-
// const config = client.config;
511
511
-
// const sasl = try std.fmt.bufPrint(
512
512
-
// &buf,
513
513
-
// "{s}\x00{s}\x00{s}",
514
514
-
// .{ config.user, config.nick, config.password },
515
515
-
// );
516
516
-
//
517
517
-
// // Create a buffer big enough for the base64 encoded string
518
518
-
// const b64_buf = try self.alloc.alloc(u8, Base64Encoder.calcSize(sasl.len));
519
519
-
// defer self.alloc.free(b64_buf);
520
520
-
// const encoded = Base64Encoder.encode(b64_buf, sasl);
521
521
-
// // Make our message
522
522
-
// const auth = try std.fmt.bufPrint(
523
523
-
// &buf,
524
524
-
// "AUTHENTICATE {s}\r\n",
525
525
-
// .{encoded},
526
526
-
// );
527
527
-
// try client.queueWrite(auth);
528
528
-
// if (config.network_id) |id| {
529
529
-
// const bind = try std.fmt.bufPrint(
530
530
-
// &buf,
531
531
-
// "BOUNCER BIND {s}\r\n",
532
532
-
// .{id},
533
533
-
// );
534
534
-
// try client.queueWrite(bind);
535
535
-
// }
536
536
-
// try client.queueWrite("CAP END\r\n");
537
537
-
// }
538
538
-
// },
539
539
-
// .RPL_WELCOME => {
540
540
-
// const now = try zeit.instant(.{});
541
541
-
// var now_buf: [30]u8 = undefined;
542
542
-
// const now_fmt = try now.time().bufPrint(&now_buf, .rfc3339);
543
543
-
//
544
544
-
// const past = try now.subtract(.{ .days = 7 });
545
545
-
// var past_buf: [30]u8 = undefined;
546
546
-
// const past_fmt = try past.time().bufPrint(&past_buf, .rfc3339);
547
547
-
//
548
548
-
// var buf: [128]u8 = undefined;
549
549
-
// const targets = try std.fmt.bufPrint(
550
550
-
// &buf,
551
551
-
// "CHATHISTORY TARGETS timestamp={s} timestamp={s} 50\r\n",
552
552
-
// .{ now_fmt, past_fmt },
553
553
-
// );
554
554
-
// try client.queueWrite(targets);
555
555
-
// // on_connect callback
556
556
-
// try lua.onConnect(lua_state, client);
557
557
-
// },
558
558
-
// .RPL_YOURHOST => {},
559
559
-
// .RPL_CREATED => {},
560
560
-
// .RPL_MYINFO => {},
561
561
-
// .RPL_ISUPPORT => {
562
562
-
// // syntax: <client> <token>[ <token>] :are supported
563
563
-
// var iter = msg.paramIterator();
564
564
-
// _ = iter.next() orelse continue; // client
565
565
-
// while (iter.next()) |token| {
566
566
-
// if (mem.eql(u8, token, "WHOX"))
567
567
-
// client.supports.whox = true
568
568
-
// else if (mem.startsWith(u8, token, "PREFIX")) {
569
569
-
// const prefix = blk: {
570
570
-
// const idx = mem.indexOfScalar(u8, token, ')') orelse
571
571
-
// // default is "@+"
572
572
-
// break :blk try self.alloc.dupe(u8, "@+");
573
573
-
// break :blk try self.alloc.dupe(u8, token[idx + 1 ..]);
574
574
-
// };
575
575
-
// client.supports.prefix = prefix;
576
576
-
// }
577
577
-
// }
578
578
-
// },
579
579
-
// .RPL_LOGGEDIN => {},
580
580
-
// .RPL_TOPIC => {
581
581
-
// // syntax: <client> <channel> :<topic>
582
582
-
// var iter = msg.paramIterator();
583
583
-
// _ = iter.next() orelse continue :loop; // client ("*")
584
584
-
// const channel_name = iter.next() orelse continue :loop; // channel
585
585
-
// const topic = iter.next() orelse continue :loop; // topic
586
586
-
//
587
587
-
// var channel = try client.getOrCreateChannel(channel_name);
588
588
-
// if (channel.topic) |old_topic| {
589
589
-
// self.alloc.free(old_topic);
590
590
-
// }
591
591
-
// channel.topic = try self.alloc.dupe(u8, topic);
592
592
-
// },
593
593
-
// .RPL_SASLSUCCESS => {},
594
594
-
// .RPL_WHOREPLY => {
595
595
-
// // syntax: <client> <channel> <username> <host> <server> <nick> <flags> :<hopcount> <real name>
596
596
-
// var iter = msg.paramIterator();
597
597
-
// _ = iter.next() orelse continue :loop; // client
598
598
-
// const channel_name = iter.next() orelse continue :loop; // channel
599
599
-
// if (mem.eql(u8, channel_name, "*")) continue;
600
600
-
// _ = iter.next() orelse continue :loop; // username
601
601
-
// _ = iter.next() orelse continue :loop; // host
602
602
-
// _ = iter.next() orelse continue :loop; // server
603
603
-
// const nick = iter.next() orelse continue :loop; // nick
604
604
-
// const flags = iter.next() orelse continue :loop; // flags
605
605
-
//
606
606
-
// const user_ptr = try client.getOrCreateUser(nick);
607
607
-
// if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true;
608
608
-
// var channel = try client.getOrCreateChannel(channel_name);
609
609
-
//
610
610
-
// const prefix = for (flags) |c| {
611
611
-
// if (std.mem.indexOfScalar(u8, client.supports.prefix, c)) |_| {
612
612
-
// break c;
613
613
-
// }
614
614
-
// } else ' ';
615
615
-
//
616
616
-
// try channel.addMember(user_ptr, .{ .prefix = prefix });
617
617
-
// },
618
618
-
// .RPL_WHOSPCRPL => {
619
619
-
// // syntax: <client> <channel> <nick> <flags> :<realname>
620
620
-
// var iter = msg.paramIterator();
621
621
-
// _ = iter.next() orelse continue;
622
622
-
// const channel_name = iter.next() orelse continue; // channel
623
623
-
// const nick = iter.next() orelse continue;
624
624
-
// const flags = iter.next() orelse continue;
625
625
-
//
626
626
-
// const user_ptr = try client.getOrCreateUser(nick);
627
627
-
// if (iter.next()) |real_name| {
628
628
-
// if (user_ptr.real_name) |old_name| {
629
629
-
// self.alloc.free(old_name);
630
630
-
// }
631
631
-
// user_ptr.real_name = try self.alloc.dupe(u8, real_name);
632
632
-
// }
633
633
-
// if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true;
634
634
-
// var channel = try client.getOrCreateChannel(channel_name);
635
635
-
//
636
636
-
// const prefix = for (flags) |c| {
637
637
-
// if (std.mem.indexOfScalar(u8, client.supports.prefix, c)) |_| {
638
638
-
// break c;
639
639
-
// }
640
640
-
// } else ' ';
641
641
-
//
642
642
-
// try channel.addMember(user_ptr, .{ .prefix = prefix });
643
643
-
// },
644
644
-
// .RPL_ENDOFWHO => {
645
645
-
// // syntax: <client> <mask> :End of WHO list
646
646
-
// var iter = msg.paramIterator();
647
647
-
// _ = iter.next() orelse continue :loop; // client
648
648
-
// const channel_name = iter.next() orelse continue :loop; // channel
649
649
-
// if (mem.eql(u8, channel_name, "*")) continue;
650
650
-
// var channel = try client.getOrCreateChannel(channel_name);
651
651
-
// channel.in_flight.who = false;
652
652
-
// },
653
653
-
// .RPL_NAMREPLY => {
654
654
-
// // syntax: <client> <symbol> <channel> :[<prefix>]<nick>{ [<prefix>]<nick>}
655
655
-
// var iter = msg.paramIterator();
656
656
-
// _ = iter.next() orelse continue; // client
657
657
-
// _ = iter.next() orelse continue; // symbol
658
658
-
// const channel_name = iter.next() orelse continue; // channel
659
659
-
// const names = iter.next() orelse continue;
660
660
-
// var channel = try client.getOrCreateChannel(channel_name);
661
661
-
// var name_iter = std.mem.splitScalar(u8, names, ' ');
662
662
-
// while (name_iter.next()) |name| {
663
663
-
// const nick, const prefix = for (client.supports.prefix) |ch| {
664
664
-
// if (name[0] == ch) {
665
665
-
// break .{ name[1..], name[0] };
666
666
-
// }
667
667
-
// } else .{ name, ' ' };
668
668
-
//
669
669
-
// if (prefix != ' ') {
670
670
-
// log.debug("HAS PREFIX {s}", .{name});
671
671
-
// }
672
672
-
//
673
673
-
// const user_ptr = try client.getOrCreateUser(nick);
674
674
-
//
675
675
-
// try channel.addMember(user_ptr, .{ .prefix = prefix, .sort = false });
676
676
-
// }
677
677
-
//
678
678
-
// channel.sortMembers();
679
679
-
// },
680
680
-
// .RPL_ENDOFNAMES => {
681
681
-
// // syntax: <client> <channel> :End of /NAMES list
682
682
-
// var iter = msg.paramIterator();
683
683
-
// _ = iter.next() orelse continue; // client
684
684
-
// const channel_name = iter.next() orelse continue; // channel
685
685
-
// var channel = try client.getOrCreateChannel(channel_name);
686
686
-
// channel.in_flight.names = false;
687
687
-
// },
688
688
-
// .BOUNCER => {
689
689
-
// var iter = msg.paramIterator();
690
690
-
// while (iter.next()) |param| {
691
691
-
// if (mem.eql(u8, param, "NETWORK")) {
692
692
-
// const id = iter.next() orelse continue;
693
693
-
// const attr = iter.next() orelse continue;
694
694
-
// // check if we already have this network
695
695
-
// for (self.clients.items, 0..) |cl, i| {
696
696
-
// if (cl.config.network_id) |net_id| {
697
697
-
// if (mem.eql(u8, net_id, id)) {
698
698
-
// if (mem.eql(u8, attr, "*")) {
699
699
-
// // * means the network was
700
700
-
// // deleted
701
701
-
// cl.deinit();
702
702
-
// _ = self.clients.swapRemove(i);
703
703
-
// }
704
704
-
// continue :loop;
705
705
-
// }
706
706
-
// }
707
707
-
// }
708
708
-
//
709
709
-
// var cfg = client.config;
710
710
-
// cfg.network_id = try self.alloc.dupe(u8, id);
711
711
-
//
712
712
-
// var attr_iter = std.mem.splitScalar(u8, attr, ';');
713
713
-
// while (attr_iter.next()) |kv| {
714
714
-
// const n = std.mem.indexOfScalar(u8, kv, '=') orelse continue;
715
715
-
// const key = kv[0..n];
716
716
-
// if (mem.eql(u8, key, "name"))
717
717
-
// cfg.name = try self.alloc.dupe(u8, kv[n + 1 ..])
718
718
-
// else if (mem.eql(u8, key, "nickname"))
719
719
-
// cfg.network_nick = try self.alloc.dupe(u8, kv[n + 1 ..]);
720
720
-
// }
721
721
-
// loop.postEvent(.{ .connect = cfg });
722
722
-
// }
723
723
-
// }
724
724
-
// },
725
725
-
// .AWAY => {
726
726
-
// const src = msg.source() orelse continue :loop;
727
727
-
// var iter = msg.paramIterator();
728
728
-
// const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len;
729
729
-
// const user = try client.getOrCreateUser(src[0..n]);
730
730
-
// // If there are any params, the user is away. Otherwise
731
731
-
// // they are back.
732
732
-
// user.away = if (iter.next()) |_| true else false;
733
733
-
// },
734
734
-
// .BATCH => {
735
735
-
// var iter = msg.paramIterator();
736
736
-
// const tag = iter.next() orelse continue;
737
737
-
// switch (tag[0]) {
738
738
-
// '+' => {
739
739
-
// const batch_type = iter.next() orelse continue;
740
740
-
// if (mem.eql(u8, batch_type, "chathistory")) {
741
741
-
// const target = iter.next() orelse continue;
742
742
-
// var channel = try client.getOrCreateChannel(target);
743
743
-
// channel.at_oldest = true;
744
744
-
// const duped_tag = try self.alloc.dupe(u8, tag[1..]);
745
745
-
// try client.batches.put(duped_tag, channel);
746
746
-
// }
747
747
-
// },
748
748
-
// '-' => {
749
749
-
// const key = client.batches.getKey(tag[1..]) orelse continue;
750
750
-
// var chan = client.batches.get(key) orelse @panic("key should exist here");
751
751
-
// chan.history_requested = false;
752
752
-
// _ = client.batches.remove(key);
753
753
-
// self.alloc.free(key);
754
754
-
// },
755
755
-
// else => {},
756
756
-
// }
757
757
-
// },
758
758
-
// .CHATHISTORY => {
759
759
-
// var iter = msg.paramIterator();
760
760
-
// const should_targets = iter.next() orelse continue;
761
761
-
// if (!mem.eql(u8, should_targets, "TARGETS")) continue;
762
762
-
// const target = iter.next() orelse continue;
763
763
-
// // we only add direct messages, not more channels
764
764
-
// assert(target.len > 0);
765
765
-
// if (target[0] == '#') continue;
766
766
-
//
767
767
-
// var channel = try client.getOrCreateChannel(target);
768
768
-
// const user_ptr = try client.getOrCreateUser(target);
769
769
-
// const me_ptr = try client.getOrCreateUser(client.nickname());
770
770
-
// try channel.addMember(user_ptr, .{});
771
771
-
// try channel.addMember(me_ptr, .{});
772
772
-
// // we set who_requested so we don't try to request
773
773
-
// // who on DMs
774
774
-
// channel.who_requested = true;
775
775
-
// var buf: [128]u8 = undefined;
776
776
-
// const mark_read = try std.fmt.bufPrint(
777
777
-
// &buf,
778
778
-
// "MARKREAD {s}\r\n",
779
779
-
// .{channel.name},
780
780
-
// );
781
781
-
// try client.queueWrite(mark_read);
782
782
-
// try client.requestHistory(.after, channel);
783
783
-
// },
784
784
-
// .JOIN => {
785
785
-
// // get the user
786
786
-
// const src = msg.source() orelse continue :loop;
787
787
-
// const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len;
788
788
-
// const user = try client.getOrCreateUser(src[0..n]);
789
789
-
//
790
790
-
// // get the channel
791
791
-
// var iter = msg.paramIterator();
792
792
-
// const target = iter.next() orelse continue;
793
793
-
// var channel = try client.getOrCreateChannel(target);
794
794
-
//
795
795
-
// // If it's our nick, we request chat history
796
796
-
// if (mem.eql(u8, user.nick, client.nickname())) {
797
797
-
// try client.requestHistory(.after, channel);
798
798
-
// if (self.explicit_join) {
799
799
-
// self.selectChannelName(client, target);
800
800
-
// self.explicit_join = false;
801
801
-
// }
802
802
-
// } else try channel.addMember(user, .{});
803
803
-
// },
804
804
-
// .MARKREAD => {
805
805
-
// var iter = msg.paramIterator();
806
806
-
// const target = iter.next() orelse continue;
807
807
-
// const timestamp = iter.next() orelse continue;
808
808
-
// const equal = std.mem.indexOfScalar(u8, timestamp, '=') orelse continue;
809
809
-
// const last_read = zeit.instant(.{
810
810
-
// .source = .{
811
811
-
// .iso8601 = timestamp[equal + 1 ..],
812
812
-
// },
813
813
-
// }) catch |err| {
814
814
-
// log.err("couldn't convert timestamp: {}", .{err});
815
815
-
// continue;
816
816
-
// };
817
817
-
// var channel = try client.getOrCreateChannel(target);
818
818
-
// channel.last_read = last_read.unixTimestamp();
819
819
-
// const last_msg = channel.messages.getLastOrNull() orelse continue;
820
820
-
// const time = last_msg.time() orelse continue;
821
821
-
// if (time.unixTimestamp() > channel.last_read)
822
822
-
// channel.has_unread = true
823
823
-
// else
824
824
-
// channel.has_unread = false;
825
825
-
// },
826
826
-
// .PART => {
827
827
-
// // get the user
828
828
-
// const src = msg.source() orelse continue :loop;
829
829
-
// const n = std.mem.indexOfScalar(u8, src, '!') orelse src.len;
830
830
-
// const user = try client.getOrCreateUser(src[0..n]);
831
831
-
//
832
832
-
// // get the channel
833
833
-
// var iter = msg.paramIterator();
834
834
-
// const target = iter.next() orelse continue;
835
835
-
//
836
836
-
// if (mem.eql(u8, user.nick, client.nickname())) {
837
837
-
// for (client.channels.items, 0..) |channel, i| {
838
838
-
// if (!mem.eql(u8, channel.name, target)) continue;
839
839
-
// var chan = client.channels.orderedRemove(i);
840
840
-
// self.state.buffers.selected_idx -|= 1;
841
841
-
// chan.deinit(self.alloc);
842
842
-
// break;
843
843
-
// }
844
844
-
// } else {
845
845
-
// const channel = try client.getOrCreateChannel(target);
846
846
-
// channel.removeMember(user);
847
847
-
// }
848
848
-
// },
849
849
-
// .PRIVMSG, .NOTICE => {
850
850
-
// // syntax: <target> :<message>
851
851
-
// const msg2: irc.Message = .{
852
852
-
// .bytes = try self.alloc.dupe(u8, msg.bytes),
853
853
-
// };
854
854
-
// var iter = msg2.paramIterator();
855
855
-
// const target = blk: {
856
856
-
// const tgt = iter.next() orelse continue;
857
857
-
// if (mem.eql(u8, tgt, client.nickname())) {
858
858
-
// // If the target is us, it likely has our
859
859
-
// // hostname in it.
860
860
-
// const source = msg2.source() orelse continue;
861
861
-
// const n = mem.indexOfScalar(u8, source, '!') orelse source.len;
862
862
-
// break :blk source[0..n];
863
863
-
// } else break :blk tgt;
864
864
-
// };
865
865
-
//
866
866
-
// // We handle batches separately. When we encounter a
867
867
-
// // PRIVMSG from a batch, we use the original target
868
868
-
// // from the batch start. We also never notify from a
869
869
-
// // batched message. Batched messages also require
870
870
-
// // sorting
871
871
-
// var tag_iter = msg2.tagIterator();
872
872
-
// while (tag_iter.next()) |tag| {
873
873
-
// if (mem.eql(u8, tag.key, "batch")) {
874
874
-
// const entry = client.batches.getEntry(tag.value) orelse @panic("TODO");
875
875
-
// var channel = entry.value_ptr.*;
876
876
-
// try channel.messages.append(msg2);
877
877
-
// std.sort.insertion(irc.Message, channel.messages.items, {}, irc.Message.compareTime);
878
878
-
// channel.at_oldest = false;
879
879
-
// const time = msg2.time() orelse continue;
880
880
-
// if (time.unixTimestamp() > channel.last_read) {
881
881
-
// channel.has_unread = true;
882
882
-
// const content = iter.next() orelse continue;
883
883
-
// if (std.mem.indexOf(u8, content, client.nickname())) |_| {
884
884
-
// channel.has_unread_highlight = true;
885
885
-
// }
886
886
-
// }
887
887
-
// break;
888
888
-
// }
889
889
-
// } else {
890
890
-
// // standard handling
891
891
-
// var channel = try client.getOrCreateChannel(target);
892
892
-
// try channel.messages.append(msg2);
893
893
-
// const content = iter.next() orelse continue;
894
894
-
// var has_highlight = false;
895
895
-
// {
896
896
-
// const sender: []const u8 = blk: {
897
897
-
// const src = msg2.source() orelse break :blk "";
898
898
-
// const l = std.mem.indexOfScalar(u8, src, '!') orelse
899
899
-
// std.mem.indexOfScalar(u8, src, '@') orelse
900
900
-
// src.len;
901
901
-
// break :blk src[0..l];
902
902
-
// };
903
903
-
// try lua.onMessage(lua_state, client, channel.name, sender, content);
904
904
-
// }
905
905
-
// if (std.mem.indexOf(u8, content, client.nickname())) |_| {
906
906
-
// var buf: [64]u8 = undefined;
907
907
-
// const title_or_err = if (msg2.source()) |source|
908
908
-
// std.fmt.bufPrint(&buf, "{s} - {s}", .{ channel.name, source })
909
909
-
// else
910
910
-
// std.fmt.bufPrint(&buf, "{s}", .{channel.name});
911
911
-
// const title = title_or_err catch title: {
912
912
-
// const len = @min(buf.len, channel.name.len);
913
913
-
// @memcpy(buf[0..len], channel.name[0..len]);
914
914
-
// break :title buf[0..len];
915
915
-
// };
916
916
-
// try self.vx.notify(writer, title, content);
917
917
-
// has_highlight = true;
918
918
-
// }
919
919
-
// const time = msg2.time() orelse continue;
920
920
-
// if (time.unixTimestamp() > channel.last_read) {
921
921
-
// channel.has_unread_highlight = has_highlight;
922
922
-
// channel.has_unread = true;
923
923
-
// }
924
924
-
// }
925
925
-
//
926
926
-
// // If we get a message from the current user mark the channel as
927
927
-
// // read, since they must have just sent the message.
928
928
-
// const sender: []const u8 = blk: {
929
929
-
// const src = msg2.source() orelse break :blk "";
930
930
-
// const l = std.mem.indexOfScalar(u8, src, '!') orelse
931
931
-
// std.mem.indexOfScalar(u8, src, '@') orelse
932
932
-
// src.len;
933
933
-
// break :blk src[0..l];
934
934
-
// };
935
935
-
// if (std.mem.eql(u8, sender, client.nickname())) {
936
936
-
// self.markSelectedChannelRead();
937
937
-
// }
938
938
-
// },
939
939
-
// }
940
940
-
// },
941
941
-
// }
942
942
-
// }
943
943
-
//
944
944
-
// if (redraw) {
945
945
-
// try self.draw(&input);
946
946
-
// last_frame = std.time.milliTimestamp();
947
947
-
// }
948
948
-
// }
949
949
-
// }
950
950
-
951
335
pub fn connect(self: *App, cfg: irc.Client.Config) !void {
952
336
const client = try self.alloc.create(irc.Client);
953
337
client.* = try irc.Client.init(self.alloc, self, &self.write_queue, cfg);
···
958
342
// When leaving a channel we mark it as read, so we make sure that's done
959
343
// before we change to the new channel.
960
344
self.markSelectedChannelRead();
961
961
-
962
962
-
const state = self.state.buffers;
963
963
-
if (state.selected_idx >= state.count - 1)
964
964
-
self.state.buffers.selected_idx = 0
965
965
-
else
966
966
-
self.state.buffers.selected_idx +|= 1;
345
345
+
if (self.ctx) |ctx| {
346
346
+
self.buffer_list.nextItem(ctx);
347
347
+
}
967
348
}
968
349
969
350
pub fn prevChannel(self: *App) void {
970
351
// When leaving a channel we mark it as read, so we make sure that's done
971
352
// before we change to the new channel.
972
353
self.markSelectedChannelRead();
973
973
-
974
974
-
switch (self.state.buffers.selected_idx) {
975
975
-
0 => self.state.buffers.selected_idx = self.state.buffers.count - 1,
976
976
-
else => self.state.buffers.selected_idx -|= 1,
354
354
+
if (self.ctx) |ctx| {
355
355
+
self.buffer_list.prevItem(ctx);
977
356
}
978
357
}
979
358
···
984
363
for (client.channels.items) |channel| {
985
364
if (cl == client) {
986
365
if (std.mem.eql(u8, name, channel.name)) {
987
987
-
self.state.buffers.selected_idx = i;
366
366
+
self.selectBuffer(.{ .channel = channel });
988
367
}
989
368
}
990
369
i += 1;
···
1116
495
for (client.channels.items, 0..) |search, i| {
1117
496
if (!mem.eql(u8, search.name, target)) continue;
1118
497
var chan = client.channels.orderedRemove(i);
1119
1119
-
self.state.buffers.selected_idx -|= 1;
498
498
+
self.buffer_list.cursor -|= 1;
499
499
+
self.buffer_list.ensureScroll();
1120
500
chan.deinit(self.alloc);
501
501
+
self.alloc.destroy(chan);
1121
502
break;
1122
503
}
1123
504
} else {
···
1185
566
self.buffer_list.cursor = i;
1186
567
self.buffer_list.ensureScroll();
1187
568
if (target.messageViewIsAtBottom()) target.has_unread = false;
569
569
+
if (self.ctx) |ctx| {
570
570
+
ctx.requestFocus(channel.text_field.widget()) catch {};
571
571
+
}
1188
572
return;
1189
573
}
1190
574
i += 1;
+78
-42
src/completer.zig
···
4
4
const emoji = @import("emoji.zig");
5
5
6
6
const irc = comlink.irc;
7
7
+
const vxfw = vaxis.vxfw;
7
8
const Command = comlink.Command;
8
9
9
10
const Kind = enum {
···
13
14
};
14
15
15
16
pub const Completer = struct {
17
17
+
const style: vaxis.Style = .{ .bg = .{ .index = 8 } };
18
18
+
const selected: vaxis.Style = .{ .bg = .{ .index = 8 }, .reverse = true };
19
19
+
16
20
word: []const u8,
17
21
start_idx: usize,
18
18
-
options: std.ArrayList([]const u8),
19
19
-
selected_idx: ?usize,
22
22
+
options: std.ArrayList(vxfw.Text),
20
23
widest: ?usize,
21
24
buf: [irc.maximum_message_size]u8 = undefined,
22
25
kind: Kind = .nick,
26
26
+
list_view: vxfw.ListView,
27
27
+
selected: bool,
23
28
24
24
-
pub fn init(alloc: std.mem.Allocator, line: []const u8) !Completer {
25
25
-
const start_idx = if (std.mem.lastIndexOfScalar(u8, line, ' ')) |idx| idx + 1 else 0;
26
26
-
const last_word = line[start_idx..];
27
27
-
var completer: Completer = .{
28
28
-
.options = std.ArrayList([]const u8).init(alloc),
29
29
-
.start_idx = start_idx,
30
30
-
.word = last_word,
31
31
-
.selected_idx = null,
29
29
+
pub fn init(gpa: std.mem.Allocator) Completer {
30
30
+
return .{
31
31
+
.options = std.ArrayList(vxfw.Text).init(gpa),
32
32
+
.start_idx = 0,
33
33
+
.word = "",
32
34
.widest = null,
35
35
+
.list_view = undefined,
36
36
+
.selected = false,
33
37
};
34
34
-
@memcpy(completer.buf[0..line.len], line);
35
35
-
if (last_word.len > 0 and last_word[0] == '/') {
36
36
-
completer.kind = .command;
37
37
-
try completer.findCommandMatches();
38
38
+
}
39
39
+
40
40
+
fn getWidget(ptr: *const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget {
41
41
+
const self: *Completer = @constCast(@ptrCast(@alignCast(ptr)));
42
42
+
if (idx < self.options.items.len) {
43
43
+
const item = &self.options.items[idx];
44
44
+
if (idx == cursor) {
45
45
+
item.style = selected;
46
46
+
} else {
47
47
+
item.style = style;
48
48
+
}
49
49
+
return item.widget();
38
50
}
39
39
-
if (last_word.len > 0 and last_word[0] == ':') {
40
40
-
completer.kind = .emoji;
41
41
-
try completer.findEmojiMatches();
51
51
+
return null;
52
52
+
}
53
53
+
54
54
+
pub fn reset(self: *Completer, line: []const u8) !void {
55
55
+
self.list_view = .{
56
56
+
.children = .{ .builder = .{
57
57
+
.userdata = self,
58
58
+
.buildFn = Completer.getWidget,
59
59
+
} },
60
60
+
.draw_cursor = false,
61
61
+
};
62
62
+
self.start_idx = if (std.mem.lastIndexOfScalar(u8, line, ' ')) |idx| idx + 1 else 0;
63
63
+
self.word = line[self.start_idx..];
64
64
+
@memcpy(self.buf[0..line.len], line);
65
65
+
self.options.clearAndFree();
66
66
+
self.widest = null;
67
67
+
self.kind = .nick;
68
68
+
self.selected = false;
69
69
+
70
70
+
if (self.word.len > 0 and self.word[0] == '/') {
71
71
+
self.kind = .command;
72
72
+
try self.findCommandMatches();
42
73
}
43
43
-
return completer;
74
74
+
if (self.word.len > 0 and self.word[0] == ':') {
75
75
+
self.kind = .emoji;
76
76
+
try self.findEmojiMatches();
77
77
+
}
44
78
}
45
79
46
80
pub fn deinit(self: *Completer) void {
···
50
84
/// cycles to the next option, returns the replacement text. Note that we
51
85
/// start from the bottom, so a selected_idx = 0 means we are on _the last_
52
86
/// item
53
53
-
pub fn next(self: *Completer) []const u8 {
87
87
+
pub fn next(self: *Completer, ctx: *vxfw.EventContext) []const u8 {
54
88
if (self.options.items.len == 0) return "";
55
55
-
{
56
56
-
const last_idx = self.options.items.len - 1;
57
57
-
if (self.selected_idx == null or self.selected_idx.? == last_idx)
58
58
-
self.selected_idx = 0
59
59
-
else
60
60
-
self.selected_idx.? +|= 1;
89
89
+
if (self.selected) {
90
90
+
self.list_view.prevItem(ctx);
61
91
}
92
92
+
self.selected = true;
62
93
return self.replacementText();
63
94
}
64
95
65
65
-
pub fn prev(self: *Completer) []const u8 {
96
96
+
pub fn prev(self: *Completer, ctx: *vxfw.EventContext) []const u8 {
66
97
if (self.options.items.len == 0) return "";
67
67
-
{
68
68
-
const last_idx = self.options.items.len - 1;
69
69
-
if (self.selected_idx == null or self.selected_idx.? == 0)
70
70
-
self.selected_idx = last_idx
71
71
-
else
72
72
-
self.selected_idx.? -|= 1;
73
73
-
}
98
98
+
self.list_view.nextItem(ctx);
99
99
+
self.selected = true;
74
100
return self.replacementText();
75
101
}
76
102
77
103
pub fn replacementText(self: *Completer) []const u8 {
78
78
-
if (self.selected_idx == null or self.options.items.len == 0) return "";
79
79
-
const replacement = self.options.items[self.options.items.len - 1 - self.selected_idx.?];
104
104
+
if (self.options.items.len == 0) return "";
105
105
+
const replacement_widget = self.options.items[self.list_view.cursor];
106
106
+
const replacement = replacement_widget.text;
80
107
switch (self.kind) {
81
108
.command => {
82
109
self.buf[0] = '/';
···
118
145
}
119
146
}
120
147
std.sort.insertion(irc.Channel.Member, members.items, chan, irc.Channel.compareRecentMessages);
121
121
-
self.options = try std.ArrayList([]const u8).initCapacity(alloc, members.items.len);
148
148
+
try self.options.ensureTotalCapacity(members.items.len);
122
149
for (members.items) |member| {
123
123
-
try self.options.append(member.user.nick);
150
150
+
try self.options.append(.{ .text = member.user.nick });
124
151
}
152
152
+
self.list_view.cursor = @intCast(self.options.items.len -| 1);
153
153
+
self.list_view.item_count = @intCast(self.options.items.len);
154
154
+
self.list_view.ensureScroll();
125
155
}
126
156
127
157
pub fn findCommandMatches(self: *Completer) !void {
···
130
160
for (commands) |cmd| {
131
161
if (std.mem.eql(u8, cmd, "lua_function")) continue;
132
162
if (std.ascii.startsWithIgnoreCase(cmd, self.word[1..])) {
133
133
-
try self.options.append(cmd);
163
163
+
try self.options.append(.{ .text = cmd });
134
164
}
135
165
}
136
166
var iter = Command.user_commands.keyIterator();
137
167
while (iter.next()) |cmd| {
138
168
if (std.ascii.startsWithIgnoreCase(cmd.*, self.word[1..])) {
139
139
-
try self.options.append(cmd.*);
169
169
+
try self.options.append(.{ .text = cmd.* });
140
170
}
141
171
}
172
172
+
self.list_view.cursor = @intCast(self.options.items.len -| 1);
173
173
+
self.list_view.item_count = @intCast(self.options.items.len);
174
174
+
self.list_view.ensureScroll();
142
175
}
143
176
144
177
pub fn findEmojiMatches(self: *Completer) !void {
···
148
181
149
182
for (keys, values) |shortcode, glyph| {
150
183
if (std.mem.indexOf(u8, shortcode, self.word[1..])) |_|
151
151
-
try self.options.append(glyph);
184
184
+
try self.options.append(.{ .text = glyph });
152
185
}
186
186
+
self.list_view.cursor = @intCast(self.options.items.len -| 1);
187
187
+
self.list_view.item_count = @intCast(self.options.items.len);
188
188
+
self.list_view.ensureScroll();
153
189
}
154
190
155
155
-
pub fn widestMatch(self: *Completer, win: vaxis.Window) usize {
191
191
+
pub fn widestMatch(self: *Completer, ctx: vxfw.DrawContext) usize {
156
192
if (self.widest) |w| return w;
157
193
var widest: usize = 0;
158
194
for (self.options.items) |opt| {
159
159
-
const width = win.gwidth(opt);
195
195
+
const width = ctx.stringWidth(opt.text);
160
196
if (width > widest) widest = width;
161
197
}
162
198
self.widest = widest;
+142
-15
src/irc.zig
···
5
5
const vaxis = @import("vaxis");
6
6
const zeit = @import("zeit");
7
7
8
8
+
const Completer = @import("completer.zig").Completer;
8
9
const Scrollbar = @import("Scrollbar.zig");
9
10
const testing = std.testing;
10
11
const mem = std.mem;
···
128
129
129
130
/// Pending scroll we have to handle while drawing. This could be up or down. By convention
130
131
/// we say positive is a scroll up.
131
131
-
pending: i16 = 0,
132
132
+
pending: i17 = 0,
132
133
} = .{},
133
134
134
135
message_view: struct {
135
136
mouse: ?vaxis.Mouse = null,
136
137
hovered_message: ?Message = null,
137
138
} = .{},
139
139
+
140
140
+
completer: Completer,
141
141
+
completer_shown: bool = false,
138
142
139
143
// Gutter (left side where time is printed) width
140
144
const gutter_width = 6;
···
145
149
/// Highest channel membership prefix (or empty space if no prefix)
146
150
prefix: u8,
147
151
152
152
+
channel: *Channel,
153
153
+
has_mouse: bool = false,
154
154
+
148
155
pub fn compare(_: void, lhs: Member, rhs: Member) bool {
149
156
return if (lhs.prefix != ' ' and rhs.prefix == ' ')
150
157
true
···
157
164
pub fn widget(self: *Member) vxfw.Widget {
158
165
return .{
159
166
.userdata = self,
167
167
+
.eventHandler = Member.eventHandler,
160
168
.drawFn = Member.draw,
161
169
};
162
170
}
163
171
172
172
+
fn eventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
173
173
+
const self: *Member = @ptrCast(@alignCast(ptr));
174
174
+
switch (event) {
175
175
+
.mouse => |mouse| {
176
176
+
if (!self.has_mouse) {
177
177
+
self.has_mouse = true;
178
178
+
try ctx.setMouseShape(.pointer);
179
179
+
}
180
180
+
switch (mouse.type) {
181
181
+
.press => {
182
182
+
if (mouse.button == .left) {
183
183
+
// Open a private message with this user
184
184
+
const client = self.channel.client;
185
185
+
const ch = try client.getOrCreateChannel(self.user.nick);
186
186
+
try client.requestHistory(.after, ch);
187
187
+
client.app.selectChannelName(client, ch.name);
188
188
+
return ctx.consumeAndRedraw();
189
189
+
}
190
190
+
if (mouse.button == .right) {
191
191
+
// Insert nick at cursor
192
192
+
try self.channel.text_field.insertSliceAtCursor(self.user.nick);
193
193
+
return ctx.consumeAndRedraw();
194
194
+
}
195
195
+
},
196
196
+
else => {},
197
197
+
}
198
198
+
},
199
199
+
.mouse_enter => {
200
200
+
self.has_mouse = true;
201
201
+
try ctx.setMouseShape(.pointer);
202
202
+
},
203
203
+
.mouse_leave => {
204
204
+
self.has_mouse = false;
205
205
+
try ctx.setMouseShape(.default);
206
206
+
},
207
207
+
else => {},
208
208
+
}
209
209
+
}
210
210
+
164
211
pub fn draw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
165
212
const self: *Member = @ptrCast(@alignCast(ptr));
166
166
-
const style: vaxis.Style = if (self.user.away)
213
213
+
var style: vaxis.Style = if (self.user.away)
167
214
.{ .fg = .{ .index = 8 } }
168
215
else
169
216
.{ .fg = self.user.color };
217
217
+
if (self.has_mouse) style.reverse = true;
170
218
var prefix = try ctx.arena.alloc(u8, 1);
171
219
prefix[0] = self.prefix;
172
220
const text: vxfw.RichText = .{
···
176
224
},
177
225
.softwrap = false,
178
226
};
179
179
-
return text.draw(ctx);
227
227
+
var surface = try text.draw(ctx);
228
228
+
surface.widget = self.widget();
229
229
+
return surface;
180
230
}
181
231
};
182
232
···
208
258
.draw_cursor = false,
209
259
},
210
260
.text_field = vxfw.TextField.init(gpa, unicode),
261
261
+
.completer = Completer.init(gpa),
211
262
};
212
263
213
264
self.text_field.userdata = self;
···
222
273
try self.client.print("PRIVMSG {s} :{s}\r\n", .{ self.name, input });
223
274
}
224
275
ctx.redraw = true;
276
276
+
self.completer_shown = false;
225
277
self.text_field.clearAndFree();
226
278
}
227
279
···
236
288
}
237
289
self.messages.deinit();
238
290
self.text_field.deinit();
291
291
+
self.completer.deinit();
239
292
}
240
293
241
294
pub fn compare(_: void, lhs: *Channel, rhs: *Channel) bool {
···
366
419
prefix: ?u8 = null,
367
420
sort: bool = true,
368
421
}) Allocator.Error!void {
369
369
-
if (args.prefix) |p| {
370
370
-
log.debug("adding member: nick={s}, prefix={c}", .{ user.nick, p });
371
371
-
}
372
422
for (self.members.items) |*member| {
373
423
if (user == member.user) {
374
424
// Update the prefix for an existing member if the prefix is
···
378
428
}
379
429
}
380
430
381
381
-
try self.members.append(.{ .user = user, .prefix = args.prefix orelse ' ' });
431
431
+
try self.members.append(.{
432
432
+
.user = user,
433
433
+
.prefix = args.prefix orelse ' ',
434
434
+
.channel = self,
435
435
+
});
382
436
383
437
if (args.sort) {
384
438
self.sortMembers();
···
399
453
pub fn markRead(self: *Channel) !void {
400
454
self.has_unread = false;
401
455
self.has_unread_highlight = false;
402
402
-
const last_msg = self.messages.getLast();
456
456
+
const last_msg = self.messages.getLastOrNull() orelse return;
403
457
const time_tag = last_msg.getTag("time") orelse return;
404
458
try self.client.print(
405
459
"MARKREAD {s} timestamp={s}\r\n",
···
413
467
pub fn contentWidget(self: *Channel) vxfw.Widget {
414
468
return .{
415
469
.userdata = self,
470
470
+
.captureHandler = Channel.captureEvent,
416
471
.drawFn = Channel.typeErasedViewDraw,
417
472
};
418
473
}
419
474
475
475
+
fn captureEvent(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
476
476
+
const self: *Channel = @ptrCast(@alignCast(ptr));
477
477
+
switch (event) {
478
478
+
.key_press => |key| {
479
479
+
if (key.matches(vaxis.Key.tab, .{})) {
480
480
+
ctx.redraw = true;
481
481
+
// if we already have a completion word, then we are
482
482
+
// cycling through the options
483
483
+
if (self.completer_shown) {
484
484
+
const line = self.completer.next(ctx);
485
485
+
self.text_field.clearRetainingCapacity();
486
486
+
try self.text_field.insertSliceAtCursor(line);
487
487
+
} else {
488
488
+
var completion_buf: [maximum_message_size]u8 = undefined;
489
489
+
const content = self.text_field.sliceToCursor(&completion_buf);
490
490
+
try self.completer.reset(content);
491
491
+
if (self.completer.kind == .nick) {
492
492
+
try self.completer.findMatches(self);
493
493
+
}
494
494
+
self.completer_shown = true;
495
495
+
}
496
496
+
return;
497
497
+
}
498
498
+
if (key.matches(vaxis.Key.tab, .{ .shift = true })) {
499
499
+
if (self.completer_shown) {
500
500
+
const line = self.completer.prev(ctx);
501
501
+
self.text_field.clearRetainingCapacity();
502
502
+
try self.text_field.insertSliceAtCursor(line);
503
503
+
}
504
504
+
return;
505
505
+
}
506
506
+
if (key.matches(vaxis.Key.page_up, .{})) {
507
507
+
self.scroll.pending += self.client.app.last_height / 2;
508
508
+
try self.doScroll(ctx);
509
509
+
return ctx.consumeAndRedraw();
510
510
+
}
511
511
+
if (key.matches(vaxis.Key.page_down, .{})) {
512
512
+
self.scroll.pending -|= self.client.app.last_height / 2;
513
513
+
try self.doScroll(ctx);
514
514
+
return ctx.consumeAndRedraw();
515
515
+
}
516
516
+
if (key.matches(vaxis.Key.home, .{})) {
517
517
+
self.scroll.pending -= self.scroll.offset;
518
518
+
self.scroll.msg_offset = null;
519
519
+
try self.doScroll(ctx);
520
520
+
return ctx.consumeAndRedraw();
521
521
+
}
522
522
+
if (!key.isModifier()) {
523
523
+
self.completer_shown = false;
524
524
+
}
525
525
+
},
526
526
+
else => {},
527
527
+
}
528
528
+
}
529
529
+
420
530
fn typeErasedViewDraw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
421
531
const self: *Channel = @ptrCast(@alignCast(ptr));
422
532
if (!self.who_requested) {
···
479
589
.bottom = self.scroll.offset,
480
590
};
481
591
const scrollbar_surface = try scrollbars.draw(scrollbar_ctx);
482
482
-
// Draw the text field
483
592
try children.append(.{
484
593
.origin = .{ .col = max.width - 1, .row = 2 },
485
594
.surface = scrollbar_surface,
···
490
599
.origin = .{ .col = 0, .row = max.height - 1 },
491
600
.surface = try self.text_field.draw(ctx),
492
601
});
602
602
+
603
603
+
if (self.completer_shown) {
604
604
+
const widest: u16 = @intCast(self.completer.widestMatch(ctx));
605
605
+
const completer_ctx = ctx.withConstraints(ctx.min, .{ .height = 10, .width = widest });
606
606
+
const surface = try self.completer.list_view.draw(completer_ctx);
607
607
+
const height: u16 = @intCast(@min(10, self.completer.options.items.len));
608
608
+
try children.append(.{
609
609
+
.origin = .{ .col = 0, .row = max.height -| 1 -| height },
610
610
+
.surface = surface,
611
611
+
});
612
612
+
}
493
613
494
614
return .{
495
615
.size = max,
···
578
698
579
699
// Scroll up
580
700
if (self.scroll.pending > 0) {
581
581
-
// TODO: check if we need to get more history
582
582
-
// TODO: cehck if we are at oldest, and shouldn't scroll up anymore
583
583
-
584
701
// Consume 1 line, and schedule a tick
585
702
self.scroll.offset += 1;
586
703
self.scroll.pending -= 1;
···
1346
1463
pub fn view(self: *Client) vxfw.Widget {
1347
1464
return .{
1348
1465
.userdata = self,
1466
1466
+
.eventHandler = Client.eventHandler,
1349
1467
.drawFn = Client.typeErasedViewDraw,
1350
1468
};
1469
1469
+
}
1470
1470
+
1471
1471
+
fn eventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
1472
1472
+
_ = ptr;
1473
1473
+
_ = ctx;
1474
1474
+
_ = event;
1351
1475
}
1352
1476
1353
1477
fn typeErasedViewDraw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
1354
1354
-
_ = ptr;
1478
1478
+
const self: *Client = @ptrCast(@alignCast(ptr));
1355
1479
const text: vxfw.Text = .{ .text = "content" };
1356
1356
-
return text.draw(ctx);
1480
1480
+
var surface = try text.draw(ctx);
1481
1481
+
surface.widget = self.view();
1482
1482
+
return surface;
1357
1483
}
1358
1484
1359
1485
pub fn nameWidget(self: *Client, selected: bool) vxfw.Widget {
···
1806
1932
for (client.channels.items, 0..) |channel, i| {
1807
1933
if (!mem.eql(u8, channel.name, target)) continue;
1808
1934
var chan = client.channels.orderedRemove(i);
1809
1809
-
self.app.state.buffers.selected_idx -|= 1;
1810
1935
chan.deinit(self.app.alloc);
1811
1936
self.alloc.destroy(chan);
1937
1937
+
self.app.buffer_list.cursor -|= 1;
1938
1938
+
self.app.buffer_list.ensureScroll();
1812
1939
break;
1813
1940
}
1814
1941
} else {