tangled
alpha
login
or
join now
rockorager.dev
/
comlink
2
fork
atom
an experimental irc client
2
fork
atom
overview
issues
pulls
pipelines
ui: unread line
rockorager.dev
1 year ago
334db4d4
1d82e517
+421
-308
5 changed files
expand all
collapse all
unified
split
build.zig.zon
src
app.zig
irc.zig
lua.zig
ui.zig
+2
-2
build.zig.zon
···
7
.hash = "1220affeb3fe37ef09411b5a213b5fdf9bb6568e9913bade204694648983a8b2776d",
8
},
9
.vaxis = .{
10
-
.url = "git+https://github.com/rockorager/libvaxis#01e7b6644b63ca3883ca43a509fcee62da18521e",
11
-
.hash = "1220f5d235ab148bc7c0bdf3870271145d22cd35fd6745047e66a019d98e2cd43020",
12
},
13
.zeit = .{
14
.url = "git+https://github.com/rockorager/zeit?ref=main#d943bc4bfe9e18490460dfdd64f48e997065eba8",
···
7
.hash = "1220affeb3fe37ef09411b5a213b5fdf9bb6568e9913bade204694648983a8b2776d",
8
},
9
.vaxis = .{
10
+
.url = "git+https://github.com/rockorager/libvaxis#f8672276e50e48e361bb174f0bcae72df9f0cde9",
11
+
.hash = "122059f772f1ab238d89d3005f396f46465e88fff1deb0d49effc9285aa5de29aeb2",
12
},
13
.zeit = .{
14
.url = "git+https://github.com/rockorager/zeit?ref=main#d943bc4bfe9e18490460dfdd64f48e997065eba8",
+5
-270
src/app.zig
···
990
}
991
992
/// handle a command
993
-
pub fn handleCommand(self: *App, lua_state: *Lua, buffer: irc.Buffer, cmd: []const u8) !void {
0
994
const command: comlink.Command = blk: {
995
const start: u1 = if (cmd[0] == '/') 1 else 0;
996
const end = mem.indexOfScalar(u8, cmd, ' ') orelse cmd.len;
···
1159
}
1160
1161
pub fn selectBuffer(self: *App, buffer: irc.Buffer) void {
0
1162
var i: u32 = 0;
1163
switch (buffer) {
1164
.client => |target| {
···
1179
if (channel == target) {
1180
self.buffer_list.cursor = i;
1181
self.buffer_list.ensureScroll();
0
1182
return;
1183
}
1184
i += 1;
···
1900
}
1901
}
1902
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
pub fn markSelectedChannelRead(self: *App) void {
2172
const buffer = self.selectedBuffer() orelse return;
2173
2174
switch (buffer) {
2175
.channel => |channel| {
2176
-
channel.markRead() catch return;
2177
},
2178
else => {},
2179
}
···
990
}
991
992
/// handle a command
993
+
pub fn handleCommand(self: *App, buffer: irc.Buffer, cmd: []const u8) !void {
994
+
const lua_state = self.lua;
995
const command: comlink.Command = blk: {
996
const start: u1 = if (cmd[0] == '/') 1 else 0;
997
const end = mem.indexOfScalar(u8, cmd, ' ') orelse cmd.len;
···
1160
}
1161
1162
pub fn selectBuffer(self: *App, buffer: irc.Buffer) void {
1163
+
self.markSelectedChannelRead();
1164
var i: u32 = 0;
1165
switch (buffer) {
1166
.client => |target| {
···
1181
if (channel == target) {
1182
self.buffer_list.cursor = i;
1183
self.buffer_list.ensureScroll();
1184
+
if (target.messageViewIsAtBottom()) target.has_unread = false;
1185
return;
1186
}
1187
i += 1;
···
1903
}
1904
}
1905
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1906
pub fn markSelectedChannelRead(self: *App) void {
1907
const buffer = self.selectedBuffer() orelse return;
1908
1909
switch (buffer) {
1910
.channel => |channel| {
1911
+
if (channel.messageViewIsAtBottom()) channel.markRead() catch return;
1912
},
1913
else => {},
1914
}
+412
-35
src/irc.zig
···
168
pub fn draw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
169
const self: *Member = @ptrCast(@alignCast(ptr));
170
const style: vaxis.Style = if (self.user.away)
171
-
.{ .dim = true }
172
else
173
.{ .fg = self.user.color };
174
var prefix = try ctx.arena.alloc(u8, 1);
···
213
},
214
.text_field = vxfw.TextField.init(gpa, unicode),
215
};
0
0
0
0
0
0
0
0
0
0
0
0
0
0
216
}
217
218
pub fn deinit(self: *Channel, alloc: std.mem.Allocator) void {
···
300
301
pub fn drawName(self: *Channel, ctx: vxfw.DrawContext, selected: bool) Allocator.Error!vxfw.Surface {
302
var style: vaxis.Style = .{};
303
-
if (selected) style.reverse = true;
304
if (self.has_mouse) style.bg = .{ .index = 8 };
0
0
0
0
0
0
0
0
0
0
0
305
306
-
const text: vxfw.RichText = .{
307
-
.text = &.{
308
-
.{ .text = " " },
309
-
.{ .text = self.name, .style = style },
310
-
},
311
-
.softwrap = false,
312
-
};
0
0
0
0
0
0
0
0
0
0
0
313
var surface = try text.draw(ctx);
314
// Replace the widget reference so we can handle the events
315
surface.widget = self.nameWidget(selected);
···
365
/// issue a MARKREAD command for this channel. The most recent message in the channel will be used as
366
/// the last read time
367
pub fn markRead(self: *Channel) !void {
368
-
if (!self.has_unread) return;
369
-
370
self.has_unread = false;
371
self.has_unread_highlight = false;
372
const last_msg = self.messages.getLast();
···
473
// Save this mouse state for when we draw
474
self.message_view.mouse = mouse;
475
0
476
if (mouse.type == .press and
477
mouse.button == .middle and
478
self.message_view.hovered_message != null)
···
487
return ctx.consumeAndRedraw();
488
}
489
if (mouse.button == .wheel_down) {
490
-
self.scroll.pending -|= 3;
491
ctx.consume_event = true;
492
}
493
if (mouse.button == .wheel_up) {
494
-
self.scroll.pending +|= 3;
495
ctx.consume_event = true;
496
}
497
if (self.scroll.pending != 0) {
498
-
return self.doScroll(ctx);
499
}
500
},
501
.mouse_leave => {
···
503
self.message_view.hovered_message = null;
504
ctx.redraw = true;
505
},
506
-
.tick => try self.doScroll(ctx),
0
0
507
else => {},
508
}
509
}
···
568
return self.drawMessageView(ctx);
569
}
570
0
0
0
0
0
0
0
0
0
0
0
571
fn drawMessageView(self: *Channel, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
0
572
const max = ctx.max.size();
573
if (max.width == 0 or
574
max.height == 0 or
···
642
break :blk param_iter.next() orelse "";
643
};
644
0
0
0
0
0
645
// Draw the message so we have it's wrapped height
646
-
const text: vxfw.Text = .{ .text = content };
647
const child_ctx = ctx.withConstraints(
648
.{ .width = 0, .height = 0 },
649
.{ .width = max.width -| gutter_width, .height = null },
···
741
.origin = .{ .row = row, .col = 0 },
742
.surface = try time_text.draw(child_ctx),
743
});
0
744
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 },
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
758
};
759
-
try children.append(.{
760
-
.origin = .{ .row = row, .col = gutter_width },
761
-
.surface = try sender_text.draw(child_ctx),
762
-
});
763
764
-
// Back up 1 more row for spacing
765
-
row -= 1;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
766
}
767
}
768
}
···
2060
2061
else => .{ .index = irc },
2062
};
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
2063
}
2064
2065
const CaseMapAlgo = enum {
···
168
pub fn draw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
169
const self: *Member = @ptrCast(@alignCast(ptr));
170
const style: vaxis.Style = if (self.user.away)
171
+
.{ .fg = .{ .index = 8 } }
172
else
173
.{ .fg = self.user.color };
174
var prefix = try ctx.arena.alloc(u8, 1);
···
213
},
214
.text_field = vxfw.TextField.init(gpa, unicode),
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();
230
}
231
232
pub fn deinit(self: *Channel, alloc: std.mem.Allocator) void {
···
314
315
pub fn drawName(self: *Channel, ctx: vxfw.DrawContext, selected: bool) Allocator.Error!vxfw.Surface {
316
var style: vaxis.Style = .{};
317
+
if (selected) style.bg = .{ .index = 8 };
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 };
330
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
+
349
var surface = try text.draw(ctx);
350
// Replace the widget reference so we can handle the events
351
surface.widget = self.nameWidget(selected);
···
401
/// issue a MARKREAD command for this channel. The most recent message in the channel will be used as
402
/// the last read time
403
pub fn markRead(self: *Channel) !void {
0
0
404
self.has_unread = false;
405
self.has_unread_highlight = false;
406
const last_msg = self.messages.getLast();
···
507
// Save this mouse state for when we draw
508
self.message_view.mouse = mouse;
509
510
+
// A middle press on a hovered message means we copy the content
511
if (mouse.type == .press and
512
mouse.button == .middle and
513
self.message_view.hovered_message != null)
···
522
return ctx.consumeAndRedraw();
523
}
524
if (mouse.button == .wheel_down) {
525
+
self.scroll.pending -|= 1;
526
ctx.consume_event = true;
527
}
528
if (mouse.button == .wheel_up) {
529
+
self.scroll.pending +|= 1;
530
ctx.consume_event = true;
531
}
532
if (self.scroll.pending != 0) {
533
+
try self.doScroll(ctx);
534
}
535
},
536
.mouse_leave => {
···
538
self.message_view.hovered_message = null;
539
ctx.redraw = true;
540
},
541
+
.tick => {
542
+
try self.doScroll(ctx);
543
+
},
544
else => {},
545
}
546
}
···
605
return self.drawMessageView(ctx);
606
}
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
+
619
fn drawMessageView(self: *Channel, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
620
+
self.message_view.hovered_message = null;
621
const max = ctx.max.size();
622
if (max.width == 0 or
623
max.height == 0 or
···
691
break :blk param_iter.next() orelse "";
692
};
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
+
699
// Draw the message so we have it's wrapped height
700
+
const text: vxfw.RichText = .{ .text = spans };
701
const child_ctx = ctx.withConstraints(
702
.{ .width = 0, .height = 0 },
703
.{ .width = max.width -| gutter_width, .height = null },
···
795
.origin = .{ .row = row, .col = 0 },
796
.surface = try time_text.draw(child_ctx),
797
});
798
+
}
799
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,
858
};
0
0
0
0
859
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);
880
}
881
}
882
}
···
2174
2175
else => .{ .index = irc },
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();
2440
}
2441
2442
const CaseMapAlgo = enum {
+1
-1
src/lua.zig
···
440
441
if (msg.len > 0 and msg[0] == '/') {
442
const app = getApp(lua);
443
-
app.handleCommand(lua, .{ .channel = channel }, msg) catch
444
lua.raiseErrorStr("couldn't handle command", .{});
445
return 0;
446
}
···
440
441
if (msg.len > 0 and msg[0] == '/') {
442
const app = getApp(lua);
443
+
app.handleCommand(.{ .channel = channel }, msg) catch
444
lua.raiseErrorStr("couldn't handle command", .{});
445
return 0;
446
}
+1
src/ui.zig
···
0
···
1
+
const std = @import("std");