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