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