Use Lua as a config file language (#1010)

## What are the changes about?

Adds zlua for running Lua code as the configuration file.
Missing as of this time:
- ~~Parsing custom binds and labels~~
- ~~Fix memory leaks~~
- ~~Investigate issue with animation colours~~
- ~~Add examples and default config~~

## What existing issue does this resolve?

[#976](https://codeberg.org/fairyglade/ly/issues/976)

## Pre-requisites

- [ ] I have tested & confirmed the changes work locally
- [ ] I have run `zig fmt` throughout my changes

Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1010
Reviewed-by: AnErrupTion <anerruption+codeberg@disroot.org>
This commit is contained in:
Titanium Brain
2026-08-24 15:35:01 +02:00
committed by AnErrupTion
parent 882815343f
commit b9742203ee
12 changed files with 833 additions and 39 deletions

View File

@@ -21,6 +21,13 @@ pub fn build(b: *std.Build) void {
const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize });
mod.addImport("zigini", zigini.module("zigini"));
const zlua = b.dependency("zlua", .{
.target = target,
.optimize = optimize,
.lang = .luajit,
});
mod.addImport("zlua", zlua.module("zlua"));
const translate_c = b.dependency("translate_c", .{
.target = target,
});

View File

@@ -12,6 +12,10 @@
.url = "git+https://codeberg.org/ziglang/translate-c?ref=zig-0.16.x#6fe0ffc4549f15c5f2d9432c2b4460ba90ff85ac",
.hash = "translate_c-1.0.0-Q_BUWo_5BgD4flHdUhA31zOz0XvZk9k7lQv1ouzyNXj2",
},
.zlua = .{
.url = "git+https://github.com/natecraddock/ziglua?ref=zig-0.16#8f271c82baa5fc43aa02a72f6da020c2025d9436",
.hash = "zlua-0.1.0-hGRpC2aABQD4D9PBVH3wAW8k32-I4969MRQ0CpOwoley",
},
},
.paths = .{
"build.zig",

25
ly-core/src/custom.zig Normal file
View File

@@ -0,0 +1,25 @@
const std = @import("std");
const custom = @This();
pub const CustomCommandBind = struct {
name: []const u8 = "",
cmd: []const u8 = "",
};
pub const UNDEFINED_CMD: []const u8 = "echo \"You forgot to define 'cmd'!\"";
pub const CustomCommandInfo = struct {
name: []const u8 = "",
cmd: ?[]const u8 = null,
/// To be set to the label's widget ID
id: u64 = 0,
/// In frames, the refresh rate for the `cmd` to run again
/// If 0, only run once.
refresh: u32 = 0,
counter: u32 = 0,
};
pub var binds: std.array_hash_map.String(CustomCommandBind) = undefined;
pub var labels: std.array_hash_map.String(CustomCommandInfo) = undefined;

View File

@@ -1,23 +1,57 @@
const std = @import("std");
pub const ini = @import("zigini");
pub const zlua = @import("zlua");
pub const Lua = zlua.Lua;
pub const interop = @import("interop.zig");
pub const UidRange = @import("UidRange.zig");
pub const LogFile = @import("LogFile.zig");
pub const SharedError = @import("SharedError.zig");
pub const custom = @import("custom.zig");
pub fn Parser(comptime T: type) type {
return union(enum) {
ini: IniParser(T),
lua: LuaParser(T),
pub fn errors(self: *const @This()) std.ArrayList(Error) {
return switch (self.*) {
inline else => |p| p.errors,
};
}
pub fn maybe_load_error(self: *const @This()) ?anyerror {
return switch (self.*) {
inline else => |p| p.maybe_load_error,
};
}
pub fn structure(self: *const @This()) T {
return switch (self.*) {
inline else => |p| p.structure,
};
}
pub fn deinit(self: *@This()) void {
switch (self.*) {
inline else => |*p| p.deinit(),
}
}
};
}
pub const Error = struct {
type_name: []const u8,
key: []const u8,
value: []const u8,
error_name: []const u8,
};
pub fn IniParser(comptime Struct: type) type {
return struct {
const Self = @This();
const temporary_allocator = std.heap.page_allocator;
pub const Error = struct {
type_name: []const u8,
key: []const u8,
value: []const u8,
error_name: []const u8,
};
pub var global_errors: std.ArrayList(Error) = .empty;
ini_struct: ini.Ini(Struct),
@@ -76,3 +110,283 @@ pub fn IniParser(comptime Struct: type) type {
}
};
}
pub fn LuaParser(comptime Struct: type) type {
return struct {
const Self = @This();
const temporary_allocator = std.heap.page_allocator;
pub var global_errors: std.ArrayList(Error) = .empty;
structure: Struct,
errors: std.ArrayList(Error),
maybe_load_error: ?anyerror,
allocator: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
pub fn init(
allocator: std.mem.Allocator,
path: []const u8,
) !Self {
var arena = std.heap.ArenaAllocator.init(allocator);
const arena_alloc = arena.allocator();
var maybe_load_error: ?anyerror = null;
errdefer |err| maybe_load_error = err;
const data = parseLua(arena_alloc, path) catch load_error: {
break :load_error Struct{};
};
if (global_errors.items.len != 0) {
maybe_load_error = error.InvalidConfig;
}
return .{
.structure = data,
.errors = global_errors,
.maybe_load_error = maybe_load_error,
.allocator = allocator,
.arena = arena,
};
}
fn parseLua(
allocator: std.mem.Allocator,
path: []const u8,
) !Struct {
var lua: *Lua = try .init(allocator);
defer lua.deinit();
lua.openBase();
lua.openBit();
lua.openMath();
lua.openString();
lua.openTable();
// convert to sentinel terminated slice
const spath: [:0]const u8 = try allocator.dupeSentinel(u8, path, 0);
defer allocator.free(spath);
lua.doFile(spath) catch return error.LuaError;
var data: Struct = .{};
switch (@typeInfo(Struct)) {
.@"struct" => |struc| {
const ly_type = lua.getGlobal("ly");
defer lua.pop(1); // pop ly table
if (ly_type == .nil) return error.MissingLyTable;
inline for (struc.fields) |field| {
try setField(allocator, lua, field, &data);
}
},
else => @compileError("Expected a struct."),
}
// Parse custom binds and labels
try parseCustom(lua);
return data;
}
pub fn setField(allocator: std.mem.Allocator, lua: *Lua, comptime field: std.builtin.Type.StructField, data: *Struct) !void {
const type_info = @typeInfo(field.type);
const actual_type, const is_optional = blk: {
if (type_info == .optional) {
break :blk .{ type_info.optional.child, true };
}
break :blk .{ field.type, false };
};
// push value to top of stack
_ = lua.getField(-1, field.name);
defer lua.pop(1);
// handle null, i.e. undefined fields
if (is_optional and lua.isNil(-1)) {
@field(data, field.name) = null;
return;
}
// handle missing required fields
if (lua.isNil(-1)) {
return error.MissingRequiredField;
}
const actual_type_info = @typeInfo(actual_type);
errdefer |err| {
const value = lua.toString(-1) catch "";
const duped = allocator.dupe(u8, value) catch "";
errorHandler(@typeName(field.type), field.name, duped, err);
}
// dispatch depending on type
if (actual_type_info == .int and is_optional) {
if (lua.isNumber(-1)) {
const value = try lua.toNumber(-1);
@field(data, field.name) = @trunc(value);
} else {
const str = try lua.toString(-1);
var view = try std.unicode.Utf8View.init(str);
var iter = view.iterator();
const codepoint = iter.nextCodepoint();
if (iter.nextCodepoint() != null) return error.ExpectedSingleCharacter;
@field(data, field.name) = if (codepoint) |cp| @intCast(cp) else null;
}
// non null integer
} else if (actual_type_info == .int) {
if (lua.isNumber(-1)) {
const value = try lua.toNumber(-1);
@field(data, field.name) = @trunc(value);
} else {
const str = try lua.toString(-1);
var view = try std.unicode.Utf8View.init(str);
var iter = view.iterator();
const codepoint = iter.nextCodepoint() orelse return error.EmptyString;
if (iter.nextCodepoint() != null) return error.ExpectedSingleCharacter;
@field(data, field.name) = @intCast(codepoint);
}
} else if (actual_type_info == .float) { // all floats
const value = try lua.toNumber(-1);
@field(data, field.name) = @floatCast(value);
} else if (actual_type_info == .bool) {
if (!lua.isBoolean(-1)) return error.ExpectedBoolean;
const value = lua.toBoolean(-1);
@field(data, field.name) = value;
} else if (actual_type == []const u8) {
const value = try lua.toString(-1);
const duped = try allocator.dupe(u8, value);
@field(data, field.name) = duped;
} else if (actual_type == [:0]const u8) {
const value = try lua.toString(-1);
const duped = try allocator.dupeSentinel(u8, value, 0);
@field(data, field.name) = duped;
} else if (actual_type_info == .@"enum") {
const value = try lua.toString(-1);
const variant = std.meta.stringToEnum(actual_type, value) orelse return error.InvalidVariant;
@field(data, field.name) = variant;
} else unreachable;
}
pub fn parseCustom(lua: *Lua) !void {
_ = lua.getGlobal("ly");
defer lua.pop(1); // pop ly table
if (!lua.isTable(-1)) return error.MissingLyTable;
_ = lua.getField(-1, "custom_commands");
// custom_commands can be omitted or empty, so we just skip instead of erroring
if (lua.isTable(-1)) binds: {
const len: usize = @intCast(lua.objectLen(-1));
if (len == 0) break :binds;
for (1..len + 1) |i| {
// push i-th table to stack
lua.pushInteger(@intCast(i));
const ith_table_type = lua.getTable(-2);
defer lua.pop(1); // i-th table in custom_commands
if (ith_table_type != .table) continue;
// skip command if binding isn't set or not a string
const binding_type = lua.getField(-1, "binding");
if (binding_type != .string) continue;
const binding = lua.toString(-1) catch continue;
const bindingZ = temporary_allocator.dupe(u8, binding) catch "";
std.debug.print("{s}", .{bindingZ});
lua.pop(1); // binding value
if (!custom.binds.contains(bindingZ)) {
custom.binds.put(temporary_allocator, bindingZ, .{}) catch {};
}
if (custom.binds.getPtr(bindingZ)) |command| {
// binding name
const name_type = lua.getField(-1, "name");
if (name_type != .string) continue;
const binding_name = lua.toString(-1) catch "";
command.name = temporary_allocator.dupe(u8, binding_name) catch "";
lua.pop(1); // name value
// binding command
const cmd_type = lua.getField(-1, "cmd");
if (cmd_type != .string) continue;
const binding_cmd = lua.toString(-1) catch "";
command.cmd = temporary_allocator.dupe(u8, binding_cmd) catch "";
lua.pop(1); // cmd value
}
}
}
lua.pop(1);
_ = lua.getField(-1, "custom_labels");
// custom_labels can be omitted, so we just skip instead of erroring
if (lua.isTable(-1)) labels: {
const len: usize = @intCast(lua.objectLen(-1));
if (len == 0) break :labels;
for (1..len + 1) |i| {
// push i-th table to stack
lua.pushInteger(@intCast(i));
const ith_table_type = lua.getTable(-2);
defer lua.pop(1); // i-th table in custom_labels
if (ith_table_type != .table) continue;
// skip command if binding isn't set or not a string
const label_type = lua.getField(-1, "label");
if (label_type != .string) continue;
const label = lua.toString(-1) catch continue;
const labelZ = temporary_allocator.dupe(u8, label) catch "";
lua.pop(1); // label value
if (!custom.labels.contains(labelZ)) {
custom.labels.put(temporary_allocator, labelZ, .{ .name = labelZ }) catch {};
}
if (custom.labels.getPtr(labelZ)) |label_ptr| {
// label command
const cmd_type = lua.getField(-1, "cmd");
if (cmd_type != .string) continue;
const label_cmd = lua.toString(-1) catch "";
label_ptr.cmd = temporary_allocator.dupe(u8, label_cmd) catch "";
lua.pop(1); // cmd value
// label refresh
const name_type = lua.getField(-1, "refresh");
if (name_type != .number) continue;
const label_refresh: u32 = @intCast(lua.toInteger(-1) catch 0);
label_ptr.refresh = label_refresh;
lua.pop(1); // name value
}
}
}
lua.pop(1);
}
pub fn deinit(self: *Self) void {
self.arena.deinit();
for (0..global_errors.items.len) |i| {
const err = global_errors.items[i];
temporary_allocator.free(err.type_name);
temporary_allocator.free(err.key);
temporary_allocator.free(err.value);
}
global_errors.deinit(temporary_allocator);
}
fn errorHandler(type_name: []const u8, key: []const u8, value: []const u8, err: anyerror) void {
global_errors.append(temporary_allocator, .{
.type_name = temporary_allocator.dupe(u8, type_name) catch return,
.key = temporary_allocator.dupe(u8, key) catch return,
.value = temporary_allocator.dupe(u8, value) catch return,
.error_name = @errorName(err),
}) catch return;
}
};
}