tangled
alpha
login
or
join now
rockorager.dev
/
comlink
2
fork
atom
an experimental irc client
2
fork
atom
overview
issues
pulls
pipelines
ui: add /list command
rockorager.dev
1 year ago
50dbba8e
b1d6aeeb
verified
This commit was signed with the committer's
known signature
.
rockorager.dev
SSH Key Fingerprint:
SHA256:qn/Fjy7CpbcogGEPB14Y53hLnQleZNFY9lkQnuudFLs=
+346
-3
4 changed files
expand all
collapse all
unified
split
build.zig.zon
src
app.zig
comlink.zig
irc.zig
+2
-2
build.zig.zon
···
4
4
.version = "0.0.0",
5
5
.dependencies = .{
6
6
.vaxis = .{
7
7
-
.url = "git+https://github.com/rockorager/libvaxis#6b5a011f58eb7201a18d894383b96eba4b9101f9",
8
8
-
.hash = "vaxis-0.1.0-BWNV_NzyCADDrd6hjXVEXAZSWA3FUhwoxnE4Pk0SbeRz",
7
7
+
.url = "git+https://github.com/rockorager/libvaxis#6a37605dde55898dcca4769dd3eb1e333959c209",
8
8
+
.hash = "vaxis-0.1.0-BWNV_K3yCACrTy3A5cbZElLyICx5a2O2EzPxmgVRcbKJ",
9
9
},
10
10
.zeit = .{
11
11
.url = "git+https://github.com/rockorager/zeit#44bebf856693332b168d8ba2c45b9d1ec15511de",
+30
-1
src/app.zig
···
354
354
};
355
355
try children.append(sub);
356
356
357
357
+
for (self.clients.items) |client| {
358
358
+
if (client.list_modal.is_shown) {
359
359
+
const padding: u16 = 8;
360
360
+
const modal_ctx = ctx.withConstraints(ctx.min, .{
361
361
+
.width = max.width -| padding * 2,
362
362
+
.height = max.height -| padding,
363
363
+
});
364
364
+
const border: vxfw.Border = .{ .child = client.list_modal.widget() };
365
365
+
try children.append(.{
366
366
+
.origin = .{ .row = padding / 2, .col = padding },
367
367
+
.surface = try border.draw(modal_ctx),
368
368
+
});
369
369
+
break;
370
370
+
}
371
371
+
}
372
372
+
357
373
return .{
358
374
.size = ctx.max.size(),
359
375
.widget = self.widget(),
···
518
534
},
519
535
.join => {
520
536
const start = std.mem.indexOfScalar(u8, cmd, ' ') orelse return error.InvalidCommand;
537
537
+
const chan_name = cmd[start + 1 ..];
538
538
+
for (client.channels.items) |chan| {
539
539
+
if (std.mem.eql(u8, chan.name, chan_name)) {
540
540
+
client.app.selectBuffer(.{ .channel = chan });
541
541
+
return;
542
542
+
}
543
543
+
}
521
544
const msg = try std.fmt.bufPrint(
522
545
&buf,
523
546
"JOIN {s}\r\n",
524
547
.{
525
525
-
cmd[start + 1 ..],
548
548
+
chan_name,
526
549
},
527
550
);
551
551
+
552
552
+
// Check
528
553
// Ensure buffer exists
529
554
self.explicit_join = true;
530
555
return client.queueWrite(msg);
556
556
+
},
557
557
+
.list => {
558
558
+
client.list_modal.expecting_response = true;
559
559
+
return client.queueWrite("LIST\r\n");
531
560
},
532
561
.me => {
533
562
if (channel == null) return error.InvalidCommand;
+2
src/comlink.zig
···
66
66
/// a raw irc command. Sent verbatim
67
67
quote,
68
68
join,
69
69
+
list,
69
70
me,
70
71
msg,
71
72
query,
···
86
87
const map = std.StaticStringMap(Command).initComptime(.{
87
88
.{ "quote", .quote },
88
89
.{ "join", .join },
90
90
+
.{ "list", .list },
89
91
.{ "me", .me },
90
92
.{ "msg", .msg },
91
93
.{ "query", .query },
+312
src/irc.zig
···
45
45
RPL_MYINFO, // 004
46
46
RPL_ISUPPORT, // 005
47
47
48
48
+
RPL_TRYAGAIN, // 263
49
49
+
48
50
RPL_ENDOFWHO, // 315
51
51
+
RPL_LISTSTART, // 321
52
52
+
RPL_LIST, // 322
53
53
+
RPL_LISTEND, // 323
49
54
RPL_TOPIC, // 332
50
55
RPL_WHOREPLY, // 352
51
56
RPL_NAMREPLY, // 353
···
78
83
.{ "004", .RPL_MYINFO },
79
84
.{ "005", .RPL_ISUPPORT },
80
85
86
86
+
.{ "263", .RPL_TRYAGAIN },
87
87
+
81
88
.{ "315", .RPL_ENDOFWHO },
89
89
+
.{ "321", .RPL_LISTSTART },
90
90
+
.{ "322", .RPL_LIST },
91
91
+
.{ "323", .RPL_LISTEND },
82
92
.{ "332", .RPL_TOPIC },
83
93
.{ "352", .RPL_WHOREPLY },
84
94
.{ "353", .RPL_NAMREPLY },
···
1659
1669
text_field: vxfw.TextField,
1660
1670
completer_shown: bool,
1661
1671
1672
1672
+
list_modal: ListModal,
1673
1673
+
1662
1674
pub fn init(
1663
1675
self: *Client,
1664
1676
alloc: std.mem.Allocator,
···
1684
1696
.retry_delay_s = 0,
1685
1697
.text_field = .init(alloc, app.unicode),
1686
1698
.completer_shown = false,
1699
1699
+
.list_modal = undefined,
1687
1700
};
1701
1701
+
self.list_modal.init(alloc, self);
1688
1702
self.text_field.style = .{ .bg = self.app.blendBg(10) };
1689
1703
self.text_field.userdata = self;
1690
1704
self.text_field.onSubmit = Client.onSubmit;
···
1733
1747
self.alloc.destroy(channel);
1734
1748
}
1735
1749
self.channels.deinit();
1750
1750
+
1751
1751
+
self.list_modal.deinit(self.alloc);
1736
1752
1737
1753
var user_iter = self.users.valueIterator();
1738
1754
while (user_iter.next()) |user| {
···
2090
2106
}
2091
2107
channel.topic = try self.alloc.dupe(u8, topic);
2092
2108
},
2109
2109
+
.RPL_TRYAGAIN => {
2110
2110
+
if (self.list_modal.expecting_response) {
2111
2111
+
self.list_modal.expecting_response = false;
2112
2112
+
try self.list_modal.finish(ctx);
2113
2113
+
}
2114
2114
+
},
2115
2115
+
.RPL_LISTSTART => try self.list_modal.reset(),
2116
2116
+
.RPL_LIST => {
2117
2117
+
// We might not always get a RPL_LISTSTART, so we check if we have a list already
2118
2118
+
// and if it needs reseting
2119
2119
+
if (self.list_modal.finished) {
2120
2120
+
try self.list_modal.reset();
2121
2121
+
}
2122
2122
+
self.list_modal.expecting_response = false;
2123
2123
+
try self.list_modal.addMessage(self.alloc, msg);
2124
2124
+
},
2125
2125
+
.RPL_LISTEND => try self.list_modal.finish(ctx),
2093
2126
.RPL_SASLSUCCESS => {},
2094
2127
.RPL_WHOREPLY => {
2095
2128
// syntax: <client> <channel> <username> <host> <server> <nick> <flags> :<hopcount> <real name>
···
3105
3138
pub const ChatHistoryCommand = enum {
3106
3139
before,
3107
3140
after,
3141
3141
+
};
3142
3142
+
3143
3143
+
pub const ListModal = struct {
3144
3144
+
client: *Client,
3145
3145
+
/// the individual items we received
3146
3146
+
items: std.ArrayListUnmanaged(Item),
3147
3147
+
/// the list view
3148
3148
+
list_view: vxfw.ListView,
3149
3149
+
text_field: vxfw.TextField,
3150
3150
+
3151
3151
+
filtered_items: std.ArrayList(Item),
3152
3152
+
3153
3153
+
finished: bool,
3154
3154
+
is_shown: bool,
3155
3155
+
expecting_response: bool,
3156
3156
+
3157
3157
+
focus: enum { text_field, list },
3158
3158
+
3159
3159
+
const name_width = 24;
3160
3160
+
const count_width = 8;
3161
3161
+
3162
3162
+
// Item is a single RPL_LIST response
3163
3163
+
const Item = struct {
3164
3164
+
name: []const u8,
3165
3165
+
topic: []const u8,
3166
3166
+
count_str: []const u8,
3167
3167
+
count: u32,
3168
3168
+
3169
3169
+
fn deinit(self: Item, alloc: Allocator) void {
3170
3170
+
alloc.free(self.name);
3171
3171
+
alloc.free(self.topic);
3172
3172
+
alloc.free(self.count_str);
3173
3173
+
}
3174
3174
+
3175
3175
+
fn widget(self: *Item) vxfw.Widget {
3176
3176
+
return .{
3177
3177
+
.userdata = self,
3178
3178
+
.drawFn = Item.draw,
3179
3179
+
};
3180
3180
+
}
3181
3181
+
3182
3182
+
fn lessThan(_: void, lhs: Item, rhs: Item) bool {
3183
3183
+
return lhs.count > rhs.count;
3184
3184
+
}
3185
3185
+
3186
3186
+
fn draw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
3187
3187
+
const self: *Item = @ptrCast(@alignCast(ptr));
3188
3188
+
3189
3189
+
var children: std.ArrayListUnmanaged(vxfw.SubSurface) = try .initCapacity(ctx.arena, 3);
3190
3190
+
3191
3191
+
const name_ctx = ctx.withConstraints(.{ .width = name_width, .height = 1 }, ctx.max);
3192
3192
+
const count_ctx = ctx.withConstraints(.{ .width = count_width, .height = 1 }, ctx.max);
3193
3193
+
const topic_ctx = ctx.withConstraints(.{
3194
3194
+
.width = ctx.max.width.? -| name_width -| count_width - 2,
3195
3195
+
.height = 1,
3196
3196
+
}, ctx.max);
3197
3197
+
3198
3198
+
const name: vxfw.Text = .{ .text = self.name, .softwrap = false };
3199
3199
+
const count: vxfw.Text = .{ .text = self.count_str, .softwrap = false, .text_align = .right };
3200
3200
+
const spans = try formatMessage(ctx.arena, undefined, self.topic);
3201
3201
+
const topic: vxfw.RichText = .{ .text = spans, .softwrap = false };
3202
3202
+
3203
3203
+
children.appendAssumeCapacity(.{
3204
3204
+
.origin = .{ .col = 0, .row = 0 },
3205
3205
+
.surface = try name.draw(name_ctx),
3206
3206
+
});
3207
3207
+
children.appendAssumeCapacity(.{
3208
3208
+
.origin = .{ .col = name_width, .row = 0 },
3209
3209
+
.surface = try topic.draw(topic_ctx),
3210
3210
+
});
3211
3211
+
children.appendAssumeCapacity(.{
3212
3212
+
.origin = .{ .col = ctx.max.width.? -| count_width, .row = 0 },
3213
3213
+
.surface = try count.draw(count_ctx),
3214
3214
+
});
3215
3215
+
3216
3216
+
return .{
3217
3217
+
.size = .{ .width = ctx.max.width.?, .height = 1 },
3218
3218
+
.widget = self.widget(),
3219
3219
+
.buffer = &.{},
3220
3220
+
.children = children.items,
3221
3221
+
};
3222
3222
+
}
3223
3223
+
};
3224
3224
+
3225
3225
+
fn init(self: *ListModal, gpa: Allocator, client: *Client) void {
3226
3226
+
self.* = .{
3227
3227
+
.client = client,
3228
3228
+
.filtered_items = std.ArrayList(Item).init(gpa),
3229
3229
+
.items = .empty,
3230
3230
+
.list_view = .{
3231
3231
+
.children = .{
3232
3232
+
.builder = .{
3233
3233
+
.userdata = self,
3234
3234
+
.buildFn = ListModal.getItem,
3235
3235
+
},
3236
3236
+
},
3237
3237
+
},
3238
3238
+
.text_field = .init(gpa, client.app.unicode),
3239
3239
+
.finished = true,
3240
3240
+
.is_shown = false,
3241
3241
+
.focus = .text_field,
3242
3242
+
.expecting_response = false,
3243
3243
+
};
3244
3244
+
self.text_field.style.bg = client.app.blendBg(10);
3245
3245
+
self.text_field.userdata = self;
3246
3246
+
self.text_field.onChange = ListModal.onChange;
3247
3247
+
}
3248
3248
+
3249
3249
+
fn reset(self: *ListModal) !void {
3250
3250
+
self.items.clearRetainingCapacity();
3251
3251
+
self.filtered_items.clearAndFree();
3252
3252
+
self.text_field.clearAndFree();
3253
3253
+
self.finished = false;
3254
3254
+
self.focus = .text_field;
3255
3255
+
self.is_shown = false;
3256
3256
+
}
3257
3257
+
3258
3258
+
fn show(self: *ListModal, ctx: *vxfw.EventContext) !void {
3259
3259
+
self.is_shown = true;
3260
3260
+
switch (self.focus) {
3261
3261
+
.text_field => try ctx.requestFocus(self.text_field.widget()),
3262
3262
+
.list => try ctx.requestFocus(self.list_view.widget()),
3263
3263
+
}
3264
3264
+
return ctx.consumeAndRedraw();
3265
3265
+
}
3266
3266
+
3267
3267
+
pub fn widget(self: *ListModal) vxfw.Widget {
3268
3268
+
return .{
3269
3269
+
.userdata = self,
3270
3270
+
.captureHandler = ListModal.captureHandler,
3271
3271
+
.drawFn = ListModal._draw,
3272
3272
+
};
3273
3273
+
}
3274
3274
+
3275
3275
+
fn deinit(self: *ListModal, alloc: std.mem.Allocator) void {
3276
3276
+
for (self.items.items) |item| {
3277
3277
+
item.deinit(alloc);
3278
3278
+
}
3279
3279
+
self.items.deinit(alloc);
3280
3280
+
self.filtered_items.deinit();
3281
3281
+
self.text_field.deinit();
3282
3282
+
self.* = undefined;
3283
3283
+
}
3284
3284
+
3285
3285
+
fn addMessage(self: *ListModal, alloc: Allocator, msg: Message) !void {
3286
3286
+
var iter = msg.paramIterator();
3287
3287
+
// client, we skip this one
3288
3288
+
_ = iter.next() orelse return;
3289
3289
+
const channel = iter.next() orelse {
3290
3290
+
log.warn("got RPL_LIST without channel", .{});
3291
3291
+
return;
3292
3292
+
};
3293
3293
+
const count = iter.next() orelse {
3294
3294
+
log.warn("got RPL_LIST without count", .{});
3295
3295
+
return;
3296
3296
+
};
3297
3297
+
const topic = iter.next() orelse {
3298
3298
+
log.warn("got RPL_LIST without topic", .{});
3299
3299
+
return;
3300
3300
+
};
3301
3301
+
const item: Item = .{
3302
3302
+
.name = try alloc.dupe(u8, channel),
3303
3303
+
.count_str = try alloc.dupe(u8, count),
3304
3304
+
.topic = try alloc.dupe(u8, topic),
3305
3305
+
.count = try std.fmt.parseUnsigned(u32, count, 10),
3306
3306
+
};
3307
3307
+
try self.items.append(alloc, item);
3308
3308
+
}
3309
3309
+
3310
3310
+
fn finish(self: *ListModal, ctx: *vxfw.EventContext) !void {
3311
3311
+
self.finished = true;
3312
3312
+
self.is_shown = true;
3313
3313
+
std.mem.sort(Item, self.items.items, {}, Item.lessThan);
3314
3314
+
self.filtered_items.clearRetainingCapacity();
3315
3315
+
try self.filtered_items.appendSlice(self.items.items);
3316
3316
+
try ctx.requestFocus(self.text_field.widget());
3317
3317
+
}
3318
3318
+
3319
3319
+
fn onChange(ptr: ?*anyopaque, ctx: *vxfw.EventContext, input: []const u8) anyerror!void {
3320
3320
+
const self: *ListModal = @ptrCast(@alignCast(ptr orelse unreachable));
3321
3321
+
self.filtered_items.clearRetainingCapacity();
3322
3322
+
for (self.items.items) |item| {
3323
3323
+
if (std.mem.indexOf(u8, item.name, input)) |_| {
3324
3324
+
try self.filtered_items.append(item);
3325
3325
+
} else if (std.mem.indexOf(u8, item.topic, input)) |_| {
3326
3326
+
try self.filtered_items.append(item);
3327
3327
+
}
3328
3328
+
}
3329
3329
+
return ctx.consumeAndRedraw();
3330
3330
+
}
3331
3331
+
3332
3332
+
fn captureHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
3333
3333
+
const self: *ListModal = @ptrCast(@alignCast(ptr));
3334
3334
+
switch (event) {
3335
3335
+
.key_press => |key| {
3336
3336
+
switch (self.focus) {
3337
3337
+
.text_field => {
3338
3338
+
if (key.matches(vaxis.Key.enter, .{})) {
3339
3339
+
try ctx.requestFocus(self.list_view.widget());
3340
3340
+
self.focus = .list;
3341
3341
+
return ctx.consumeAndRedraw();
3342
3342
+
} else if (key.matches(vaxis.Key.escape, .{})) {
3343
3343
+
self.close(ctx);
3344
3344
+
return;
3345
3345
+
} else if (key.matches(vaxis.Key.up, .{})) {
3346
3346
+
self.list_view.prevItem(ctx);
3347
3347
+
return ctx.consumeAndRedraw();
3348
3348
+
} else if (key.matches(vaxis.Key.down, .{})) {
3349
3349
+
self.list_view.nextItem(ctx);
3350
3350
+
return ctx.consumeAndRedraw();
3351
3351
+
}
3352
3352
+
},
3353
3353
+
.list => {
3354
3354
+
if (key.matches(vaxis.Key.escape, .{})) {
3355
3355
+
try ctx.requestFocus(self.text_field.widget());
3356
3356
+
self.focus = .text_field;
3357
3357
+
return ctx.consumeAndRedraw();
3358
3358
+
} else if (key.matches(vaxis.Key.enter, .{})) {
3359
3359
+
if (self.filtered_items.items.len > 0) {
3360
3360
+
// join the selected room, and deinit the view
3361
3361
+
var buf: [128]u8 = undefined;
3362
3362
+
const item = self.filtered_items.items[self.list_view.cursor];
3363
3363
+
const cmd = try std.fmt.bufPrint(&buf, "/join {s}", .{item.name});
3364
3364
+
try self.client.app.handleCommand(.{ .client = self.client }, cmd);
3365
3365
+
}
3366
3366
+
self.close(ctx);
3367
3367
+
return;
3368
3368
+
}
3369
3369
+
},
3370
3370
+
}
3371
3371
+
},
3372
3372
+
else => {},
3373
3373
+
}
3374
3374
+
}
3375
3375
+
3376
3376
+
fn close(self: *ListModal, ctx: *vxfw.EventContext) void {
3377
3377
+
self.is_shown = false;
3378
3378
+
const selected = self.client.app.selectedBuffer() orelse unreachable;
3379
3379
+
self.client.app.selectBuffer(selected);
3380
3380
+
return ctx.consumeAndRedraw();
3381
3381
+
}
3382
3382
+
3383
3383
+
fn getItem(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget {
3384
3384
+
const self: *const ListModal = @ptrCast(@alignCast(ptr));
3385
3385
+
if (idx < self.filtered_items.items.len) {
3386
3386
+
return self.filtered_items.items[idx].widget();
3387
3387
+
}
3388
3388
+
return null;
3389
3389
+
}
3390
3390
+
3391
3391
+
fn _draw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
3392
3392
+
const self: *ListModal = @ptrCast(@alignCast(ptr));
3393
3393
+
return self.draw(ctx);
3394
3394
+
}
3395
3395
+
3396
3396
+
fn draw(self: *ListModal, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
3397
3397
+
const max = ctx.max.size();
3398
3398
+
var children: std.ArrayListUnmanaged(vxfw.SubSurface) = .empty;
3399
3399
+
3400
3400
+
try children.append(ctx.arena, .{
3401
3401
+
.origin = .{ .col = 0, .row = 0 },
3402
3402
+
.surface = try self.text_field.draw(ctx),
3403
3403
+
});
3404
3404
+
const list_ctx = ctx.withConstraints(
3405
3405
+
ctx.min,
3406
3406
+
.{ .width = max.width, .height = max.height - 2 },
3407
3407
+
);
3408
3408
+
try children.append(ctx.arena, .{
3409
3409
+
.origin = .{ .col = 0, .row = 2 },
3410
3410
+
.surface = try self.list_view.draw(list_ctx),
3411
3411
+
});
3412
3412
+
3413
3413
+
return .{
3414
3414
+
.size = max,
3415
3415
+
.widget = self.widget(),
3416
3416
+
.buffer = &.{},
3417
3417
+
.children = children.items,
3418
3418
+
};
3419
3419
+
}
3108
3420
};
3109
3421
3110
3422
test "caseFold" {