From 33c4c328b661a1c0ef85d75be9fcb7af0455cec3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 12:43:15 -0800 Subject: [PATCH 1/8] config: file formatter --- src/config.zig | 1 + src/config/formatter.zig | 91 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/config/formatter.zig diff --git a/src/config.zig b/src/config.zig index 78e033361..73c014a01 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,6 +1,7 @@ const builtin = @import("builtin"); pub usingnamespace @import("config/key.zig"); +pub usingnamespace @import("config/formatter.zig"); pub const Config = @import("config/Config.zig"); pub const string = @import("config/string.zig"); pub const edit = @import("config/edit.zig"); diff --git a/src/config/formatter.zig b/src/config/formatter.zig new file mode 100644 index 000000000..ab5a8667f --- /dev/null +++ b/src/config/formatter.zig @@ -0,0 +1,91 @@ +const std = @import("std"); +const Config = @import("Config.zig"); + +/// FileFormatter is a formatter implementation that outputs the +/// config in a file-like format. This uses more generous whitespace, +/// can include comments, etc. +pub const FileFormatter = struct { + config: *const Config, + + /// Implements std.fmt so it can be used directly with std.fmt. + pub fn format( + self: FileFormatter, + comptime layout: []const u8, + opts: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = layout; + _ = opts; + + inline for (@typeInfo(Config).Struct.fields) |field| { + if (field.name[0] == '_') continue; + try self.formatField( + field.type, + field.name, + @field(self.config, field.name), + writer, + ); + } + } + + fn formatField( + self: FileFormatter, + comptime T: type, + name: []const u8, + value: T, + writer: anytype, + ) !void { + switch (@typeInfo(T)) { + .Bool, .Int => { + try writer.print("{s} = {}\n", .{ name, value }); + return; + }, + + .Float => { + try writer.print("{s} = {d}\n", .{ name, value }); + return; + }, + + .Optional => |info| if (value) |inner| { + try self.formatField( + info.child, + name, + inner, + writer, + ); + } else { + try writer.print("{s} = \n", .{name}); + }, + + .Pointer => switch (T) { + []const u8, + [:0]const u8, + => { + try writer.print("{s} = {s}\n", .{ name, value }); + }, + + else => {}, + }, + + else => {}, + } + + // TODO: make a compiler error so we can detect when + // we don't support a type. + } +}; + +test "format default config" { + const testing = std.testing; + const alloc = testing.allocator; + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + + const fmt: FileFormatter = .{ .config = &cfg }; + try std.fmt.format(buf.writer(), "{}", .{fmt}); + + std.log.warn("{s}", .{buf.items}); +} From 9369baac60508d492b3e97bc9343983e11e5a31f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 12:50:11 -0800 Subject: [PATCH 2/8] cli: empty field resets optionals to null --- src/cli/args.zig | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/cli/args.zig b/src/cli/args.zig index 773457cf8..abca087e3 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -170,6 +170,22 @@ fn parseIntoField( inline for (info.Struct.fields) |field| { if (field.name[0] != '_' and mem.eql(u8, field.name, key)) { + // If the field is optional then consider scenarios we reset + // the value to being unset. We allow unsetting optionals + // whenever the value is "". + // + // At the time of writing this, empty string isn't a desirable + // value for any optional field under any realistic scenario. + // + // We don't allow unset values to set optional fields to + // null because unset value for booleans always means true. + if (@typeInfo(field.type) == .Optional) optional: { + if (std.mem.eql(u8, "", value orelse break :optional)) { + @field(dst, field.name) = null; + return; + } + } + // For optional fields, we just treat it as the child type. // This lets optional fields default to null but get set by // the CLI. @@ -617,6 +633,10 @@ test "parseIntoField: optional field" { // True try parseIntoField(@TypeOf(data), alloc, &data, "a", "1"); try testing.expectEqual(true, data.a.?); + + // Unset + try parseIntoField(@TypeOf(data), alloc, &data, "a", ""); + try testing.expect(data.a == null); } test "parseIntoField: struct with parse func" { From 32a1c6ec0653da66b96382857228e5101564d3cb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 14:41:49 -0800 Subject: [PATCH 3/8] config: ability to format all field types except tagged unions --- src/config/Config.zig | 139 +++++++++++++++++++++++++++++++++++++++ src/config/formatter.zig | 92 ++++++++++++++++++++++---- 2 files changed, 219 insertions(+), 12 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index e323d5ce9..b888c42fd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2178,6 +2178,19 @@ pub const Color = packed struct(u24) { return std.meta.eql(self, other); } + /// Used by Formatter + pub fn formatEntry(self: Color, formatter: anytype) !void { + var buf: [128]u8 = undefined; + try formatter.formatEntry( + []const u8, + std.fmt.bufPrint( + &buf, + "#{x:0>2}{x:0>2}{x:0>2}", + .{ self.r, self.g, self.b }, + ) catch return error.OutOfMemory, + ); + } + /// fromHex parses a color from a hex value such as #RRGGBB. The "#" /// is optional. pub fn fromHex(input: []const u8) !Color { @@ -2251,6 +2264,21 @@ pub const Palette = struct { return std.meta.eql(self, other); } + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + var buf: [128]u8 = undefined; + for (0.., self.value) |k, v| { + try formatter.formatEntry( + []const u8, + std.fmt.bufPrint( + &buf, + "{d}=#{x:0>2}{x:0>2}{x:0>2}", + .{ k, v.r, v.g, v.b }, + ) catch return error.OutOfMemory, + ); + } + } + test "parseCLI" { const testing = std.testing; @@ -2314,6 +2342,19 @@ pub const RepeatableString = struct { } else return true; } + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + // If no items, we want to render an empty field. + if (self.list.items.len == 0) { + try formatter.formatEntry(void, {}); + return; + } + + for (self.list.items) |value| { + try formatter.formatEntry([]const u8, value); + } + } + test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); @@ -2355,6 +2396,11 @@ pub const RepeatablePath = struct { return self.value.equal(other.value); } + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + try self.value.formatEntry(formatter); + } + /// Expand all the paths relative to the base directory. pub fn expand( self: *Self, @@ -2442,6 +2488,26 @@ pub const RepeatableFontVariation = struct { } else return true; } + /// Used by Formatter + pub fn formatEntry( + self: Self, + formatter: anytype, + ) !void { + if (self.list.items.len == 0) { + try formatter.formatEntry(void, {}); + return; + } + + var buf: [128]u8 = undefined; + for (self.list.items) |value| { + const str = std.fmt.bufPrint(&buf, "{s}={d}", .{ + value.id.str(), + value.value, + }) catch return error.OutOfMemory; + try formatter.formatEntry([]const u8, str); + } + } + test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); @@ -2561,6 +2627,29 @@ pub const Keybinds = struct { return true; } + /// Used by Formatter + pub fn formatEntry(self: Keybinds, formatter: anytype) !void { + if (self.set.bindings.size == 0) { + try formatter.formatEntry(void, {}); + return; + } + + var buf: [1024]u8 = undefined; + var iter = self.set.bindings.iterator(); + while (iter.next()) |next| { + const k = next.key_ptr.*; + const v = next.value_ptr.*; + try formatter.formatEntry( + []const u8, + std.fmt.bufPrint( + &buf, + "{}={}", + .{ k, v }, + ) catch return error.OutOfMemory, + ); + } + } + test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); @@ -2618,6 +2707,49 @@ pub const RepeatableCodepointMap = struct { } else return true; } + /// Used by Formatter + pub fn formatEntry( + self: Self, + formatter: anytype, + ) !void { + if (self.map.list.len == 0) { + try formatter.formatEntry(void, {}); + return; + } + + var buf: [1024]u8 = undefined; + const ranges = self.map.list.items(.range); + const descriptors = self.map.list.items(.descriptor); + for (ranges, descriptors) |range, descriptor| { + if (range[0] == range[1]) { + try formatter.formatEntry( + []const u8, + std.fmt.bufPrint( + &buf, + "U+{X:0>4}={s}", + .{ + range[0], + descriptor.family orelse "", + }, + ) catch return error.OutOfMemory, + ); + } else { + try formatter.formatEntry( + []const u8, + std.fmt.bufPrint( + &buf, + "U+{X:0>4}-U{X:0>4}={s}", + .{ + range[0], + range[1], + descriptor.family orelse "", + }, + ) catch return error.OutOfMemory, + ); + } + } + } + /// Parses the list of Unicode codepoint ranges. Valid syntax: /// /// "" (empty returns null) @@ -2836,6 +2968,13 @@ pub const RepeatableLink = struct { _ = other; return true; } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + // This currently can't be set so we don't format anything. + _ = self; + _ = formatter; + } }; /// Options for copy on select behavior. diff --git a/src/config/formatter.zig b/src/config/formatter.zig index ab5a8667f..26be9a816 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -19,7 +19,7 @@ pub const FileFormatter = struct { inline for (@typeInfo(Config).Struct.fields) |field| { if (field.name[0] == '_') continue; - try self.formatField( + try self.formatEntry( field.type, field.name, @field(self.config, field.name), @@ -28,13 +28,32 @@ pub const FileFormatter = struct { } } - fn formatField( + pub fn formatEntry( self: FileFormatter, comptime T: type, name: []const u8, value: T, writer: anytype, ) !void { + const EntryFormatter = struct { + parent: *const FileFormatter, + name: []const u8, + writer: @TypeOf(writer), + + pub fn formatEntry( + self_entry: @This(), + comptime EntryT: type, + value_entry: EntryT, + ) !void { + return self_entry.parent.formatEntry( + EntryT, + self_entry.name, + value_entry, + self_entry.writer, + ); + } + }; + switch (@typeInfo(T)) { .Bool, .Int => { try writer.print("{s} = {}\n", .{ name, value }); @@ -46,15 +65,29 @@ pub const FileFormatter = struct { return; }, - .Optional => |info| if (value) |inner| { - try self.formatField( - info.child, - name, - inner, - writer, - ); - } else { + .Enum => { + try writer.print("{s} = {s}\n", .{ name, @tagName(value) }); + return; + }, + + .Void => { try writer.print("{s} = \n", .{name}); + return; + }, + + .Optional => |info| { + if (value) |inner| { + try self.formatEntry( + info.child, + name, + inner, + writer, + ); + } else { + try writer.print("{s} = \n", .{name}); + } + + return; }, .Pointer => switch (T) { @@ -62,16 +95,51 @@ pub const FileFormatter = struct { [:0]const u8, => { try writer.print("{s} = {s}\n", .{ name, value }); + return; }, else => {}, }, + // Structs of all types require a "formatEntry" function + // to be defined which will be called to format the value. + // This is given the formatter in use so that they can + // call BACK to our formatEntry to write each primitive + // value. + .Struct => |info| if (@hasDecl(T, "formatEntry")) { + try value.formatEntry(EntryFormatter{ + .parent = &self, + .name = name, + .writer = writer, + }); + return; + } else switch (info.layout) { + // Packed structs we special case. + .Packed => { + try writer.print("{s} = ", .{name}); + inline for (info.fields, 0..) |field, i| { + if (i > 0) try writer.print(",", .{}); + try writer.print("{s}{s}", .{ + if (!@field(value, field.name)) "no-" else "", + field.name, + }); + } + try writer.print("\n", .{}); + return; + }, + + else => {}, + }, + + // TODO + .Union => return, + else => {}, } - // TODO: make a compiler error so we can detect when - // we don't support a type. + // Compile error so that we can catch missing cases. + @compileLog(T); + @compileError("missing case for type"); } }; From 2bf37843f3310df06639947a25160c4bd108adcc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 15:07:32 -0800 Subject: [PATCH 4/8] config: tests for all custom formatEntry calls --- src/config/Config.zig | 143 ++++++++++++++++++++++- src/config/formatter.zig | 239 ++++++++++++++++++++------------------- 2 files changed, 265 insertions(+), 117 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index b888c42fd..da46351dd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -15,6 +15,7 @@ const internal_os = @import("../os/main.zig"); const cli = @import("../cli.zig"); const Command = @import("../Command.zig"); +const formatterpkg = @import("formatter.zig"); const url = @import("url.zig"); const Key = @import("key.zig").Key; const KeyValue = @import("key.zig").Value; @@ -2231,6 +2232,16 @@ pub const Color = packed struct(u24) { test "parseCLI from name" { try std.testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.parseCLI("black")); } + + test "formatConfig" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var color: Color = .{ .r = 10, .g = 11, .b = 12 }; + try color.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = #0a0b0c\n", buf.items); + } }; /// Palette is the 256 color palette for 256-color mode. This is still @@ -2295,6 +2306,16 @@ pub const Palette = struct { var p: Self = .{}; try testing.expectError(error.Overflow, p.parseCLI("256=#AABBCC")); } + + test "formatConfig" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var list: Self = .{}; + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = 0=#1d1f21\n", buf.items[0..14]); + } }; /// RepeatableString is a string value that can be repeated to accumulate @@ -2369,6 +2390,47 @@ pub const RepeatableString = struct { try list.parseCLI(alloc, ""); try testing.expectEqual(@as(usize, 0), list.list.items.len); } + + test "formatConfig empty" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var list: Self = .{}; + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + } + + test "formatConfig single item" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "A"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); + } + + test "formatConfig multiple items" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "A"); + try list.parseCLI(alloc, "B"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = A\na = B\n", buf.items); + } }; /// RepeatablePath is like repeatable string but represents a path value. @@ -2549,6 +2611,21 @@ pub const RepeatableFontVariation = struct { .value = -15, }, list.list.items[1]); } + + test "formatConfig single" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "wght = 200"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = wght=200\n", buf.items); + } }; /// Stores a set of keybinds. @@ -2660,6 +2737,21 @@ pub const Keybinds = struct { try set.parseCLI(alloc, "shift+a=copy_to_clipboard"); try set.parseCLI(alloc, "shift+a=csi:hello"); } + + test "formatConfig single" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Keybinds = .{}; + try list.parseCLI(alloc, "shift+a=csi:hello"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.items); + } }; /// See "font-codepoint-map" for documentation. @@ -2738,7 +2830,7 @@ pub const RepeatableCodepointMap = struct { []const u8, std.fmt.bufPrint( &buf, - "U+{X:0>4}-U{X:0>4}={s}", + "U+{X:0>4}-U+{X:0>4}={s}", .{ range[0], range[1], @@ -2883,6 +2975,55 @@ pub const RepeatableCodepointMap = struct { try testing.expectEqualStrings("Courier", entry.descriptor.family.?); } } + + test "formatConfig single" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "U+ABCD=Comic Sans"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = U+ABCD=Comic Sans\n", buf.items); + } + + test "formatConfig range" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "U+0001 - U+0005=Verdana"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = U+0001-U+0005=Verdana\n", buf.items); + } + + test "formatConfig multiple" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "U+0006-U+0009, U+ABCD=Courier"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, + \\a = U+0006-U+0009=Courier + \\a = U+ABCD=Courier + \\ + , buf.items); + } }; pub const FontStyle = union(enum) { diff --git a/src/config/formatter.zig b/src/config/formatter.zig index 26be9a816..e6a5887ad 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -1,6 +1,127 @@ +const formatter = @This(); const std = @import("std"); const Config = @import("Config.zig"); +/// Returns a single entry formatter for the given field name and writer. +pub fn entryFormatter( + name: []const u8, + writer: anytype, +) EntryFormatter(@TypeOf(writer)) { + return .{ .name = name, .writer = writer }; +} + +/// The entry formatter type for a given writer. +pub fn EntryFormatter(comptime WriterType: type) type { + return struct { + name: []const u8, + writer: WriterType, + + pub fn formatEntry( + self: @This(), + comptime T: type, + value: T, + ) !void { + return formatter.formatEntry( + T, + self.name, + value, + self.writer, + ); + } + }; +} + +/// Format a single type with the given name and value. +pub fn formatEntry( + comptime T: type, + name: []const u8, + value: T, + writer: anytype, +) !void { + switch (@typeInfo(T)) { + .Bool, .Int => { + try writer.print("{s} = {}\n", .{ name, value }); + return; + }, + + .Float => { + try writer.print("{s} = {d}\n", .{ name, value }); + return; + }, + + .Enum => { + try writer.print("{s} = {s}\n", .{ name, @tagName(value) }); + return; + }, + + .Void => { + try writer.print("{s} = \n", .{name}); + return; + }, + + .Optional => |info| { + if (value) |inner| { + try formatEntry( + info.child, + name, + inner, + writer, + ); + } else { + try writer.print("{s} = \n", .{name}); + } + + return; + }, + + .Pointer => switch (T) { + []const u8, + [:0]const u8, + => { + try writer.print("{s} = {s}\n", .{ name, value }); + return; + }, + + else => {}, + }, + + // Structs of all types require a "formatEntry" function + // to be defined which will be called to format the value. + // This is given the formatter in use so that they can + // call BACK to our formatEntry to write each primitive + // value. + .Struct => |info| if (@hasDecl(T, "formatEntry")) { + try value.formatEntry(entryFormatter(name, writer)); + return; + } else switch (info.layout) { + // Packed structs we special case. + .Packed => { + try writer.print("{s} = ", .{name}); + inline for (info.fields, 0..) |field, i| { + if (i > 0) try writer.print(",", .{}); + try writer.print("{s}{s}", .{ + if (!@field(value, field.name)) "no-" else "", + field.name, + }); + } + try writer.print("\n", .{}); + return; + }, + + else => {}, + }, + + // TODO + .Union => return, + + else => {}, + } + + // Compile error so that we can catch missing cases. + @compileLog(T); + @compileError("missing case for type"); +} + /// FileFormatter is a formatter implementation that outputs the /// config in a file-like format. This uses more generous whitespace, /// can include comments, etc. @@ -19,7 +140,7 @@ pub const FileFormatter = struct { inline for (@typeInfo(Config).Struct.fields) |field| { if (field.name[0] == '_') continue; - try self.formatEntry( + try formatEntry( field.type, field.name, @field(self.config, field.name), @@ -27,120 +148,6 @@ pub const FileFormatter = struct { ); } } - - pub fn formatEntry( - self: FileFormatter, - comptime T: type, - name: []const u8, - value: T, - writer: anytype, - ) !void { - const EntryFormatter = struct { - parent: *const FileFormatter, - name: []const u8, - writer: @TypeOf(writer), - - pub fn formatEntry( - self_entry: @This(), - comptime EntryT: type, - value_entry: EntryT, - ) !void { - return self_entry.parent.formatEntry( - EntryT, - self_entry.name, - value_entry, - self_entry.writer, - ); - } - }; - - switch (@typeInfo(T)) { - .Bool, .Int => { - try writer.print("{s} = {}\n", .{ name, value }); - return; - }, - - .Float => { - try writer.print("{s} = {d}\n", .{ name, value }); - return; - }, - - .Enum => { - try writer.print("{s} = {s}\n", .{ name, @tagName(value) }); - return; - }, - - .Void => { - try writer.print("{s} = \n", .{name}); - return; - }, - - .Optional => |info| { - if (value) |inner| { - try self.formatEntry( - info.child, - name, - inner, - writer, - ); - } else { - try writer.print("{s} = \n", .{name}); - } - - return; - }, - - .Pointer => switch (T) { - []const u8, - [:0]const u8, - => { - try writer.print("{s} = {s}\n", .{ name, value }); - return; - }, - - else => {}, - }, - - // Structs of all types require a "formatEntry" function - // to be defined which will be called to format the value. - // This is given the formatter in use so that they can - // call BACK to our formatEntry to write each primitive - // value. - .Struct => |info| if (@hasDecl(T, "formatEntry")) { - try value.formatEntry(EntryFormatter{ - .parent = &self, - .name = name, - .writer = writer, - }); - return; - } else switch (info.layout) { - // Packed structs we special case. - .Packed => { - try writer.print("{s} = ", .{name}); - inline for (info.fields, 0..) |field, i| { - if (i > 0) try writer.print(",", .{}); - try writer.print("{s}{s}", .{ - if (!@field(value, field.name)) "no-" else "", - field.name, - }); - } - try writer.print("\n", .{}); - return; - }, - - else => {}, - }, - - // TODO - .Union => return, - - else => {}, - } - - // Compile error so that we can catch missing cases. - @compileLog(T); - @compileError("missing case for type"); - } }; test "format default config" { @@ -155,5 +162,5 @@ test "format default config" { const fmt: FileFormatter = .{ .config = &cfg }; try std.fmt.format(buf.writer(), "{}", .{fmt}); - std.log.warn("{s}", .{buf.items}); + //std.log.warn("{s}", .{buf.items}); } From dbb808ae933e202e7a6b80fcc7df3dfd25c15f9c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 15:13:43 -0800 Subject: [PATCH 5/8] config: tests for formatEntry --- src/config/formatter.zig | 108 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/config/formatter.zig b/src/config/formatter.zig index e6a5887ad..68bf5ee2e 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -159,8 +159,116 @@ test "format default config" { var buf = std.ArrayList(u8).init(alloc); defer buf.deinit(); + // We just make sure this works without errors. We aren't asserting output. const fmt: FileFormatter = .{ .config = &cfg }; try std.fmt.format(buf.writer(), "{}", .{fmt}); //std.log.warn("{s}", .{buf.items}); } + +test "formatEntry bool" { + const testing = std.testing; + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry(bool, "a", true, buf.writer()); + try testing.expectEqualStrings("a = true\n", buf.items); + } + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry(bool, "a", false, buf.writer()); + try testing.expectEqualStrings("a = false\n", buf.items); + } +} + +test "formatEntry int" { + const testing = std.testing; + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry(u8, "a", 123, buf.writer()); + try testing.expectEqualStrings("a = 123\n", buf.items); + } +} + +test "formatEntry float" { + const testing = std.testing; + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry(f64, "a", 0.7, buf.writer()); + try testing.expectEqualStrings("a = 0.7\n", buf.items); + } +} + +test "formatEntry enum" { + const testing = std.testing; + const Enum = enum { one, two, three }; + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry(Enum, "a", .two, buf.writer()); + try testing.expectEqualStrings("a = two\n", buf.items); + } +} + +test "formatEntry void" { + const testing = std.testing; + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry(void, "a", {}, buf.writer()); + try testing.expectEqualStrings("a = \n", buf.items); + } +} + +test "formatEntry optional" { + const testing = std.testing; + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry(?bool, "a", null, buf.writer()); + try testing.expectEqualStrings("a = \n", buf.items); + } + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry(?bool, "a", false, buf.writer()); + try testing.expectEqualStrings("a = false\n", buf.items); + } +} + +test "formatEntry string" { + const testing = std.testing; + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry([]const u8, "a", "hello", buf.writer()); + try testing.expectEqualStrings("a = hello\n", buf.items); + } +} + +test "formatEntry packed struct" { + const testing = std.testing; + const Value = packed struct { + one: bool = true, + two: bool = false, + }; + + { + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + try formatEntry(Value, "a", .{}, buf.writer()); + try testing.expectEqualStrings("a = one,no-two\n", buf.items); + } +} From 95a67e5f061bf10742002c4a0cea360e6172f929 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 15:24:17 -0800 Subject: [PATCH 6/8] config: support only formatting changed fields --- src/config/formatter.zig | 58 +++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/src/config/formatter.zig b/src/config/formatter.zig index 68bf5ee2e..2588ee196 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -1,6 +1,8 @@ const formatter = @This(); const std = @import("std"); +const Allocator = std.mem.Allocator; const Config = @import("Config.zig"); +const Key = @import("key.zig").Key; /// Returns a single entry formatter for the given field name and writer. pub fn entryFormatter( @@ -126,8 +128,12 @@ pub fn formatEntry( /// config in a file-like format. This uses more generous whitespace, /// can include comments, etc. pub const FileFormatter = struct { + alloc: Allocator, config: *const Config, + /// Only include changed values from the default. + changed: bool = false, + /// Implements std.fmt so it can be used directly with std.fmt. pub fn format( self: FileFormatter, @@ -138,14 +144,31 @@ pub const FileFormatter = struct { _ = layout; _ = opts; + // If we're change-tracking then we need the default config to + // compare against. + var default: ?Config = if (self.changed) + try Config.default(self.alloc) + else + null; + defer if (default) |*v| v.deinit(); + inline for (@typeInfo(Config).Struct.fields) |field| { if (field.name[0] == '_') continue; - try formatEntry( - field.type, - field.name, - @field(self.config, field.name), - writer, - ); + + const value = @field(self.config, field.name); + const do_format = if (default) |d| format: { + const key = @field(Key, field.name); + break :format d.changed(self.config, key); + } else true; + + if (do_format) { + try formatEntry( + field.type, + field.name, + value, + writer, + ); + } } } }; @@ -160,7 +183,28 @@ test "format default config" { defer buf.deinit(); // We just make sure this works without errors. We aren't asserting output. - const fmt: FileFormatter = .{ .config = &cfg }; + const fmt: FileFormatter = .{ .alloc = alloc, .config = &cfg }; + try std.fmt.format(buf.writer(), "{}", .{fmt}); + + //std.log.warn("{s}", .{buf.items}); +} + +test "format default config changed" { + const testing = std.testing; + const alloc = testing.allocator; + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.@"font-size" = 42; + + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + + // We just make sure this works without errors. We aren't asserting output. + const fmt: FileFormatter = .{ + .alloc = alloc, + .config = &cfg, + .changed = true, + }; try std.fmt.format(buf.writer(), "{}", .{fmt}); //std.log.warn("{s}", .{buf.items}); From daf297cee20fee3f7b0289568783f369abb32009 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 15:35:16 -0800 Subject: [PATCH 7/8] config: union type formatters --- src/config/Config.zig | 59 +++++++++++++++++++++++++++++++++++++++ src/config/formatter.zig | 6 ++-- src/font/face/Metrics.zig | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index da46351dd..86a046889 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3065,6 +3065,20 @@ pub const FontStyle = union(enum) { }; } + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + switch (self) { + .default, .false => try formatter.formatEntry( + []const u8, + @tagName(self), + ), + + .name => |name| { + try formatter.formatEntry([:0]const u8, name); + }, + } + } + test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); @@ -3081,6 +3095,51 @@ pub const FontStyle = union(enum) { try p.parseCLI(alloc, "bold"); try testing.expectEqualStrings("bold", p.name); } + + test "formatConfig default" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var p: Self = .{ .default = {} }; + try p.parseCLI(alloc, "default"); + try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = default\n", buf.items); + } + + test "formatConfig false" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var p: Self = .{ .default = {} }; + try p.parseCLI(alloc, "false"); + try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = false\n", buf.items); + } + + test "formatConfig named" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var p: Self = .{ .default = {} }; + try p.parseCLI(alloc, "bold"); + try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = bold\n", buf.items); + } }; /// See "link" for documentation. diff --git a/src/config/formatter.zig b/src/config/formatter.zig index 2588ee196..d298ef319 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -113,8 +113,10 @@ pub fn formatEntry( else => {}, }, - // TODO - .Union => return, + .Union => if (@hasDecl(T, "formatEntry")) { + try value.formatEntry(entryFormatter(name, writer)); + return; + }, else => {}, } diff --git a/src/font/face/Metrics.zig b/src/font/face/Metrics.zig index 0d2af177c..e8f318d48 100644 --- a/src/font/face/Metrics.zig +++ b/src/font/face/Metrics.zig @@ -119,6 +119,34 @@ pub const Modifier = union(enum) { return try parse(input orelse return error.ValueRequired); } + /// Used by config formatter + pub fn formatEntry(self: Modifier, formatter: anytype) !void { + var buf: [1024]u8 = undefined; + switch (self) { + .percent => |v| { + try formatter.formatEntry( + []const u8, + std.fmt.bufPrint( + &buf, + "{d}%", + .{(v - 1) * 100}, + ) catch return error.OutOfMemory, + ); + }, + + .absolute => |v| { + try formatter.formatEntry( + []const u8, + std.fmt.bufPrint( + &buf, + "{d}", + .{v}, + ) catch return error.OutOfMemory, + ); + }, + } + } + /// Apply a modifier to a numeric value. pub fn apply(self: Modifier, v: u32) u32 { return switch (self) { @@ -140,6 +168,28 @@ pub const Modifier = union(enum) { }, }; } + + test "formatConfig percent" { + const configpkg = @import("../../config.zig"); + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + const p = try parseCLI("24%"); + try p.formatEntry(configpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = 24%\n", buf.items); + } + + test "formatConfig absolute" { + const configpkg = @import("../../config.zig"); + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + const p = try parseCLI("-30"); + try p.formatEntry(configpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = -30\n", buf.items); + } }; /// Key is an enum of all the available metrics keys. From 64e3721bb7b160c92aad6090abdd70dabd63a4a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 15:42:43 -0800 Subject: [PATCH 8/8] config: formatter can output docs --- src/config/formatter.zig | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/config/formatter.zig b/src/config/formatter.zig index d298ef319..62a395825 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -1,6 +1,7 @@ const formatter = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; +const help_strings = @import("help_strings"); const Config = @import("Config.zig"); const Key = @import("key.zig").Key; @@ -133,6 +134,9 @@ pub const FileFormatter = struct { alloc: Allocator, config: *const Config, + /// Include comments for documentation of each key + docs: bool = false, + /// Only include changed values from the default. changed: bool = false, @@ -164,12 +168,23 @@ pub const FileFormatter = struct { } else true; if (do_format) { + const do_docs = self.docs and @hasDecl(help_strings.Config, field.name); + if (do_docs) { + const help = @field(help_strings.Config, field.name); + var lines = std.mem.splitScalar(u8, help, '\n'); + while (lines.next()) |line| { + try writer.print("# {s}\n", .{line}); + } + } + try formatEntry( field.type, field.name, value, writer, ); + + if (do_docs) try writer.print("\n", .{}); } } } @@ -185,7 +200,10 @@ test "format default config" { defer buf.deinit(); // We just make sure this works without errors. We aren't asserting output. - const fmt: FileFormatter = .{ .alloc = alloc, .config = &cfg }; + const fmt: FileFormatter = .{ + .alloc = alloc, + .config = &cfg, + }; try std.fmt.format(buf.writer(), "{}", .{fmt}); //std.log.warn("{s}", .{buf.items});