tangled
alpha
login
or
join now
rockorager.dev
/
comlink
2
fork
atom
an experimental irc client
2
fork
atom
overview
issues
pulls
pipelines
ui: flesh out channel view
rockorager.dev
1 year ago
c43db93c
aebbc4a5
+163
-38
3 changed files
expand all
collapse all
unified
split
src
app.zig
irc.zig
main.zig
+17
-30
src/app.zig
···
83
write_queue: comlink.WriteQueue,
84
write_thread: std.Thread,
85
86
-
lhs: vxfw.SplitView,
87
-
rhs: vxfw.SplitView,
88
buffer_list: vxfw.ListView,
0
0
0
89
90
/// initialize vaxis, lua state
91
-
pub fn init(self: *App, gpa: std.mem.Allocator) !void {
92
self.* = .{
93
.alloc = gpa,
94
.state = .{},
···
100
.lua = undefined,
101
.write_queue = .{},
102
.write_thread = undefined,
103
-
.lhs = .{
104
.width = self.state.buffers.width,
105
.lhs = self.buffer_list.widget(),
106
-
.rhs = self.rhs.widget(),
107
-
},
108
-
.rhs = .{
109
-
.width = self.state.members.width,
110
-
.constrain = .rhs,
111
-
.lhs = self.contentWidget(),
112
-
.rhs = self.memberWidget(),
113
},
114
.explicit_join = false,
115
.bundle = .{},
···
125
},
126
.draw_cursor = false,
127
},
0
128
};
129
130
self.lua = try Lua.init(&self.alloc);
···
233
234
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
235
const self: *App = @ptrCast(@alignCast(ptr));
0
0
0
0
0
0
236
237
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena);
238
_ = &children;
···
248
249
const sub: vxfw.SubSurface = .{
250
.origin = .{ .col = 0, .row = 0 },
251
-
.surface = try self.lhs.widget().draw(ctx),
252
};
253
try children.append(sub);
254
···
260
};
261
}
262
263
-
fn bufferWidget(self: *App) vxfw.Widget {
264
-
return .{
265
-
.userdata = self,
266
-
.captureHandler = null,
267
-
.eventHandler = null,
268
-
.drawFn = App.typeErasedBufferDrawFn,
269
-
};
270
-
}
271
-
272
fn bufferBuilderFn(ptr: *const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget {
273
const self: *const App = @ptrCast(@alignCast(ptr));
274
var i: usize = 0;
···
281
}
282
}
283
return null;
284
-
}
285
-
286
-
fn typeErasedBufferDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
287
-
const self: *App = @ptrCast(@alignCast(ptr));
288
-
_ = self;
289
-
const text: vxfw.Text = .{ .text = "buffers" };
290
-
return text.draw(ctx);
291
}
292
293
fn contentWidget(self: *App) vxfw.Widget {
···
1154
pub fn selectedBuffer(self: *App) ?irc.Buffer {
1155
var i: usize = 0;
1156
for (self.clients.items) |client| {
1157
-
if (i == self.state.buffers.selected_idx) return .{ .client = client };
1158
i += 1;
1159
for (client.channels.items) |channel| {
1160
-
if (i == self.state.buffers.selected_idx) return .{ .channel = channel };
1161
i += 1;
1162
}
1163
}
···
83
write_queue: comlink.WriteQueue,
84
write_thread: std.Thread,
85
86
+
view: vxfw.SplitView,
0
87
buffer_list: vxfw.ListView,
88
+
unicode: *const vaxis.Unicode,
89
+
90
+
const default_rhs: vxfw.Text = .{ .text = "TODO: update this text" };
91
92
/// initialize vaxis, lua state
93
+
pub fn init(self: *App, gpa: std.mem.Allocator, unicode: *const vaxis.Unicode) !void {
94
self.* = .{
95
.alloc = gpa,
96
.state = .{},
···
102
.lua = undefined,
103
.write_queue = .{},
104
.write_thread = undefined,
105
+
.view = .{
106
.width = self.state.buffers.width,
107
.lhs = self.buffer_list.widget(),
108
+
.rhs = default_rhs.widget(),
0
0
0
0
0
0
109
},
110
.explicit_join = false,
111
.bundle = .{},
···
121
},
122
.draw_cursor = false,
123
},
124
+
.unicode = unicode,
125
};
126
127
self.lua = try Lua.init(&self.alloc);
···
230
231
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
232
const self: *App = @ptrCast(@alignCast(ptr));
233
+
if (self.selectedBuffer()) |buffer| {
234
+
switch (buffer) {
235
+
.client => |client| self.view.rhs = client.view(),
236
+
.channel => |channel| self.view.rhs = channel.view.widget(),
237
+
}
238
+
} else self.view.rhs = default_rhs.widget();
239
240
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena);
241
_ = &children;
···
251
252
const sub: vxfw.SubSurface = .{
253
.origin = .{ .col = 0, .row = 0 },
254
+
.surface = try self.view.widget().draw(ctx),
255
};
256
try children.append(sub);
257
···
263
};
264
}
265
0
0
0
0
0
0
0
0
0
266
fn bufferBuilderFn(ptr: *const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget {
267
const self: *const App = @ptrCast(@alignCast(ptr));
268
var i: usize = 0;
···
275
}
276
}
277
return null;
0
0
0
0
0
0
0
278
}
279
280
fn contentWidget(self: *App) vxfw.Widget {
···
1141
pub fn selectedBuffer(self: *App) ?irc.Buffer {
1142
var i: usize = 0;
1143
for (self.clients.items) |client| {
1144
+
if (i == self.buffer_list.cursor) return .{ .client = client };
1145
i += 1;
1146
for (client.channels.items) |channel| {
1147
+
if (i == self.buffer_list.cursor) return .{ .channel = channel };
1148
i += 1;
1149
}
1150
}
+145
-7
src/irc.zig
···
119
120
has_mouse: bool = false,
121
0
0
0
0
0
122
pub const Member = struct {
123
user: *User,
124
···
133
else
134
std.ascii.orderIgnoreCase(lhs.user.nick, rhs.user.nick).compare(.lt);
135
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
136
};
137
138
-
pub fn deinit(self: *const Channel, alloc: std.mem.Allocator) void {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
139
alloc.free(self.name);
140
self.members.deinit();
141
if (self.topic) |topic| {
···
145
alloc.free(msg.bytes);
146
}
147
self.messages.deinit();
0
148
}
149
150
pub fn compare(_: void, lhs: *Channel, rhs: *Channel) bool {
···
188
try ctx.setMouseShape(.pointer);
189
if (mouse.type == .press and mouse.button == .left) {
190
self.client.app.selectBuffer(.{ .channel = self });
0
191
return ctx.consumeAndRedraw();
192
}
193
},
···
283
time_tag,
284
},
285
);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
286
}
287
};
288
···
630
}
631
batches.deinit();
632
self.fifo.deinit();
0
0
0
0
0
0
0
0
0
0
0
0
0
633
}
634
635
pub fn nameWidget(self: *Client, selected: bool) vxfw.Widget {
···
1350
if (caseFold(name, channel.name)) return channel;
1351
}
1352
const channel = try self.alloc.create(Channel);
1353
-
channel.* = .{
1354
-
.name = try self.alloc.dupe(u8, name),
1355
-
.members = std.ArrayList(Channel.Member).init(self.alloc),
1356
-
.messages = std.ArrayList(Message).init(self.alloc),
1357
-
.client = self,
1358
-
};
1359
try self.channels.append(channel);
1360
1361
std.sort.insertion(*Channel, self.channels.items, {}, Channel.compare);
···
119
120
has_mouse: bool = false,
121
122
+
view: vxfw.SplitView,
123
+
message_view: vxfw.ListView,
124
+
member_view: vxfw.ListView,
125
+
text_field: vxfw.TextField,
126
+
127
pub const Member = struct {
128
user: *User,
129
···
138
else
139
std.ascii.orderIgnoreCase(lhs.user.nick, rhs.user.nick).compare(.lt);
140
}
141
+
142
+
pub fn widget(self: *Member) vxfw.Widget {
143
+
return .{
144
+
.userdata = self,
145
+
.drawFn = Member.draw,
146
+
};
147
+
}
148
+
149
+
pub fn draw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
150
+
const self: *Member = @ptrCast(@alignCast(ptr));
151
+
const style: vaxis.Style = if (self.user.away)
152
+
.{ .dim = true }
153
+
else
154
+
.{ .fg = self.user.color };
155
+
var prefix = try ctx.arena.alloc(u8, 1);
156
+
prefix[0] = self.prefix;
157
+
const text: vxfw.RichText = .{
158
+
.text = &.{
159
+
.{ .text = prefix, .style = style },
160
+
.{ .text = self.user.nick, .style = style },
161
+
},
162
+
.softwrap = false,
163
+
};
164
+
return text.draw(ctx);
165
+
}
166
};
167
168
+
pub fn init(
169
+
self: *Channel,
170
+
gpa: Allocator,
171
+
client: *Client,
172
+
name: []const u8,
173
+
unicode: *const vaxis.Unicode,
174
+
) Allocator.Error!void {
175
+
self.* = .{
176
+
.name = try gpa.dupe(u8, name),
177
+
.members = std.ArrayList(Channel.Member).init(gpa),
178
+
.messages = std.ArrayList(Message).init(gpa),
179
+
.client = client,
180
+
.view = .{
181
+
.lhs = self.contentWidget(),
182
+
.rhs = self.member_view.widget(),
183
+
.width = 16,
184
+
.constrain = .rhs,
185
+
},
186
+
.message_view = .{ .children = .{ .slice = &.{} } },
187
+
.member_view = .{
188
+
.children = .{
189
+
.builder = .{
190
+
.userdata = self,
191
+
.buildFn = Channel.buildMemberList,
192
+
},
193
+
},
194
+
.draw_cursor = false,
195
+
},
196
+
.text_field = vxfw.TextField.init(gpa, unicode),
197
+
};
198
+
}
199
+
200
+
pub fn deinit(self: *Channel, alloc: std.mem.Allocator) void {
201
alloc.free(self.name);
202
self.members.deinit();
203
if (self.topic) |topic| {
···
207
alloc.free(msg.bytes);
208
}
209
self.messages.deinit();
210
+
self.text_field.deinit();
211
}
212
213
pub fn compare(_: void, lhs: *Channel, rhs: *Channel) bool {
···
251
try ctx.setMouseShape(.pointer);
252
if (mouse.type == .press and mouse.button == .left) {
253
self.client.app.selectBuffer(.{ .channel = self });
254
+
try ctx.requestFocus(self.text_field.widget());
255
return ctx.consumeAndRedraw();
256
}
257
},
···
347
time_tag,
348
},
349
);
350
+
}
351
+
352
+
pub fn contentWidget(self: *Channel) vxfw.Widget {
353
+
return .{
354
+
.userdata = self,
355
+
.drawFn = Channel.typeErasedViewDraw,
356
+
};
357
+
}
358
+
359
+
fn typeErasedViewDraw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
360
+
const self: *Channel = @ptrCast(@alignCast(ptr));
361
+
362
+
const max = ctx.max.size();
363
+
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena);
364
+
365
+
{
366
+
// Draw the topic
367
+
const topic: vxfw.Text = .{
368
+
.text = self.topic orelse "",
369
+
.softwrap = false,
370
+
};
371
+
372
+
const topic_sub: vxfw.SubSurface = .{
373
+
.origin = .{ .col = 0, .row = 0 },
374
+
.surface = try topic.draw(ctx),
375
+
};
376
+
377
+
try children.append(topic_sub);
378
+
379
+
// Draw a border below the topic
380
+
const bot = "─";
381
+
var writer = try std.ArrayList(u8).initCapacity(ctx.arena, bot.len * max.width);
382
+
try writer.writer().writeBytesNTimes(bot, max.width);
383
+
384
+
const border: vxfw.Text = .{
385
+
.text = writer.items,
386
+
.softwrap = false,
387
+
};
388
+
389
+
const topic_border: vxfw.SubSurface = .{
390
+
.origin = .{ .col = 0, .row = 1 },
391
+
.surface = try border.draw(ctx),
392
+
};
393
+
try children.append(topic_border);
394
+
}
395
+
396
+
// Draw the text field
397
+
try children.append(.{
398
+
.origin = .{ .col = 0, .row = max.height - 1 },
399
+
.surface = try self.text_field.draw(ctx),
400
+
});
401
+
402
+
return .{
403
+
.size = max,
404
+
.widget = self.contentWidget(),
405
+
.buffer = &.{},
406
+
.children = children.items,
407
+
};
408
+
}
409
+
410
+
fn buildMemberList(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget {
411
+
const self: *const Channel = @ptrCast(@alignCast(ptr));
412
+
if (idx < self.members.items.len) {
413
+
return self.members.items[idx].widget();
414
+
}
415
+
return null;
416
}
417
};
418
···
760
}
761
batches.deinit();
762
self.fifo.deinit();
763
+
}
764
+
765
+
pub fn view(self: *Client) vxfw.Widget {
766
+
return .{
767
+
.userdata = self,
768
+
.drawFn = Client.typeErasedViewDraw,
769
+
};
770
+
}
771
+
772
+
fn typeErasedViewDraw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
773
+
_ = ptr;
774
+
const text: vxfw.Text = .{ .text = "content" };
775
+
return text.draw(ctx);
776
}
777
778
pub fn nameWidget(self: *Client, selected: bool) vxfw.Widget {
···
1493
if (caseFold(name, channel.name)) return channel;
1494
}
1495
const channel = try self.alloc.create(Channel);
1496
+
try channel.init(self.alloc, self, name, self.app.unicode);
0
0
0
0
0
1497
try self.channels.append(channel);
1498
1499
std.sort.insertion(*Channel, self.channels.items, {}, Channel.compare);
+1
-1
src/main.zig
···
89
// defer app.deinit();
90
91
var comlink_app: comlink.App = undefined;
92
-
try comlink_app.init(gpa.allocator());
93
defer comlink_app.deinit();
94
95
try app.run(comlink_app.widget(), .{});
···
89
// defer app.deinit();
90
91
var comlink_app: comlink.App = undefined;
92
+
try comlink_app.init(gpa.allocator(), &app.vx.unicode);
93
defer comlink_app.deinit();
94
95
try app.run(comlink_app.widget(), .{});