~rockorager/comlink

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH v2] ui: add highest channel membership prefix to nicklist

Details
Message ID
<20240715141102.16572-1-greg@gpanders.com>
DKIM signature
pass
Download raw message
Patch: +88 -41
Adds the highest channel membership prefix flag (e.g. @ or +) next to
each user's nick in the nicklist.

Since prefixes are specific to user+channel combination, we add a Member
struct that contains a pointer to a User and a prefix character, and
each Channel then contains a list of Members. For now, the prefix is
only a single character (u8) which contains the highest prefix for that
user (as determined from the WHOX or NAMES reply). If comlink ever
enables the multi-prefix extension we may want to change this to be a
string, but since we only display a single character I think this is ok
for now.

---
v2:
  - Use std.mem.asBytes instead of the horrific @ptrCast business

 src/App.zig | 70 ++++++++++++++++++++++++++++++++++-------------------
 src/irc.zig | 59 ++++++++++++++++++++++++++++++++------------
 2 files changed, 88 insertions(+), 41 deletions(-)

diff --git a/src/App.zig b/src/App.zig
index 80e613f..d5bcba7 100644
--- a/src/App.zig
+++ b/src/App.zig
@@ -514,7 +514,14 @@ pub fn run(self: *App) !void {
                            const user_ptr = try msg.client.getOrCreateUser(nick);
                            if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true;
                            var channel = try msg.client.getOrCreateChannel(channel_name);
                            try channel.addMember(user_ptr);

                            const prefix = for (flags) |c| {
                                if (std.mem.indexOfScalar(u8, msg.client.supports.prefix, c)) |_| {
                                    break c;
                                }
                            } else ' ';

                            try channel.addMember(user_ptr, .{ .prefix = prefix });
                        },
                        .RPL_WHOSPCRPL => {
                            // syntax: <client> <channel> <nick> <flags>
@@ -527,7 +534,14 @@ pub fn run(self: *App) !void {
                            const user_ptr = try msg.client.getOrCreateUser(nick);
                            if (mem.indexOfScalar(u8, flags, 'G')) |_| user_ptr.away = true;
                            var channel = try msg.client.getOrCreateChannel(channel_name);
                            try channel.addMember(user_ptr);

                            const prefix = for (flags) |c| {
                                if (std.mem.indexOfScalar(u8, msg.client.supports.prefix, c)) |_| {
                                    break c;
                                }
                            } else ' ';

                            try channel.addMember(user_ptr, .{ .prefix = prefix });
                        },
                        .RPL_ENDOFWHO => {
                            // syntax: <client> <mask> :End of WHO list
@@ -548,17 +562,23 @@ pub fn run(self: *App) !void {
                            var channel = try msg.client.getOrCreateChannel(channel_name);
                            var name_iter = std.mem.splitScalar(u8, names, ' ');
                            while (name_iter.next()) |name| {
                                const has_prefix = for (msg.client.supports.prefix) |ch| {
                                    if (name[0] == ch) break true;
                                } else false;
                                const nick, const prefix = for (msg.client.supports.prefix) |ch| {
                                    if (name[0] == ch) {
                                        break .{ name[1..], name[0] };
                                    }
                                } else
                                    .{ name, ' ' };

                                if (has_prefix) log.debug("HAS PREFIX {s}", .{name});
                                const user_ptr = if (has_prefix)
                                    try msg.client.getOrCreateUser(name[1..])
                                else
                                    try msg.client.getOrCreateUser(name);
                                try channel.addMember(user_ptr);
                                if (prefix != ' ') {
                                    log.debug("HAS PREFIX {s}", .{name});
                                }

                                const user_ptr = try msg.client.getOrCreateUser(nick);

                                try channel.addMember(user_ptr, .{ .prefix = prefix, .sort = false });
                            }

                            channel.sortMembers();
                        },
                        .RPL_ENDOFNAMES => {
                            // syntax: <client> <channel> :End of /NAMES list
@@ -648,8 +668,8 @@ pub fn run(self: *App) !void {
                            var channel = try msg.client.getOrCreateChannel(target);
                            const user_ptr = try msg.client.getOrCreateUser(target);
                            const me_ptr = try msg.client.getOrCreateUser(msg.client.config.nick);
                            try channel.addMember(user_ptr);
                            try channel.addMember(me_ptr);
                            try channel.addMember(user_ptr, .{});
                            try channel.addMember(me_ptr, .{});
                            // we set who_requested so we don't try to request
                            // who on DMs
                            channel.who_requested = true;
@@ -677,7 +697,7 @@ pub fn run(self: *App) !void {
                            if (mem.eql(u8, user.nick, msg.client.config.nick))
                                try self.requestHistory(msg.client, .after, channel)
                            else
                                try channel.addMember(user);
                                try channel.addMember(user, .{});
                        },
                        .MARKREAD => {
                            var iter = msg.paramIterator();
@@ -1194,17 +1214,17 @@ const Completer = struct {
    pub fn findMatches(self: *Completer, chan: *irc.Channel) !void {
        if (self.options.items.len > 0) return;
        const alloc = self.options.allocator;
        var members = std.ArrayList(*irc.User).init(alloc);
        var members = std.ArrayList(irc.Channel.Member).init(alloc);
        defer members.deinit();
        for (chan.members.items) |member| {
            if (std.ascii.startsWithIgnoreCase(member.nick, self.word)) {
            if (std.ascii.startsWithIgnoreCase(member.user.nick, self.word)) {
                try members.append(member);
            }
        }
        std.sort.insertion(*irc.User, members.items, chan, irc.Channel.compareRecentMessages);
        std.sort.insertion(irc.Channel.Member, members.items, chan, irc.Channel.compareRecentMessages);
        self.options = try std.ArrayList([]const u8).initCapacity(alloc, members.items.len);
        for (members.items) |member| {
            try self.options.append(member.nick);
            try self.options.append(member.user.nick);
        }
    }

@@ -1367,8 +1387,8 @@ pub fn whox(self: *App, client: *Client, channel: *irc.Channel) !void {
    {
        const other = try client.getOrCreateUser(channel.name);
        const me = try client.getOrCreateUser(client.config.nick);
        try channel.addMember(other);
        try channel.addMember(me);
        try channel.addMember(other, .{});
        try channel.addMember(me, .{});
        return;
    }
    // Only use WHO if we have WHOX and away-notify. Without
@@ -1615,20 +1635,20 @@ fn draw(self: *App) !void {
                self.state.members.scroll_offset = @min(self.state.members.scroll_offset, channel.members.items.len -| member_list_win.height);

                var member_row: usize = 0;
                for (channel.members.items) |member| {
                for (channel.members.items) |*member| {
                    defer member_row += 1;
                    if (member_row < self.state.members.scroll_offset) continue;
                    var member_seg = [_]vaxis.Segment{
                        .{
                            .text = " ",
                            .text = std.mem.asBytes(&member.prefix),
                        },
                        .{
                            .text = member.nick,
                            .text = member.user.nick,
                            .style = .{
                                .fg = if (member.away)
                                .fg = if (member.user.away)
                                    .{ .index = 8 }
                                else
                                    member.color,
                                    member.user.color,
                            },
                        },
                    };
diff --git a/src/irc.zig b/src/irc.zig
index 7acd545..e7bad8a 100644
--- a/src/irc.zig
+++ b/src/irc.zig
@@ -84,7 +84,7 @@ pub const Channel = struct {
    client: *Client,
    name: []const u8,
    topic: ?[]const u8 = null,
    members: std.ArrayList(*User),
    members: std.ArrayList(Member),
    in_flight: struct {
        who: bool = false,
        names: bool = false,
@@ -98,6 +98,22 @@ pub const Channel = struct {
    has_unread: bool = false,
    has_unread_highlight: bool = false,

    pub const Member = struct {
        user: *User,

        /// Highest channel membership prefix (or empty space if no prefix)
        prefix: u8,

        pub fn compare(_: void, lhs: Member, rhs: Member) bool {
            return if (lhs.prefix != ' ' and rhs.prefix == ' ')
                true
            else if (lhs.prefix == ' ' and rhs.prefix != ' ')
                false
            else
                std.ascii.orderIgnoreCase(lhs.user.nick, rhs.user.nick).compare(.lt);
        }
    };

    pub fn deinit(self: *const Channel, alloc: std.mem.Allocator) void {
        alloc.free(self.name);
        self.members.deinit();
@@ -114,7 +130,7 @@ pub const Channel = struct {
        return std.ascii.orderIgnoreCase(lhs.name, rhs.name).compare(std.math.CompareOperator.lt);
    }

    pub fn compareRecentMessages(self: *Channel, lhs: *User, rhs: *User) bool {
    pub fn compareRecentMessages(self: *Channel, lhs: Member, rhs: Member) bool {
        var l: i64 = 0;
        var r: i64 = 0;
        var iter = std.mem.reverseIterator(self.messages.items);
@@ -123,10 +139,10 @@ pub const Channel = struct {
                const bang = std.mem.indexOfScalar(u8, source, '!') orelse source.len;
                const nick = source[0..bang];

                if (l == 0 and msg.time != null and std.mem.eql(u8, lhs.nick, nick)) {
                if (l == 0 and msg.time != null and std.mem.eql(u8, lhs.user.nick, nick)) {
                    log.debug("L!!", .{});
                    l = msg.time.?.instant().unixTimestamp();
                } else if (r == 0 and msg.time != null and std.mem.eql(u8, rhs.nick, nick))
                } else if (r == 0 and msg.time != null and std.mem.eql(u8, rhs.user.nick, nick))
                    r = msg.time.?.instant().unixTimestamp();
            }
            if (l > 0 and r > 0) break;
@@ -135,20 +151,35 @@ pub const Channel = struct {
    }

    pub fn sortMembers(self: *Channel) void {
        std.sort.insertion(*User, self.members.items, {}, User.compare);
        std.sort.insertion(Member, self.members.items, {}, Member.compare);
    }

    pub fn addMember(self: *Channel, user: *User) !void {
        for (self.members.items) |member| {
            if (user == member) return;
    pub fn addMember(self: *Channel, user: *User, args: struct {
        prefix: ?u8 = null,
        sort: bool = true,
    }) !void {
        if (args.prefix) |p| {
            log.debug("adding member: nick={s}, prefix={c}", .{user.nick, p});
        }
        for (self.members.items) |*member| {
            if (user == member.user) {
                // Update the prefix for an existing member if the prefix is
                // known
                if (args.prefix) |p| member.prefix = p;
                return;
            }
        }

        try self.members.append(.{ .user = user, .prefix = args.prefix orelse ' ' });

        if (args.sort) {
            self.sortMembers();
        }
        try self.members.append(user);
        self.sortMembers();
    }

    pub fn removeMember(self: *Channel, user: *User) void {
        for (self.members.items, 0..) |member, i| {
            if (user == member) {
            if (user == member.user) {
                _ = self.members.orderedRemove(i);
                return;
            }
@@ -164,10 +195,6 @@ pub const User = struct {
    pub fn deinit(self: *const User, alloc: std.mem.Allocator) void {
        alloc.free(self.nick);
    }

    pub fn compare(_: void, lhs: *User, rhs: *User) bool {
        return std.ascii.orderIgnoreCase(lhs.nick, rhs.nick).compare(std.math.CompareOperator.lt);
    }
};

/// an irc message
@@ -709,7 +736,7 @@ pub const Client = struct {
        }
        const channel: Channel = .{
            .name = try self.alloc.dupe(u8, name),
            .members = std.ArrayList(*User).init(self.alloc),
            .members = std.ArrayList(Channel.Member).init(self.alloc),
            .messages = std.ArrayList(Message).init(self.alloc),
            .client = self,
        };
-- 
2.45.2
Details
Message ID
<D2QZ9KG60XWS.1VOCKOM4YXUDF@timculverhouse.com>
In-Reply-To
<20240715141102.16572-1-greg@gpanders.com> (view parent)
DKIM signature
pass
Download raw message
On Mon Jul 15, 2024 at 9:11 AM CDT, Gregory Anders wrote:
> Adds the highest channel membership prefix flag (e.g. @ or +) next to
> each user's nick in the nicklist.
>
> Since prefixes are specific to user+channel combination, we add a Member
> struct that contains a pointer to a User and a prefix character, and
> each Channel then contains a list of Members. For now, the prefix is
> only a single character (u8) which contains the highest prefix for that
> user (as determined from the WHOX or NAMES reply). If comlink ever
> enables the multi-prefix extension we may want to change this to be a
> string, but since we only display a single character I think this is ok
> for now.
>
> ---

Hey Greg -

Looks great! I think at some point some styling to prefixes would be good but I
have no clue on what they should look like (I don't really pay attention to them
at all so I'm not sure what they mean nor how important each one is).

Applied: https://git.sr.ht/~rockorager/comlink/commit/dc0a09286c0c7d2c177ba0cd4d9c79a445c1b33c

-- 
Tim
Reply to thread Export thread (mbox)