tangled
alpha
login
or
join now
rockorager.dev
/
comlink
2
fork
atom
an experimental irc client
2
fork
atom
overview
issues
pulls
pipelines
remove unused code
rockorager.dev
1 year ago
8afedaf0
5dbcd379
+16
-712
1 changed file
expand all
collapse all
unified
split
src
app.zig
+16
-712
src/app.zig
···
344
344
self.markSelectedChannelRead();
345
345
if (self.ctx) |ctx| {
346
346
self.buffer_list.nextItem(ctx);
347
347
+
if (self.selectedBuffer()) |buffer| {
348
348
+
switch (buffer) {
349
349
+
.client => {},
350
350
+
.channel => |channel| {
351
351
+
ctx.requestFocus(channel.text_field.widget()) catch {};
352
352
+
},
353
353
+
}
354
354
+
}
347
355
}
348
356
}
349
357
···
353
361
self.markSelectedChannelRead();
354
362
if (self.ctx) |ctx| {
355
363
self.buffer_list.prevItem(ctx);
364
364
+
if (self.selectedBuffer()) |buffer| {
365
365
+
switch (buffer) {
366
366
+
.client => {},
367
367
+
.channel => |channel| {
368
368
+
ctx.requestFocus(channel.text_field.widget()) catch {};
369
369
+
},
370
370
+
}
371
371
+
}
356
372
}
357
373
}
358
374
···
575
591
}
576
592
}
577
593
},
578
578
-
}
579
579
-
}
580
580
-
581
581
-
fn draw(self: *App, input: *TextInput) !void {
582
582
-
var arena = std.heap.ArenaAllocator.init(self.alloc);
583
583
-
defer arena.deinit();
584
584
-
const allocator = arena.allocator();
585
585
-
586
586
-
// reset window state
587
587
-
const win = self.vx.window();
588
588
-
win.clear();
589
589
-
self.vx.setMouseShape(.default);
590
590
-
591
591
-
// Handle resize of sidebars
592
592
-
if (self.state.mouse) |mouse| {
593
593
-
if (self.state.buffers.resizing) {
594
594
-
self.state.buffers.width = @min(mouse.col, win.width -| self.state.members.width);
595
595
-
} else if (self.state.members.resizing) {
596
596
-
self.state.members.width = win.width -| mouse.col + 1;
597
597
-
}
598
598
-
599
599
-
if (mouse.col == self.state.buffers.width) {
600
600
-
self.vx.setMouseShape(.@"ew-resize");
601
601
-
switch (mouse.type) {
602
602
-
.press => {
603
603
-
if (mouse.button == .left) self.state.buffers.resizing = true;
604
604
-
},
605
605
-
.release => self.state.buffers.resizing = false,
606
606
-
else => {},
607
607
-
}
608
608
-
} else if (mouse.col == win.width -| self.state.members.width + 1) {
609
609
-
self.vx.setMouseShape(.@"ew-resize");
610
610
-
switch (mouse.type) {
611
611
-
.press => {
612
612
-
if (mouse.button == .left) self.state.members.resizing = true;
613
613
-
},
614
614
-
.release => self.state.members.resizing = false,
615
615
-
else => {},
616
616
-
}
617
617
-
}
618
618
-
}
619
619
-
620
620
-
// Define the layout
621
621
-
const buf_list_w = self.state.buffers.width;
622
622
-
const mbr_list_w = self.state.members.width;
623
623
-
const message_list_width = win.width -| buf_list_w -| mbr_list_w;
624
624
-
625
625
-
const channel_list_win = win.child(.{
626
626
-
.width = .{ .limit = self.state.buffers.width + 1 },
627
627
-
.border = .{ .where = .right },
628
628
-
});
629
629
-
630
630
-
const member_list_win = win.child(.{
631
631
-
.x_off = buf_list_w + message_list_width + 1,
632
632
-
.border = .{ .where = .left },
633
633
-
});
634
634
-
635
635
-
const middle_win = win.child(.{
636
636
-
.x_off = buf_list_w + 1,
637
637
-
.width = .{ .limit = message_list_width },
638
638
-
});
639
639
-
640
640
-
const topic_win = middle_win.child(.{
641
641
-
.height = .{ .limit = 2 },
642
642
-
.border = .{ .where = .bottom },
643
643
-
});
644
644
-
645
645
-
const message_list_win = middle_win.child(.{
646
646
-
.y_off = 2,
647
647
-
.height = .{ .limit = middle_win.height -| 4 },
648
648
-
.width = .{ .limit = middle_win.width -| 1 },
649
649
-
});
650
650
-
651
651
-
// Draw the buffer list
652
652
-
try self.drawBufferList(self.clients.items, channel_list_win);
653
653
-
654
654
-
// Get our currently selected buffer and draw it
655
655
-
const buffer = self.selectedBuffer() orelse return;
656
656
-
switch (buffer) {
657
657
-
.client => {}, // nothing to do
658
658
-
659
659
-
.channel => |channel| {
660
660
-
// Request WHO if we don't already have it
661
661
-
if (!channel.who_requested) try channel.client.whox(channel);
662
662
-
663
663
-
// Set the title of the terminal
664
664
-
{
665
665
-
var buf: [64]u8 = undefined;
666
666
-
const title = std.fmt.bufPrint(&buf, "{s} - comlink", .{channel.name}) catch title: {
667
667
-
// If the channel name is too long to fit in our buffer just truncate
668
668
-
const len = @min(buf.len, channel.name.len);
669
669
-
@memcpy(buf[0..len], channel.name[0..len]);
670
670
-
break :title buf[0..len];
671
671
-
};
672
672
-
try self.vx.setTitle(self.tty.anyWriter(), title);
673
673
-
}
674
674
-
675
675
-
// Draw the topic
676
676
-
try self.drawTopic(topic_win, channel.topic orelse "");
677
677
-
678
678
-
// Draw the member list
679
679
-
try self.drawMemberList(member_list_win, channel);
680
680
-
681
681
-
// Draw the message list
682
682
-
try self.drawMessageList(allocator, message_list_win, channel);
683
683
-
684
684
-
// draw a scrollbar
685
685
-
{
686
686
-
const scrollbar: Scrollbar = .{
687
687
-
.total = channel.messages.items.len,
688
688
-
.view_size = message_list_win.height / 3, // ~3 lines per message
689
689
-
.bottom = self.state.messages.scroll_offset,
690
690
-
};
691
691
-
const scrollbar_win = middle_win.child(.{
692
692
-
.x_off = message_list_win.width,
693
693
-
.y_off = 2,
694
694
-
.height = .{ .limit = middle_win.height -| 4 },
695
695
-
});
696
696
-
scrollbar.draw(scrollbar_win);
697
697
-
}
698
698
-
699
699
-
// draw the completion list
700
700
-
if (self.completer) |*completer| {
701
701
-
try completer.findMatches(channel);
702
702
-
703
703
-
var completion_style: vaxis.Style = .{ .bg = .{ .index = 8 } };
704
704
-
const completion_win = middle_win.child(.{
705
705
-
.width = .{ .limit = completer.widestMatch(win) + 1 },
706
706
-
.height = .{ .limit = @min(completer.numMatches(), middle_win.height -| 1) },
707
707
-
.x_off = completer.start_idx,
708
708
-
.y_off = middle_win.height -| completer.numMatches() -| 1,
709
709
-
});
710
710
-
completion_win.fill(.{
711
711
-
.char = .{ .grapheme = " ", .width = 1 },
712
712
-
.style = completion_style,
713
713
-
});
714
714
-
var completion_row: usize = 0;
715
715
-
while (completion_row < completion_win.height) : (completion_row += 1) {
716
716
-
log.debug("COMPLETION ROW {d}, selected_idx {d}", .{ completion_row, completer.selected_idx orelse 0 });
717
717
-
if (completer.selected_idx) |idx| {
718
718
-
if (completion_row == idx)
719
719
-
completion_style.reverse = true
720
720
-
else {
721
721
-
completion_style = .{ .bg = .{ .index = 8 } };
722
722
-
}
723
723
-
}
724
724
-
var seg = [_]vaxis.Segment{
725
725
-
.{
726
726
-
.text = completer.options.items[completer.options.items.len - 1 - completion_row],
727
727
-
.style = completion_style,
728
728
-
},
729
729
-
.{
730
730
-
.text = " ",
731
731
-
.style = completion_style,
732
732
-
},
733
733
-
};
734
734
-
_ = try completion_win.print(&seg, .{
735
735
-
.row_offset = completion_win.height -| completion_row -| 1,
736
736
-
});
737
737
-
}
738
738
-
}
739
739
-
},
740
740
-
}
741
741
-
742
742
-
const input_win = middle_win.child(.{
743
743
-
.y_off = win.height -| 1,
744
744
-
.width = .{ .limit = middle_win.width -| 7 },
745
745
-
.height = .{ .limit = 1 },
746
746
-
});
747
747
-
const len_win = middle_win.child(.{
748
748
-
.x_off = input_win.width,
749
749
-
.y_off = win.height -| 1,
750
750
-
.width = .{ .limit = 7 },
751
751
-
.height = .{ .limit = 1 },
752
752
-
});
753
753
-
const buf_name_len = blk: {
754
754
-
const sel_buf = self.selectedBuffer() orelse @panic("no buffer");
755
755
-
switch (sel_buf) {
756
756
-
.channel => |chan| break :blk chan.name.len,
757
757
-
else => break :blk 0,
758
758
-
}
759
759
-
};
760
760
-
// PRIVMSG <channel_name> :<message>\r\n = 12 bytes of overhead
761
761
-
const max_len = irc.maximum_message_size - buf_name_len - 12;
762
762
-
var len_buf: [7]u8 = undefined;
763
763
-
const msg_len = input.buf.realLength();
764
764
-
_ = try std.fmt.bufPrint(&len_buf, "{d: >3}/{d}", .{ msg_len, max_len });
765
765
-
766
766
-
var len_segs = [_]vaxis.Segment{
767
767
-
.{
768
768
-
.text = len_buf[0..3],
769
769
-
.style = .{ .fg = if (msg_len > max_len)
770
770
-
.{ .index = 1 }
771
771
-
else
772
772
-
.{ .index = 8 } },
773
773
-
},
774
774
-
.{
775
775
-
.text = len_buf[3..],
776
776
-
.style = .{ .fg = .{ .index = 8 } },
777
777
-
},
778
778
-
};
779
779
-
780
780
-
_ = try len_win.print(&len_segs, .{});
781
781
-
input.draw(input_win);
782
782
-
783
783
-
if (self.state.paste.showDialog()) {
784
784
-
// Draw a modal dialog for how to handle multi-line paste
785
785
-
const multiline_paste_win = vaxis.widgets.alignment.center(win, win.width - 10, win.height - 10);
786
786
-
const bordered = vaxis.widgets.border.all(multiline_paste_win, .{});
787
787
-
bordered.clear();
788
788
-
const warning_width: usize = 37;
789
789
-
const title_win = multiline_paste_win.child(.{
790
790
-
.height = .{ .limit = 2 },
791
791
-
.y_off = 1,
792
792
-
.x_off = multiline_paste_win.width / 2 - warning_width / 2,
793
793
-
});
794
794
-
const title_seg = [_]vaxis.Segment{
795
795
-
.{
796
796
-
.text = "/!\\ Warning: Multiline paste detected",
797
797
-
.style = .{
798
798
-
.fg = .{ .index = 3 },
799
799
-
.bold = true,
800
800
-
},
801
801
-
},
802
802
-
};
803
803
-
_ = try title_win.print(&title_seg, .{ .wrap = .none });
804
804
-
var segs = [_]vaxis.Segment{
805
805
-
.{ .text = self.paste_buffer.items },
806
806
-
};
807
807
-
_ = try bordered.print(&segs, .{ .wrap = .grapheme, .row_offset = 2 });
808
808
-
// const button: Button = .{
809
809
-
// .label = "Accept",
810
810
-
// .style = .{ .bg = .{ .index = 7 } },
811
811
-
// };
812
812
-
// try button.draw(bordered.child(.{
813
813
-
// .x_off = 3,
814
814
-
// .y_off = bordered.height - 4,
815
815
-
// .height = .{ .limit = 3 },
816
816
-
// .width = .{ .limit = 10 },
817
817
-
// }));
818
818
-
}
819
819
-
820
820
-
var buffered = self.tty.bufferedWriter();
821
821
-
try self.vx.render(buffered.writer().any());
822
822
-
try buffered.flush();
823
823
-
}
824
824
-
825
825
-
fn drawMessageList(
826
826
-
self: *App,
827
827
-
arena: std.mem.Allocator,
828
828
-
win: vaxis.Window,
829
829
-
channel: *irc.Channel,
830
830
-
) !void {
831
831
-
if (channel.messages.items.len == 0) return;
832
832
-
const client = channel.client;
833
833
-
const last_msg_idx = channel.messages.items.len -| self.state.messages.scroll_offset;
834
834
-
const messages = channel.messages.items[0..@max(1, last_msg_idx)];
835
835
-
// We draw a gutter for time information
836
836
-
const gutter_width: usize = 6;
837
837
-
838
838
-
// Our message list is offset by the gutter width
839
839
-
const message_offset_win = win.child(.{ .x_off = gutter_width });
840
840
-
841
841
-
// Handle mouse
842
842
-
if (win.hasMouse(self.state.mouse)) |mouse| {
843
843
-
switch (mouse.button) {
844
844
-
.wheel_up => {
845
845
-
self.state.messages.scroll_offset +|= 1;
846
846
-
self.state.mouse.?.button = .none;
847
847
-
self.state.messages.pending_scroll += 2;
848
848
-
},
849
849
-
.wheel_down => {
850
850
-
self.state.messages.scroll_offset -|= 1;
851
851
-
self.state.mouse.?.button = .none;
852
852
-
self.state.messages.pending_scroll -= 2;
853
853
-
},
854
854
-
else => {},
855
855
-
}
856
856
-
}
857
857
-
self.state.messages.scroll_offset = @min(
858
858
-
self.state.messages.scroll_offset,
859
859
-
channel.messages.items.len -| 1,
860
860
-
);
861
861
-
862
862
-
// Define a few state variables for the loop
863
863
-
const last_msg = messages[messages.len -| 1];
864
864
-
865
865
-
// Initialize prev_time to the time of the last message, falling back to "now"
866
866
-
var prev_time: zeit.Instant = last_msg.localTime(&self.tz) orelse
867
867
-
try zeit.instant(.{ .source = .now, .timezone = &self.tz });
868
868
-
869
869
-
// Initialize prev_sender to the sender of the last message
870
870
-
var prev_sender: []const u8 = if (last_msg.source()) |src| blk: {
871
871
-
if (std.mem.indexOfScalar(u8, src, '!')) |idx|
872
872
-
break :blk src[0..idx];
873
873
-
if (std.mem.indexOfScalar(u8, src, '@')) |idx|
874
874
-
break :blk src[0..idx];
875
875
-
break :blk src;
876
876
-
} else "";
877
877
-
878
878
-
// y_off is the row we are printing on
879
879
-
var y_off: usize = win.height;
880
880
-
881
881
-
// Formatted message segments
882
882
-
var segments = std.ArrayList(vaxis.Segment).init(arena);
883
883
-
884
884
-
var msg_iter = std.mem.reverseIterator(messages);
885
885
-
var i: usize = messages.len;
886
886
-
while (msg_iter.next()) |message| {
887
887
-
i -|= 1;
888
888
-
segments.clearRetainingCapacity();
889
889
-
890
890
-
// Get the sender nick
891
891
-
const sender: []const u8 = if (message.source()) |src| blk: {
892
892
-
if (std.mem.indexOfScalar(u8, src, '!')) |idx|
893
893
-
break :blk src[0..idx];
894
894
-
if (std.mem.indexOfScalar(u8, src, '@')) |idx|
895
895
-
break :blk src[0..idx];
896
896
-
break :blk src;
897
897
-
} else "";
898
898
-
899
899
-
// Save sender state after this loop
900
900
-
defer prev_sender = sender;
901
901
-
902
902
-
// Before we print the message, we need to decide if we should print the sender name of
903
903
-
// the previous message. There are two cases we do this:
904
904
-
// 1. The previous message was sent by someone other than the current message
905
905
-
// 2. A certain amount of time has elapsed between messages
906
906
-
//
907
907
-
// Each case requires that we have space in the window to print the sender (y_off > 0)
908
908
-
const time_gap = if (message.localTime(&self.tz)) |time| blk: {
909
909
-
// Save message state for next loop
910
910
-
defer prev_time = time;
911
911
-
// time_gap is true when the difference between this message and last message is
912
912
-
// greater than 5 minutes
913
913
-
break :blk (prev_time.timestamp_ns -| time.timestamp_ns) > (5 * std.time.ns_per_min);
914
914
-
} else false;
915
915
-
916
916
-
// Print the sender of the previous message
917
917
-
if (y_off > 0 and (time_gap or !std.mem.eql(u8, prev_sender, sender))) {
918
918
-
// Go up one line
919
919
-
y_off -|= 1;
920
920
-
921
921
-
// Get the user so we have the correct color
922
922
-
const user = try client.getOrCreateUser(prev_sender);
923
923
-
const sender_win = message_offset_win.child(.{
924
924
-
.y_off = y_off,
925
925
-
.height = .{ .limit = 1 },
926
926
-
});
927
927
-
928
928
-
// We will use the result to see if our mouse is hovering over the nickname
929
929
-
const sender_result = try sender_win.printSegment(
930
930
-
.{
931
931
-
.text = prev_sender,
932
932
-
.style = .{ .fg = user.color, .bold = true },
933
933
-
},
934
934
-
.{ .wrap = .none },
935
935
-
);
936
936
-
937
937
-
// If our mouse is over the nickname, we set it to a pointer
938
938
-
const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } });
939
939
-
if (result_win.hasMouse(self.state.mouse)) |_| {
940
940
-
self.vx.setMouseShape(.pointer);
941
941
-
// If we have a realname we print it
942
942
-
if (user.real_name) |real_name| {
943
943
-
_ = try sender_win.printSegment(
944
944
-
.{
945
945
-
.text = real_name,
946
946
-
.style = .{ .italic = true, .dim = true },
947
947
-
},
948
948
-
.{
949
949
-
.wrap = .none,
950
950
-
.col_offset = sender_result.col + 1,
951
951
-
},
952
952
-
);
953
953
-
}
954
954
-
}
955
955
-
956
956
-
// Go up one more line to print the next message
957
957
-
y_off -|= 1;
958
958
-
}
959
959
-
960
960
-
// We are out of space
961
961
-
if (y_off == 0) break;
962
962
-
963
963
-
const user = try client.getOrCreateUser(sender);
964
964
-
try format.message(&segments, user, message);
965
965
-
966
966
-
// Get the line count for this message
967
967
-
const content_height = lineCountForWindow(message_offset_win, segments.items);
968
968
-
969
969
-
const content_win = message_offset_win.child(
970
970
-
.{
971
971
-
.y_off = y_off -| content_height,
972
972
-
.height = .{ .limit = content_height },
973
973
-
},
974
974
-
);
975
975
-
if (content_win.hasMouse(self.state.mouse)) |mouse| {
976
976
-
var bg_idx: u8 = 8;
977
977
-
if (mouse.type == .press and mouse.button == .middle) {
978
978
-
var list = std.ArrayList(u8).init(self.alloc);
979
979
-
defer list.deinit();
980
980
-
for (segments.items) |item| {
981
981
-
try list.appendSlice(item.text);
982
982
-
}
983
983
-
try self.vx.copyToSystemClipboard(self.tty.anyWriter(), list.items, self.alloc);
984
984
-
bg_idx = 3;
985
985
-
}
986
986
-
content_win.fill(.{
987
987
-
.char = .{
988
988
-
.grapheme = " ",
989
989
-
.width = 1,
990
990
-
},
991
991
-
.style = .{
992
992
-
.bg = .{ .index = bg_idx },
993
993
-
},
994
994
-
});
995
995
-
for (segments.items) |*item| {
996
996
-
item.style.bg = .{ .index = bg_idx };
997
997
-
}
998
998
-
}
999
999
-
var iter = message.paramIterator();
1000
1000
-
// target is the channel, and we already handled that
1001
1001
-
_ = iter.next() orelse continue;
1002
1002
-
1003
1003
-
const content = iter.next() orelse continue;
1004
1004
-
if (std.mem.indexOf(u8, content, client.nickname())) |_| {
1005
1005
-
for (segments.items) |*item| {
1006
1006
-
if (item.style.fg == .default)
1007
1007
-
item.style.fg = .{ .index = 3 };
1008
1008
-
}
1009
1009
-
}
1010
1010
-
1011
1011
-
// Color the background of unread messages gray.
1012
1012
-
if (message.localTime(&self.tz)) |instant| {
1013
1013
-
if (instant.unixTimestamp() > channel.last_read) {
1014
1014
-
for (segments.items) |*item| {
1015
1015
-
item.style.bg = .{ .index = 8 };
1016
1016
-
}
1017
1017
-
}
1018
1018
-
}
1019
1019
-
1020
1020
-
_ = try content_win.print(
1021
1021
-
segments.items,
1022
1022
-
.{
1023
1023
-
.wrap = .word,
1024
1024
-
},
1025
1025
-
);
1026
1026
-
if (content_height > y_off) break;
1027
1027
-
const gutter = win.child(.{
1028
1028
-
.y_off = y_off -| content_height,
1029
1029
-
.width = .{ .limit = 6 },
1030
1030
-
});
1031
1031
-
1032
1032
-
if (message.localTime(&self.tz)) |instant| {
1033
1033
-
var date: bool = false;
1034
1034
-
const time = instant.time();
1035
1035
-
var buf = try std.fmt.allocPrint(
1036
1036
-
arena,
1037
1037
-
"{d:0>2}:{d:0>2}",
1038
1038
-
.{ time.hour, time.minute },
1039
1039
-
);
1040
1040
-
if (i != 0 and channel.messages.items[i - 1].time() != null) {
1041
1041
-
const prev = channel.messages.items[i - 1].localTime(&self.tz).?.time();
1042
1042
-
if (time.day != prev.day) {
1043
1043
-
date = true;
1044
1044
-
buf = try std.fmt.allocPrint(
1045
1045
-
arena,
1046
1046
-
"{d:0>2}/{d:0>2}",
1047
1047
-
.{ @intFromEnum(time.month), time.day },
1048
1048
-
);
1049
1049
-
}
1050
1050
-
}
1051
1051
-
if (i == 0) {
1052
1052
-
date = true;
1053
1053
-
buf = try std.fmt.allocPrint(
1054
1054
-
arena,
1055
1055
-
"{d:0>2}/{d:0>2}",
1056
1056
-
.{ @intFromEnum(time.month), time.day },
1057
1057
-
);
1058
1058
-
}
1059
1059
-
const fg: vaxis.Color = if (date)
1060
1060
-
.default
1061
1061
-
else
1062
1062
-
.{ .index = 8 };
1063
1063
-
var time_seg = [_]vaxis.Segment{
1064
1064
-
.{
1065
1065
-
.text = buf,
1066
1066
-
.style = .{ .fg = fg },
1067
1067
-
},
1068
1068
-
};
1069
1069
-
_ = try gutter.print(&time_seg, .{});
1070
1070
-
}
1071
1071
-
1072
1072
-
y_off -|= content_height;
1073
1073
-
1074
1074
-
// If we are on the first message, print the sender
1075
1075
-
if (i == 0) {
1076
1076
-
y_off -|= 1;
1077
1077
-
const sender_win = win.child(.{
1078
1078
-
.x_off = 6,
1079
1079
-
.y_off = y_off,
1080
1080
-
.height = .{ .limit = 1 },
1081
1081
-
});
1082
1082
-
const sender_result = try sender_win.print(
1083
1083
-
&.{.{
1084
1084
-
.text = sender,
1085
1085
-
.style = .{
1086
1086
-
.fg = user.color,
1087
1087
-
.bold = true,
1088
1088
-
},
1089
1089
-
}},
1090
1090
-
.{ .wrap = .word },
1091
1091
-
);
1092
1092
-
const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } });
1093
1093
-
if (result_win.hasMouse(self.state.mouse)) |_| {
1094
1094
-
self.vx.setMouseShape(.pointer);
1095
1095
-
}
1096
1096
-
}
1097
1097
-
1098
1098
-
// if we are on the oldest message, request more history
1099
1099
-
if (i == 0 and !channel.at_oldest) {
1100
1100
-
try client.requestHistory(.before, channel);
1101
1101
-
}
1102
1102
-
}
1103
1103
-
}
1104
1104
-
1105
1105
-
fn drawMemberList(self: *App, win: vaxis.Window, channel: *irc.Channel) !void {
1106
1106
-
// Handle mouse
1107
1107
-
{
1108
1108
-
if (win.hasMouse(self.state.mouse)) |mouse| {
1109
1109
-
switch (mouse.button) {
1110
1110
-
.wheel_up => {
1111
1111
-
self.state.members.scroll_offset -|= 3;
1112
1112
-
self.state.mouse.?.button = .none;
1113
1113
-
},
1114
1114
-
.wheel_down => {
1115
1115
-
self.state.members.scroll_offset +|= 3;
1116
1116
-
self.state.mouse.?.button = .none;
1117
1117
-
},
1118
1118
-
else => {},
1119
1119
-
}
1120
1120
-
}
1121
1121
-
1122
1122
-
self.state.members.scroll_offset = @min(
1123
1123
-
self.state.members.scroll_offset,
1124
1124
-
channel.members.items.len -| win.height,
1125
1125
-
);
1126
1126
-
}
1127
1127
-
1128
1128
-
// Draw the list
1129
1129
-
var member_row: usize = 0;
1130
1130
-
for (channel.members.items) |*member| {
1131
1131
-
defer member_row += 1;
1132
1132
-
if (member_row < self.state.members.scroll_offset) continue;
1133
1133
-
var member_seg = [_]vaxis.Segment{
1134
1134
-
.{
1135
1135
-
.text = std.mem.asBytes(&member.prefix),
1136
1136
-
},
1137
1137
-
.{
1138
1138
-
.text = member.user.nick,
1139
1139
-
.style = .{
1140
1140
-
.fg = if (member.user.away)
1141
1141
-
.{ .index = 8 }
1142
1142
-
else
1143
1143
-
member.user.color,
1144
1144
-
},
1145
1145
-
},
1146
1146
-
};
1147
1147
-
_ = try win.print(&member_seg, .{
1148
1148
-
.row_offset = member_row -| self.state.members.scroll_offset,
1149
1149
-
});
1150
1150
-
}
1151
1151
-
}
1152
1152
-
1153
1153
-
fn drawTopic(_: *App, win: vaxis.Window, topic: []const u8) !void {
1154
1154
-
_ = try win.printSegment(.{ .text = topic }, .{ .wrap = .none });
1155
1155
-
}
1156
1156
-
1157
1157
-
fn drawBufferList(self: *App, clients: []*irc.Client, win: vaxis.Window) !void {
1158
1158
-
// Handle mouse
1159
1159
-
{
1160
1160
-
if (win.hasMouse(self.state.mouse)) |mouse| {
1161
1161
-
switch (mouse.button) {
1162
1162
-
.wheel_up => {
1163
1163
-
self.state.buffers.scroll_offset -|= 3;
1164
1164
-
self.state.mouse.?.button = .none;
1165
1165
-
},
1166
1166
-
.wheel_down => {
1167
1167
-
self.state.buffers.scroll_offset +|= 3;
1168
1168
-
self.state.mouse.?.button = .none;
1169
1169
-
},
1170
1170
-
else => {},
1171
1171
-
}
1172
1172
-
}
1173
1173
-
1174
1174
-
self.state.buffers.scroll_offset = @min(
1175
1175
-
self.state.buffers.scroll_offset,
1176
1176
-
self.state.buffers.count -| win.height,
1177
1177
-
);
1178
1178
-
}
1179
1179
-
const buf_list_w = self.state.buffers.width;
1180
1180
-
var row: usize = 0;
1181
1181
-
1182
1182
-
defer self.state.buffers.count = row;
1183
1183
-
for (clients) |client| {
1184
1184
-
const scroll_offset = self.state.buffers.scroll_offset;
1185
1185
-
if (!(row < scroll_offset)) {
1186
1186
-
var style: vaxis.Style = if (row == self.state.buffers.selected_idx)
1187
1187
-
.{
1188
1188
-
.fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
1189
1189
-
.reverse = true,
1190
1190
-
}
1191
1191
-
else
1192
1192
-
.{
1193
1193
-
.fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
1194
1194
-
};
1195
1195
-
const network_win = win.child(.{
1196
1196
-
.y_off = row,
1197
1197
-
.height = .{ .limit = 1 },
1198
1198
-
});
1199
1199
-
if (network_win.hasMouse(self.state.mouse)) |_| {
1200
1200
-
self.vx.setMouseShape(.pointer);
1201
1201
-
style.bg = .{ .index = 8 };
1202
1202
-
}
1203
1203
-
_ = try network_win.print(
1204
1204
-
&.{.{
1205
1205
-
.text = client.config.name orelse client.config.server,
1206
1206
-
.style = style,
1207
1207
-
}},
1208
1208
-
.{},
1209
1209
-
);
1210
1210
-
if (network_win.hasMouse(self.state.mouse)) |_| {
1211
1211
-
self.vx.setMouseShape(.pointer);
1212
1212
-
}
1213
1213
-
}
1214
1214
-
row += 1;
1215
1215
-
for (client.channels.items) |*channel| {
1216
1216
-
defer row += 1;
1217
1217
-
if (row < scroll_offset) continue;
1218
1218
-
const channel_win = win.child(.{
1219
1219
-
.y_off = row -| scroll_offset,
1220
1220
-
.height = .{ .limit = 1 },
1221
1221
-
});
1222
1222
-
if (channel_win.hasMouse(self.state.mouse)) |mouse| {
1223
1223
-
if (mouse.type == .press and mouse.button == .left and self.state.buffers.selected_idx != row) {
1224
1224
-
// When leaving a channel we mark it as read, so we make sure that's done
1225
1225
-
// before we change to the new channel.
1226
1226
-
self.markSelectedChannelRead();
1227
1227
-
self.state.buffers.selected_idx = row;
1228
1228
-
}
1229
1229
-
}
1230
1230
-
1231
1231
-
const is_current = row == self.state.buffers.selected_idx;
1232
1232
-
var chan_style: vaxis.Style = if (is_current)
1233
1233
-
.{
1234
1234
-
.fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
1235
1235
-
.reverse = true,
1236
1236
-
}
1237
1237
-
else if (channel.has_unread)
1238
1238
-
.{
1239
1239
-
.fg = .{ .index = 4 },
1240
1240
-
.bold = true,
1241
1241
-
}
1242
1242
-
else
1243
1243
-
.{
1244
1244
-
.fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
1245
1245
-
};
1246
1246
-
const prefix: []const u8 = if (channel.name[0] == '#') "#" else "";
1247
1247
-
const name_offset: usize = if (prefix.len > 0) 1 else 0;
1248
1248
-
1249
1249
-
if (channel_win.hasMouse(self.state.mouse)) |mouse| {
1250
1250
-
self.vx.setMouseShape(.pointer);
1251
1251
-
if (mouse.button == .left)
1252
1252
-
chan_style.reverse = true
1253
1253
-
else
1254
1254
-
chan_style.bg = .{ .index = 8 };
1255
1255
-
}
1256
1256
-
1257
1257
-
const first_seg: vaxis.Segment = if (channel.has_unread_highlight)
1258
1258
-
.{ .text = " ●︎", .style = .{ .fg = .{ .index = 1 } } }
1259
1259
-
else
1260
1260
-
.{ .text = " " };
1261
1261
-
1262
1262
-
var chan_seg = [_]vaxis.Segment{
1263
1263
-
first_seg,
1264
1264
-
.{
1265
1265
-
.text = prefix,
1266
1266
-
.style = .{ .fg = .{ .index = 8 } },
1267
1267
-
},
1268
1268
-
.{
1269
1269
-
.text = channel.name[name_offset..],
1270
1270
-
.style = chan_style,
1271
1271
-
},
1272
1272
-
};
1273
1273
-
const result = try channel_win.print(
1274
1274
-
&chan_seg,
1275
1275
-
.{},
1276
1276
-
);
1277
1277
-
if (result.overflow)
1278
1278
-
win.writeCell(
1279
1279
-
buf_list_w -| 1,
1280
1280
-
row -| scroll_offset,
1281
1281
-
.{
1282
1282
-
.char = .{
1283
1283
-
.grapheme = "…",
1284
1284
-
.width = 1,
1285
1285
-
},
1286
1286
-
.style = chan_style,
1287
1287
-
},
1288
1288
-
);
1289
1289
-
}
1290
594
}
1291
595
}
1292
596