summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMitchell Hashimoto <mitchell.hashimoto@gmail.com>2024-01-20 16:37:48 -0800
committerGitHub <noreply@github.com>2024-01-20 16:37:48 -0800
commit8d95f514cc93cd07f94eef8b87bdecfdbc5e559d (patch)
tree78fbed5669d2234e1296ee85961f9402ba8bac8d /src
parentd32667e46ca849857eaed2b587fa9595d93ce726 (diff)
parent64e3721bb7b160c92aad6090abdd70dabd63a4a2 (diff)
Merge pull request #1341 from mitchellh/format-config
config: support encoding back to string
Diffstat (limited to 'src')
-rw-r--r--src/cli/args.zig20
-rw-r--r--src/config.zig1
-rw-r--r--src/config/Config.zig339
-rw-r--r--src/config/formatter.zig338
-rw-r--r--src/font/face/Metrics.zig50
5 files changed, 748 insertions, 0 deletions
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" {
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/Config.zig b/src/config/Config.zig
index e323d5ce9..86a046889 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;
@@ -2178,6 +2179,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 {
@@ -2218,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
@@ -2251,6 +2275,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;
@@ -2267,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
@@ -2314,6 +2363,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);
@@ -2328,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.
@@ -2355,6 +2458,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 +2550,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);
@@ -2483,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.
@@ -2561,6 +2704,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);
@@ -2571,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.
@@ -2618,6 +2799,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)
@@ -2751,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) {
@@ -2792,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);
@@ -2808,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.
@@ -2836,6 +3168,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
new file mode 100644
index 000000000..62a395825
--- /dev/null
+++ b/src/config/formatter.zig
@@ -0,0 +1,338 @@
+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);
+ }
+}
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.