a73x

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"));
}