mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
cli: config structure supports tagged unions
The syntax of tagged unions is `tag:value`. This matches the tagged union parsing syntax for keybindings (i.e. `new_split:right`). I'm adding this now on its own without a user-facing feature because I can see some places we might use this and I want to separate this out. There is already a PR open now that can utilize this (#2231).
This commit is contained in:
147
src/cli/args.zig
147
src/cli/args.zig
@ -268,7 +268,13 @@ fn parseIntoField(
|
||||
value orelse return error.ValueRequired,
|
||||
),
|
||||
|
||||
else => unreachable,
|
||||
.Union => try parseTaggedUnion(
|
||||
Field,
|
||||
alloc,
|
||||
value orelse return error.ValueRequired,
|
||||
),
|
||||
|
||||
else => @compileError("unsupported field type"),
|
||||
},
|
||||
};
|
||||
|
||||
@ -279,6 +285,52 @@ fn parseIntoField(
|
||||
return error.InvalidField;
|
||||
}
|
||||
|
||||
fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
||||
const info = @typeInfo(T).Union;
|
||||
assert(@typeInfo(info.tag_type.?) == .Enum);
|
||||
|
||||
// Get the union tag that is being set. We support values with no colon
|
||||
// if the value is void so its not an error to have no colon.
|
||||
const colon_idx = mem.indexOf(u8, v, ":") orelse v.len;
|
||||
const tag_str = std.mem.trim(u8, v[0..colon_idx], whitespace);
|
||||
const value = if (colon_idx < v.len) v[colon_idx + 1 ..] else "";
|
||||
|
||||
// Find the field in the union that matches the tag.
|
||||
inline for (info.fields) |field| {
|
||||
if (mem.eql(u8, field.name, tag_str)) {
|
||||
// Special case void types where we don't need a value.
|
||||
if (field.type == void) {
|
||||
if (value.len > 0) return error.InvalidValue;
|
||||
return @unionInit(T, field.name, {});
|
||||
}
|
||||
|
||||
// We need to create a struct that looks like this union field.
|
||||
// This lets us use parseIntoField as if its a dedicated struct.
|
||||
const Target = @Type(.{ .Struct = .{
|
||||
.layout = .auto,
|
||||
.fields = &.{.{
|
||||
.name = field.name,
|
||||
.type = field.type,
|
||||
.default_value = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(field.type),
|
||||
}},
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
|
||||
// Parse the value into the struct
|
||||
var t: Target = undefined;
|
||||
try parseIntoField(Target, alloc, &t, field.name, value);
|
||||
|
||||
// Build our union
|
||||
return @unionInit(T, field.name, @field(t, field.name));
|
||||
}
|
||||
}
|
||||
|
||||
return error.InvalidValue;
|
||||
}
|
||||
|
||||
fn parsePackedStruct(comptime T: type, v: []const u8) !T {
|
||||
const info = @typeInfo(T).Struct;
|
||||
assert(info.layout == .@"packed");
|
||||
@ -742,6 +794,99 @@ test "parseIntoField: struct with parse func with unsupported error tracking" {
|
||||
);
|
||||
}
|
||||
|
||||
test "parseIntoField: tagged union" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
value: union(enum) {
|
||||
a: u8,
|
||||
b: u8,
|
||||
c: void,
|
||||
d: []const u8,
|
||||
} = undefined,
|
||||
} = .{};
|
||||
|
||||
// Set one field
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:1");
|
||||
try testing.expectEqual(1, data.value.a);
|
||||
|
||||
// Set another
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "value", "b:2");
|
||||
try testing.expectEqual(2, data.value.b);
|
||||
|
||||
// Set void field
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "value", "c");
|
||||
try testing.expectEqual({}, data.value.c);
|
||||
|
||||
// Set string field
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "value", "d:hello");
|
||||
try testing.expectEqualStrings("hello", data.value.d);
|
||||
}
|
||||
|
||||
test "parseIntoField: tagged union unknown filed" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
value: union(enum) {
|
||||
a: u8,
|
||||
b: u8,
|
||||
} = undefined,
|
||||
} = .{};
|
||||
|
||||
try testing.expectError(
|
||||
error.InvalidValue,
|
||||
parseIntoField(@TypeOf(data), alloc, &data, "value", "c:1"),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseIntoField: tagged union invalid field value" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
value: union(enum) {
|
||||
a: u8,
|
||||
b: u8,
|
||||
} = undefined,
|
||||
} = .{};
|
||||
|
||||
try testing.expectError(
|
||||
error.InvalidValue,
|
||||
parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseIntoField: tagged union missing tag" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
value: union(enum) {
|
||||
a: u8,
|
||||
b: u8,
|
||||
} = undefined,
|
||||
} = .{};
|
||||
|
||||
try testing.expectError(
|
||||
error.InvalidValue,
|
||||
parseIntoField(@TypeOf(data), alloc, &data, "value", "a"),
|
||||
);
|
||||
try testing.expectError(
|
||||
error.InvalidValue,
|
||||
parseIntoField(@TypeOf(data), alloc, &data, "value", ":a"),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns an iterator (implements "next") that reads CLI args by line.
|
||||
/// Each CLI arg is expected to be a single line. This is used to implement
|
||||
/// configuration files.
|
||||
|
Reference in New Issue
Block a user