config: overhaul help string generation

This commit is contained in:
Leah Amelia Chen
2025-01-12 12:44:10 +01:00
parent caddf59db5
commit 51a96e0d2a
9 changed files with 976 additions and 857 deletions

View File

@ -8,6 +8,7 @@ const builtin = @import("builtin");
const apprt = @import("../apprt.zig");
const font = @import("../font/main.zig");
const renderer = @import("../renderer.zig");
const configpkg = @import("../config.zig");
const Command = @import("../Command.zig");
const WasmTarget = @import("../os/wasm/target.zig").Target;
@ -396,7 +397,7 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
.{self.version},
));
step.addOption(
ReleaseChannel,
configpkg.Config.ReleaseChannel,
"release_channel",
channel: {
const pre = self.version.pre orelse break :channel .stable;
@ -492,12 +493,3 @@ pub const ExeEntrypoint = enum {
bench_grapheme_break,
bench_page_init,
};
/// The release channel for the build.
pub const ReleaseChannel = enum {
/// Unstable builds on every commit.
tip,
/// Stable tagged releases.
stable,
};

View File

@ -34,6 +34,7 @@ pub fn genConfig(writer: anytype, cli: bool) !void {
if (cli) try writer.writeAll("--");
try writer.writeAll(field.name);
try writer.writeAll("`**\n\n");
if (@hasDecl(help_strings.Config, field.name)) {
var iter = std.mem.splitScalar(u8, @field(help_strings.Config, field.name), '\n');
var first = true;

View File

@ -1,6 +1,7 @@
const std = @import("std");
const Config = @import("../../config/Config.zig");
const help_strings = @import("help_strings");
const formatter = @import("../../config/formatter.zig");
pub fn main() !void {
const output = std.io.getStdOut().writer();
@ -77,10 +78,16 @@ pub fn genConfig(writer: anytype) !void {
callout_note,
callout_warning,
} = null;
var block_indent: usize = 0;
while (iter.next()) |line| {
var indent: usize = 0;
while (indent < line.len and line[indent] == ' ') : (indent += 1) {}
const s = line[indent..];
while (iter.next()) |s| {
// Empty line resets our block
if (std.mem.eql(u8, s, "")) {
try writer.writeByteNTimes(' ', block_indent);
try endBlock(writer, block);
block = null;
@ -90,30 +97,35 @@ pub fn genConfig(writer: anytype) !void {
// If we don't have a block figure out our type.
const first: bool = block == null;
if (block == null) {
if (std.mem.startsWith(u8, s, " ")) {
if (block == null) block: {
if (indent == 4) {
block = .code;
try writer.writeAll("```\n");
} else if (std.ascii.startsWithIgnoreCase(s, "note:")) {
break :block;
}
try writer.writeByteNTimes(' ', indent);
block_indent = indent;
if (std.ascii.startsWithIgnoreCase(s, "note:")) {
block = .callout_note;
try writer.writeAll("<Note>\n");
try writer.writeByteNTimes(' ', indent);
} else if (std.ascii.startsWithIgnoreCase(s, "warning:")) {
block = .callout_warning;
try writer.writeAll("<Warning>\n");
try writer.writeByteNTimes(' ', indent);
} else {
block = .text;
}
} else if (block != .code) {
try writer.writeByteNTimes(' ', indent);
}
try writer.writeAll(switch (block.?) {
.text => s,
.callout_note => if (first) s["note:".len..] else s,
.callout_warning => if (first) s["warning:".len..] else s,
.code => if (std.mem.startsWith(u8, s, " "))
s[4..]
else
s,
.text, .code => s,
.callout_note => std.mem.trim(u8, if (first) s["note:".len..] else s, " "),
.callout_warning => std.mem.trim(u8, if (first) s["warning:".len..] else s, " "),
});
try writer.writeAll("\n");
}

View File

@ -11,15 +11,14 @@ const font = @import("font/main.zig");
const rendererpkg = @import("renderer.zig");
const WasmTarget = @import("os/wasm/target.zig").Target;
const BuildConfig = @import("build/Config.zig");
pub const ReleaseChannel = BuildConfig.ReleaseChannel;
const Config = @import("config/Config.zig");
/// The semantic version of this build.
pub const version = options.app_version;
pub const version_string = options.app_version_string;
/// The release channel for this build.
pub const release_channel = std.meta.stringToEnum(ReleaseChannel, @tagName(options.release_channel)).?;
pub const release_channel = std.meta.stringToEnum(Config.ReleaseChannel, @tagName(options.release_channel)).?;
/// The optimization mode as a string.
pub const mode_string = mode: {

View File

@ -121,12 +121,12 @@ pub const Action = enum {
// for all commands by just changing this one place.
if (std.mem.eql(u8, field.name, @tagName(self))) {
const stdout = std.io.getStdOut().writer();
const text = @field(help_strings.Action, field.name) ++ "\n";
stdout.writeAll(text) catch |write_err| {
std.log.warn("failed to write help text: {}\n", .{write_err});
break :err 1;
};
// const stdout = std.io.getStdOut().writer();
// const text = @field(help_strings.Action, field.name) ++ "\n";
// stdout.writeAll(text) catch |write_err| {
// std.log.warn("failed to write help text: {}\n", .{write_err});
// break :err 1;
// };
break :err 0;
}

View File

@ -41,10 +41,10 @@ pub fn run(alloc: Allocator) !u8 {
try stdout.print("{s}", .{field.name});
if (opts.docs) {
try stdout.print(":\n", .{});
var iter = std.mem.splitScalar(u8, std.mem.trimRight(u8, @field(help_strings.KeybindAction, field.name), &std.ascii.whitespace), '\n');
while (iter.next()) |line| {
try stdout.print(" {s}\n", .{line});
}
// var iter = std.mem.splitScalar(u8, std.mem.trimRight(u8, @field(help_strings.KeybindAction, field.name), &std.ascii.whitespace), '\n');
// while (iter.next()) |line| {
// try stdout.print(" {s}\n", .{line});
// }
} else {
try stdout.print("\n", .{});
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
const Metrics = @This();
const std = @import("std");
const Config = @import("../config/Config.zig");
/// Recommended cell width and height for a monospace grid using this font.
cell_width: u32,
@ -257,130 +258,7 @@ fn clamp(self: *Metrics) void {
/// little space as possible.
pub const ModifierSet = std.AutoHashMapUnmanaged(Key, Modifier);
/// A modifier to apply to a metrics value. The modifier value represents
/// a delta, so percent is a percentage to change, not a percentage of.
/// For example, "20%" is 20% larger, not 20% of the value. Likewise,
/// an absolute value of "20" is 20 larger, not literally 20.
pub const Modifier = union(enum) {
percent: f64,
absolute: i32,
/// Parses the modifier value. If the value ends in "%" it is assumed
/// to be a percent, otherwise the value is parsed as an integer.
pub fn parse(input: []const u8) !Modifier {
if (input.len == 0) return error.InvalidFormat;
if (input[input.len - 1] == '%') {
var percent = std.fmt.parseFloat(
f64,
input[0 .. input.len - 1],
) catch return error.InvalidFormat;
percent /= 100;
if (percent <= -1) return .{ .percent = 0 };
if (percent < 0) return .{ .percent = 1 + percent };
return .{ .percent = 1 + percent };
}
return .{
.absolute = std.fmt.parseInt(i32, input, 10) catch
return error.InvalidFormat,
};
}
/// So it works with the config framework.
pub fn parseCLI(input: ?[]const u8) !Modifier {
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: anytype) @TypeOf(v) {
const T = @TypeOf(v);
const signed = @typeInfo(T).Int.signedness == .signed;
return switch (self) {
.percent => |p| percent: {
const p_clamped: f64 = @max(0, p);
const v_f64: f64 = @floatFromInt(v);
const applied_f64: f64 = @round(v_f64 * p_clamped);
const applied_T: T = @intFromFloat(applied_f64);
break :percent applied_T;
},
.absolute => |abs| absolute: {
const v_i64: i64 = @intCast(v);
const abs_i64: i64 = @intCast(abs);
const applied_i64: i64 = v_i64 +| abs_i64;
const clamped_i64: i64 = if (signed) applied_i64 else @max(0, applied_i64);
const applied_T: T = std.math.cast(T, clamped_i64) orelse
std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64)));
break :absolute applied_T;
},
};
}
/// Hash using the hasher.
pub fn hash(self: Modifier, hasher: anytype) void {
const autoHash = std.hash.autoHash;
autoHash(hasher, std.meta.activeTag(self));
switch (self) {
// floats can't be hashed directly so we bitcast to i64.
// for the purpose of what we're trying to do this seems
// good enough but I would prefer value hashing.
.percent => |v| autoHash(hasher, @as(i64, @bitCast(v))),
.absolute => |v| autoHash(hasher, v),
}
}
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);
}
};
pub const Modifier = Config.MetricModifier;
/// Key is an enum of all the available metrics keys.
pub const Key = key: {

View File

@ -27,46 +27,15 @@ fn genConfig(alloc: std.mem.Allocator, writer: anytype) !void {
var ast = try std.zig.Ast.parse(alloc, @embedFile("config/Config.zig"), .zig);
defer ast.deinit(alloc);
try writer.writeAll(
\\/// Configuration help
\\pub const Config = struct {
\\
\\
try genStringsStruct(
alloc,
writer,
ast,
"Config",
0,
ast.rootDecls(),
0,
);
inline for (@typeInfo(Config).Struct.fields) |field| {
if (field.name[0] == '_') continue;
try genConfigField(alloc, writer, ast, field.name);
}
try writer.writeAll("};\n");
}
fn genConfigField(
alloc: std.mem.Allocator,
writer: anytype,
ast: std.zig.Ast,
comptime field: []const u8,
) !void {
const tokens = ast.tokens.items(.tag);
for (tokens, 0..) |token, i| {
// We only care about identifiers that are preceded by doc comments.
if (token != .identifier) continue;
if (tokens[i - 1] != .doc_comment) continue;
// Identifier may have @"" so we strip that.
const name = ast.tokenSlice(@intCast(i));
const key = if (name[0] == '@') name[2 .. name.len - 1] else name;
if (!std.mem.eql(u8, key, field)) continue;
const comment = try extractDocComments(alloc, ast, @intCast(i - 1), tokens);
try writer.writeAll("pub const ");
try writer.writeAll(name);
try writer.writeAll(": [:0]const u8 = \n");
try writer.writeAll(comment);
try writer.writeAll("\n");
break;
}
}
fn genActions(alloc: std.mem.Allocator, writer: anytype) !void {
@ -86,6 +55,7 @@ fn genActions(alloc: std.mem.Allocator, writer: anytype) !void {
var ast = try std.zig.Ast.parse(alloc, @embedFile(action_file), .zig);
defer ast.deinit(alloc);
const tokens: []std.zig.Token.Tag = ast.tokens.items(.tag);
for (tokens, 0..) |token, i| {
@ -102,12 +72,18 @@ fn genActions(alloc: std.mem.Allocator, writer: anytype) !void {
std.process.exit(1);
}
const comment = try extractDocComments(alloc, ast, @intCast(i - 2), tokens);
const comment = try extractDocComments(
alloc,
ast,
@intCast(i - 2),
0,
) orelse continue;
try writer.writeAll("pub const @\"");
try writer.writeAll(field.name);
try writer.writeAll("\" = \n");
try writer.writeAll(comment);
try writer.writeAll("\n\n");
try writer.writeAll(";\n\n");
break;
}
}
@ -120,26 +96,337 @@ fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void {
defer ast.deinit(alloc);
try writer.writeAll(
\\/// keybind actions help
\\pub const KeybindAction = struct {
\\
\\};
);
// for (ast.rootDecls()) |decl| {
// if (ast.fullVarDecl(decl)) |var_decl| {
// var buf: [2]std.zig.Ast.TokenIndex = undefined;
// const decl_container = ast.fullContainerDecl(&buf, var_decl.ast.init_node) orelse continue;
// const name = ast.tokenSlice(var_decl.ast.mut_token + 1);
// if (!std.mem.eql(u8, name, "Action")) continue;
// _ = decl_container;
// _ = writer;
// try genStringsStruct(
// alloc,
// writer,
// ast,
// "KeybindAction",
// decl_container.ast.members,
// var_decl.firstToken() - 1,
// );
// return;
// }
// }
}
fn genStringsStruct(
alloc: std.mem.Allocator,
writer: anytype,
ast: std.zig.Ast,
name: []const u8,
main_token: std.zig.Ast.TokenIndex,
members: []const std.zig.Ast.Node.Index,
doc_comment_token: ?std.zig.Ast.TokenIndex,
) !void {
var fields: std.ArrayListUnmanaged(std.zig.Ast.full.ContainerField) = .{};
defer fields.deinit(alloc);
var decls: std.ArrayListUnmanaged(std.zig.Ast.full.VarDecl) = .{};
defer decls.deinit(alloc);
try writer.print("pub const {s} = struct {{\n", .{name});
for (members) |member| {
if (ast.fullContainerField(member)) |field| {
try fields.append(alloc, field);
} else if (ast.fullVarDecl(member)) |var_decl| {
// Is it defining a subtype?
try decls.append(alloc, var_decl);
}
}
for (fields.items) |field| {
try genConfigField(alloc, writer, ast, field, decls.items);
}
for (decls.items) |var_decl| {
var buf: [2]std.zig.Ast.TokenIndex = undefined;
const decl_container = ast.fullContainerDecl(&buf, var_decl.ast.init_node) orelse continue;
try genStringsStruct(
alloc,
writer,
ast,
ast.tokenSlice(var_decl.ast.mut_token + 1),
decl_container.ast.main_token,
decl_container.ast.members,
var_decl.firstToken() - 1,
);
}
try writer.writeAll(
\\pub const @"DOC-COMMENT": []const u8 =
\\
);
inline for (@typeInfo(KeybindAction).Union.fields) |field| {
if (field.name[0] == '_') continue;
try genConfigField(alloc, writer, ast, field.name);
if (doc_comment_token) |token| {
if (try extractDocComments(
alloc,
ast,
@intCast(token),
0,
)) |comment| {
try writer.writeAll(comment);
}
}
try writer.writeAll("};\n");
try genValidValues(
alloc,
writer,
ast,
main_token,
members,
);
try writer.writeAll(
\\ \\
\\;
\\};
);
}
fn genValidValues(
alloc: std.mem.Allocator,
writer: anytype,
ast: std.zig.Ast,
main_token: std.zig.Ast.TokenIndex,
members: []const std.zig.Ast.Node.Index,
) !void {
const token_tags = ast.tokens.items(.tag);
const ContainerType = enum {
@"enum",
@"union",
bitfield,
};
const container_type: ContainerType = switch (token_tags[main_token]) {
.keyword_enum => .@"enum",
.keyword_union => .@"union",
.keyword_struct => switch (token_tags[main_token - 1]) {
.keyword_packed => .bitfield,
else => return,
},
else => return,
};
try writer.writeAll(
\\\\
\\\\Valid values:
\\\\
\\
);
for (members) |member| {
const field = ast.fullContainerField(member) orelse continue;
var field_name = ast.tokenSlice(field.ast.main_token);
if (std.mem.startsWith(u8, field_name, "@\"")) {
field_name = field_name[2..][0 .. field_name.len - 3];
}
try writer.writeAll(
\\\\ -
);
switch (container_type) {
.@"enum" => try writer.print(" `{s}`\n", .{field_name}),
.@"union" => {
const field_type = ast.getNodeSource(field.ast.type_expr);
// Only generate the field name if the field is "enum-variant-like":
// type is void or nonexistent.
if (field.ast.main_token == ast.firstToken(field.ast.type_expr) or
std.mem.eql(u8, field_type, "void"))
{
try writer.print(" `{s}`\n", .{field_name});
}
},
.bitfield => {
const default_value = ast.tokenSlice(ast.firstToken(field.ast.value_expr));
const is_default = std.mem.eql(u8, default_value, "true");
if (is_default) {
try writer.print(" [x] `{s}` (Enabled by default)\n", .{field_name});
} else {
try writer.print(" [ ] `{s}`\n", .{field_name});
}
},
}
try writer.writeAll(
\\\\
\\
);
if (try extractDocComments(
alloc,
ast,
field.firstToken() - 1,
3, // 4 indents would be an indented code block
)) |comment| {
try writer.writeAll(comment);
try writer.writeAll(
\\\\
\\
);
}
}
}
fn genConfigField(
alloc: std.mem.Allocator,
writer: anytype,
ast: std.zig.Ast,
field: std.zig.Ast.full.ContainerField,
decls: []std.zig.Ast.full.VarDecl,
) !void {
const name = ast.tokenSlice(field.ast.main_token);
if (name[0] == '_') return;
// Escape special identifiers that are valid as enum variants but not as field names
const special_identifiers = &[_][]const u8{ "true", "false", "null" };
const is_special = for (special_identifiers) |special| {
if (std.mem.eql(u8, name, special)) break true;
} else false;
const comment = try extractDocComments(
alloc,
ast,
field.firstToken() - 1,
0,
) orelse return;
try writer.writeAll("pub const ");
if (is_special) try writer.writeAll("@\"");
try writer.writeAll(name);
if (is_special) try writer.writeAll("\"");
try writer.writeAll(": [:0]const u8 = \n");
try writer.writeAll(comment);
const type_name = ast.tokenSlice(ast.lastToken(field.ast.type_expr));
for (decls) |decl| {
if (std.mem.eql(u8, type_name, ast.tokenSlice(decl.ast.mut_token + 1))) {
try writer.writeAll(
\\\\
\\\\
\\++
\\
);
try writer.writeAll(type_name);
try writer.writeAll(
\\.@"DOC-COMMENT"
\\
);
}
}
try genDefaultValue(writer, ast, field);
try writer.writeAll(";\n");
}
fn genDefaultValue(
writer: anytype,
ast: std.zig.Ast,
field: std.zig.Ast.full.ContainerField,
) !void {
const value = ast.nodes.get(field.ast.value_expr);
switch (value.tag) {
.number_literal, .string_literal => {
try writer.writeAll(
\\++
\\\\
\\\\Defaults to `{s}`.
, ast.getNodeSource(field.ast.value_expr));
},
.identifier, .number_literal, .string_literal, .enum_literal => {
try writer.writeAll(
\\++
\\\\
\\\\
\\\\
);
const default = switch (value.tag) {
.enum_literal, .identifier => id: {
// Escape @"blah"
const slice = ast.tokenSlice(value.main_token);
break :id if (std.mem.startsWith(u8, slice, "@\""))
slice[2..][0 .. slice.len - 3]
else
slice;
},
.number_literal => ast.tokenSlice(value.main_token),
// We really don't know. Guess.
else => ast.getNodeSource(field.ast.value_expr),
};
// var default = ast.getNodeSource(field.ast.value_expr);
// if (default[0] == '.') {
// default = default[1..];
// }
if (std.mem.eql(u8, default, "null")) {
try writer.writeAll("Unset by default.\n");
return;
}
const default_type_node = ast.nodes.get(field.ast.type_expr);
// ?bool is still semantically boolean
const default_type = if (default_type_node.tag == .optional_type)
ast.getNodeSource(default_type_node.data.lhs)
else
ast.getNodeSource(field.ast.type_expr);
// There are some enums/tagged unions with variants called `true`
// or `false`, and it's not accurate to call them enabled or
// disabled in some circumstances.
// Thus we only consider booleans here.
if (std.mem.eql(u8, default_type, "bool")) {
if (std.mem.eql(u8, default, "true")) {
try writer.writeAll("Enabled by default.\n");
} else if (std.mem.eql(u8, default, "false")) {
try writer.writeAll("Disabled by default.\n");
}
return;
}
try writer.print("Defaults to `{s}`.\n", .{default});
},
else => {},
}
}
fn extractDocComments(
alloc: std.mem.Allocator,
ast: std.zig.Ast,
index: std.zig.Ast.TokenIndex,
tokens: []std.zig.Token.Tag,
) ![]const u8 {
comptime indent: usize,
) !?[]const u8 {
if (index == 0) return null;
const tokens = ast.tokens.items(.tag);
// Find the first index of the doc comments. The doc comments are
// always stacked on top of each other so we can just go backwards.
const start_idx: usize = start_idx: for (0..index) |i| {
@ -161,14 +448,15 @@ fn extractDocComments(
var buffer = std.ArrayList(u8).init(alloc);
const writer = buffer.writer();
const prefix = findCommonPrefix(lines);
if (lines.items.len == 0) return null;
for (lines.items) |line| {
try writer.writeAll(" \\\\");
try writer.writeAll(" \\\\" ++ " " ** indent);
try writer.writeAll(line[@min(prefix, line.len)..]);
try writer.writeAll("\n");
}
try writer.writeAll(";\n");
return buffer.toOwnedSlice();
return try buffer.toOwnedSlice();
}
fn findCommonPrefix(lines: std.ArrayList([]const u8)) usize {