I am writing a painting program, during development I decided I wanted a command line for running a subset of functions in my program. Stuff like setting the brush size, the color, loading and saving. Kind of like the vim commands.
In order to avoid repeating myself, I built this compile time reflection thing. So far it all works but I would like to know If there is a better way to do this sort of thing.
I have included a toy version of it below.
The code
const std = @import("std");
const State = struct {
color: [4]f32 = .{ 0, 0, 0, 0 },
size: f32 = 1,
origin: [2]f32 = .{ 0, 0 },
};
const NamedColor = enum {
red,
green,
blue,
yellow,
purple,
orange,
fn fcolor(self: @This()) [4]f32 {
return switch (self) {
.red => .{ 1, 0, 0, 1 },
.green => .{ 0, 1, 0, 1 },
.blue => .{ 0, 0, 1, 1 },
.yellow => .{ 1, 1, 0, 1 },
.purple => .{ 1, 0, 1, 1 },
.orange => .{ 1, 1, 0, 1 },
};
}
};
pub const CommandFuncs = struct {
pub fn set_color_rgba(state: *State, r: f32, g: f32, b: f32, a: f32) void {
state.color = .{
@max(0.0, @min(1.0, r)),
@max(0.0, @min(1.0, g)),
@max(0.0, @min(1.0, b)),
@max(0.0, @min(1.0, a)),
};
}
pub fn set_size(state: *State, s: f32) void {
state.size = s;
}
pub fn set_color(state: *State, color: []const u8) void {
const res = std.meta.stringToEnum(NamedColor, color);
if (res) |col| state.color = col.fcolor() else print("! not a color\n");
}
pub fn move(state: *State, x: f32, y: f32) void {
state.origin = .{
x + state.origin[0],
y + state.origin[1],
};
}
};
const Command = struct {
name: []const u8,
func: *const fn (*State, []const []const u8) void,
n_args: usize,
};
const commandcount = @typeInfo(CommandFuncs).@"struct".decls.len;
pub const commands: [commandcount]Command = blk: {
var c: [commandcount]Command = undefined;
for (@typeInfo(CommandFuncs).@"struct".decls, 0..) |decl, i| {
const func = @field(CommandFuncs, decl.name);
const func_type = @TypeOf(func);
const params = @typeInfo(func_type).@"fn".params;
if (params.len == 0) @panic("Command needs at least contoller param");
const Wrapper = struct {
pub fn wrapper_func(state: *State, args: []const []const u8) void {
var func_args: std.meta.ArgsTuple(func_type) = undefined;
func_args[0] = state;
inline for (params[1..], 0..) |p, j| {
if (p.type) |p_type| switch (p_type) {
u32 => func_args[j + 1] = std.fmt.parseInt(u32, args[j], 10) catch {
print("! bad u32\n");
return;
},
f32 => func_args[j + 1] = std.fmt.parseFloat(f32, args[j]) catch {
print("! bad f32\n");
return;
},
[]const u8 => func_args[j + 1] = args[j],
else => @compileError("type not supported\n"),
};
}
@call(.auto, func, func_args);
}
};
c[i] = .{
.name = decl.name,
.func = Wrapper.wrapper_func,
.n_args = params.len - 1,
};
}
break :blk c;
};
pub fn main() !void {
const allocator = std.heap.page_allocator;
var state = State{};
CommandFuncs.set_size(&state, 2);
while (true) {
print(">>> ");
const read_line = try std.io.getStdIn().reader().readUntilDelimiterAlloc(allocator, '\n', 256);
var tokens = std.mem.tokenizeAny(u8, read_line, " ");
if (tokens.next()) |first_token| {
var found = false;
for (commands) |command| {
if (std.mem.eql(u8, first_token, command.name)) {
found = true;
var arg_list = std.ArrayListUnmanaged([]const u8).empty;
while (tokens.next()) |tok| try arg_list.append(allocator, tok);
if (arg_list.items.len == command.n_args) {
command.func(&state, arg_list.items);
print_state(&state);
} else {
print("! wrong number of args\n");
}
break;
}
}
if (!found) print("! command not found\n");
} else print("! no input\n");
}
}
fn print(s: []const u8) void {
std.io.getStdOut().writeAll(s) catch @panic("failed to write to stdout");
}
fn print_state(state: *State) void {
const writer = std.io.getStdOut().writer();
writer.print(".color = {d}\n.size = {d}\n", .{ state.color, state.size }) catch @panic("failed to write to stdout");
}