this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

ui: refactor drawing

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>

+846 -450
+37
src/Scrollbar.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + 4 + const Scrollbar = @This(); 5 + 6 + /// character to use for the scrollbar 7 + character: vaxis.Cell.Character = .{ .grapheme = "▐", .width = 1 }, 8 + 9 + /// style to draw the bar character with 10 + style: vaxis.Style = .{}, 11 + 12 + /// The index of the bottom-most item, with 0 being "at the bottom" 13 + bottom: usize = 0, 14 + 15 + /// total items in the list 16 + total: usize, 17 + 18 + /// total items that fit within the view area 19 + view_size: usize, 20 + 21 + pub fn draw(self: Scrollbar, win: vaxis.Window) void { 22 + // don't draw when 0 items 23 + if (self.total < 1) return; 24 + 25 + // don't draw when all items can be shown 26 + if (self.view_size >= self.total) return; 27 + 28 + // (view_size / total) * window height = size of the scroll bar 29 + const bar_height = @max(std.math.divCeil(usize, self.view_size * win.height, self.total) catch unreachable, 1); 30 + 31 + // The row of the last cell of the bottom of the bar 32 + const bar_bottom = (win.height - 1) - (std.math.divCeil(usize, self.bottom * win.height, self.total) catch unreachable); 33 + 34 + var i: usize = 0; 35 + while (i <= bar_height) : (i += 1) 36 + win.writeCell(0, bar_bottom -| i, .{ .char = self.character, .style = self.style }); 37 + }
+788 -450
src/app.zig
··· 3 3 const vaxis = @import("vaxis"); 4 4 const zeit = @import("zeit"); 5 5 const ziglua = @import("ziglua"); 6 + const Scrollbar = @import("Scrollbar.zig"); 6 7 7 8 const irc = comlink.irc; 8 9 const lua = comlink.lua; ··· 29 30 } = .{}, 30 31 messages: struct { 31 32 scroll_offset: usize = 0, 33 + pending_scroll: isize = 0, 32 34 } = .{}, 33 35 buffers: struct { 34 36 scroll_offset: usize = 0, ··· 66 68 67 69 state: State = .{}, 68 70 69 - content_segments: std.ArrayList(vaxis.Segment), 70 - 71 71 completer: ?Completer = null, 72 72 73 73 should_quit: bool = false, ··· 86 86 .env = env, 87 87 .vx = vx, 88 88 .tty = try vaxis.Tty.init(), 89 - .content_segments = std.ArrayList(vaxis.Segment).init(alloc), 90 89 .binds = try std.ArrayList(Bind).initCapacity(alloc, 16), 91 90 .paste_buffer = std.ArrayList(u8).init(alloc), 92 91 .tz = try zeit.local(alloc, &env), ··· 147 146 self.vx.deinit(self.alloc, self.tty.anyWriter()); 148 147 self.tty.deinit(); 149 148 150 - self.content_segments.deinit(); 151 149 if (self.completer) |*completer| completer.deinit(); 152 150 self.binds.deinit(); 153 151 self.paste_buffer.deinit(); ··· 182 180 var input = TextInput.init(self.alloc, &self.vx.unicode); 183 181 defer input.deinit(); 184 182 183 + var last_frame: i64 = std.time.milliTimestamp(); 185 184 loop: while (!self.should_quit) { 186 - loop.pollEvent(); 185 + var redraw: bool = false; 186 + std.time.sleep(8 * std.time.ns_per_ms); 187 + if (self.state.messages.pending_scroll != 0) { 188 + redraw = true; 189 + if (self.state.messages.pending_scroll > 0) { 190 + self.state.messages.pending_scroll -= 1; 191 + self.state.messages.scroll_offset += 1; 192 + } else { 193 + self.state.messages.pending_scroll += 1; 194 + self.state.messages.scroll_offset -|= 1; 195 + } 196 + } 187 197 while (loop.tryEvent()) |event| { 198 + redraw = true; 188 199 switch (event) { 189 200 .redraw => {}, 190 201 .key_press => |key| { ··· 734 745 } 735 746 } 736 747 737 - try self.draw(&input); 748 + if (redraw) { 749 + try self.draw(&input); 750 + last_frame = std.time.milliTimestamp(); 751 + } 738 752 } 739 753 } 740 754 pub fn nextChannel(self: *App) void { ··· 913 927 } 914 928 } 915 929 930 + // Define the layout 916 931 const buf_list_w = self.state.buffers.width; 917 932 const mbr_list_w = self.state.members.width; 918 933 const message_list_width = win.width -| buf_list_w -| mbr_list_w; ··· 937 952 .border = .{ .where = .bottom }, 938 953 }); 939 954 940 - var row: usize = 0; 941 - for (self.clients.items) |client| { 942 - var style: vaxis.Style = if (row == self.state.buffers.selected_idx) 943 - .{ 944 - .fg = if (client.status == .disconnected) .{ .index = 8 } else .default, 945 - .reverse = true, 946 - } 947 - else 948 - .{ 949 - .fg = if (client.status == .disconnected) .{ .index = 8 } else .default, 950 - }; 951 - const network_win = channel_list_win.child(.{ 952 - .y_off = row, 953 - .height = .{ .limit = 1 }, 954 - }); 955 - if (network_win.hasMouse(self.state.mouse)) |_| { 956 - self.vx.setMouseShape(.pointer); 957 - style.bg = .{ .index = 8 }; 958 - } 959 - _ = try network_win.print( 960 - &.{.{ 961 - .text = client.config.name orelse client.config.server, 962 - .style = style, 963 - }}, 964 - .{}, 965 - ); 966 - if (network_win.hasMouse(self.state.mouse)) |_| { 967 - self.vx.setMouseShape(.pointer); 968 - } 969 - row += 1; 955 + const message_list_win = middle_win.child(.{ 956 + .y_off = 2, 957 + .height = .{ .limit = middle_win.height -| 4 }, 958 + .width = .{ .limit = middle_win.width -| 1 }, 959 + }); 970 960 971 - for (client.channels.items) |*channel| { 972 - const channel_win = channel_list_win.child(.{ 973 - .y_off = row, 974 - .height = .{ .limit = 1 }, 975 - }); 976 - if (channel_win.hasMouse(self.state.mouse)) |mouse| { 977 - if (mouse.type == .press and mouse.button == .left) { 978 - self.state.buffers.selected_idx = row; 979 - } 980 - } 961 + // Draw the buffer list 962 + try self.drawBufferList(self.clients.items, channel_list_win); 981 963 982 - const is_current = row == self.state.buffers.selected_idx; 983 - var chan_style: vaxis.Style = if (is_current) 984 - .{ 985 - .fg = if (client.status == .disconnected) .{ .index = 8 } else .default, 986 - .reverse = true, 987 - } 988 - else if (channel.has_unread) 989 - .{ 990 - .fg = .{ .index = 4 }, 991 - .bold = true, 992 - } 993 - else 994 - .{ 995 - .fg = if (client.status == .disconnected) .{ .index = 8 } else .default, 996 - }; 997 - defer row += 1; 998 - const prefix: []const u8 = if (channel.name[0] == '#') "#" else ""; 999 - const name_offset: usize = if (prefix.len > 0) 1 else 0; 964 + // Get our currently selected buffer and draw it 965 + const buffer = self.selectedBuffer() orelse return; 966 + switch (buffer) { 967 + .client => {}, // nothing to do 1000 968 1001 - if (channel_win.hasMouse(self.state.mouse)) |mouse| { 1002 - self.vx.setMouseShape(.pointer); 1003 - if (mouse.button == .left) 1004 - chan_style.reverse = true 1005 - else 1006 - chan_style.bg = .{ .index = 8 }; 1007 - } 969 + .channel => |channel| { 970 + // Mark the channel as read 971 + try channel.markRead(); 1008 972 1009 - const first_seg: vaxis.Segment = if (channel.has_unread_highlight) 1010 - .{ .text = " ●︎", .style = .{ .fg = .{ .index = 1 } } } 1011 - else 1012 - .{ .text = " " }; 973 + // Request WHO if we don't already have it 974 + if (!channel.who_requested) try channel.client.whox(channel); 1013 975 1014 - var chan_seg = [_]vaxis.Segment{ 1015 - first_seg, 1016 - .{ 1017 - .text = prefix, 1018 - .style = .{ .fg = .{ .index = 8 } }, 1019 - }, 1020 - .{ 1021 - .text = channel.name[name_offset..], 1022 - .style = chan_style, 1023 - }, 1024 - }; 1025 - const result = try channel_win.print( 1026 - &chan_seg, 1027 - .{}, 1028 - ); 1029 - if (result.overflow) 1030 - channel_list_win.writeCell( 1031 - buf_list_w -| 1, 1032 - row, 1033 - .{ 1034 - .char = .{ 1035 - .grapheme = "…", 1036 - .width = 1, 1037 - }, 1038 - .style = chan_style, 1039 - }, 1040 - ); 1041 - if (is_current) { 1042 - var write_buf: [128]u8 = undefined; 1043 - if (channel.has_unread) { 1044 - channel.has_unread = false; 1045 - channel.has_unread_highlight = false; 1046 - const last_msg = channel.messages.getLast(); 1047 - var tag_iter = last_msg.tagIterator(); 1048 - while (tag_iter.next()) |tag| { 1049 - if (!std.mem.eql(u8, tag.key, "time")) continue; 1050 - const mark_read = try std.fmt.bufPrint( 1051 - &write_buf, 1052 - "MARKREAD {s} timestamp={s}\r\n", 1053 - .{ 1054 - channel.name, 1055 - tag.value, 1056 - }, 1057 - ); 1058 - try client.queueWrite(mark_read); 1059 - } 1060 - } 1061 - if (!channel.who_requested) try client.whox(channel); 1062 - var topic_seg = [_]vaxis.Segment{ 1063 - .{ 1064 - .text = channel.topic orelse "", 1065 - }, 976 + // Set the title of the terminal 977 + { 978 + var buf: [64]u8 = undefined; 979 + const title = std.fmt.bufPrint(&buf, "{s} - comlink", .{channel.name}) catch title: { 980 + // If the channel name is too long to fit in our buffer just truncate 981 + const len = @min(buf.len, channel.name.len); 982 + @memcpy(buf[0..len], channel.name[0..len]); 983 + break :title buf[0..len]; 1066 984 }; 1067 - _ = try topic_win.print(&topic_seg, .{ .wrap = .none }); 985 + try self.vx.setTitle(self.tty.anyWriter(), title); 986 + } 1068 987 1069 - { 1070 - var buf: [64]u8 = undefined; 1071 - const title = std.fmt.bufPrint(&buf, "{s} - comlink", .{channel.name}) catch title: { 1072 - // If the channel name is too long to fit in our buffer just truncate 1073 - const len = @min(buf.len, channel.name.len); 1074 - @memcpy(buf[0..len], channel.name[0..len]); 1075 - break :title buf[0..len]; 1076 - }; 1077 - try self.vx.setTitle(self.tty.anyWriter(), title); 1078 - } 988 + // Draw the topic 989 + try self.drawTopic(topic_win, channel.topic orelse ""); 1079 990 1080 - if (member_list_win.hasMouse(self.state.mouse)) |mouse| { 1081 - switch (mouse.button) { 1082 - .wheel_up => { 1083 - self.state.members.scroll_offset -|= 3; 1084 - self.state.mouse.?.button = .none; 1085 - }, 1086 - .wheel_down => { 1087 - self.state.members.scroll_offset +|= 3; 1088 - self.state.mouse.?.button = .none; 1089 - }, 1090 - else => {}, 1091 - } 1092 - } 991 + // Draw the member list 992 + try self.drawMemberList(member_list_win, channel); 1093 993 1094 - self.state.members.scroll_offset = @min(self.state.members.scroll_offset, channel.members.items.len -| member_list_win.height); 994 + // Draw the message list 995 + try self.drawMessageList(allocator, message_list_win, channel); 1095 996 1096 - var member_row: usize = 0; 1097 - for (channel.members.items) |*member| { 1098 - defer member_row += 1; 1099 - if (member_row < self.state.members.scroll_offset) continue; 1100 - var member_seg = [_]vaxis.Segment{ 1101 - .{ 1102 - .text = std.mem.asBytes(&member.prefix), 1103 - }, 1104 - .{ 1105 - .text = member.user.nick, 1106 - .style = .{ 1107 - .fg = if (member.user.away) 1108 - .{ .index = 8 } 1109 - else 1110 - member.user.color, 1111 - }, 1112 - }, 1113 - }; 1114 - _ = try member_list_win.print( 1115 - &member_seg, 1116 - .{ 1117 - .row_offset = member_row - self.state.members.scroll_offset, 1118 - }, 1119 - ); 1120 - } 1121 - 1122 - // loop the messages and print from the last line to current 1123 - // line 1124 - const message_list_win = middle_win.child(.{ 997 + // draw a scrollbar 998 + { 999 + const scrollbar: Scrollbar = .{ 1000 + .total = channel.messages.items.len, 1001 + .view_size = message_list_win.height / 3, // ~3 lines per message 1002 + .bottom = self.state.messages.scroll_offset, 1003 + }; 1004 + const scrollbar_win = middle_win.child(.{ 1005 + .x_off = message_list_win.width, 1125 1006 .y_off = 2, 1126 - .height = .{ .limit = middle_win.height -| 3 }, 1127 - .width = .{ .limit = middle_win.width -| 1 }, 1007 + .height = .{ .limit = middle_win.height -| 4 }, 1128 1008 }); 1129 - if (message_list_win.hasMouse(self.state.mouse)) |mouse| { 1130 - switch (mouse.button) { 1131 - .wheel_up => { 1132 - self.state.messages.scroll_offset +|= 3; 1133 - self.state.mouse.?.button = .none; 1134 - }, 1135 - .wheel_down => { 1136 - self.state.messages.scroll_offset -|= 3; 1137 - self.state.mouse.?.button = .none; 1138 - }, 1139 - else => {}, 1140 - } 1141 - } 1142 - self.state.messages.scroll_offset = @min(self.state.messages.scroll_offset, channel.messages.items.len -| 1); 1143 - const message_offset_win = message_list_win.child(.{ 1144 - .x_off = 6, 1145 - }); 1009 + scrollbar.draw(scrollbar_win); 1010 + } 1146 1011 1147 - var prev_sender: ?[]const u8 = null; 1148 - var prev_time: ?zeit.Instant = null; 1149 - var i: usize = channel.messages.items.len -| self.state.messages.scroll_offset; 1150 - var y_off: usize = message_list_win.height -| 1; 1151 - while (i > 0) { 1152 - i -= 1; 1153 - var message = channel.messages.items[i]; 1154 - // syntax: <target> <message> 1155 - 1156 - const sender: []const u8 = blk: { 1157 - const src = message.source() orelse break :blk ""; 1158 - const l = std.mem.indexOfScalar(u8, src, '!') orelse 1159 - std.mem.indexOfScalar(u8, src, '@') orelse 1160 - src.len; 1161 - break :blk src[0..l]; 1162 - }; 1163 - { 1164 - // If this sender is not the same as the previous 1165 - // printed message OR if the previous message was sent 1166 - // more than some interval ago, then we'll print the 1167 - // previous sender and keep going 1168 - defer prev_sender = sender; 1169 - defer prev_time = message.time(); 1012 + // draw the completion list 1013 + if (self.completer) |*completer| { 1014 + try completer.findMatches(channel); 1170 1015 1171 - const time_gap: bool = time_gap: { 1172 - // We are iterating through the messages in reverse, 1173 - // so the timestamp of the "previous" message is 1174 - // greater than the timestamp of the current message 1175 - const t1 = if (message.time()) |t| t.timestamp else break :time_gap false; 1176 - const t2 = if (prev_time) |t| t.timestamp else break :time_gap false; 1177 - break :time_gap @divTrunc(t2 - t1, std.time.ns_per_min) > 5; 1178 - }; 1179 - 1180 - if (y_off > 0 and 1181 - prev_sender != null and 1182 - (time_gap or !mem.eql(u8, sender, prev_sender.?))) 1183 - { 1184 - y_off -|= 1; 1185 - const user = try client.getOrCreateUser(prev_sender.?); 1186 - const sender_win = message_list_win.child(.{ 1187 - .x_off = 6, 1188 - .y_off = y_off, 1189 - .height = .{ .limit = 1 }, 1190 - }); 1191 - const sender_result = try sender_win.print( 1192 - &.{.{ 1193 - .text = prev_sender.?, 1194 - .style = .{ 1195 - .fg = user.color, 1196 - .bold = true, 1197 - }, 1198 - }}, 1199 - .{ .wrap = .word }, 1200 - ); 1201 - y_off -|= 1; 1202 - const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } }); 1203 - if (result_win.hasMouse(self.state.mouse)) |_| { 1204 - self.vx.setMouseShape(.pointer); 1205 - } 1016 + var completion_style: vaxis.Style = .{ .bg = .{ .index = 8 } }; 1017 + const completion_win = middle_win.child(.{ 1018 + .width = .{ .limit = completer.widestMatch(win) + 1 }, 1019 + .height = .{ .limit = @min(completer.numMatches(), middle_win.height -| 1) }, 1020 + .x_off = completer.start_idx, 1021 + .y_off = middle_win.height -| completer.numMatches() -| 1, 1022 + }); 1023 + completion_win.fill(.{ 1024 + .char = .{ .grapheme = " ", .width = 1 }, 1025 + .style = completion_style, 1026 + }); 1027 + var completion_row: usize = 0; 1028 + while (completion_row < completion_win.height) : (completion_row += 1) { 1029 + log.debug("COMPLETION ROW {d}, selected_idx {d}", .{ completion_row, completer.selected_idx orelse 0 }); 1030 + if (completer.selected_idx) |idx| { 1031 + if (completion_row == idx) 1032 + completion_style.reverse = true 1033 + else { 1034 + completion_style = .{ .bg = .{ .index = 8 } }; 1206 1035 } 1207 1036 } 1208 - 1209 - if (y_off == 0) break; 1210 - 1211 - try self.formatMessageContent(client, message); 1212 - defer self.content_segments.clearRetainingCapacity(); 1213 - // print the content first 1214 - const print_result = try message_offset_win.print(self.content_segments.items, .{ 1215 - .wrap = .word, 1216 - .commit = false, 1217 - }); 1218 - const content_height = if (print_result.col == 0) 1219 - print_result.row 1220 - else 1221 - print_result.row + 1; 1222 - 1223 - const height = if (content_height > y_off) content_height - y_off else content_height; 1224 - if (height == 0) break; 1225 - const content_win = message_offset_win.child( 1037 + var seg = [_]vaxis.Segment{ 1226 1038 .{ 1227 - .y_off = y_off -| content_height, 1228 - .height = .{ .limit = height }, 1039 + .text = completer.options.items[completer.options.items.len - 1 - completion_row], 1040 + .style = completion_style, 1229 1041 }, 1230 - ); 1231 - if (content_win.hasMouse(self.state.mouse)) |mouse| { 1232 - var bg_idx: u8 = 8; 1233 - if (mouse.type == .press and mouse.button == .middle) { 1234 - var list = std.ArrayList(u8).init(self.alloc); 1235 - defer list.deinit(); 1236 - for (self.content_segments.items) |item| { 1237 - try list.appendSlice(item.text); 1238 - } 1239 - try self.vx.copyToSystemClipboard(self.tty.anyWriter(), list.items, self.alloc); 1240 - bg_idx = 3; 1241 - } 1242 - content_win.fill(.{ 1243 - .char = .{ 1244 - .grapheme = " ", 1245 - .width = 1, 1246 - }, 1247 - .style = .{ 1248 - .bg = .{ .index = bg_idx }, 1249 - }, 1250 - }); 1251 - for (self.content_segments.items) |*item| { 1252 - item.style.bg = .{ .index = bg_idx }; 1253 - } 1254 - } 1255 - var iter = message.paramIterator(); 1256 - // target is the channel, and we already handled that 1257 - _ = iter.next() orelse continue; 1258 - 1259 - const content = iter.next() orelse continue; 1260 - if (std.mem.indexOf(u8, content, client.config.nick)) |_| { 1261 - for (self.content_segments.items) |*item| { 1262 - if (item.style.fg == .default) 1263 - item.style.fg = .{ .index = 3 }; 1264 - } 1265 - } 1266 - _ = try content_win.print( 1267 - self.content_segments.items, 1268 1042 .{ 1269 - .wrap = .word, 1270 - // .skip_n_rows = content_height - content_win.height, 1043 + .text = " ", 1044 + .style = completion_style, 1271 1045 }, 1272 - ); 1273 - if (content_height > y_off) break; 1274 - const gutter = message_list_win.child(.{ 1275 - .y_off = y_off -| content_height, 1276 - .width = .{ .limit = 6 }, 1277 - }); 1278 - 1279 - if (message.localTime(&self.tz)) |instant| { 1280 - var date: bool = false; 1281 - const time = instant.time(); 1282 - var buf = try std.fmt.allocPrint( 1283 - allocator, 1284 - "{d:0>2}:{d:0>2}", 1285 - .{ time.hour, time.minute }, 1286 - ); 1287 - if (i != 0 and channel.messages.items[i - 1].time() != null) { 1288 - const prev = channel.messages.items[i - 1].localTime(&self.tz).?.time(); 1289 - if (time.day != prev.day) { 1290 - date = true; 1291 - buf = try std.fmt.allocPrint( 1292 - allocator, 1293 - "{d:0>2}/{d:0>2}", 1294 - .{ @intFromEnum(time.month), time.day }, 1295 - ); 1296 - } 1297 - } 1298 - if (i == 0) { 1299 - date = true; 1300 - buf = try std.fmt.allocPrint( 1301 - allocator, 1302 - "{d:0>2}/{d:0>2}", 1303 - .{ @intFromEnum(time.month), time.day }, 1304 - ); 1305 - } 1306 - const fg: vaxis.Color = if (date) 1307 - .default 1308 - else 1309 - .{ .index = 8 }; 1310 - var time_seg = [_]vaxis.Segment{ 1311 - .{ 1312 - .text = buf, 1313 - .style = .{ .fg = fg }, 1314 - }, 1315 - }; 1316 - _ = try gutter.print(&time_seg, .{}); 1317 - } 1318 - 1319 - y_off -|= content_height; 1320 - 1321 - // If we are on the first message, print the sender 1322 - if (i == 0) { 1323 - y_off -|= 1; 1324 - const user = try client.getOrCreateUser(sender); 1325 - const sender_win = message_list_win.child(.{ 1326 - .x_off = 6, 1327 - .y_off = y_off, 1328 - .height = .{ .limit = 1 }, 1329 - }); 1330 - const sender_result = try sender_win.print( 1331 - &.{.{ 1332 - .text = sender, 1333 - .style = .{ 1334 - .fg = user.color, 1335 - .bold = true, 1336 - }, 1337 - }}, 1338 - .{ .wrap = .word }, 1339 - ); 1340 - const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } }); 1341 - if (result_win.hasMouse(self.state.mouse)) |_| { 1342 - self.vx.setMouseShape(.pointer); 1343 - } 1344 - } 1345 - 1346 - // if we are on the oldest message, request more history 1347 - if (i == 0 and !channel.at_oldest) { 1348 - try client.requestHistory(.before, channel); 1349 - } 1350 - } 1351 - { 1352 - // draw a scrollbar 1353 - var scrollbar: vaxis.widgets.Scrollbar = .{ 1354 - .total = channel.messages.items.len, 1355 - .view_size = channel.messages.items.len -| self.state.messages.scroll_offset -| i, 1356 - .top = i, 1357 1046 }; 1358 - const scrollbar_win = middle_win.child(.{ 1359 - .x_off = message_list_win.width, 1360 - .y_off = 2, 1361 - .height = .{ .limit = middle_win.height -| 3 }, 1047 + _ = try completion_win.print(&seg, .{ 1048 + .row_offset = completion_win.height -| completion_row -| 1, 1362 1049 }); 1363 - scrollbar.draw(scrollbar_win); 1364 - } 1365 - if (self.completer) |*completer| { 1366 - try completer.findMatches(channel); 1367 - 1368 - var completion_style: vaxis.Style = .{ .bg = .{ .index = 8 } }; 1369 - const completion_win = middle_win.child(.{ 1370 - .width = .{ .limit = completer.widestMatch(win) + 1 }, 1371 - .height = .{ .limit = @min(completer.numMatches(), middle_win.height -| 1) }, 1372 - .x_off = completer.start_idx, 1373 - .y_off = middle_win.height -| completer.numMatches() -| 1, 1374 - }); 1375 - completion_win.fill(.{ 1376 - .char = .{ .grapheme = " ", .width = 1 }, 1377 - .style = completion_style, 1378 - }); 1379 - var completion_row: usize = 0; 1380 - while (completion_row < completion_win.height) : (completion_row += 1) { 1381 - log.debug("COMPLETION ROW {d}, selected_idx {d}", .{ completion_row, completer.selected_idx orelse 0 }); 1382 - if (completer.selected_idx) |idx| { 1383 - if (completion_row == idx) 1384 - completion_style.reverse = true 1385 - else { 1386 - completion_style = .{ .bg = .{ .index = 8 } }; 1387 - } 1388 - } 1389 - var seg = [_]vaxis.Segment{ 1390 - .{ 1391 - .text = completer.options.items[completer.options.items.len - 1 - completion_row], 1392 - .style = completion_style, 1393 - }, 1394 - .{ 1395 - .text = " ", 1396 - .style = completion_style, 1397 - }, 1398 - }; 1399 - _ = try completion_win.print(&seg, .{ 1400 - .row_offset = completion_win.height -| completion_row -| 1, 1401 - }); 1402 - } 1403 1050 } 1404 1051 } 1405 - } 1052 + }, 1406 1053 } 1407 1054 1408 1055 const input_win = middle_win.child(.{ ··· 1483 1130 // })); 1484 1131 } 1485 1132 1486 - self.state.buffers.count = row; 1487 - 1488 1133 var buffered = self.tty.bufferedWriter(); 1489 1134 try self.vx.render(buffered.writer().any()); 1490 1135 try buffered.flush(); 1491 1136 } 1492 1137 1138 + fn drawMessageList( 1139 + self: *App, 1140 + arena: std.mem.Allocator, 1141 + win: vaxis.Window, 1142 + channel: *irc.Channel, 1143 + ) !void { 1144 + if (channel.messages.items.len == 0) return; 1145 + const client = channel.client; 1146 + const messages = channel.messages.items[0 .. channel.messages.items.len - self.state.messages.scroll_offset]; 1147 + // We draw a gutter for time information 1148 + const gutter_width: usize = 6; 1149 + 1150 + // Our message list is offset by the gutter width 1151 + const message_offset_win = win.child(.{ .x_off = gutter_width }); 1152 + 1153 + // Handle mouse 1154 + if (win.hasMouse(self.state.mouse)) |mouse| { 1155 + switch (mouse.button) { 1156 + .wheel_up => { 1157 + self.state.messages.scroll_offset +|= 1; 1158 + self.state.mouse.?.button = .none; 1159 + self.state.messages.pending_scroll += 2; 1160 + }, 1161 + .wheel_down => { 1162 + self.state.messages.scroll_offset -|= 1; 1163 + self.state.mouse.?.button = .none; 1164 + self.state.messages.pending_scroll -= 2; 1165 + }, 1166 + else => {}, 1167 + } 1168 + } 1169 + self.state.messages.scroll_offset = @min( 1170 + self.state.messages.scroll_offset, 1171 + channel.messages.items.len -| 1, 1172 + ); 1173 + 1174 + // Define a few state variables for the loop 1175 + const last_msg = messages[messages.len - 1]; 1176 + 1177 + // Initialize prev_time to the time of the last message, falling back to "now" 1178 + var prev_time: zeit.Instant = last_msg.localTime(&self.tz) orelse 1179 + try zeit.instant(.{ .source = .now, .timezone = &self.tz }); 1180 + 1181 + // Initialize prev_sender to the sender of the last message 1182 + var prev_sender: []const u8 = if (last_msg.source()) |src| blk: { 1183 + if (std.mem.indexOfScalar(u8, src, '!')) |idx| 1184 + break :blk src[0..idx]; 1185 + if (std.mem.indexOfScalar(u8, src, '@')) |idx| 1186 + break :blk src[0..idx]; 1187 + break :blk src; 1188 + } else ""; 1189 + 1190 + // y_off is the row we are printing on 1191 + var y_off: usize = win.height; 1192 + 1193 + // Formatted message segments 1194 + var segments = std.ArrayList(vaxis.Segment).init(arena); 1195 + 1196 + var msg_iter = std.mem.reverseIterator(messages); 1197 + var i: usize = messages.len; 1198 + while (msg_iter.next()) |message| { 1199 + i -|= 1; 1200 + segments.clearRetainingCapacity(); 1201 + 1202 + // Get the sender nick 1203 + const sender: []const u8 = if (message.source()) |src| blk: { 1204 + if (std.mem.indexOfScalar(u8, src, '!')) |idx| 1205 + break :blk src[0..idx]; 1206 + if (std.mem.indexOfScalar(u8, src, '@')) |idx| 1207 + break :blk src[0..idx]; 1208 + break :blk src; 1209 + } else ""; 1210 + 1211 + // Save sender state after this loop 1212 + defer prev_sender = sender; 1213 + 1214 + // Before we print the message, we need to decide if we should print the sender name of 1215 + // the previous message. There are two cases we do this: 1216 + // 1. The previous message was sent by someone other than the current message 1217 + // 2. A certain amount of time has elapsed between messages 1218 + // 1219 + // Each case requires that we have space in the window to print the sender (y_off > 0) 1220 + const time_gap = if (message.localTime(&self.tz)) |time| blk: { 1221 + // Save message state for next loop 1222 + defer prev_time = time; 1223 + // time_gap is true when the difference between this message and last message is 1224 + // greater than 5 minutes 1225 + break :blk (prev_time.timestamp -| time.timestamp) > (5 * std.time.ns_per_min); 1226 + } else false; 1227 + 1228 + // Print the sender of the previous message 1229 + if (y_off > 0 and (time_gap or !std.mem.eql(u8, prev_sender, sender))) { 1230 + // Go up one line 1231 + y_off -|= 1; 1232 + 1233 + // Get the user so we have the correct color 1234 + const user = try client.getOrCreateUser(prev_sender); 1235 + const sender_win = message_offset_win.child(.{ 1236 + .y_off = y_off, 1237 + .height = .{ .limit = 1 }, 1238 + }); 1239 + 1240 + // We will use the result to see if our mouse is hovering over the nickname 1241 + const sender_result = try sender_win.printSegment( 1242 + .{ 1243 + .text = prev_sender, 1244 + .style = .{ .fg = user.color, .bold = true }, 1245 + }, 1246 + .{ .wrap = .none }, 1247 + ); 1248 + 1249 + // If our mouse is over the nickname, we set it to a pointer 1250 + const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } }); 1251 + if (result_win.hasMouse(self.state.mouse)) |_| { 1252 + self.vx.setMouseShape(.pointer); 1253 + } 1254 + 1255 + // Go up one more line to print the next message 1256 + y_off -|= 1; 1257 + } 1258 + 1259 + // We are out of space 1260 + if (y_off == 0) break; 1261 + 1262 + const user = try client.getOrCreateUser(sender); 1263 + try formatMessage(&segments, user, message); 1264 + 1265 + // Get the line count for this message 1266 + const content_height = lineCountForWindow(message_offset_win, segments.items); 1267 + 1268 + const content_win = message_offset_win.child( 1269 + .{ 1270 + .y_off = y_off -| content_height, 1271 + .height = .{ .limit = content_height }, 1272 + }, 1273 + ); 1274 + if (content_win.hasMouse(self.state.mouse)) |mouse| { 1275 + var bg_idx: u8 = 8; 1276 + if (mouse.type == .press and mouse.button == .middle) { 1277 + var list = std.ArrayList(u8).init(self.alloc); 1278 + defer list.deinit(); 1279 + for (segments.items) |item| { 1280 + try list.appendSlice(item.text); 1281 + } 1282 + try self.vx.copyToSystemClipboard(self.tty.anyWriter(), list.items, self.alloc); 1283 + bg_idx = 3; 1284 + } 1285 + content_win.fill(.{ 1286 + .char = .{ 1287 + .grapheme = " ", 1288 + .width = 1, 1289 + }, 1290 + .style = .{ 1291 + .bg = .{ .index = bg_idx }, 1292 + }, 1293 + }); 1294 + for (segments.items) |*item| { 1295 + item.style.bg = .{ .index = bg_idx }; 1296 + } 1297 + } 1298 + var iter = message.paramIterator(); 1299 + // target is the channel, and we already handled that 1300 + _ = iter.next() orelse continue; 1301 + 1302 + const content = iter.next() orelse continue; 1303 + if (std.mem.indexOf(u8, content, client.config.nick)) |_| { 1304 + for (segments.items) |*item| { 1305 + if (item.style.fg == .default) 1306 + item.style.fg = .{ .index = 3 }; 1307 + } 1308 + } 1309 + _ = try content_win.print( 1310 + segments.items, 1311 + .{ 1312 + .wrap = .word, 1313 + }, 1314 + ); 1315 + if (content_height > y_off) break; 1316 + const gutter = win.child(.{ 1317 + .y_off = y_off -| content_height, 1318 + .width = .{ .limit = 6 }, 1319 + }); 1320 + 1321 + if (message.localTime(&self.tz)) |instant| { 1322 + var date: bool = false; 1323 + const time = instant.time(); 1324 + var buf = try std.fmt.allocPrint( 1325 + arena, 1326 + "{d:0>2}:{d:0>2}", 1327 + .{ time.hour, time.minute }, 1328 + ); 1329 + if (i != 0 and channel.messages.items[i - 1].time() != null) { 1330 + const prev = channel.messages.items[i - 1].localTime(&self.tz).?.time(); 1331 + if (time.day != prev.day) { 1332 + date = true; 1333 + buf = try std.fmt.allocPrint( 1334 + arena, 1335 + "{d:0>2}/{d:0>2}", 1336 + .{ @intFromEnum(time.month), time.day }, 1337 + ); 1338 + } 1339 + } 1340 + if (i == 0) { 1341 + date = true; 1342 + buf = try std.fmt.allocPrint( 1343 + arena, 1344 + "{d:0>2}/{d:0>2}", 1345 + .{ @intFromEnum(time.month), time.day }, 1346 + ); 1347 + } 1348 + const fg: vaxis.Color = if (date) 1349 + .default 1350 + else 1351 + .{ .index = 8 }; 1352 + var time_seg = [_]vaxis.Segment{ 1353 + .{ 1354 + .text = buf, 1355 + .style = .{ .fg = fg }, 1356 + }, 1357 + }; 1358 + _ = try gutter.print(&time_seg, .{}); 1359 + } 1360 + 1361 + y_off -|= content_height; 1362 + 1363 + // If we are on the first message, print the sender 1364 + if (i == 0) { 1365 + y_off -|= 1; 1366 + const sender_win = win.child(.{ 1367 + .x_off = 6, 1368 + .y_off = y_off, 1369 + .height = .{ .limit = 1 }, 1370 + }); 1371 + const sender_result = try sender_win.print( 1372 + &.{.{ 1373 + .text = sender, 1374 + .style = .{ 1375 + .fg = user.color, 1376 + .bold = true, 1377 + }, 1378 + }}, 1379 + .{ .wrap = .word }, 1380 + ); 1381 + const result_win = sender_win.child(.{ .width = .{ .limit = sender_result.col } }); 1382 + if (result_win.hasMouse(self.state.mouse)) |_| { 1383 + self.vx.setMouseShape(.pointer); 1384 + } 1385 + } 1386 + 1387 + // if we are on the oldest message, request more history 1388 + if (i == 0 and !channel.at_oldest) { 1389 + try client.requestHistory(.before, channel); 1390 + } 1391 + } 1392 + } 1393 + 1394 + fn drawMemberList(self: *App, win: vaxis.Window, channel: *irc.Channel) !void { 1395 + // Handle mouse 1396 + { 1397 + if (win.hasMouse(self.state.mouse)) |mouse| { 1398 + switch (mouse.button) { 1399 + .wheel_up => { 1400 + self.state.members.scroll_offset -|= 3; 1401 + self.state.mouse.?.button = .none; 1402 + }, 1403 + .wheel_down => { 1404 + self.state.members.scroll_offset +|= 3; 1405 + self.state.mouse.?.button = .none; 1406 + }, 1407 + else => {}, 1408 + } 1409 + } 1410 + 1411 + self.state.members.scroll_offset = @min( 1412 + self.state.members.scroll_offset, 1413 + channel.members.items.len -| win.height, 1414 + ); 1415 + } 1416 + 1417 + // Draw the list 1418 + var member_row: usize = 0; 1419 + for (channel.members.items) |*member| { 1420 + defer member_row += 1; 1421 + if (member_row < self.state.members.scroll_offset) continue; 1422 + const member_seg = [_]vaxis.Segment{ 1423 + .{ 1424 + .text = std.mem.asBytes(&member.prefix), 1425 + }, 1426 + .{ 1427 + .text = member.user.nick, 1428 + .style = .{ 1429 + .fg = if (member.user.away) 1430 + .{ .index = 8 } 1431 + else 1432 + member.user.color, 1433 + }, 1434 + }, 1435 + }; 1436 + _ = try win.print(&member_seg, .{ 1437 + .row_offset = member_row -| self.state.members.scroll_offset, 1438 + }); 1439 + } 1440 + } 1441 + 1442 + fn drawTopic(_: *App, win: vaxis.Window, topic: []const u8) !void { 1443 + _ = try win.printSegment(.{ .text = topic }, .{ .wrap = .none }); 1444 + } 1445 + 1446 + fn drawBufferList(self: *App, clients: []*irc.Client, win: vaxis.Window) !void { 1447 + const buf_list_w = self.state.buffers.width; 1448 + var row: usize = 0; 1449 + 1450 + defer self.state.buffers.count = row; 1451 + for (clients) |client| { 1452 + var style: vaxis.Style = if (row == self.state.buffers.selected_idx) 1453 + .{ 1454 + .fg = if (client.status == .disconnected) .{ .index = 8 } else .default, 1455 + .reverse = true, 1456 + } 1457 + else 1458 + .{ 1459 + .fg = if (client.status == .disconnected) .{ .index = 8 } else .default, 1460 + }; 1461 + const network_win = win.child(.{ 1462 + .y_off = row, 1463 + .height = .{ .limit = 1 }, 1464 + }); 1465 + if (network_win.hasMouse(self.state.mouse)) |_| { 1466 + self.vx.setMouseShape(.pointer); 1467 + style.bg = .{ .index = 8 }; 1468 + } 1469 + _ = try network_win.print( 1470 + &.{.{ 1471 + .text = client.config.name orelse client.config.server, 1472 + .style = style, 1473 + }}, 1474 + .{}, 1475 + ); 1476 + if (network_win.hasMouse(self.state.mouse)) |_| { 1477 + self.vx.setMouseShape(.pointer); 1478 + } 1479 + row += 1; 1480 + for (client.channels.items) |*channel| { 1481 + const channel_win = win.child(.{ 1482 + .y_off = row, 1483 + .height = .{ .limit = 1 }, 1484 + }); 1485 + if (channel_win.hasMouse(self.state.mouse)) |mouse| { 1486 + if (mouse.type == .press and mouse.button == .left) { 1487 + self.state.buffers.selected_idx = row; 1488 + } 1489 + } 1490 + 1491 + const is_current = row == self.state.buffers.selected_idx; 1492 + var chan_style: vaxis.Style = if (is_current) 1493 + .{ 1494 + .fg = if (client.status == .disconnected) .{ .index = 8 } else .default, 1495 + .reverse = true, 1496 + } 1497 + else if (channel.has_unread) 1498 + .{ 1499 + .fg = .{ .index = 4 }, 1500 + .bold = true, 1501 + } 1502 + else 1503 + .{ 1504 + .fg = if (client.status == .disconnected) .{ .index = 8 } else .default, 1505 + }; 1506 + defer row += 1; 1507 + const prefix: []const u8 = if (channel.name[0] == '#') "#" else ""; 1508 + const name_offset: usize = if (prefix.len > 0) 1 else 0; 1509 + 1510 + if (channel_win.hasMouse(self.state.mouse)) |mouse| { 1511 + self.vx.setMouseShape(.pointer); 1512 + if (mouse.button == .left) 1513 + chan_style.reverse = true 1514 + else 1515 + chan_style.bg = .{ .index = 8 }; 1516 + } 1517 + 1518 + const first_seg: vaxis.Segment = if (channel.has_unread_highlight) 1519 + .{ .text = " ●︎", .style = .{ .fg = .{ .index = 1 } } } 1520 + else 1521 + .{ .text = " " }; 1522 + 1523 + var chan_seg = [_]vaxis.Segment{ 1524 + first_seg, 1525 + .{ 1526 + .text = prefix, 1527 + .style = .{ .fg = .{ .index = 8 } }, 1528 + }, 1529 + .{ 1530 + .text = channel.name[name_offset..], 1531 + .style = chan_style, 1532 + }, 1533 + }; 1534 + const result = try channel_win.print( 1535 + &chan_seg, 1536 + .{}, 1537 + ); 1538 + if (result.overflow) 1539 + win.writeCell( 1540 + buf_list_w -| 1, 1541 + row, 1542 + .{ 1543 + .char = .{ 1544 + .grapheme = "…", 1545 + .width = 1, 1546 + }, 1547 + .style = chan_style, 1548 + }, 1549 + ); 1550 + } 1551 + } 1552 + } 1553 + 1493 1554 /// generate vaxis.Segments for the message content 1494 1555 fn formatMessageContent(self: *App, client: *irc.Client, msg: irc.Message) !void { 1495 1556 const ColorState = enum { ··· 1782 1843 } 1783 1844 } 1784 1845 } 1846 + 1847 + /// generate vaxis.Segments for the message content 1848 + fn formatMessage(segments: *std.ArrayList(vaxis.Segment), user: *const irc.User, msg: irc.Message) !void { 1849 + const ColorState = enum { 1850 + ground, 1851 + fg, 1852 + bg, 1853 + }; 1854 + const LinkState = enum { 1855 + h, 1856 + t1, 1857 + t2, 1858 + p, 1859 + s, 1860 + colon, 1861 + slash, 1862 + consume, 1863 + }; 1864 + 1865 + var iter = msg.paramIterator(); 1866 + _ = iter.next() orelse return error.InvalidMessage; 1867 + const content = iter.next() orelse return error.InvalidMessage; 1868 + var start: usize = 0; 1869 + var i: usize = 0; 1870 + var style: vaxis.Style = .{}; 1871 + while (i < content.len) : (i += 1) { 1872 + const b = content[i]; 1873 + switch (b) { 1874 + 0x01 => { 1875 + if (i == 0 and 1876 + content.len > 7 and 1877 + mem.startsWith(u8, content[1..], "ACTION")) 1878 + { 1879 + style.italic = true; 1880 + const user_style: vaxis.Style = .{ 1881 + .fg = user.color, 1882 + .italic = true, 1883 + }; 1884 + try segments.append(.{ 1885 + .text = user.nick, 1886 + .style = user_style, 1887 + }); 1888 + i += 6; // "ACTION" 1889 + } else { 1890 + try segments.append(.{ 1891 + .text = content[start..i], 1892 + .style = style, 1893 + }); 1894 + } 1895 + start = i + 1; 1896 + }, 1897 + 0x02 => { 1898 + try segments.append(.{ 1899 + .text = content[start..i], 1900 + .style = style, 1901 + }); 1902 + style.bold = !style.bold; 1903 + start = i + 1; 1904 + }, 1905 + 0x03 => { 1906 + try segments.append(.{ 1907 + .text = content[start..i], 1908 + .style = style, 1909 + }); 1910 + i += 1; 1911 + var state: ColorState = .ground; 1912 + var fg_idx: ?u8 = null; 1913 + var bg_idx: ?u8 = null; 1914 + while (i < content.len) : (i += 1) { 1915 + const d = content[i]; 1916 + switch (state) { 1917 + .ground => { 1918 + switch (d) { 1919 + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 1920 + state = .fg; 1921 + fg_idx = d - '0'; 1922 + }, 1923 + else => { 1924 + style.fg = .default; 1925 + style.bg = .default; 1926 + start = i; 1927 + break; 1928 + }, 1929 + } 1930 + }, 1931 + .fg => { 1932 + switch (d) { 1933 + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 1934 + const fg = fg_idx orelse 0; 1935 + if (fg > 9) { 1936 + style.fg = irc.toVaxisColor(fg); 1937 + start = i; 1938 + break; 1939 + } else { 1940 + fg_idx = fg * 10 + (d - '0'); 1941 + } 1942 + }, 1943 + else => { 1944 + if (fg_idx) |fg| { 1945 + style.fg = irc.toVaxisColor(fg); 1946 + start = i; 1947 + } 1948 + if (d == ',') state = .bg else break; 1949 + }, 1950 + } 1951 + }, 1952 + .bg => { 1953 + switch (d) { 1954 + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 1955 + const bg = bg_idx orelse 0; 1956 + if (i - start == 2) { 1957 + style.bg = irc.toVaxisColor(bg); 1958 + start = i; 1959 + break; 1960 + } else { 1961 + bg_idx = bg * 10 + (d - '0'); 1962 + } 1963 + }, 1964 + else => { 1965 + if (bg_idx) |bg| { 1966 + style.bg = irc.toVaxisColor(bg); 1967 + start = i; 1968 + } 1969 + break; 1970 + }, 1971 + } 1972 + }, 1973 + } 1974 + } 1975 + }, 1976 + 0x0F => { 1977 + try segments.append(.{ 1978 + .text = content[start..i], 1979 + .style = style, 1980 + }); 1981 + style = .{}; 1982 + start = i + 1; 1983 + }, 1984 + 0x16 => { 1985 + try segments.append(.{ 1986 + .text = content[start..i], 1987 + .style = style, 1988 + }); 1989 + style.reverse = !style.reverse; 1990 + start = i + 1; 1991 + }, 1992 + 0x1D => { 1993 + try segments.append(.{ 1994 + .text = content[start..i], 1995 + .style = style, 1996 + }); 1997 + style.italic = !style.italic; 1998 + start = i + 1; 1999 + }, 2000 + 0x1E => { 2001 + try segments.append(.{ 2002 + .text = content[start..i], 2003 + .style = style, 2004 + }); 2005 + style.strikethrough = !style.strikethrough; 2006 + start = i + 1; 2007 + }, 2008 + 0x1F => { 2009 + try segments.append(.{ 2010 + .text = content[start..i], 2011 + .style = style, 2012 + }); 2013 + 2014 + style.ul_style = if (style.ul_style == .off) .single else .off; 2015 + start = i + 1; 2016 + }, 2017 + else => { 2018 + if (b == 'h') { 2019 + var state: LinkState = .h; 2020 + const h_start = i; 2021 + // consume until a space or EOF 2022 + i += 1; 2023 + while (i < content.len) : (i += 1) { 2024 + const b1 = content[i]; 2025 + switch (state) { 2026 + .h => { 2027 + if (b1 == 't') state = .t1 else break; 2028 + }, 2029 + .t1 => { 2030 + if (b1 == 't') state = .t2 else break; 2031 + }, 2032 + .t2 => { 2033 + if (b1 == 'p') state = .p else break; 2034 + }, 2035 + .p => { 2036 + if (b1 == 's') 2037 + state = .s 2038 + else if (b1 == ':') 2039 + state = .colon 2040 + else 2041 + break; 2042 + }, 2043 + .s => { 2044 + if (b1 == ':') state = .colon else break; 2045 + }, 2046 + .colon => { 2047 + if (b1 == '/') state = .slash else break; 2048 + }, 2049 + .slash => { 2050 + if (b1 == '/') { 2051 + state = .consume; 2052 + try segments.append(.{ 2053 + .text = content[start..h_start], 2054 + .style = style, 2055 + }); 2056 + start = h_start; 2057 + } else break; 2058 + }, 2059 + .consume => { 2060 + switch (b1) { 2061 + 0x00...0x20, 0x7F => { 2062 + try segments.append(.{ 2063 + .text = content[h_start..i], 2064 + .style = .{ 2065 + .fg = .{ .index = 4 }, 2066 + }, 2067 + .link = .{ 2068 + .uri = content[h_start..i], 2069 + }, 2070 + }); 2071 + start = i; 2072 + // backup one 2073 + i -= 1; 2074 + break; 2075 + }, 2076 + else => { 2077 + if (i == content.len) { 2078 + try segments.append(.{ 2079 + .text = content[h_start..], 2080 + .style = .{ 2081 + .fg = .{ .index = 4 }, 2082 + }, 2083 + .link = .{ 2084 + .uri = content[h_start..], 2085 + }, 2086 + }); 2087 + return; 2088 + } 2089 + }, 2090 + } 2091 + }, 2092 + } 2093 + } 2094 + } 2095 + }, 2096 + } 2097 + } 2098 + if (start < i and start < content.len) { 2099 + try segments.append(.{ 2100 + .text = content[start..], 2101 + .style = style, 2102 + }); 2103 + } 2104 + } 2105 + 2106 + /// Returns the number of lines the segments would consume in the given window 2107 + fn lineCountForWindow(win: vaxis.Window, segments: []const vaxis.Segment) usize { 2108 + // Fastpath if we have fewer bytes than the width 2109 + var byte_count: usize = 0; 2110 + for (segments) |segment| { 2111 + byte_count += segment.text.len; 2112 + } 2113 + // One line if we are fewer bytes than the width 2114 + if (byte_count <= win.width) return 1; 2115 + 2116 + // Slow path. We have to layout the text 2117 + const result = win.print(segments, .{ .commit = false, .wrap = .word }) catch return 0; 2118 + if (result.col == 0) 2119 + return result.row 2120 + else 2121 + return result.row + 1; 2122 + }
+21
src/irc.zig
··· 196 196 } 197 197 } 198 198 } 199 + 200 + /// issue a MARKREAD command for this channel. The most recent message in the channel will be used as 201 + /// the last read time 202 + pub fn markRead(self: *Channel) !void { 203 + if (!self.has_unread) return; 204 + 205 + self.has_unread = false; 206 + self.has_unread_highlight = false; 207 + const last_msg = self.messages.getLast(); 208 + const time_tag = last_msg.getTag("time") orelse return; 209 + var write_buf: [128]u8 = undefined; 210 + const mark_read = try std.fmt.bufPrint( 211 + &write_buf, 212 + "MARKREAD {s} timestamp={s}\r\n", 213 + .{ 214 + self.name, 215 + time_tag, 216 + }, 217 + ); 218 + try self.client.queueWrite(mark_read); 219 + } 199 220 }; 200 221 201 222 pub const User = struct {