an experimental irc client

ui: unread line

+421 -308
+2 -2
build.zig.zon
··· 7 7 .hash = "1220affeb3fe37ef09411b5a213b5fdf9bb6568e9913bade204694648983a8b2776d", 8 8 }, 9 9 .vaxis = .{ 10 - .url = "git+https://github.com/rockorager/libvaxis#01e7b6644b63ca3883ca43a509fcee62da18521e", 11 - .hash = "1220f5d235ab148bc7c0bdf3870271145d22cd35fd6745047e66a019d98e2cd43020", 10 + .url = "git+https://github.com/rockorager/libvaxis#f8672276e50e48e361bb174f0bcae72df9f0cde9", 11 + .hash = "122059f772f1ab238d89d3005f396f46465e88fff1deb0d49effc9285aa5de29aeb2", 12 12 }, 13 13 .zeit = .{ 14 14 .url = "git+https://github.com/rockorager/zeit?ref=main#d943bc4bfe9e18490460dfdd64f48e997065eba8",
+5 -270
src/app.zig
··· 990 990 } 991 991 992 992 /// handle a command 993 - pub fn handleCommand(self: *App, lua_state: *Lua, buffer: irc.Buffer, cmd: []const u8) !void { 993 + pub fn handleCommand(self: *App, buffer: irc.Buffer, cmd: []const u8) !void { 994 + const lua_state = self.lua; 994 995 const command: comlink.Command = blk: { 995 996 const start: u1 = if (cmd[0] == '/') 1 else 0; 996 997 const end = mem.indexOfScalar(u8, cmd, ' ') orelse cmd.len; ··· 1159 1160 } 1160 1161 1161 1162 pub fn selectBuffer(self: *App, buffer: irc.Buffer) void { 1163 + self.markSelectedChannelRead(); 1162 1164 var i: u32 = 0; 1163 1165 switch (buffer) { 1164 1166 .client => |target| { ··· 1179 1181 if (channel == target) { 1180 1182 self.buffer_list.cursor = i; 1181 1183 self.buffer_list.ensureScroll(); 1184 + if (target.messageViewIsAtBottom()) target.has_unread = false; 1182 1185 return; 1183 1186 } 1184 1187 i += 1; ··· 1900 1903 } 1901 1904 } 1902 1905 1903 - /// generate vaxis.Segments for the message content 1904 - fn formatMessageContent(self: *App, client: *irc.Client, msg: irc.Message) !void { 1905 - const ColorState = enum { 1906 - ground, 1907 - fg, 1908 - bg, 1909 - }; 1910 - const LinkState = enum { 1911 - h, 1912 - t1, 1913 - t2, 1914 - p, 1915 - s, 1916 - colon, 1917 - slash, 1918 - consume, 1919 - }; 1920 - 1921 - var iter = msg.paramIterator(); 1922 - _ = iter.next() orelse return error.InvalidMessage; 1923 - const content = iter.next() orelse return error.InvalidMessage; 1924 - var start: usize = 0; 1925 - var i: usize = 0; 1926 - var style: vaxis.Style = .{}; 1927 - while (i < content.len) : (i += 1) { 1928 - const b = content[i]; 1929 - switch (b) { 1930 - 0x01 => { // https://modern.ircdocs.horse/ctcp 1931 - if (i == 0 and 1932 - content.len > 7 and 1933 - mem.startsWith(u8, content[1..], "ACTION")) 1934 - { 1935 - // get the user of this message 1936 - const sender: []const u8 = blk: { 1937 - const src = msg.source() orelse break :blk ""; 1938 - const l = std.mem.indexOfScalar(u8, src, '!') orelse 1939 - std.mem.indexOfScalar(u8, src, '@') orelse 1940 - src.len; 1941 - break :blk src[0..l]; 1942 - }; 1943 - const user = try client.getOrCreateUser(sender); 1944 - style.italic = true; 1945 - const user_style: vaxis.Style = .{ 1946 - .fg = user.color, 1947 - .italic = true, 1948 - }; 1949 - try self.content_segments.append(.{ 1950 - .text = user.nick, 1951 - .style = user_style, 1952 - }); 1953 - i += 6; // "ACTION" 1954 - } else { 1955 - try self.content_segments.append(.{ 1956 - .text = content[start..i], 1957 - .style = style, 1958 - }); 1959 - } 1960 - start = i + 1; 1961 - }, 1962 - 0x02 => { 1963 - try self.content_segments.append(.{ 1964 - .text = content[start..i], 1965 - .style = style, 1966 - }); 1967 - style.bold = !style.bold; 1968 - start = i + 1; 1969 - }, 1970 - 0x03 => { 1971 - try self.content_segments.append(.{ 1972 - .text = content[start..i], 1973 - .style = style, 1974 - }); 1975 - i += 1; 1976 - var state: ColorState = .ground; 1977 - var fg_idx: ?u8 = null; 1978 - var bg_idx: ?u8 = null; 1979 - while (i < content.len) : (i += 1) { 1980 - const d = content[i]; 1981 - switch (state) { 1982 - .ground => { 1983 - switch (d) { 1984 - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 1985 - state = .fg; 1986 - fg_idx = d - '0'; 1987 - }, 1988 - else => { 1989 - style.fg = .default; 1990 - style.bg = .default; 1991 - start = i; 1992 - break; 1993 - }, 1994 - } 1995 - }, 1996 - .fg => { 1997 - switch (d) { 1998 - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 1999 - const fg = fg_idx orelse 0; 2000 - if (fg > 9) { 2001 - style.fg = irc.toVaxisColor(fg); 2002 - start = i; 2003 - break; 2004 - } else { 2005 - fg_idx = fg * 10 + (d - '0'); 2006 - } 2007 - }, 2008 - else => { 2009 - if (fg_idx) |fg| { 2010 - style.fg = irc.toVaxisColor(fg); 2011 - start = i; 2012 - } 2013 - if (d == ',') state = .bg else break; 2014 - }, 2015 - } 2016 - }, 2017 - .bg => { 2018 - switch (d) { 2019 - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 2020 - const bg = bg_idx orelse 0; 2021 - if (i - start == 2) { 2022 - style.bg = irc.toVaxisColor(bg); 2023 - start = i; 2024 - break; 2025 - } else { 2026 - bg_idx = bg * 10 + (d - '0'); 2027 - } 2028 - }, 2029 - else => { 2030 - if (bg_idx) |bg| { 2031 - style.bg = irc.toVaxisColor(bg); 2032 - start = i; 2033 - } 2034 - break; 2035 - }, 2036 - } 2037 - }, 2038 - } 2039 - } 2040 - }, 2041 - 0x0F => { 2042 - try self.content_segments.append(.{ 2043 - .text = content[start..i], 2044 - .style = style, 2045 - }); 2046 - style = .{}; 2047 - start = i + 1; 2048 - }, 2049 - 0x16 => { 2050 - try self.content_segments.append(.{ 2051 - .text = content[start..i], 2052 - .style = style, 2053 - }); 2054 - style.reverse = !style.reverse; 2055 - start = i + 1; 2056 - }, 2057 - 0x1D => { 2058 - try self.content_segments.append(.{ 2059 - .text = content[start..i], 2060 - .style = style, 2061 - }); 2062 - style.italic = !style.italic; 2063 - start = i + 1; 2064 - }, 2065 - 0x1E => { 2066 - try self.content_segments.append(.{ 2067 - .text = content[start..i], 2068 - .style = style, 2069 - }); 2070 - style.strikethrough = !style.strikethrough; 2071 - start = i + 1; 2072 - }, 2073 - 0x1F => { 2074 - try self.content_segments.append(.{ 2075 - .text = content[start..i], 2076 - .style = style, 2077 - }); 2078 - 2079 - style.ul_style = if (style.ul_style == .off) .single else .off; 2080 - start = i + 1; 2081 - }, 2082 - else => { 2083 - if (b == 'h') { 2084 - var state: LinkState = .h; 2085 - const h_start = i; 2086 - // consume until a space or EOF 2087 - i += 1; 2088 - while (i < content.len) : (i += 1) { 2089 - const b1 = content[i]; 2090 - switch (state) { 2091 - .h => { 2092 - if (b1 == 't') state = .t1 else break; 2093 - }, 2094 - .t1 => { 2095 - if (b1 == 't') state = .t2 else break; 2096 - }, 2097 - .t2 => { 2098 - if (b1 == 'p') state = .p else break; 2099 - }, 2100 - .p => { 2101 - if (b1 == 's') 2102 - state = .s 2103 - else if (b1 == ':') 2104 - state = .colon 2105 - else 2106 - break; 2107 - }, 2108 - .s => { 2109 - if (b1 == ':') state = .colon else break; 2110 - }, 2111 - .colon => { 2112 - if (b1 == '/') state = .slash else break; 2113 - }, 2114 - .slash => { 2115 - if (b1 == '/') { 2116 - state = .consume; 2117 - try self.content_segments.append(.{ 2118 - .text = content[start..h_start], 2119 - .style = style, 2120 - }); 2121 - start = h_start; 2122 - } else break; 2123 - }, 2124 - .consume => { 2125 - switch (b1) { 2126 - 0x00...0x20, 0x7F => { 2127 - try self.content_segments.append(.{ 2128 - .text = content[h_start..i], 2129 - .style = .{ 2130 - .fg = .{ .index = 4 }, 2131 - }, 2132 - .link = .{ 2133 - .uri = content[h_start..i], 2134 - }, 2135 - }); 2136 - start = i; 2137 - // backup one 2138 - i -= 1; 2139 - break; 2140 - }, 2141 - else => { 2142 - if (i == content.len) { 2143 - try self.content_segments.append(.{ 2144 - .text = content[h_start..], 2145 - .style = .{ 2146 - .fg = .{ .index = 4 }, 2147 - }, 2148 - .link = .{ 2149 - .uri = content[h_start..], 2150 - }, 2151 - }); 2152 - return; 2153 - } 2154 - }, 2155 - } 2156 - }, 2157 - } 2158 - } 2159 - } 2160 - }, 2161 - } 2162 - } 2163 - if (start < i and start < content.len) { 2164 - try self.content_segments.append(.{ 2165 - .text = content[start..], 2166 - .style = style, 2167 - }); 2168 - } 2169 - } 2170 - 2171 1906 pub fn markSelectedChannelRead(self: *App) void { 2172 1907 const buffer = self.selectedBuffer() orelse return; 2173 1908 2174 1909 switch (buffer) { 2175 1910 .channel => |channel| { 2176 - channel.markRead() catch return; 1911 + if (channel.messageViewIsAtBottom()) channel.markRead() catch return; 2177 1912 }, 2178 1913 else => {}, 2179 1914 }
+412 -35
src/irc.zig
··· 168 168 pub fn draw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 169 169 const self: *Member = @ptrCast(@alignCast(ptr)); 170 170 const style: vaxis.Style = if (self.user.away) 171 - .{ .dim = true } 171 + .{ .fg = .{ .index = 8 } } 172 172 else 173 173 .{ .fg = self.user.color }; 174 174 var prefix = try ctx.arena.alloc(u8, 1); ··· 213 213 }, 214 214 .text_field = vxfw.TextField.init(gpa, unicode), 215 215 }; 216 + 217 + self.text_field.userdata = self; 218 + self.text_field.onSubmit = Channel.onSubmit; 219 + } 220 + 221 + fn onSubmit(ptr: ?*anyopaque, ctx: *vxfw.EventContext, input: []const u8) anyerror!void { 222 + const self: *Channel = @ptrCast(@alignCast(ptr orelse unreachable)); 223 + if (std.mem.startsWith(u8, input, "/")) { 224 + try self.client.app.handleCommand(.{ .channel = self }, input); 225 + } else { 226 + try self.client.print("PRIVMSG {s} :{s}\r\n", .{ self.name, input }); 227 + } 228 + ctx.redraw = true; 229 + self.text_field.clearAndFree(); 216 230 } 217 231 218 232 pub fn deinit(self: *Channel, alloc: std.mem.Allocator) void { ··· 300 314 301 315 pub fn drawName(self: *Channel, ctx: vxfw.DrawContext, selected: bool) Allocator.Error!vxfw.Surface { 302 316 var style: vaxis.Style = .{}; 303 - if (selected) style.reverse = true; 317 + if (selected) style.bg = .{ .index = 8 }; 304 318 if (self.has_mouse) style.bg = .{ .index = 8 }; 319 + if (self.client.app.selectedBuffer()) |buffer| { 320 + switch (buffer) { 321 + .client => {}, 322 + .channel => |channel| { 323 + if (channel == self and self.messageViewIsAtBottom()) { 324 + self.has_unread = false; 325 + } 326 + }, 327 + } 328 + } 329 + if (self.has_unread) style.fg = .{ .index = 4 }; 305 330 306 - const text: vxfw.RichText = .{ 307 - .text = &.{ 308 - .{ .text = " " }, 309 - .{ .text = self.name, .style = style }, 310 - }, 311 - .softwrap = false, 312 - }; 331 + const text: vxfw.RichText = if (std.mem.startsWith(u8, self.name, "#")) 332 + .{ 333 + .text = &.{ 334 + .{ .text = " " }, 335 + .{ .text = "#", .style = .{ .fg = .{ .index = 8 } } }, 336 + .{ .text = self.name[1..], .style = style }, 337 + }, 338 + .softwrap = false, 339 + } 340 + else 341 + .{ 342 + .text = &.{ 343 + .{ .text = " " }, 344 + .{ .text = self.name, .style = style }, 345 + }, 346 + .softwrap = false, 347 + }; 348 + 313 349 var surface = try text.draw(ctx); 314 350 // Replace the widget reference so we can handle the events 315 351 surface.widget = self.nameWidget(selected); ··· 365 401 /// issue a MARKREAD command for this channel. The most recent message in the channel will be used as 366 402 /// the last read time 367 403 pub fn markRead(self: *Channel) !void { 368 - if (!self.has_unread) return; 369 - 370 404 self.has_unread = false; 371 405 self.has_unread_highlight = false; 372 406 const last_msg = self.messages.getLast(); ··· 473 507 // Save this mouse state for when we draw 474 508 self.message_view.mouse = mouse; 475 509 510 + // A middle press on a hovered message means we copy the content 476 511 if (mouse.type == .press and 477 512 mouse.button == .middle and 478 513 self.message_view.hovered_message != null) ··· 487 522 return ctx.consumeAndRedraw(); 488 523 } 489 524 if (mouse.button == .wheel_down) { 490 - self.scroll.pending -|= 3; 525 + self.scroll.pending -|= 1; 491 526 ctx.consume_event = true; 492 527 } 493 528 if (mouse.button == .wheel_up) { 494 - self.scroll.pending +|= 3; 529 + self.scroll.pending +|= 1; 495 530 ctx.consume_event = true; 496 531 } 497 532 if (self.scroll.pending != 0) { 498 - return self.doScroll(ctx); 533 + try self.doScroll(ctx); 499 534 } 500 535 }, 501 536 .mouse_leave => { ··· 503 538 self.message_view.hovered_message = null; 504 539 ctx.redraw = true; 505 540 }, 506 - .tick => try self.doScroll(ctx), 541 + .tick => { 542 + try self.doScroll(ctx); 543 + }, 507 544 else => {}, 508 545 } 509 546 } ··· 568 605 return self.drawMessageView(ctx); 569 606 } 570 607 608 + pub fn messageViewIsAtBottom(self: *Channel) bool { 609 + if (self.scroll.msg_offset) |msg_offset| { 610 + return self.scroll.offset == 0 and 611 + msg_offset == self.messages.items.len and 612 + self.scroll.pending == 0; 613 + } 614 + return self.scroll.offset == 0 and 615 + self.scroll.msg_offset == null and 616 + self.scroll.pending == 0; 617 + } 618 + 571 619 fn drawMessageView(self: *Channel, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 620 + self.message_view.hovered_message = null; 572 621 const max = ctx.max.size(); 573 622 if (max.width == 0 or 574 623 max.height == 0 or ··· 642 691 break :blk param_iter.next() orelse ""; 643 692 }; 644 693 694 + // Get the user ref for this sender 695 + const user = try self.client.getOrCreateUser(sender); 696 + 697 + const spans = try formatMessage(ctx.arena, user, content); 698 + 645 699 // Draw the message so we have it's wrapped height 646 - const text: vxfw.Text = .{ .text = content }; 700 + const text: vxfw.RichText = .{ .text = spans }; 647 701 const child_ctx = ctx.withConstraints( 648 702 .{ .width = 0, .height = 0 }, 649 703 .{ .width = max.width -| gutter_width, .height = null }, ··· 741 795 .origin = .{ .row = row, .col = 0 }, 742 796 .surface = try time_text.draw(child_ctx), 743 797 }); 798 + } 744 799 745 - // Check if we need to print the sender of this message. We do this when the timegap 746 - // between this message and next message is > 5 minutes, or if the sender is 747 - // different 748 - if (sender.len > 0 and 749 - printSender(sender, next_sender, maybe_instant, maybe_next_instant)) 750 - { 751 - // Back up one row to print 752 - row -= 1; 753 - // If we need to print the sender, it will be *this* messages sender 754 - const user = try self.client.getOrCreateUser(sender); 755 - const sender_text: vxfw.Text = .{ 756 - .text = user.nick, 757 - .style = .{ .fg = user.color, .bold = true }, 800 + var printed_sender: bool = false; 801 + // Check if we need to print the sender of this message. We do this when the timegap 802 + // between this message and next message is > 5 minutes, or if the sender is 803 + // different 804 + if (sender.len > 0 and 805 + printSender(sender, next_sender, maybe_instant, maybe_next_instant)) 806 + { 807 + // Back up one row to print 808 + row -= 1; 809 + // If we need to print the sender, it will be *this* messages sender 810 + const sender_text: vxfw.Text = .{ 811 + .text = user.nick, 812 + .style = .{ .fg = user.color, .bold = true }, 813 + }; 814 + const sender_surface = try sender_text.draw(child_ctx); 815 + try children.append(.{ 816 + .origin = .{ .row = row, .col = gutter_width }, 817 + .surface = sender_surface, 818 + }); 819 + if (self.message_view.mouse) |mouse| { 820 + if (mouse.row == row and 821 + mouse.col >= gutter_width and 822 + user.real_name != null) 823 + { 824 + const realname: vxfw.Text = .{ 825 + .text = user.real_name orelse unreachable, 826 + .style = .{ .fg = .{ .index = 8 }, .italic = true }, 827 + }; 828 + try children.append(.{ 829 + .origin = .{ 830 + .row = row, 831 + .col = gutter_width + sender_surface.size.width + 1, 832 + }, 833 + .surface = try realname.draw(child_ctx), 834 + }); 835 + } 836 + } 837 + 838 + // Back up 1 more row for spacing 839 + row -= 1; 840 + printed_sender = true; 841 + } 842 + 843 + // Check if we should print a "last read" line. If the next message we will print is 844 + // before the last_read, and this message is after the last_read then it is our border. 845 + // Before 846 + if (maybe_next_instant != null and maybe_instant != null) { 847 + const this = maybe_instant.?.unixTimestamp(); 848 + const next = maybe_next_instant.?.unixTimestamp(); 849 + if (this > self.last_read and next <= self.last_read) { 850 + const bot = "─"; 851 + var writer = try std.ArrayList(u8).initCapacity(ctx.arena, bot.len * max.width); 852 + try writer.writer().writeBytesNTimes(bot, max.width); 853 + 854 + const border: vxfw.Text = .{ 855 + .text = writer.items, 856 + .style = .{ .fg = .{ .index = 1 } }, 857 + .softwrap = false, 758 858 }; 759 - try children.append(.{ 760 - .origin = .{ .row = row, .col = gutter_width }, 761 - .surface = try sender_text.draw(child_ctx), 762 - }); 763 859 764 - // Back up 1 more row for spacing 765 - row -= 1; 860 + // We don't need to backup a line if we printed the sender 861 + if (!printed_sender) row -= 1; 862 + 863 + const unread: vxfw.SubSurface = .{ 864 + .origin = .{ .col = 0, .row = row }, 865 + .surface = try border.draw(ctx), 866 + }; 867 + try children.append(unread); 868 + const new: vxfw.RichText = .{ 869 + .text = &.{ 870 + .{ .text = "", .style = .{ .fg = .{ .index = 1 } } }, 871 + .{ .text = " New ", .style = .{ .fg = .{ .index = 1 }, .reverse = true } }, 872 + }, 873 + .softwrap = false, 874 + }; 875 + const new_sub: vxfw.SubSurface = .{ 876 + .origin = .{ .col = max.width - 6, .row = row }, 877 + .surface = try new.draw(ctx), 878 + }; 879 + try children.append(new_sub); 766 880 } 767 881 } 768 882 } ··· 2060 2174 2061 2175 else => .{ .index = irc }, 2062 2176 }; 2177 + } 2178 + /// generate TextSpans for the message content 2179 + fn formatMessage( 2180 + arena: Allocator, 2181 + user: *User, 2182 + content: []const u8, 2183 + ) Allocator.Error![]vxfw.RichText.TextSpan { 2184 + const ColorState = enum { 2185 + ground, 2186 + fg, 2187 + bg, 2188 + }; 2189 + const LinkState = enum { 2190 + h, 2191 + t1, 2192 + t2, 2193 + p, 2194 + s, 2195 + colon, 2196 + slash, 2197 + consume, 2198 + }; 2199 + 2200 + var spans = std.ArrayList(vxfw.RichText.TextSpan).init(arena); 2201 + 2202 + var start: usize = 0; 2203 + var i: usize = 0; 2204 + var style: vaxis.Style = .{}; 2205 + while (i < content.len) : (i += 1) { 2206 + const b = content[i]; 2207 + switch (b) { 2208 + 0x01 => { // https://modern.ircdocs.horse/ctcp 2209 + if (i == 0 and 2210 + content.len > 7 and 2211 + mem.startsWith(u8, content[1..], "ACTION")) 2212 + { 2213 + // get the user of this message 2214 + style.italic = true; 2215 + const user_style: vaxis.Style = .{ 2216 + .fg = user.color, 2217 + .italic = true, 2218 + }; 2219 + try spans.append(.{ 2220 + .text = user.nick, 2221 + .style = user_style, 2222 + }); 2223 + i += 6; // "ACTION" 2224 + } else { 2225 + try spans.append(.{ 2226 + .text = content[start..i], 2227 + .style = style, 2228 + }); 2229 + } 2230 + start = i + 1; 2231 + }, 2232 + 0x02 => { 2233 + try spans.append(.{ 2234 + .text = content[start..i], 2235 + .style = style, 2236 + }); 2237 + style.bold = !style.bold; 2238 + start = i + 1; 2239 + }, 2240 + 0x03 => { 2241 + try spans.append(.{ 2242 + .text = content[start..i], 2243 + .style = style, 2244 + }); 2245 + i += 1; 2246 + var state: ColorState = .ground; 2247 + var fg_idx: ?u8 = null; 2248 + var bg_idx: ?u8 = null; 2249 + while (i < content.len) : (i += 1) { 2250 + const d = content[i]; 2251 + switch (state) { 2252 + .ground => { 2253 + switch (d) { 2254 + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 2255 + state = .fg; 2256 + fg_idx = d - '0'; 2257 + }, 2258 + else => { 2259 + style.fg = .default; 2260 + style.bg = .default; 2261 + start = i; 2262 + break; 2263 + }, 2264 + } 2265 + }, 2266 + .fg => { 2267 + switch (d) { 2268 + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 2269 + const fg = fg_idx orelse 0; 2270 + if (fg > 9) { 2271 + style.fg = toVaxisColor(fg); 2272 + start = i; 2273 + break; 2274 + } else { 2275 + fg_idx = fg * 10 + (d - '0'); 2276 + } 2277 + }, 2278 + else => { 2279 + if (fg_idx) |fg| { 2280 + style.fg = toVaxisColor(fg); 2281 + start = i; 2282 + } 2283 + if (d == ',') state = .bg else break; 2284 + }, 2285 + } 2286 + }, 2287 + .bg => { 2288 + switch (d) { 2289 + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { 2290 + const bg = bg_idx orelse 0; 2291 + if (i - start == 2) { 2292 + style.bg = toVaxisColor(bg); 2293 + start = i; 2294 + break; 2295 + } else { 2296 + bg_idx = bg * 10 + (d - '0'); 2297 + } 2298 + }, 2299 + else => { 2300 + if (bg_idx) |bg| { 2301 + style.bg = toVaxisColor(bg); 2302 + start = i; 2303 + } 2304 + break; 2305 + }, 2306 + } 2307 + }, 2308 + } 2309 + } 2310 + }, 2311 + 0x0F => { 2312 + try spans.append(.{ 2313 + .text = content[start..i], 2314 + .style = style, 2315 + }); 2316 + style = .{}; 2317 + start = i + 1; 2318 + }, 2319 + 0x16 => { 2320 + try spans.append(.{ 2321 + .text = content[start..i], 2322 + .style = style, 2323 + }); 2324 + style.reverse = !style.reverse; 2325 + start = i + 1; 2326 + }, 2327 + 0x1D => { 2328 + try spans.append(.{ 2329 + .text = content[start..i], 2330 + .style = style, 2331 + }); 2332 + style.italic = !style.italic; 2333 + start = i + 1; 2334 + }, 2335 + 0x1E => { 2336 + try spans.append(.{ 2337 + .text = content[start..i], 2338 + .style = style, 2339 + }); 2340 + style.strikethrough = !style.strikethrough; 2341 + start = i + 1; 2342 + }, 2343 + 0x1F => { 2344 + try spans.append(.{ 2345 + .text = content[start..i], 2346 + .style = style, 2347 + }); 2348 + 2349 + style.ul_style = if (style.ul_style == .off) .single else .off; 2350 + start = i + 1; 2351 + }, 2352 + else => { 2353 + if (b == 'h') { 2354 + var state: LinkState = .h; 2355 + const h_start = i; 2356 + // consume until a space or EOF 2357 + i += 1; 2358 + while (i < content.len) : (i += 1) { 2359 + const b1 = content[i]; 2360 + switch (state) { 2361 + .h => { 2362 + if (b1 == 't') state = .t1 else break; 2363 + }, 2364 + .t1 => { 2365 + if (b1 == 't') state = .t2 else break; 2366 + }, 2367 + .t2 => { 2368 + if (b1 == 'p') state = .p else break; 2369 + }, 2370 + .p => { 2371 + if (b1 == 's') 2372 + state = .s 2373 + else if (b1 == ':') 2374 + state = .colon 2375 + else 2376 + break; 2377 + }, 2378 + .s => { 2379 + if (b1 == ':') state = .colon else break; 2380 + }, 2381 + .colon => { 2382 + if (b1 == '/') state = .slash else break; 2383 + }, 2384 + .slash => { 2385 + if (b1 == '/') { 2386 + state = .consume; 2387 + try spans.append(.{ 2388 + .text = content[start..h_start], 2389 + .style = style, 2390 + }); 2391 + start = h_start; 2392 + } else break; 2393 + }, 2394 + .consume => { 2395 + switch (b1) { 2396 + 0x00...0x20, 0x7F => { 2397 + try spans.append(.{ 2398 + .text = content[h_start..i], 2399 + .style = .{ 2400 + .fg = .{ .index = 4 }, 2401 + }, 2402 + .link = .{ 2403 + .uri = content[h_start..i], 2404 + }, 2405 + }); 2406 + start = i; 2407 + // backup one 2408 + i -= 1; 2409 + break; 2410 + }, 2411 + else => { 2412 + if (i == content.len) { 2413 + try spans.append(.{ 2414 + .text = content[h_start..], 2415 + .style = .{ 2416 + .fg = .{ .index = 4 }, 2417 + }, 2418 + .link = .{ 2419 + .uri = content[h_start..], 2420 + }, 2421 + }); 2422 + break; 2423 + } 2424 + }, 2425 + } 2426 + }, 2427 + } 2428 + } 2429 + } 2430 + }, 2431 + } 2432 + } 2433 + if (start < i and start < content.len) { 2434 + try spans.append(.{ 2435 + .text = content[start..], 2436 + .style = style, 2437 + }); 2438 + } 2439 + return spans.toOwnedSlice(); 2063 2440 } 2064 2441 2065 2442 const CaseMapAlgo = enum {
+1 -1
src/lua.zig
··· 440 440 441 441 if (msg.len > 0 and msg[0] == '/') { 442 442 const app = getApp(lua); 443 - app.handleCommand(lua, .{ .channel = channel }, msg) catch 443 + app.handleCommand(.{ .channel = channel }, msg) catch 444 444 lua.raiseErrorStr("couldn't handle command", .{}); 445 445 return 0; 446 446 }
+1
src/ui.zig
··· 1 + const std = @import("std");