an experimental irc client

config: add global configuration infra

Add ability to add global configuration

rockorager.dev 53be1861 03b76b47

verified
+103 -3
+9
docs/comlink.3.scd
··· 8 8 9 9 *local comlink = require*(_"comlink"_) 10 10 11 + *comlink.setup*(_config_) 12 + 11 13 *local conn = comlink.connect*(_config_) 12 14 13 15 *local channel = comlink.selected_channel*() ··· 24 26 25 27 The comlink module is the entrypoint into configuring and scripting comlink. 26 28 This module provides application level API calls. 29 + 30 + *comlink.setup* 31 + Accepts a configuration table. This table defines the global application 32 + configuration. The table has the following required fields: 33 + 34 + - *markread_on_focus*: boolean, whether to update unread indicator on 35 + focus gain 27 36 28 37 *comlink.connect* 29 38 Accepts a configuration table. This table defines the server
+10
docs/comlink.lua
··· 5 5 ---@class comlink 6 6 local comlink = {} 7 7 8 + ---@class Configuration 9 + --- 10 + ---@field markread_on_focus boolean When true, the unread indicator will be reset when comlink 11 + ---regains focus 12 + 13 + ---Global configuration 14 + --- 15 + ---@param cfg Configuration Comlink global configuration 16 + function comlink.setup(cfg) end 17 + 8 18 ---@class ConnectionConfiguration 9 19 --- 10 20 ---@field server string The server to connect to, eg "chat.sr.ht"
+2
src/app.zig
··· 42 42 }; 43 43 44 44 pub const App = struct { 45 + config: comlink.Config, 45 46 explicit_join: bool, 46 47 alloc: std.mem.Allocator, 47 48 /// System certificate bundle ··· 87 88 pub fn init(self: *App, gpa: std.mem.Allocator, unicode: *const vaxis.Unicode) !void { 88 89 self.* = .{ 89 90 .alloc = gpa, 91 + .config = .{}, 90 92 .state = .{}, 91 93 .clients = std.ArrayList(*irc.Client).init(gpa), 92 94 .env = try std.process.getEnvMap(gpa),
+48
src/comlink.zig
··· 4 4 const vaxis = @import("vaxis"); 5 5 pub const irc = @import("irc.zig"); 6 6 pub const lua = @import("lua.zig"); 7 + const ziglua = @import("ziglua"); 7 8 8 9 pub const App = app.App; 9 10 pub const Completer = completer.Completer; ··· 12 13 pub const Bind = struct { 13 14 key: vaxis.Key, 14 15 command: Command, 16 + }; 17 + 18 + pub const Config = struct { 19 + markread_on_focus: bool = false, 20 + 21 + pub fn Fields() type { 22 + const config_fields = std.meta.fieldNames(Config); 23 + var fields: [config_fields.len]std.builtin.Type.EnumField = undefined; 24 + 25 + for (config_fields, 0..) |f, i| { 26 + fields[i] = .{ 27 + .name = f, 28 + .value = i, 29 + }; 30 + } 31 + 32 + return @Type(.{ 33 + .Enum = .{ 34 + .decls = &.{}, 35 + .tag_type = u16, 36 + .fields = &fields, 37 + .is_exhaustive = true, 38 + }, 39 + }); 40 + } 41 + 42 + pub fn fieldToLuaType(field: []const u8) ziglua.LuaType { 43 + const fields = std.meta.fields(Config); 44 + inline for (fields) |f| { 45 + if (std.mem.eql(u8, field, f.name)) { 46 + switch (@typeInfo(f.type)) { 47 + .Bool => return .boolean, 48 + .Int, .ComptimeInt => return .number, 49 + .Pointer => |ptr_info| { 50 + switch (ptr_info.size) { 51 + .Slice => { 52 + if (ptr_info.child == u8) return .string; 53 + }, 54 + else => {}, 55 + } 56 + }, 57 + else => return .nil, 58 + } 59 + } 60 + } 61 + return .nil; 62 + } 15 63 }; 16 64 17 65 pub const Command = union(enum) {
+5 -3
src/irc.zig
··· 1192 1192 self.scroll_to_last_read = false; 1193 1193 } 1194 1194 1195 - if (self.has_unread and self.client.app.has_focus and self.messageViewIsAtBottom()) { 1195 + if (self.client.app.config.markread_on_focus and 1196 + self.has_unread and 1197 + self.client.app.has_focus and 1198 + self.messageViewIsAtBottom()) 1199 + { 1196 1200 try self.markRead(); 1197 1201 } 1198 1202 ··· 1564 1568 channels: std.ArrayList(*Channel), 1565 1569 users: std.StringHashMap(*User), 1566 1570 1567 - should_close: bool = false, 1568 1571 status: std.atomic.Value(Status), 1569 1572 1570 1573 caps: Capabilities = .{}, ··· 1609 1612 1610 1613 /// Closes the connection 1611 1614 pub fn close(self: *Client) void { 1612 - self.should_close = true; 1613 1615 if (self.status.load(.unordered) == .disconnected) return; 1614 1616 if (self.config.tls) { 1615 1617 self.client.close() catch {};
+29
src/lua.zig
··· 167 167 pub fn preloader(lua: *Lua) i32 { 168 168 const fns = [_]ziglua.FnReg{ 169 169 .{ .name = "bind", .func = ziglua.wrap(bind) }, 170 + .{ .name = "setup", .func = ziglua.wrap(setup) }, 170 171 .{ .name = "connect", .func = ziglua.wrap(connect) }, 171 172 .{ .name = "log", .func = ziglua.wrap(log) }, 172 173 .{ .name = "notify", .func = ziglua.wrap(notify) }, ··· 176 177 lua.newLibTable(&fns); // [table] 177 178 lua.setFuncs(&fns, 0); // [table] 178 179 return 1; 180 + } 181 + 182 + /// Sets global configuration 183 + fn setup(lua: *Lua) i32 { 184 + defer lua.pop(1); // [] 185 + lua.argCheck(lua.isTable(1), 1, "expected a table"); 186 + // [table] 187 + const app = getApp(lua); 188 + const fields = std.meta.fieldNames(comlink.Config); 189 + for (fields) |field| { 190 + defer lua.pop(1); // [table] 191 + const lua_type = lua.getField(1, field); // [table,type] 192 + if (lua_type == .nil) { 193 + // The field wasn't present 194 + continue; 195 + } 196 + const expected_type = comlink.Config.fieldToLuaType(field); 197 + if (lua_type != expected_type) { 198 + std.log.warn("unexpected type: {}, expected {}", .{ lua_type, expected_type }); 199 + continue; 200 + } 201 + 202 + const field_enum = std.meta.stringToEnum(comlink.Config.Fields(), field) orelse continue; 203 + switch (field_enum) { 204 + .markread_on_focus => app.config.markread_on_focus = lua.toBoolean(1), 205 + } 206 + } 207 + return 0; 179 208 } 180 209 181 210 /// creates a keybind. Accepts one or two string.