this repo has no description
3
fork

Configure Feed

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

at 0deb411fccc38a10d46e2427f51580c813ff4e65 205 lines 7.5 kB view raw
1const std = @import("std"); 2const comlink = @import("comlink.zig"); 3const vaxis = @import("vaxis"); 4const emoji = @import("emoji.zig"); 5 6const irc = comlink.irc; 7const vxfw = vaxis.vxfw; 8const Command = comlink.Command; 9 10const Kind = enum { 11 command, 12 emoji, 13 nick, 14}; 15 16pub const Completer = struct { 17 const style: vaxis.Style = .{ .bg = .{ .index = 8 } }; 18 const selected: vaxis.Style = .{ .bg = .{ .index = 8 }, .reverse = true }; 19 20 word: []const u8, 21 start_idx: usize, 22 options: std.ArrayList(vxfw.Text), 23 widest: ?usize, 24 buf: [irc.maximum_message_size]u8 = undefined, 25 kind: Kind = .nick, 26 list_view: vxfw.ListView, 27 selected: bool, 28 29 pub fn init(gpa: std.mem.Allocator) Completer { 30 return .{ 31 .options = std.ArrayList(vxfw.Text).init(gpa), 32 .start_idx = 0, 33 .word = "", 34 .widest = null, 35 .list_view = undefined, 36 .selected = false, 37 }; 38 } 39 40 fn getWidget(ptr: *const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget { 41 const self: *Completer = @constCast(@ptrCast(@alignCast(ptr))); 42 if (idx < self.options.items.len) { 43 const item = &self.options.items[idx]; 44 if (idx == cursor) { 45 item.style = selected; 46 } else { 47 item.style = style; 48 } 49 return item.widget(); 50 } 51 return null; 52 } 53 54 pub fn reset(self: *Completer, line: []const u8) !void { 55 self.list_view = .{ 56 .children = .{ .builder = .{ 57 .userdata = self, 58 .buildFn = Completer.getWidget, 59 } }, 60 .draw_cursor = false, 61 }; 62 self.start_idx = if (std.mem.lastIndexOfScalar(u8, line, ' ')) |idx| idx + 1 else 0; 63 self.word = line[self.start_idx..]; 64 @memcpy(self.buf[0..line.len], line); 65 self.options.clearAndFree(); 66 self.widest = null; 67 self.kind = .nick; 68 self.selected = false; 69 70 if (self.word.len > 0 and self.word[0] == '/') { 71 self.kind = .command; 72 try self.findCommandMatches(); 73 } 74 if (self.word.len > 0 and self.word[0] == ':') { 75 self.kind = .emoji; 76 try self.findEmojiMatches(); 77 } 78 } 79 80 pub fn deinit(self: *Completer) void { 81 self.options.deinit(); 82 } 83 84 /// cycles to the next option, returns the replacement text. Note that we 85 /// start from the bottom, so a selected_idx = 0 means we are on _the last_ 86 /// item 87 pub fn next(self: *Completer, ctx: *vxfw.EventContext) []const u8 { 88 if (self.options.items.len == 0) return ""; 89 if (self.selected) { 90 self.list_view.prevItem(ctx); 91 } 92 self.selected = true; 93 return self.replacementText(); 94 } 95 96 pub fn prev(self: *Completer, ctx: *vxfw.EventContext) []const u8 { 97 if (self.options.items.len == 0) return ""; 98 self.list_view.nextItem(ctx); 99 self.selected = true; 100 return self.replacementText(); 101 } 102 103 pub fn replacementText(self: *Completer) []const u8 { 104 if (self.options.items.len == 0) return ""; 105 const replacement_widget = self.options.items[self.list_view.cursor]; 106 const replacement = replacement_widget.text; 107 switch (self.kind) { 108 .command => { 109 self.buf[0] = '/'; 110 @memcpy(self.buf[1 .. 1 + replacement.len], replacement); 111 const append_space = if (Command.fromString(replacement)) |cmd| 112 cmd.appendSpace() 113 else 114 true; 115 if (append_space) self.buf[1 + replacement.len] = ' '; 116 return self.buf[0 .. 1 + replacement.len + @as(u1, if (append_space) 1 else 0)]; 117 }, 118 .emoji => { 119 const start = self.start_idx; 120 @memcpy(self.buf[start .. start + replacement.len], replacement); 121 return self.buf[0 .. start + replacement.len]; 122 }, 123 .nick => { 124 const start = self.start_idx; 125 @memcpy(self.buf[start .. start + replacement.len], replacement); 126 if (self.start_idx == 0) { 127 @memcpy(self.buf[start + replacement.len .. start + replacement.len + 2], ": "); 128 return self.buf[0 .. start + replacement.len + 2]; 129 } else { 130 @memcpy(self.buf[start + replacement.len .. start + replacement.len + 1], " "); 131 return self.buf[0 .. start + replacement.len + 1]; 132 } 133 }, 134 } 135 } 136 137 pub fn findMatches(self: *Completer, chan: *irc.Channel) !void { 138 if (self.options.items.len > 0) return; 139 const alloc = self.options.allocator; 140 var members = std.ArrayList(irc.Channel.Member).init(alloc); 141 defer members.deinit(); 142 for (chan.members.items) |member| { 143 if (std.ascii.startsWithIgnoreCase(member.user.nick, self.word)) { 144 try members.append(member); 145 } 146 } 147 std.sort.insertion(irc.Channel.Member, members.items, chan, irc.Channel.compareRecentMessages); 148 try self.options.ensureTotalCapacity(members.items.len); 149 for (members.items) |member| { 150 try self.options.append(.{ .text = member.user.nick }); 151 } 152 self.list_view.cursor = @intCast(self.options.items.len -| 1); 153 self.list_view.item_count = @intCast(self.options.items.len); 154 self.list_view.ensureScroll(); 155 } 156 157 pub fn findCommandMatches(self: *Completer) !void { 158 if (self.options.items.len > 0) return; 159 const commands = std.meta.fieldNames(Command); 160 for (commands) |cmd| { 161 if (std.mem.eql(u8, cmd, "lua_function")) continue; 162 if (std.ascii.startsWithIgnoreCase(cmd, self.word[1..])) { 163 try self.options.append(.{ .text = cmd }); 164 } 165 } 166 var iter = Command.user_commands.keyIterator(); 167 while (iter.next()) |cmd| { 168 if (std.ascii.startsWithIgnoreCase(cmd.*, self.word[1..])) { 169 try self.options.append(.{ .text = cmd.* }); 170 } 171 } 172 self.list_view.cursor = @intCast(self.options.items.len -| 1); 173 self.list_view.item_count = @intCast(self.options.items.len); 174 self.list_view.ensureScroll(); 175 } 176 177 pub fn findEmojiMatches(self: *Completer) !void { 178 if (self.options.items.len > 0) return; 179 const keys = emoji.map.keys(); 180 const values = emoji.map.values(); 181 182 for (keys, values) |shortcode, glyph| { 183 if (std.mem.indexOf(u8, shortcode, self.word[1..])) |_| 184 try self.options.append(.{ .text = glyph }); 185 } 186 self.list_view.cursor = @intCast(self.options.items.len -| 1); 187 self.list_view.item_count = @intCast(self.options.items.len); 188 self.list_view.ensureScroll(); 189 } 190 191 pub fn widestMatch(self: *Completer, ctx: vxfw.DrawContext) usize { 192 if (self.widest) |w| return w; 193 var widest: usize = 0; 194 for (self.options.items) |opt| { 195 const width = ctx.stringWidth(opt.text); 196 if (width > widest) widest = width; 197 } 198 self.widest = widest; 199 return widest; 200 } 201 202 pub fn numMatches(self: *Completer) usize { 203 return self.options.items.len; 204 } 205};