Merge pull request #1341 from mitchellh/format-config

config: support encoding back to string
This commit is contained in:
Mitchell Hashimoto
2024-01-20 16:37:48 -08:00
committed by GitHub
5 changed files with 748 additions and 0 deletions

View File

@ -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" {

View File

@ -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");

View File

@ -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.

338
src/config/formatter.zig Normal file
View File

@ -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);
}
}

View File

@ -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.