NOCLIP/source/help.zig
torque 4695374f27
help: actually bother to clean up memory
Even though the goal is for this to be run with an arena allocator,
nothing is currently enforcing that, so we should try to keep tidy.
This still leaks memory like crazy without an arena allocator, though.
2023-04-06 18:31:29 -07:00

505 lines
17 KiB
Zig

const std = @import("std");
const NoclipError = @import("./errors.zig").NoclipError;
const ncmeta = @import("./meta.zig");
const FixedCount = @import("./parameters.zig").FixedCount;
const parser = @import("./parser.zig");
const AlignablePair = struct {
left: []const u8,
right: []const u8,
};
const OptionDescription = struct {
pairs: []AlignablePair,
just: usize,
};
pub fn StructuredPrinter(comptime Writer: type) type {
return struct {
wrap_width: usize = 100,
writer: Writer,
pub fn print_pair(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
try self.writer.writeByteNTimes(' ', leading_indent);
const left = std.mem.trim(u8, pair.left, " \n");
try self.writer.writeAll(left);
const offset: usize = leading_indent + left.len;
// TODO: lol return a real error
if (offset > tabstop) return NoclipError.UnexpectedFailure;
try self.writer.writeByteNTimes(' ', tabstop - offset);
try self.print_rewrap(std.mem.trim(u8, pair.right, " \n"), tabstop);
try self.writer.writeByte('\n');
}
pub fn print_pair_brief(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
const brief = ncmeta.partition(u8, pair.right, &[_][]const u8{"\n\n"})[0];
const simulacrum: AlignablePair = .{
.left = pair.left,
.right = brief,
};
try self.print_pair(simulacrum, leading_indent, tabstop);
}
pub fn print_wrapped(self: *@This(), text: []const u8, leading_indent: usize) !void {
try self.writer.writeByteNTimes(' ', leading_indent);
try self.print_rewrap(std.mem.trim(u8, text, "\n"), leading_indent);
}
fn print_rewrap(self: *@This(), text: []const u8, indent: usize) !void {
// TODO: lol return a real error
if (indent >= self.wrap_width) return NoclipError.UnexpectedFailure;
// this assumes output stream has already had the first line properly
// indented.
var splitter = std.mem.split(u8, text, "\n");
var location: usize = indent;
while (splitter.next()) |line| {
if (line.len == 0) {
// we have a trailing line that needs to be cleaned up
if (location > indent)
_ = try self.clear_line(indent);
location = try self.clear_line(indent);
continue;
}
var choppee = line;
var need_forced_break = false;
choppa: while (choppee.len > 0) {
const breakoff = self.wrap_width - location;
if (breakoff >= choppee.len) {
if (location > indent)
try self.writer.writeByte(' ');
try self.writer.writeAll(choppee);
location += choppee.len;
break;
}
var split = breakoff;
while (choppee[split] != ' ') : (split -= 1) {
if (split == 0) {
// we have encountered a word that is too long to break,
// so force breaking it
if (need_forced_break) {
split = breakoff;
break;
}
if (location != indent)
location = try self.clear_line(indent);
need_forced_break = true;
continue :choppa;
}
}
if (location > indent)
try self.writer.writeByte(' ');
try self.writer.writeAll(choppee[0..split]);
location = try self.clear_line(indent);
choppee = choppee[split + 1 ..];
}
}
}
fn clear_line(self: *@This(), indent: usize) !usize {
try self.writer.writeByte('\n');
try self.writer.writeByteNTimes(' ', indent);
return indent;
}
};
}
pub fn HelpBuilder(comptime command: anytype) type {
const help_info = opt_info(command.generate());
return struct {
writebuffer: std.ArrayList(u8),
pub fn init(allocator: std.mem.Allocator) @This() {
return @This(){
.writebuffer = std.ArrayList(u8).init(allocator),
};
}
pub fn build_message(
self: *@This(),
name: []const u8,
subcommands: parser.CommandMap,
) ![]const u8 {
const writer = self.writebuffer.writer();
try writer.print(
"Usage: {s}{s}{s}{s}\n\n",
.{
name,
self.option_brief(),
try self.args_brief(),
self.subcommands_brief(subcommands),
},
);
var printer = StructuredPrinter(@TypeOf(writer)){ .writer = writer };
try printer.print_wrapped(command.description, 2);
try writer.writeAll("\n\n");
const arguments = try self.describe_arguments();
const options = try self.describe_options();
const env_vars = try self.describe_env();
const subcs = try self.describe_subcommands(subcommands);
const max_just = @max(arguments.just, @max(options.just, @max(env_vars.just, subcs.just)));
if (arguments.pairs.len > 0) {
try writer.writeAll("Arguments:\n");
for (arguments.pairs) |pair|
try printer.print_pair(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
if (options.pairs.len > 0) {
try writer.writeAll("Options:\n");
for (options.pairs) |pair|
try printer.print_pair(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
if (env_vars.pairs.len > 0) {
try writer.writeAll("Environment variables:\n");
for (env_vars.pairs) |pair|
try printer.print_pair(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
if (subcs.pairs.len > 0) {
try writer.writeAll("Subcommands:\n");
for (subcs.pairs) |pair|
try printer.print_pair_brief(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
return self.writebuffer.toOwnedSlice();
}
fn option_brief(_: @This()) []const u8 {
return if (comptime help_info.options.len > 0)
" [options...]"
else
"";
}
fn args_brief(self: @This()) ![]const u8 {
var buf = std.ArrayList(u8).init(self.writebuffer.allocator);
defer buf.deinit();
const writer = buf.writer();
for (comptime help_info.arguments) |arg| {
try writer.writeAll(" ");
if (!arg.required) try writer.writeAll("[");
try writer.writeByte('<');
try writer.writeAll(arg.name);
if (arg.multi)
try writer.print(" [{s} ...]", .{arg.name});
try writer.writeByte('>');
if (!arg.required) try writer.writeAll("]");
}
return buf.toOwnedSlice();
}
fn subcommands_brief(_: @This(), subcommands: parser.CommandMap) []const u8 {
return if (subcommands.count() > 0)
" <subcommand ...>"
else
"";
}
fn describe_arguments(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
for (comptime help_info.arguments) |arg| {
if (arg.description.len == 0) continue;
const pair: AlignablePair = .{
.left = arg.name,
.right = arg.description,
};
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
}
return .{
.pairs = try pairs.toOwnedSlice(),
.just = just,
};
}
fn describe_options(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
for (comptime help_info.options) |opt| {
const pair = try self.describe_option(opt);
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
}
return .{
.pairs = try pairs.toOwnedSlice(),
.just = just,
};
}
fn describe_option(self: @This(), opt: OptHelp) !AlignablePair {
var buffer = std.ArrayList(u8).init(self.writebuffer.allocator);
defer buffer.deinit();
const writer = buffer.writer();
if (comptime opt.short_truthy) |tag| {
if (buffer.items.len > 0) try writer.writeAll(", ");
try writer.writeAll(tag);
}
if (comptime opt.long_truthy) |tag| {
if (buffer.items.len > 0) try writer.writeAll(", ");
try writer.writeAll(tag);
}
var falsy_seen = false;
if (comptime opt.short_falsy) |tag| {
if (buffer.items.len > 0)
try writer.writeAll(" / ")
else
try writer.writeAll("/ ");
try writer.writeAll(tag);
falsy_seen = true;
}
if (comptime opt.long_falsy) |tag| {
if (falsy_seen)
try writer.writeAll(", ")
else if (buffer.items.len > 0)
try writer.writeAll(" / ");
try writer.writeAll(tag);
}
if (opt.value_count > 0) {
try writer.print(" <{s}>", .{opt.type_name});
}
const left = try buffer.toOwnedSlice();
if (comptime opt.required) {
try writer.writeAll("[required]");
}
if (comptime opt.description.len > 0) {
if (buffer.items.len > 0) try writer.writeAll(" ");
try writer.writeAll(opt.description);
}
if (comptime opt.env_var) |env| {
if (buffer.items.len > 0) try writer.writeAll(" ");
try writer.print("(env: {s})", .{env});
}
if (comptime opt.default) |def| {
if (buffer.items.len > 0) try writer.writeAll(" ");
try writer.print("(default: {s})", .{def});
}
const right = try buffer.toOwnedSlice();
return .{ .left = left, .right = right };
}
fn describe_env(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
for (comptime help_info.env_vars) |env| {
if (env.description.len == 0) continue;
const pair: AlignablePair = .{
.left = env.env_var,
.right = env.description,
};
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
}
return .{
.pairs = try pairs.toOwnedSlice(),
.just = just,
};
}
fn describe_subcommands(self: @This(), subcommands: parser.CommandMap) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
var iter = subcommands.keyIterator();
while (iter.next()) |key| {
const pair: AlignablePair = .{
.left = key.*,
.right = subcommands.get(key.*).?.describe(),
};
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
}
return .{
.pairs = try pairs.toOwnedSlice(),
.just = just,
};
}
};
}
const CommandHelp = struct {
options: []const OptHelp,
arguments: []const ArgHelp,
env_vars: []const EnvHelp,
};
const OptHelp = struct {
short_truthy: ?[]const u8 = null,
long_truthy: ?[]const u8 = null,
short_falsy: ?[]const u8 = null,
long_falsy: ?[]const u8 = null,
env_var: ?[]const u8 = null,
description: []const u8 = "",
type_name: []const u8 = "",
extra: []const u8 = "",
default: ?[]const u8 = null,
// this is the pivot
value_count: FixedCount = 0,
required: bool = false,
multi: bool = false,
};
const EnvHelp = struct {
env_var: []const u8 = "",
description: []const u8 = "",
default: ?[]const u8 = null,
};
const ArgHelp = struct {
name: []const u8 = "",
description: []const u8 = "",
type_name: []const u8 = "",
multi: bool = false,
required: bool = true,
};
pub fn opt_info(comptime command: anytype) CommandHelp {
// TODO: this could be runtime and it would be slightly simpler.
comptime {
var options: []const OptHelp = &[_]OptHelp{};
var env_vars: []const EnvHelp = &[_]EnvHelp{};
var arguments: []const ArgHelp = &[_]ArgHelp{};
var last_name: []const u8 = "";
var last_option: OptHelp = .{};
paramloop: for (command) |param| {
const PType = @TypeOf(param);
if (PType.param_type == .Ordinal) {
arguments = arguments ++ &[_]ArgHelp{.{
.name = param.name,
.description = param.description,
.type_name = param.nice_type_name,
.multi = PType.multi,
.required = param.required,
}};
continue :paramloop;
}
if (!std.mem.eql(u8, last_name, param.name)) {
if (last_name.len > 0) {
if (env_only(last_option)) {
env_vars = env_vars ++ &[_]EnvHelp{.{
.env_var = last_option.env_var,
.description = last_option.description,
.default = last_option.default,
}};
} else {
options = options ++ &[_]OptHelp{last_option};
}
}
last_name = param.name;
last_option = .{};
}
if (PType.is_flag) {
switch (param.flag_bias) {
.truthy => {
last_option.short_truthy = param.short_tag;
last_option.long_truthy = param.long_tag;
},
.falsy => {
last_option.short_falsy = param.short_tag;
last_option.long_falsy = param.long_tag;
},
.unbiased => last_option.env_var = param.env_var,
}
} else {
last_option.short_truthy = param.short_tag;
last_option.long_truthy = param.long_tag;
last_option.env_var = param.env_var;
last_option.value_count = PType.value_count.fixed;
}
last_option.type_name = param.nice_type_name;
last_option.description = param.description;
last_option.required = param.required;
last_option.multi = PType.multi;
if (param.default) |def| {
var buf = ncmeta.ComptimeSliceBuffer{};
const writer = buf.writer();
// TODO: this is only acceptable for some types. It behaves poorly on
// enum-based choice types because it prints the whole type name rather
// than just the tag name. Roll our own eventually.
writer.print("{any}", .{def}) catch @compileError("whoah");
last_option.default = buf.buffer;
}
}
if (last_name.len > 0) {
if (env_only(last_option)) {
env_vars = env_vars ++ &[_]EnvHelp{.{
.env_var = last_option.env_var.?,
.description = last_option.description,
.default = last_option.default,
}};
} else {
options = options ++ &[_]OptHelp{last_option};
}
}
return .{
.options = options,
.arguments = arguments,
.env_vars = env_vars,
};
}
}
inline fn env_only(option: OptHelp) bool {
return option.short_truthy == null and
option.long_truthy == null and
option.short_falsy == null and
option.long_falsy == null;
}