src/gui/config.zig
Ref: Size: 7.8 KiB History
//! Small, explicit native appearance and font configuration parser.
const std = @import("std");
const font_options = @import("font_options.zig");
pub const Settings = struct {
families: []const [:0]const u8 = &.{},
size_points: ?f64 = null,
theme: ?[:0]const u8 = null,
theme_line: usize = 0,
foreground: ?u32 = null,
background: ?u32 = null,
cursor_color: ?u32 = null,
palette: [256]?u32 = [_]?u32{null} ** 256,
pub fn deinit(self: *Settings, alloc: std.mem.Allocator) void {
for (self.families) |family| alloc.free(family);
if (self.families.len != 0) alloc.free(self.families);
if (self.theme) |theme| alloc.free(theme);
self.* = .{};
}
};
pub const Error = error{ InvalidSyntax, UnknownKey, InvalidValue, InvalidColor, InvalidPalette, InvalidThemeName, MissingFamily, OutOfMemory };
pub fn parseColor(text: []const u8) Error!u32 {
var s = text;
if (s.len > 0 and s[0] == '#') s = s[1..];
if (s.len != 6) return error.InvalidValue;
for (s) |ch| if (!((ch >= '0' and ch <= '9') or (ch >= 'a' and ch <= 'f') or (ch >= 'A' and ch <= 'F'))) return error.InvalidValue;
const rgb = std.fmt.parseInt(u24, s, 16) catch return error.InvalidValue;
return (@as(u32, rgb) << 8) | 0xff;
}
pub const PalettePair = struct { index: u8, color: u32 };
pub fn parsePalette(text: []const u8) Error!PalettePair {
const eq = std.mem.indexOfScalar(u8, text, '=') orelse return error.InvalidPalette;
if (eq == 0 or eq + 1 >= text.len) return error.InvalidPalette;
for (text[0..eq]) |ch| if (ch < '0' or ch > '9') return error.InvalidPalette;
const index = std.fmt.parseInt(u8, text[0..eq], 10) catch return error.InvalidPalette;
return .{ .index = index, .color = parseColor(std.mem.trim(u8, text[eq + 1 ..], " \t")) catch return error.InvalidPalette };
}
fn valueText(raw: []const u8, alloc: std.mem.Allocator) Error![:0]const u8 {
var value = raw;
if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') value = value[1 .. value.len - 1];
if (std.mem.indexOfScalar(u8, value, '"') != null or
std.mem.indexOfScalar(u8, value, 0) != null or
!std.unicode.utf8ValidateSlice(value)) return error.InvalidSyntax;
if (std.mem.trim(u8, value, " \t").len == 0) return error.MissingFamily;
return alloc.dupeZ(u8, value);
}
pub fn parse(alloc: std.mem.Allocator, bytes: []const u8, line_out: ?*usize) Error!Settings {
var out: Settings = .{};
var families: std.ArrayListUnmanaged([:0]const u8) = .empty;
errdefer {
for (families.items) |family| alloc.free(family);
families.deinit(alloc);
if (out.theme) |t| alloc.free(t);
}
var it = std.mem.splitScalar(u8, bytes, '\n');
var line_no: usize = 0;
while (it.next()) |raw| {
line_no += 1;
if (line_out) |p| p.* = line_no;
const line = std.mem.trim(u8, raw, " \t\r");
if (line.len == 0 or line[0] == '#') continue;
const eq = std.mem.indexOfScalar(u8, line, '=') orelse {
return error.InvalidSyntax;
};
const key = std.mem.trim(u8, line[0..eq], " \t");
const value = std.mem.trim(u8, line[eq + 1 ..], " \t");
if (key.len == 0 or value.len == 0) {
return error.InvalidSyntax;
}
if (std.mem.eql(u8, key, "font-family")) {
const family = try valueText(value, alloc);
families.append(alloc, family) catch |err| {
alloc.free(family);
return err;
};
} else if (std.mem.eql(u8, key, "font-size")) {
if (out.size_points != null) {
return error.InvalidSyntax;
}
out.size_points = font_options.parsePointSize(value) catch return error.InvalidValue;
} else if (std.mem.eql(u8, key, "theme")) {
if (out.theme != null) return error.InvalidSyntax;
out.theme = valueText(value, alloc) catch |err| return if (err == error.MissingFamily) error.InvalidThemeName else err;
out.theme_line = line_no;
} else if (std.mem.eql(u8, key, "foreground") or std.mem.eql(u8, key, "background") or std.mem.eql(u8, key, "cursor-color")) {
const parsed = parseColor(value) catch return error.InvalidColor;
if (std.mem.eql(u8, key, "foreground")) {
if (out.foreground != null) return error.InvalidSyntax;
out.foreground = parsed;
} else if (std.mem.eql(u8, key, "background")) {
if (out.background != null) return error.InvalidSyntax;
out.background = parsed;
} else {
if (out.cursor_color != null) return error.InvalidSyntax;
out.cursor_color = parsed;
}
} else if (std.mem.eql(u8, key, "palette")) {
const pair = parsePalette(value) catch return error.InvalidPalette;
if (out.palette[pair.index] != null) return error.InvalidSyntax;
out.palette[pair.index] = pair.color;
} else {
return error.UnknownKey;
}
}
out.families = try families.toOwnedSlice(alloc);
return out;
}
pub fn load(alloc: std.mem.Allocator, path: []const u8, line_out: ?*usize) !Settings {
const file = std.fs.cwd().openFile(path, .{}) catch |err| if (err == error.FileNotFound) return .{} else return err;
defer file.close();
const bytes = try file.readToEndAlloc(alloc, 64 * 1024);
defer alloc.free(bytes);
return parse(alloc, bytes, line_out);
}
test "config parses comments quotes and decimal points" {
var s = try parse(std.testing.allocator, "# native\nfont-family = \"Noto Sans Mono\"\nfont-size = 12.5\n", null);
defer s.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 1), s.families.len);
try std.testing.expectEqualStrings("Noto Sans Mono", s.families[0]);
try std.testing.expectEqual(@as(f64, 12.5), s.size_points.?);
try std.testing.expectError(error.UnknownKey, parse(std.testing.allocator, "font-weight = bold\n", null));
try std.testing.expectError(error.InvalidValue, parse(std.testing.allocator, "font-size = nan\n", null));
}
test "config preserves repeated font families in fallback order" {
var s = try parse(std.testing.allocator, "font-family = Primary Mono\nfont-family = \"Adwaita Mono\"\n", null);
defer s.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 2), s.families.len);
try std.testing.expectEqualStrings("Primary Mono", s.families[0]);
try std.testing.expectEqualStrings("Adwaita Mono", s.families[1]);
}
test "config refuses singleton duplicates and malformed text at the actual line" {
const a = std.testing.allocator;
var line: usize = 0;
try std.testing.expectError(error.InvalidSyntax, parse(a, "# header\nfont-family = \"broken\n", &line));
try std.testing.expectEqual(@as(usize, 2), line);
try std.testing.expectError(error.InvalidSyntax, parse(a, "font-size = 12\nfont-size = 13\n", &line));
try std.testing.expectError(error.InvalidSyntax, parse(a, "font-family = mono\x00space\n", null));
try std.testing.expectError(error.InvalidSyntax, parse(a, "font-family = \"mono\"space\"\n", null));
try std.testing.expectError(error.MissingFamily, parse(a, "font-family = \" \"\n", null));
}
test "colors require exact hexadecimal and decimal palette syntax" {
try std.testing.expectEqual(@as(u32, 0x112233ff), try parseColor("#112233"));
try std.testing.expectError(error.InvalidValue, parseColor("+11223"));
try std.testing.expectError(error.InvalidValue, parseColor("11223_3"));
try std.testing.expectError(error.InvalidPalette, parsePalette("+1=#112233"));
try std.testing.expectEqual(@as(u8, 255), (try parsePalette("255=abcdef")).index);
try std.testing.expectError(error.InvalidPalette, parsePalette("256=abcdef"));
}