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; /// 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 => {}, }, .Union => if (@hasDecl(T, "formatEntry")) { try value.formatEntry(entryFormatter(name, writer)); 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. 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, /// 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; // 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; 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) { 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", .{}); } } } }; 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(); // We just make sure this works without errors. We aren't asserting output. 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}); } 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); } }