···11+const std = @import("std");
22+const vaxis = @import("vaxis");
33+44+const Scrollbar = @This();
55+66+/// character to use for the scrollbar
77+character: vaxis.Cell.Character = .{ .grapheme = "▐", .width = 1 },
88+99+/// style to draw the bar character with
1010+style: vaxis.Style = .{},
1111+1212+/// The index of the bottom-most item, with 0 being "at the bottom"
1313+bottom: usize = 0,
1414+1515+/// total items in the list
1616+total: usize,
1717+1818+/// total items that fit within the view area
1919+view_size: usize,
2020+2121+pub fn draw(self: Scrollbar, win: vaxis.Window) void {
2222+ // don't draw when 0 items
2323+ if (self.total < 1) return;
2424+2525+ // don't draw when all items can be shown
2626+ if (self.view_size >= self.total) return;
2727+2828+ // (view_size / total) * window height = size of the scroll bar
2929+ const bar_height = @max(std.math.divCeil(usize, self.view_size * win.height, self.total) catch unreachable, 1);
3030+3131+ // The row of the last cell of the bottom of the bar
3232+ const bar_bottom = (win.height - 1) - (std.math.divCeil(usize, self.bottom * win.height, self.total) catch unreachable);
3333+3434+ var i: usize = 0;
3535+ while (i <= bar_height) : (i += 1)
3636+ win.writeCell(0, bar_bottom -| i, .{ .char = self.character, .style = self.style });
3737+}
+788-450
src/app.zig
···33const vaxis = @import("vaxis");
44const zeit = @import("zeit");
55const ziglua = @import("ziglua");
66+const Scrollbar = @import("Scrollbar.zig");
6778const irc = comlink.irc;
89const lua = comlink.lua;
···2930 } = .{},
3031 messages: struct {
3132 scroll_offset: usize = 0,
3333+ pending_scroll: isize = 0,
3234 } = .{},
3335 buffers: struct {
3436 scroll_offset: usize = 0,
···66686769 state: State = .{},
68706969- content_segments: std.ArrayList(vaxis.Segment),
7070-7171 completer: ?Completer = null,
72727373 should_quit: bool = false,
···8686 .env = env,
8787 .vx = vx,
8888 .tty = try vaxis.Tty.init(),
8989- .content_segments = std.ArrayList(vaxis.Segment).init(alloc),
9089 .binds = try std.ArrayList(Bind).initCapacity(alloc, 16),
9190 .paste_buffer = std.ArrayList(u8).init(alloc),
9291 .tz = try zeit.local(alloc, &env),
···147146 self.vx.deinit(self.alloc, self.tty.anyWriter());
148147 self.tty.deinit();
149148150150- self.content_segments.deinit();
151149 if (self.completer) |*completer| completer.deinit();
152150 self.binds.deinit();
153151 self.paste_buffer.deinit();
···182180 var input = TextInput.init(self.alloc, &self.vx.unicode);
183181 defer input.deinit();
184182183183+ var last_frame: i64 = std.time.milliTimestamp();
185184 loop: while (!self.should_quit) {
186186- loop.pollEvent();
185185+ var redraw: bool = false;
186186+ std.time.sleep(8 * std.time.ns_per_ms);
187187+ if (self.state.messages.pending_scroll != 0) {
188188+ redraw = true;
189189+ if (self.state.messages.pending_scroll > 0) {
190190+ self.state.messages.pending_scroll -= 1;
191191+ self.state.messages.scroll_offset += 1;
192192+ } else {
193193+ self.state.messages.pending_scroll += 1;
194194+ self.state.messages.scroll_offset -|= 1;
195195+ }
196196+ }
187197 while (loop.tryEvent()) |event| {
198198+ redraw = true;
188199 switch (event) {
189200 .redraw => {},
190201 .key_press => |key| {
···734745 }
735746 }
736747737737- try self.draw(&input);
748748+ if (redraw) {
749749+ try self.draw(&input);
750750+ last_frame = std.time.milliTimestamp();
751751+ }
738752 }
739753 }
740754 pub fn nextChannel(self: *App) void {
···913927 }
914928 }
915929930930+ // Define the layout
916931 const buf_list_w = self.state.buffers.width;
917932 const mbr_list_w = self.state.members.width;
918933 const message_list_width = win.width -| buf_list_w -| mbr_list_w;
···937952 .border = .{ .where = .bottom },
938953 });
939954940940- var row: usize = 0;
941941- for (self.clients.items) |client| {
942942- var style: vaxis.Style = if (row == self.state.buffers.selected_idx)
943943- .{
944944- .fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
945945- .reverse = true,
946946- }
947947- else
948948- .{
949949- .fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
950950- };
951951- const network_win = channel_list_win.child(.{
952952- .y_off = row,
953953- .height = .{ .limit = 1 },
954954- });
955955- if (network_win.hasMouse(self.state.mouse)) |_| {
956956- self.vx.setMouseShape(.pointer);
957957- style.bg = .{ .index = 8 };
958958- }
959959- _ = try network_win.print(
960960- &.{.{
961961- .text = client.config.name orelse client.config.server,
962962- .style = style,
963963- }},
964964- .{},
965965- );
966966- if (network_win.hasMouse(self.state.mouse)) |_| {
967967- self.vx.setMouseShape(.pointer);
968968- }
969969- row += 1;
955955+ const message_list_win = middle_win.child(.{
956956+ .y_off = 2,
957957+ .height = .{ .limit = middle_win.height -| 4 },
958958+ .width = .{ .limit = middle_win.width -| 1 },
959959+ });
970960971971- for (client.channels.items) |*channel| {
972972- const channel_win = channel_list_win.child(.{
973973- .y_off = row,
974974- .height = .{ .limit = 1 },
975975- });
976976- if (channel_win.hasMouse(self.state.mouse)) |mouse| {
977977- if (mouse.type == .press and mouse.button == .left) {
978978- self.state.buffers.selected_idx = row;
979979- }
980980- }
961961+ // Draw the buffer list
962962+ try self.drawBufferList(self.clients.items, channel_list_win);
981963982982- const is_current = row == self.state.buffers.selected_idx;
983983- var chan_style: vaxis.Style = if (is_current)
984984- .{
985985- .fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
986986- .reverse = true,
987987- }
988988- else if (channel.has_unread)
989989- .{
990990- .fg = .{ .index = 4 },
991991- .bold = true,
992992- }
993993- else
994994- .{
995995- .fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
996996- };
997997- defer row += 1;
998998- const prefix: []const u8 = if (channel.name[0] == '#') "#" else "";
999999- const name_offset: usize = if (prefix.len > 0) 1 else 0;
964964+ // Get our currently selected buffer and draw it
965965+ const buffer = self.selectedBuffer() orelse return;
966966+ switch (buffer) {
967967+ .client => {}, // nothing to do
100096810011001- if (channel_win.hasMouse(self.state.mouse)) |mouse| {
10021002- self.vx.setMouseShape(.pointer);
10031003- if (mouse.button == .left)
10041004- chan_style.reverse = true
10051005- else
10061006- chan_style.bg = .{ .index = 8 };
10071007- }
969969+ .channel => |channel| {
970970+ // Mark the channel as read
971971+ try channel.markRead();
100897210091009- const first_seg: vaxis.Segment = if (channel.has_unread_highlight)
10101010- .{ .text = " ●︎", .style = .{ .fg = .{ .index = 1 } } }
10111011- else
10121012- .{ .text = " " };
973973+ // Request WHO if we don't already have it
974974+ if (!channel.who_requested) try channel.client.whox(channel);
101397510141014- var chan_seg = [_]vaxis.Segment{
10151015- first_seg,
10161016- .{
10171017- .text = prefix,
10181018- .style = .{ .fg = .{ .index = 8 } },
10191019- },
10201020- .{
10211021- .text = channel.name[name_offset..],
10221022- .style = chan_style,
10231023- },
10241024- };
10251025- const result = try channel_win.print(
10261026- &chan_seg,
10271027- .{},
10281028- );
10291029- if (result.overflow)
10301030- channel_list_win.writeCell(
10311031- buf_list_w -| 1,
10321032- row,
10331033- .{
10341034- .char = .{
10351035- .grapheme = "…",
10361036- .width = 1,
10371037- },
10381038- .style = chan_style,
10391039- },
10401040- );
10411041- if (is_current) {
10421042- var write_buf: [128]u8 = undefined;
10431043- if (channel.has_unread) {
10441044- channel.has_unread = false;
10451045- channel.has_unread_highlight = false;
10461046- const last_msg = channel.messages.getLast();
10471047- var tag_iter = last_msg.tagIterator();
10481048- while (tag_iter.next()) |tag| {
10491049- if (!std.mem.eql(u8, tag.key, "time")) continue;
10501050- const mark_read = try std.fmt.bufPrint(
10511051- &write_buf,
10521052- "MARKREAD {s} timestamp={s}\r\n",
10531053- .{
10541054- channel.name,
10551055- tag.value,
10561056- },
10571057- );
10581058- try client.queueWrite(mark_read);
10591059- }
10601060- }
10611061- if (!channel.who_requested) try client.whox(channel);
10621062- var topic_seg = [_]vaxis.Segment{
10631063- .{
10641064- .text = channel.topic orelse "",
10651065- },
976976+ // Set the title of the terminal
977977+ {
978978+ var buf: [64]u8 = undefined;
979979+ const title = std.fmt.bufPrint(&buf, "{s} - comlink", .{channel.name}) catch title: {
980980+ // If the channel name is too long to fit in our buffer just truncate
981981+ const len = @min(buf.len, channel.name.len);
982982+ @memcpy(buf[0..len], channel.name[0..len]);
983983+ break :title buf[0..len];
1066984 };
10671067- _ = try topic_win.print(&topic_seg, .{ .wrap = .none });
985985+ try self.vx.setTitle(self.tty.anyWriter(), title);
986986+ }
106898710691069- {
10701070- var buf: [64]u8 = undefined;
10711071- const title = std.fmt.bufPrint(&buf, "{s} - comlink", .{channel.name}) catch title: {
10721072- // If the channel name is too long to fit in our buffer just truncate
10731073- const len = @min(buf.len, channel.name.len);
10741074- @memcpy(buf[0..len], channel.name[0..len]);
10751075- break :title buf[0..len];
10761076- };
10771077- try self.vx.setTitle(self.tty.anyWriter(), title);
10781078- }
988988+ // Draw the topic
989989+ try self.drawTopic(topic_win, channel.topic orelse "");
107999010801080- if (member_list_win.hasMouse(self.state.mouse)) |mouse| {
10811081- switch (mouse.button) {
10821082- .wheel_up => {
10831083- self.state.members.scroll_offset -|= 3;
10841084- self.state.mouse.?.button = .none;
10851085- },
10861086- .wheel_down => {
10871087- self.state.members.scroll_offset +|= 3;
10881088- self.state.mouse.?.button = .none;
10891089- },
10901090- else => {},
10911091- }
10921092- }
991991+ // Draw the member list
992992+ try self.drawMemberList(member_list_win, channel);
109399310941094- self.state.members.scroll_offset = @min(self.state.members.scroll_offset, channel.members.items.len -| member_list_win.height);
994994+ // Draw the message list
995995+ try self.drawMessageList(allocator, message_list_win, channel);
109599610961096- var member_row: usize = 0;
10971097- for (channel.members.items) |*member| {
10981098- defer member_row += 1;
10991099- if (member_row < self.state.members.scroll_offset) continue;
11001100- var member_seg = [_]vaxis.Segment{
11011101- .{
11021102- .text = std.mem.asBytes(&member.prefix),
11031103- },
11041104- .{
11051105- .text = member.user.nick,
11061106- .style = .{
11071107- .fg = if (member.user.away)
11081108- .{ .index = 8 }
11091109- else
11101110- member.user.color,
11111111- },
11121112- },
11131113- };
11141114- _ = try member_list_win.print(
11151115- &member_seg,
11161116- .{
11171117- .row_offset = member_row - self.state.members.scroll_offset,
11181118- },
11191119- );
11201120- }
11211121-11221122- // loop the messages and print from the last line to current
11231123- // line
11241124- const message_list_win = middle_win.child(.{
997997+ // draw a scrollbar
998998+ {
999999+ const scrollbar: Scrollbar = .{
10001000+ .total = channel.messages.items.len,
10011001+ .view_size = message_list_win.height / 3, // ~3 lines per message
10021002+ .bottom = self.state.messages.scroll_offset,
10031003+ };
10041004+ const scrollbar_win = middle_win.child(.{
10051005+ .x_off = message_list_win.width,
11251006 .y_off = 2,
11261126- .height = .{ .limit = middle_win.height -| 3 },
11271127- .width = .{ .limit = middle_win.width -| 1 },
10071007+ .height = .{ .limit = middle_win.height -| 4 },
11281008 });
11291129- if (message_list_win.hasMouse(self.state.mouse)) |mouse| {
11301130- switch (mouse.button) {
11311131- .wheel_up => {
11321132- self.state.messages.scroll_offset +|= 3;
11331133- self.state.mouse.?.button = .none;
11341134- },
11351135- .wheel_down => {
11361136- self.state.messages.scroll_offset -|= 3;
11371137- self.state.mouse.?.button = .none;
11381138- },
11391139- else => {},
11401140- }
11411141- }
11421142- self.state.messages.scroll_offset = @min(self.state.messages.scroll_offset, channel.messages.items.len -| 1);
11431143- const message_offset_win = message_list_win.child(.{
11441144- .x_off = 6,
11451145- });
10091009+ scrollbar.draw(scrollbar_win);
10101010+ }
1146101111471147- var prev_sender: ?[]const u8 = null;
11481148- var prev_time: ?zeit.Instant = null;
11491149- var i: usize = channel.messages.items.len -| self.state.messages.scroll_offset;
11501150- var y_off: usize = message_list_win.height -| 1;
11511151- while (i > 0) {
11521152- i -= 1;
11531153- var message = channel.messages.items[i];
11541154- // syntax: <target> <message>
11551155-11561156- const sender: []const u8 = blk: {
11571157- const src = message.source() orelse break :blk "";
11581158- const l = std.mem.indexOfScalar(u8, src, '!') orelse
11591159- std.mem.indexOfScalar(u8, src, '@') orelse
11601160- src.len;
11611161- break :blk src[0..l];
11621162- };
11631163- {
11641164- // If this sender is not the same as the previous
11651165- // printed message OR if the previous message was sent
11661166- // more than some interval ago, then we'll print the
11671167- // previous sender and keep going
11681168- defer prev_sender = sender;
11691169- defer prev_time = message.time();
10121012+ // draw the completion list
10131013+ if (self.completer) |*completer| {
10141014+ try completer.findMatches(channel);
1170101511711171- const time_gap: bool = time_gap: {
11721172- // We are iterating through the messages in reverse,
11731173- // so the timestamp of the "previous" message is
11741174- // greater than the timestamp of the current message
11751175- const t1 = if (message.time()) |t| t.timestamp else break :time_gap false;
11761176- const t2 = if (prev_time) |t| t.timestamp else break :time_gap false;
11771177- break :time_gap @divTrunc(t2 - t1, std.time.ns_per_min) > 5;
11781178- };
11791179-11801180- if (y_off > 0 and
11811181- prev_sender != null and
11821182- (time_gap or !mem.eql(u8, sender, prev_sender.?)))
11831183- {
11841184- y_off -|= 1;
11851185- const user = try client.getOrCreateUser(prev_sender.?);
11861186- const sender_win = message_list_win.child(.{
11871187- .x_off = 6,
11881188- .y_off = y_off,
11891189- .height = .{ .limit = 1 },
11901190- });
11911191- const sender_result = try sender_win.print(
11921192- &.{.{
11931193- .text = prev_sender.?,
11941194- .style = .{
11951195- .fg = user.color,
11961196- .bold = true,
11971197- },
11981198- }},
11991199- .{ .wrap = .word },
12001200- );
12011201- y_off -|= 1;
12021202- const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } });
12031203- if (result_win.hasMouse(self.state.mouse)) |_| {
12041204- self.vx.setMouseShape(.pointer);
12051205- }
10161016+ var completion_style: vaxis.Style = .{ .bg = .{ .index = 8 } };
10171017+ const completion_win = middle_win.child(.{
10181018+ .width = .{ .limit = completer.widestMatch(win) + 1 },
10191019+ .height = .{ .limit = @min(completer.numMatches(), middle_win.height -| 1) },
10201020+ .x_off = completer.start_idx,
10211021+ .y_off = middle_win.height -| completer.numMatches() -| 1,
10221022+ });
10231023+ completion_win.fill(.{
10241024+ .char = .{ .grapheme = " ", .width = 1 },
10251025+ .style = completion_style,
10261026+ });
10271027+ var completion_row: usize = 0;
10281028+ while (completion_row < completion_win.height) : (completion_row += 1) {
10291029+ log.debug("COMPLETION ROW {d}, selected_idx {d}", .{ completion_row, completer.selected_idx orelse 0 });
10301030+ if (completer.selected_idx) |idx| {
10311031+ if (completion_row == idx)
10321032+ completion_style.reverse = true
10331033+ else {
10341034+ completion_style = .{ .bg = .{ .index = 8 } };
12061035 }
12071036 }
12081208-12091209- if (y_off == 0) break;
12101210-12111211- try self.formatMessageContent(client, message);
12121212- defer self.content_segments.clearRetainingCapacity();
12131213- // print the content first
12141214- const print_result = try message_offset_win.print(self.content_segments.items, .{
12151215- .wrap = .word,
12161216- .commit = false,
12171217- });
12181218- const content_height = if (print_result.col == 0)
12191219- print_result.row
12201220- else
12211221- print_result.row + 1;
12221222-12231223- const height = if (content_height > y_off) content_height - y_off else content_height;
12241224- if (height == 0) break;
12251225- const content_win = message_offset_win.child(
10371037+ var seg = [_]vaxis.Segment{
12261038 .{
12271227- .y_off = y_off -| content_height,
12281228- .height = .{ .limit = height },
10391039+ .text = completer.options.items[completer.options.items.len - 1 - completion_row],
10401040+ .style = completion_style,
12291041 },
12301230- );
12311231- if (content_win.hasMouse(self.state.mouse)) |mouse| {
12321232- var bg_idx: u8 = 8;
12331233- if (mouse.type == .press and mouse.button == .middle) {
12341234- var list = std.ArrayList(u8).init(self.alloc);
12351235- defer list.deinit();
12361236- for (self.content_segments.items) |item| {
12371237- try list.appendSlice(item.text);
12381238- }
12391239- try self.vx.copyToSystemClipboard(self.tty.anyWriter(), list.items, self.alloc);
12401240- bg_idx = 3;
12411241- }
12421242- content_win.fill(.{
12431243- .char = .{
12441244- .grapheme = " ",
12451245- .width = 1,
12461246- },
12471247- .style = .{
12481248- .bg = .{ .index = bg_idx },
12491249- },
12501250- });
12511251- for (self.content_segments.items) |*item| {
12521252- item.style.bg = .{ .index = bg_idx };
12531253- }
12541254- }
12551255- var iter = message.paramIterator();
12561256- // target is the channel, and we already handled that
12571257- _ = iter.next() orelse continue;
12581258-12591259- const content = iter.next() orelse continue;
12601260- if (std.mem.indexOf(u8, content, client.config.nick)) |_| {
12611261- for (self.content_segments.items) |*item| {
12621262- if (item.style.fg == .default)
12631263- item.style.fg = .{ .index = 3 };
12641264- }
12651265- }
12661266- _ = try content_win.print(
12671267- self.content_segments.items,
12681042 .{
12691269- .wrap = .word,
12701270- // .skip_n_rows = content_height - content_win.height,
10431043+ .text = " ",
10441044+ .style = completion_style,
12711045 },
12721272- );
12731273- if (content_height > y_off) break;
12741274- const gutter = message_list_win.child(.{
12751275- .y_off = y_off -| content_height,
12761276- .width = .{ .limit = 6 },
12771277- });
12781278-12791279- if (message.localTime(&self.tz)) |instant| {
12801280- var date: bool = false;
12811281- const time = instant.time();
12821282- var buf = try std.fmt.allocPrint(
12831283- allocator,
12841284- "{d:0>2}:{d:0>2}",
12851285- .{ time.hour, time.minute },
12861286- );
12871287- if (i != 0 and channel.messages.items[i - 1].time() != null) {
12881288- const prev = channel.messages.items[i - 1].localTime(&self.tz).?.time();
12891289- if (time.day != prev.day) {
12901290- date = true;
12911291- buf = try std.fmt.allocPrint(
12921292- allocator,
12931293- "{d:0>2}/{d:0>2}",
12941294- .{ @intFromEnum(time.month), time.day },
12951295- );
12961296- }
12971297- }
12981298- if (i == 0) {
12991299- date = true;
13001300- buf = try std.fmt.allocPrint(
13011301- allocator,
13021302- "{d:0>2}/{d:0>2}",
13031303- .{ @intFromEnum(time.month), time.day },
13041304- );
13051305- }
13061306- const fg: vaxis.Color = if (date)
13071307- .default
13081308- else
13091309- .{ .index = 8 };
13101310- var time_seg = [_]vaxis.Segment{
13111311- .{
13121312- .text = buf,
13131313- .style = .{ .fg = fg },
13141314- },
13151315- };
13161316- _ = try gutter.print(&time_seg, .{});
13171317- }
13181318-13191319- y_off -|= content_height;
13201320-13211321- // If we are on the first message, print the sender
13221322- if (i == 0) {
13231323- y_off -|= 1;
13241324- const user = try client.getOrCreateUser(sender);
13251325- const sender_win = message_list_win.child(.{
13261326- .x_off = 6,
13271327- .y_off = y_off,
13281328- .height = .{ .limit = 1 },
13291329- });
13301330- const sender_result = try sender_win.print(
13311331- &.{.{
13321332- .text = sender,
13331333- .style = .{
13341334- .fg = user.color,
13351335- .bold = true,
13361336- },
13371337- }},
13381338- .{ .wrap = .word },
13391339- );
13401340- const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } });
13411341- if (result_win.hasMouse(self.state.mouse)) |_| {
13421342- self.vx.setMouseShape(.pointer);
13431343- }
13441344- }
13451345-13461346- // if we are on the oldest message, request more history
13471347- if (i == 0 and !channel.at_oldest) {
13481348- try client.requestHistory(.before, channel);
13491349- }
13501350- }
13511351- {
13521352- // draw a scrollbar
13531353- var scrollbar: vaxis.widgets.Scrollbar = .{
13541354- .total = channel.messages.items.len,
13551355- .view_size = channel.messages.items.len -| self.state.messages.scroll_offset -| i,
13561356- .top = i,
13571046 };
13581358- const scrollbar_win = middle_win.child(.{
13591359- .x_off = message_list_win.width,
13601360- .y_off = 2,
13611361- .height = .{ .limit = middle_win.height -| 3 },
10471047+ _ = try completion_win.print(&seg, .{
10481048+ .row_offset = completion_win.height -| completion_row -| 1,
13621049 });
13631363- scrollbar.draw(scrollbar_win);
13641364- }
13651365- if (self.completer) |*completer| {
13661366- try completer.findMatches(channel);
13671367-13681368- var completion_style: vaxis.Style = .{ .bg = .{ .index = 8 } };
13691369- const completion_win = middle_win.child(.{
13701370- .width = .{ .limit = completer.widestMatch(win) + 1 },
13711371- .height = .{ .limit = @min(completer.numMatches(), middle_win.height -| 1) },
13721372- .x_off = completer.start_idx,
13731373- .y_off = middle_win.height -| completer.numMatches() -| 1,
13741374- });
13751375- completion_win.fill(.{
13761376- .char = .{ .grapheme = " ", .width = 1 },
13771377- .style = completion_style,
13781378- });
13791379- var completion_row: usize = 0;
13801380- while (completion_row < completion_win.height) : (completion_row += 1) {
13811381- log.debug("COMPLETION ROW {d}, selected_idx {d}", .{ completion_row, completer.selected_idx orelse 0 });
13821382- if (completer.selected_idx) |idx| {
13831383- if (completion_row == idx)
13841384- completion_style.reverse = true
13851385- else {
13861386- completion_style = .{ .bg = .{ .index = 8 } };
13871387- }
13881388- }
13891389- var seg = [_]vaxis.Segment{
13901390- .{
13911391- .text = completer.options.items[completer.options.items.len - 1 - completion_row],
13921392- .style = completion_style,
13931393- },
13941394- .{
13951395- .text = " ",
13961396- .style = completion_style,
13971397- },
13981398- };
13991399- _ = try completion_win.print(&seg, .{
14001400- .row_offset = completion_win.height -| completion_row -| 1,
14011401- });
14021402- }
14031050 }
14041051 }
14051405- }
10521052+ },
14061053 }
1407105414081055 const input_win = middle_win.child(.{
···14831130 // }));
14841131 }
1485113214861486- self.state.buffers.count = row;
14871487-14881133 var buffered = self.tty.bufferedWriter();
14891134 try self.vx.render(buffered.writer().any());
14901135 try buffered.flush();
14911136 }
1492113711381138+ fn drawMessageList(
11391139+ self: *App,
11401140+ arena: std.mem.Allocator,
11411141+ win: vaxis.Window,
11421142+ channel: *irc.Channel,
11431143+ ) !void {
11441144+ if (channel.messages.items.len == 0) return;
11451145+ const client = channel.client;
11461146+ const messages = channel.messages.items[0 .. channel.messages.items.len - self.state.messages.scroll_offset];
11471147+ // We draw a gutter for time information
11481148+ const gutter_width: usize = 6;
11491149+11501150+ // Our message list is offset by the gutter width
11511151+ const message_offset_win = win.child(.{ .x_off = gutter_width });
11521152+11531153+ // Handle mouse
11541154+ if (win.hasMouse(self.state.mouse)) |mouse| {
11551155+ switch (mouse.button) {
11561156+ .wheel_up => {
11571157+ self.state.messages.scroll_offset +|= 1;
11581158+ self.state.mouse.?.button = .none;
11591159+ self.state.messages.pending_scroll += 2;
11601160+ },
11611161+ .wheel_down => {
11621162+ self.state.messages.scroll_offset -|= 1;
11631163+ self.state.mouse.?.button = .none;
11641164+ self.state.messages.pending_scroll -= 2;
11651165+ },
11661166+ else => {},
11671167+ }
11681168+ }
11691169+ self.state.messages.scroll_offset = @min(
11701170+ self.state.messages.scroll_offset,
11711171+ channel.messages.items.len -| 1,
11721172+ );
11731173+11741174+ // Define a few state variables for the loop
11751175+ const last_msg = messages[messages.len - 1];
11761176+11771177+ // Initialize prev_time to the time of the last message, falling back to "now"
11781178+ var prev_time: zeit.Instant = last_msg.localTime(&self.tz) orelse
11791179+ try zeit.instant(.{ .source = .now, .timezone = &self.tz });
11801180+11811181+ // Initialize prev_sender to the sender of the last message
11821182+ var prev_sender: []const u8 = if (last_msg.source()) |src| blk: {
11831183+ if (std.mem.indexOfScalar(u8, src, '!')) |idx|
11841184+ break :blk src[0..idx];
11851185+ if (std.mem.indexOfScalar(u8, src, '@')) |idx|
11861186+ break :blk src[0..idx];
11871187+ break :blk src;
11881188+ } else "";
11891189+11901190+ // y_off is the row we are printing on
11911191+ var y_off: usize = win.height;
11921192+11931193+ // Formatted message segments
11941194+ var segments = std.ArrayList(vaxis.Segment).init(arena);
11951195+11961196+ var msg_iter = std.mem.reverseIterator(messages);
11971197+ var i: usize = messages.len;
11981198+ while (msg_iter.next()) |message| {
11991199+ i -|= 1;
12001200+ segments.clearRetainingCapacity();
12011201+12021202+ // Get the sender nick
12031203+ const sender: []const u8 = if (message.source()) |src| blk: {
12041204+ if (std.mem.indexOfScalar(u8, src, '!')) |idx|
12051205+ break :blk src[0..idx];
12061206+ if (std.mem.indexOfScalar(u8, src, '@')) |idx|
12071207+ break :blk src[0..idx];
12081208+ break :blk src;
12091209+ } else "";
12101210+12111211+ // Save sender state after this loop
12121212+ defer prev_sender = sender;
12131213+12141214+ // Before we print the message, we need to decide if we should print the sender name of
12151215+ // the previous message. There are two cases we do this:
12161216+ // 1. The previous message was sent by someone other than the current message
12171217+ // 2. A certain amount of time has elapsed between messages
12181218+ //
12191219+ // Each case requires that we have space in the window to print the sender (y_off > 0)
12201220+ const time_gap = if (message.localTime(&self.tz)) |time| blk: {
12211221+ // Save message state for next loop
12221222+ defer prev_time = time;
12231223+ // time_gap is true when the difference between this message and last message is
12241224+ // greater than 5 minutes
12251225+ break :blk (prev_time.timestamp -| time.timestamp) > (5 * std.time.ns_per_min);
12261226+ } else false;
12271227+12281228+ // Print the sender of the previous message
12291229+ if (y_off > 0 and (time_gap or !std.mem.eql(u8, prev_sender, sender))) {
12301230+ // Go up one line
12311231+ y_off -|= 1;
12321232+12331233+ // Get the user so we have the correct color
12341234+ const user = try client.getOrCreateUser(prev_sender);
12351235+ const sender_win = message_offset_win.child(.{
12361236+ .y_off = y_off,
12371237+ .height = .{ .limit = 1 },
12381238+ });
12391239+12401240+ // We will use the result to see if our mouse is hovering over the nickname
12411241+ const sender_result = try sender_win.printSegment(
12421242+ .{
12431243+ .text = prev_sender,
12441244+ .style = .{ .fg = user.color, .bold = true },
12451245+ },
12461246+ .{ .wrap = .none },
12471247+ );
12481248+12491249+ // If our mouse is over the nickname, we set it to a pointer
12501250+ const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } });
12511251+ if (result_win.hasMouse(self.state.mouse)) |_| {
12521252+ self.vx.setMouseShape(.pointer);
12531253+ }
12541254+12551255+ // Go up one more line to print the next message
12561256+ y_off -|= 1;
12571257+ }
12581258+12591259+ // We are out of space
12601260+ if (y_off == 0) break;
12611261+12621262+ const user = try client.getOrCreateUser(sender);
12631263+ try formatMessage(&segments, user, message);
12641264+12651265+ // Get the line count for this message
12661266+ const content_height = lineCountForWindow(message_offset_win, segments.items);
12671267+12681268+ const content_win = message_offset_win.child(
12691269+ .{
12701270+ .y_off = y_off -| content_height,
12711271+ .height = .{ .limit = content_height },
12721272+ },
12731273+ );
12741274+ if (content_win.hasMouse(self.state.mouse)) |mouse| {
12751275+ var bg_idx: u8 = 8;
12761276+ if (mouse.type == .press and mouse.button == .middle) {
12771277+ var list = std.ArrayList(u8).init(self.alloc);
12781278+ defer list.deinit();
12791279+ for (segments.items) |item| {
12801280+ try list.appendSlice(item.text);
12811281+ }
12821282+ try self.vx.copyToSystemClipboard(self.tty.anyWriter(), list.items, self.alloc);
12831283+ bg_idx = 3;
12841284+ }
12851285+ content_win.fill(.{
12861286+ .char = .{
12871287+ .grapheme = " ",
12881288+ .width = 1,
12891289+ },
12901290+ .style = .{
12911291+ .bg = .{ .index = bg_idx },
12921292+ },
12931293+ });
12941294+ for (segments.items) |*item| {
12951295+ item.style.bg = .{ .index = bg_idx };
12961296+ }
12971297+ }
12981298+ var iter = message.paramIterator();
12991299+ // target is the channel, and we already handled that
13001300+ _ = iter.next() orelse continue;
13011301+13021302+ const content = iter.next() orelse continue;
13031303+ if (std.mem.indexOf(u8, content, client.config.nick)) |_| {
13041304+ for (segments.items) |*item| {
13051305+ if (item.style.fg == .default)
13061306+ item.style.fg = .{ .index = 3 };
13071307+ }
13081308+ }
13091309+ _ = try content_win.print(
13101310+ segments.items,
13111311+ .{
13121312+ .wrap = .word,
13131313+ },
13141314+ );
13151315+ if (content_height > y_off) break;
13161316+ const gutter = win.child(.{
13171317+ .y_off = y_off -| content_height,
13181318+ .width = .{ .limit = 6 },
13191319+ });
13201320+13211321+ if (message.localTime(&self.tz)) |instant| {
13221322+ var date: bool = false;
13231323+ const time = instant.time();
13241324+ var buf = try std.fmt.allocPrint(
13251325+ arena,
13261326+ "{d:0>2}:{d:0>2}",
13271327+ .{ time.hour, time.minute },
13281328+ );
13291329+ if (i != 0 and channel.messages.items[i - 1].time() != null) {
13301330+ const prev = channel.messages.items[i - 1].localTime(&self.tz).?.time();
13311331+ if (time.day != prev.day) {
13321332+ date = true;
13331333+ buf = try std.fmt.allocPrint(
13341334+ arena,
13351335+ "{d:0>2}/{d:0>2}",
13361336+ .{ @intFromEnum(time.month), time.day },
13371337+ );
13381338+ }
13391339+ }
13401340+ if (i == 0) {
13411341+ date = true;
13421342+ buf = try std.fmt.allocPrint(
13431343+ arena,
13441344+ "{d:0>2}/{d:0>2}",
13451345+ .{ @intFromEnum(time.month), time.day },
13461346+ );
13471347+ }
13481348+ const fg: vaxis.Color = if (date)
13491349+ .default
13501350+ else
13511351+ .{ .index = 8 };
13521352+ var time_seg = [_]vaxis.Segment{
13531353+ .{
13541354+ .text = buf,
13551355+ .style = .{ .fg = fg },
13561356+ },
13571357+ };
13581358+ _ = try gutter.print(&time_seg, .{});
13591359+ }
13601360+13611361+ y_off -|= content_height;
13621362+13631363+ // If we are on the first message, print the sender
13641364+ if (i == 0) {
13651365+ y_off -|= 1;
13661366+ const sender_win = win.child(.{
13671367+ .x_off = 6,
13681368+ .y_off = y_off,
13691369+ .height = .{ .limit = 1 },
13701370+ });
13711371+ const sender_result = try sender_win.print(
13721372+ &.{.{
13731373+ .text = sender,
13741374+ .style = .{
13751375+ .fg = user.color,
13761376+ .bold = true,
13771377+ },
13781378+ }},
13791379+ .{ .wrap = .word },
13801380+ );
13811381+ const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } });
13821382+ if (result_win.hasMouse(self.state.mouse)) |_| {
13831383+ self.vx.setMouseShape(.pointer);
13841384+ }
13851385+ }
13861386+13871387+ // if we are on the oldest message, request more history
13881388+ if (i == 0 and !channel.at_oldest) {
13891389+ try client.requestHistory(.before, channel);
13901390+ }
13911391+ }
13921392+ }
13931393+13941394+ fn drawMemberList(self: *App, win: vaxis.Window, channel: *irc.Channel) !void {
13951395+ // Handle mouse
13961396+ {
13971397+ if (win.hasMouse(self.state.mouse)) |mouse| {
13981398+ switch (mouse.button) {
13991399+ .wheel_up => {
14001400+ self.state.members.scroll_offset -|= 3;
14011401+ self.state.mouse.?.button = .none;
14021402+ },
14031403+ .wheel_down => {
14041404+ self.state.members.scroll_offset +|= 3;
14051405+ self.state.mouse.?.button = .none;
14061406+ },
14071407+ else => {},
14081408+ }
14091409+ }
14101410+14111411+ self.state.members.scroll_offset = @min(
14121412+ self.state.members.scroll_offset,
14131413+ channel.members.items.len -| win.height,
14141414+ );
14151415+ }
14161416+14171417+ // Draw the list
14181418+ var member_row: usize = 0;
14191419+ for (channel.members.items) |*member| {
14201420+ defer member_row += 1;
14211421+ if (member_row < self.state.members.scroll_offset) continue;
14221422+ const member_seg = [_]vaxis.Segment{
14231423+ .{
14241424+ .text = std.mem.asBytes(&member.prefix),
14251425+ },
14261426+ .{
14271427+ .text = member.user.nick,
14281428+ .style = .{
14291429+ .fg = if (member.user.away)
14301430+ .{ .index = 8 }
14311431+ else
14321432+ member.user.color,
14331433+ },
14341434+ },
14351435+ };
14361436+ _ = try win.print(&member_seg, .{
14371437+ .row_offset = member_row -| self.state.members.scroll_offset,
14381438+ });
14391439+ }
14401440+ }
14411441+14421442+ fn drawTopic(_: *App, win: vaxis.Window, topic: []const u8) !void {
14431443+ _ = try win.printSegment(.{ .text = topic }, .{ .wrap = .none });
14441444+ }
14451445+14461446+ fn drawBufferList(self: *App, clients: []*irc.Client, win: vaxis.Window) !void {
14471447+ const buf_list_w = self.state.buffers.width;
14481448+ var row: usize = 0;
14491449+14501450+ defer self.state.buffers.count = row;
14511451+ for (clients) |client| {
14521452+ var style: vaxis.Style = if (row == self.state.buffers.selected_idx)
14531453+ .{
14541454+ .fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
14551455+ .reverse = true,
14561456+ }
14571457+ else
14581458+ .{
14591459+ .fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
14601460+ };
14611461+ const network_win = win.child(.{
14621462+ .y_off = row,
14631463+ .height = .{ .limit = 1 },
14641464+ });
14651465+ if (network_win.hasMouse(self.state.mouse)) |_| {
14661466+ self.vx.setMouseShape(.pointer);
14671467+ style.bg = .{ .index = 8 };
14681468+ }
14691469+ _ = try network_win.print(
14701470+ &.{.{
14711471+ .text = client.config.name orelse client.config.server,
14721472+ .style = style,
14731473+ }},
14741474+ .{},
14751475+ );
14761476+ if (network_win.hasMouse(self.state.mouse)) |_| {
14771477+ self.vx.setMouseShape(.pointer);
14781478+ }
14791479+ row += 1;
14801480+ for (client.channels.items) |*channel| {
14811481+ const channel_win = win.child(.{
14821482+ .y_off = row,
14831483+ .height = .{ .limit = 1 },
14841484+ });
14851485+ if (channel_win.hasMouse(self.state.mouse)) |mouse| {
14861486+ if (mouse.type == .press and mouse.button == .left) {
14871487+ self.state.buffers.selected_idx = row;
14881488+ }
14891489+ }
14901490+14911491+ const is_current = row == self.state.buffers.selected_idx;
14921492+ var chan_style: vaxis.Style = if (is_current)
14931493+ .{
14941494+ .fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
14951495+ .reverse = true,
14961496+ }
14971497+ else if (channel.has_unread)
14981498+ .{
14991499+ .fg = .{ .index = 4 },
15001500+ .bold = true,
15011501+ }
15021502+ else
15031503+ .{
15041504+ .fg = if (client.status == .disconnected) .{ .index = 8 } else .default,
15051505+ };
15061506+ defer row += 1;
15071507+ const prefix: []const u8 = if (channel.name[0] == '#') "#" else "";
15081508+ const name_offset: usize = if (prefix.len > 0) 1 else 0;
15091509+15101510+ if (channel_win.hasMouse(self.state.mouse)) |mouse| {
15111511+ self.vx.setMouseShape(.pointer);
15121512+ if (mouse.button == .left)
15131513+ chan_style.reverse = true
15141514+ else
15151515+ chan_style.bg = .{ .index = 8 };
15161516+ }
15171517+15181518+ const first_seg: vaxis.Segment = if (channel.has_unread_highlight)
15191519+ .{ .text = " ●︎", .style = .{ .fg = .{ .index = 1 } } }
15201520+ else
15211521+ .{ .text = " " };
15221522+15231523+ var chan_seg = [_]vaxis.Segment{
15241524+ first_seg,
15251525+ .{
15261526+ .text = prefix,
15271527+ .style = .{ .fg = .{ .index = 8 } },
15281528+ },
15291529+ .{
15301530+ .text = channel.name[name_offset..],
15311531+ .style = chan_style,
15321532+ },
15331533+ };
15341534+ const result = try channel_win.print(
15351535+ &chan_seg,
15361536+ .{},
15371537+ );
15381538+ if (result.overflow)
15391539+ win.writeCell(
15401540+ buf_list_w -| 1,
15411541+ row,
15421542+ .{
15431543+ .char = .{
15441544+ .grapheme = "…",
15451545+ .width = 1,
15461546+ },
15471547+ .style = chan_style,
15481548+ },
15491549+ );
15501550+ }
15511551+ }
15521552+ }
15531553+14931554 /// generate vaxis.Segments for the message content
14941555 fn formatMessageContent(self: *App, client: *irc.Client, msg: irc.Message) !void {
14951556 const ColorState = enum {
···17821843 }
17831844 }
17841845}
18461846+18471847+/// generate vaxis.Segments for the message content
18481848+fn formatMessage(segments: *std.ArrayList(vaxis.Segment), user: *const irc.User, msg: irc.Message) !void {
18491849+ const ColorState = enum {
18501850+ ground,
18511851+ fg,
18521852+ bg,
18531853+ };
18541854+ const LinkState = enum {
18551855+ h,
18561856+ t1,
18571857+ t2,
18581858+ p,
18591859+ s,
18601860+ colon,
18611861+ slash,
18621862+ consume,
18631863+ };
18641864+18651865+ var iter = msg.paramIterator();
18661866+ _ = iter.next() orelse return error.InvalidMessage;
18671867+ const content = iter.next() orelse return error.InvalidMessage;
18681868+ var start: usize = 0;
18691869+ var i: usize = 0;
18701870+ var style: vaxis.Style = .{};
18711871+ while (i < content.len) : (i += 1) {
18721872+ const b = content[i];
18731873+ switch (b) {
18741874+ 0x01 => {
18751875+ if (i == 0 and
18761876+ content.len > 7 and
18771877+ mem.startsWith(u8, content[1..], "ACTION"))
18781878+ {
18791879+ style.italic = true;
18801880+ const user_style: vaxis.Style = .{
18811881+ .fg = user.color,
18821882+ .italic = true,
18831883+ };
18841884+ try segments.append(.{
18851885+ .text = user.nick,
18861886+ .style = user_style,
18871887+ });
18881888+ i += 6; // "ACTION"
18891889+ } else {
18901890+ try segments.append(.{
18911891+ .text = content[start..i],
18921892+ .style = style,
18931893+ });
18941894+ }
18951895+ start = i + 1;
18961896+ },
18971897+ 0x02 => {
18981898+ try segments.append(.{
18991899+ .text = content[start..i],
19001900+ .style = style,
19011901+ });
19021902+ style.bold = !style.bold;
19031903+ start = i + 1;
19041904+ },
19051905+ 0x03 => {
19061906+ try segments.append(.{
19071907+ .text = content[start..i],
19081908+ .style = style,
19091909+ });
19101910+ i += 1;
19111911+ var state: ColorState = .ground;
19121912+ var fg_idx: ?u8 = null;
19131913+ var bg_idx: ?u8 = null;
19141914+ while (i < content.len) : (i += 1) {
19151915+ const d = content[i];
19161916+ switch (state) {
19171917+ .ground => {
19181918+ switch (d) {
19191919+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => {
19201920+ state = .fg;
19211921+ fg_idx = d - '0';
19221922+ },
19231923+ else => {
19241924+ style.fg = .default;
19251925+ style.bg = .default;
19261926+ start = i;
19271927+ break;
19281928+ },
19291929+ }
19301930+ },
19311931+ .fg => {
19321932+ switch (d) {
19331933+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => {
19341934+ const fg = fg_idx orelse 0;
19351935+ if (fg > 9) {
19361936+ style.fg = irc.toVaxisColor(fg);
19371937+ start = i;
19381938+ break;
19391939+ } else {
19401940+ fg_idx = fg * 10 + (d - '0');
19411941+ }
19421942+ },
19431943+ else => {
19441944+ if (fg_idx) |fg| {
19451945+ style.fg = irc.toVaxisColor(fg);
19461946+ start = i;
19471947+ }
19481948+ if (d == ',') state = .bg else break;
19491949+ },
19501950+ }
19511951+ },
19521952+ .bg => {
19531953+ switch (d) {
19541954+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => {
19551955+ const bg = bg_idx orelse 0;
19561956+ if (i - start == 2) {
19571957+ style.bg = irc.toVaxisColor(bg);
19581958+ start = i;
19591959+ break;
19601960+ } else {
19611961+ bg_idx = bg * 10 + (d - '0');
19621962+ }
19631963+ },
19641964+ else => {
19651965+ if (bg_idx) |bg| {
19661966+ style.bg = irc.toVaxisColor(bg);
19671967+ start = i;
19681968+ }
19691969+ break;
19701970+ },
19711971+ }
19721972+ },
19731973+ }
19741974+ }
19751975+ },
19761976+ 0x0F => {
19771977+ try segments.append(.{
19781978+ .text = content[start..i],
19791979+ .style = style,
19801980+ });
19811981+ style = .{};
19821982+ start = i + 1;
19831983+ },
19841984+ 0x16 => {
19851985+ try segments.append(.{
19861986+ .text = content[start..i],
19871987+ .style = style,
19881988+ });
19891989+ style.reverse = !style.reverse;
19901990+ start = i + 1;
19911991+ },
19921992+ 0x1D => {
19931993+ try segments.append(.{
19941994+ .text = content[start..i],
19951995+ .style = style,
19961996+ });
19971997+ style.italic = !style.italic;
19981998+ start = i + 1;
19991999+ },
20002000+ 0x1E => {
20012001+ try segments.append(.{
20022002+ .text = content[start..i],
20032003+ .style = style,
20042004+ });
20052005+ style.strikethrough = !style.strikethrough;
20062006+ start = i + 1;
20072007+ },
20082008+ 0x1F => {
20092009+ try segments.append(.{
20102010+ .text = content[start..i],
20112011+ .style = style,
20122012+ });
20132013+20142014+ style.ul_style = if (style.ul_style == .off) .single else .off;
20152015+ start = i + 1;
20162016+ },
20172017+ else => {
20182018+ if (b == 'h') {
20192019+ var state: LinkState = .h;
20202020+ const h_start = i;
20212021+ // consume until a space or EOF
20222022+ i += 1;
20232023+ while (i < content.len) : (i += 1) {
20242024+ const b1 = content[i];
20252025+ switch (state) {
20262026+ .h => {
20272027+ if (b1 == 't') state = .t1 else break;
20282028+ },
20292029+ .t1 => {
20302030+ if (b1 == 't') state = .t2 else break;
20312031+ },
20322032+ .t2 => {
20332033+ if (b1 == 'p') state = .p else break;
20342034+ },
20352035+ .p => {
20362036+ if (b1 == 's')
20372037+ state = .s
20382038+ else if (b1 == ':')
20392039+ state = .colon
20402040+ else
20412041+ break;
20422042+ },
20432043+ .s => {
20442044+ if (b1 == ':') state = .colon else break;
20452045+ },
20462046+ .colon => {
20472047+ if (b1 == '/') state = .slash else break;
20482048+ },
20492049+ .slash => {
20502050+ if (b1 == '/') {
20512051+ state = .consume;
20522052+ try segments.append(.{
20532053+ .text = content[start..h_start],
20542054+ .style = style,
20552055+ });
20562056+ start = h_start;
20572057+ } else break;
20582058+ },
20592059+ .consume => {
20602060+ switch (b1) {
20612061+ 0x00...0x20, 0x7F => {
20622062+ try segments.append(.{
20632063+ .text = content[h_start..i],
20642064+ .style = .{
20652065+ .fg = .{ .index = 4 },
20662066+ },
20672067+ .link = .{
20682068+ .uri = content[h_start..i],
20692069+ },
20702070+ });
20712071+ start = i;
20722072+ // backup one
20732073+ i -= 1;
20742074+ break;
20752075+ },
20762076+ else => {
20772077+ if (i == content.len) {
20782078+ try segments.append(.{
20792079+ .text = content[h_start..],
20802080+ .style = .{
20812081+ .fg = .{ .index = 4 },
20822082+ },
20832083+ .link = .{
20842084+ .uri = content[h_start..],
20852085+ },
20862086+ });
20872087+ return;
20882088+ }
20892089+ },
20902090+ }
20912091+ },
20922092+ }
20932093+ }
20942094+ }
20952095+ },
20962096+ }
20972097+ }
20982098+ if (start < i and start < content.len) {
20992099+ try segments.append(.{
21002100+ .text = content[start..],
21012101+ .style = style,
21022102+ });
21032103+ }
21042104+}
21052105+21062106+/// Returns the number of lines the segments would consume in the given window
21072107+fn lineCountForWindow(win: vaxis.Window, segments: []const vaxis.Segment) usize {
21082108+ // Fastpath if we have fewer bytes than the width
21092109+ var byte_count: usize = 0;
21102110+ for (segments) |segment| {
21112111+ byte_count += segment.text.len;
21122112+ }
21132113+ // One line if we are fewer bytes than the width
21142114+ if (byte_count <= win.width) return 1;
21152115+21162116+ // Slow path. We have to layout the text
21172117+ const result = win.print(segments, .{ .commit = false, .wrap = .word }) catch return 0;
21182118+ if (result.col == 0)
21192119+ return result.row
21202120+ else
21212121+ return result.row + 1;
21222122+}
+21
src/irc.zig
···196196 }
197197 }
198198 }
199199+200200+ /// issue a MARKREAD command for this channel. The most recent message in the channel will be used as
201201+ /// the last read time
202202+ pub fn markRead(self: *Channel) !void {
203203+ if (!self.has_unread) return;
204204+205205+ self.has_unread = false;
206206+ self.has_unread_highlight = false;
207207+ const last_msg = self.messages.getLast();
208208+ const time_tag = last_msg.getTag("time") orelse return;
209209+ var write_buf: [128]u8 = undefined;
210210+ const mark_read = try std.fmt.bufPrint(
211211+ &write_buf,
212212+ "MARKREAD {s} timestamp={s}\r\n",
213213+ .{
214214+ self.name,
215215+ time_tag,
216216+ },
217217+ );
218218+ try self.client.queueWrite(mark_read);
219219+ }
199220};
200221201222pub const User = struct {