an experimental irc client
at ec2fb6ec28aa8aa40e3c97fa3ebb23afeba605bf 169 lines 6.2 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 Command = comlink.Command; 8 9const Kind = enum { 10 command, 11 emoji, 12 nick, 13}; 14 15pub const Completer = struct { 16 word: []const u8, 17 start_idx: usize, 18 options: std.ArrayList([]const u8), 19 selected_idx: ?usize, 20 widest: ?usize, 21 buf: [irc.maximum_message_size]u8 = undefined, 22 kind: Kind = .nick, 23 24 pub fn init(alloc: std.mem.Allocator, line: []const u8) !Completer { 25 const start_idx = if (std.mem.lastIndexOfScalar(u8, line, ' ')) |idx| idx + 1 else 0; 26 const last_word = line[start_idx..]; 27 var completer: Completer = .{ 28 .options = std.ArrayList([]const u8).init(alloc), 29 .start_idx = start_idx, 30 .word = last_word, 31 .selected_idx = null, 32 .widest = null, 33 }; 34 @memcpy(completer.buf[0..line.len], line); 35 if (last_word.len > 0 and last_word[0] == '/') { 36 completer.kind = .command; 37 try completer.findCommandMatches(); 38 } 39 if (last_word.len > 0 and last_word[0] == ':') { 40 completer.kind = .emoji; 41 try completer.findEmojiMatches(); 42 } 43 return completer; 44 } 45 46 pub fn deinit(self: *Completer) void { 47 self.options.deinit(); 48 } 49 50 /// cycles to the next option, returns the replacement text. Note that we 51 /// start from the bottom, so a selected_idx = 0 means we are on _the last_ 52 /// item 53 pub fn next(self: *Completer) []const u8 { 54 if (self.options.items.len == 0) return ""; 55 { 56 const last_idx = self.options.items.len - 1; 57 if (self.selected_idx == null or self.selected_idx.? == last_idx) 58 self.selected_idx = 0 59 else 60 self.selected_idx.? +|= 1; 61 } 62 return self.replacementText(); 63 } 64 65 pub fn prev(self: *Completer) []const u8 { 66 if (self.options.items.len == 0) return ""; 67 { 68 const last_idx = self.options.items.len - 1; 69 if (self.selected_idx == null or self.selected_idx.? == 0) 70 self.selected_idx = last_idx 71 else 72 self.selected_idx.? -|= 1; 73 } 74 return self.replacementText(); 75 } 76 77 pub fn replacementText(self: *Completer) []const u8 { 78 if (self.selected_idx == null or self.options.items.len == 0) return ""; 79 const replacement = self.options.items[self.options.items.len - 1 - self.selected_idx.?]; 80 switch (self.kind) { 81 .command => { 82 self.buf[0] = '/'; 83 @memcpy(self.buf[1 .. 1 + replacement.len], replacement); 84 const append_space = if (Command.fromString(replacement)) |cmd| 85 cmd.appendSpace() 86 else 87 true; 88 if (append_space) self.buf[1 + replacement.len] = ' '; 89 return self.buf[0 .. 1 + replacement.len + @as(u1, if (append_space) 1 else 0)]; 90 }, 91 .emoji => { 92 const start = self.start_idx; 93 @memcpy(self.buf[start .. start + replacement.len], replacement); 94 return self.buf[0 .. start + replacement.len]; 95 }, 96 .nick => { 97 const start = self.start_idx; 98 @memcpy(self.buf[start .. start + replacement.len], replacement); 99 if (self.start_idx == 0) { 100 @memcpy(self.buf[start + replacement.len .. start + replacement.len + 2], ": "); 101 return self.buf[0 .. start + replacement.len + 2]; 102 } else { 103 @memcpy(self.buf[start + replacement.len .. start + replacement.len + 1], " "); 104 return self.buf[0 .. start + replacement.len + 1]; 105 } 106 }, 107 } 108 } 109 110 pub fn findMatches(self: *Completer, chan: *irc.Channel) !void { 111 if (self.options.items.len > 0) return; 112 const alloc = self.options.allocator; 113 var members = std.ArrayList(irc.Channel.Member).init(alloc); 114 defer members.deinit(); 115 for (chan.members.items) |member| { 116 if (std.ascii.startsWithIgnoreCase(member.user.nick, self.word)) { 117 try members.append(member); 118 } 119 } 120 std.sort.insertion(irc.Channel.Member, members.items, chan, irc.Channel.compareRecentMessages); 121 self.options = try std.ArrayList([]const u8).initCapacity(alloc, members.items.len); 122 for (members.items) |member| { 123 try self.options.append(member.user.nick); 124 } 125 } 126 127 pub fn findCommandMatches(self: *Completer) !void { 128 if (self.options.items.len > 0) return; 129 const commands = std.meta.fieldNames(Command); 130 for (commands) |cmd| { 131 if (std.mem.eql(u8, cmd, "lua_function")) continue; 132 if (std.ascii.startsWithIgnoreCase(cmd, self.word[1..])) { 133 try self.options.append(cmd); 134 } 135 } 136 var iter = Command.user_commands.keyIterator(); 137 while (iter.next()) |cmd| { 138 if (std.ascii.startsWithIgnoreCase(cmd.*, self.word[1..])) { 139 try self.options.append(cmd.*); 140 } 141 } 142 } 143 144 pub fn findEmojiMatches(self: *Completer) !void { 145 if (self.options.items.len > 0) return; 146 const keys = emoji.map.keys(); 147 const values = emoji.map.values(); 148 149 for (keys, values) |shortcode, glyph| { 150 if (std.mem.indexOf(u8, shortcode, self.word[1..])) |_| 151 try self.options.append(glyph); 152 } 153 } 154 155 pub fn widestMatch(self: *Completer, win: vaxis.Window) usize { 156 if (self.widest) |w| return w; 157 var widest: usize = 0; 158 for (self.options.items) |opt| { 159 const width = win.gwidth(opt); 160 if (width > widest) widest = width; 161 } 162 self.widest = widest; 163 return widest; 164 } 165 166 pub fn numMatches(self: *Completer) usize { 167 return self.options.items.len; 168 } 169};