config.zig

5.4 KB · raw

1const std = @import("std");2const types = @import("types.zig");3const errors = @import("errors.zig");45pub fn load(arena: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !types.NinaConfig {6    const home = environ.get("HOME") orelse {7        errors.error_info.setSuggestion("set HOME environment variable", .{});8        return error.ConfigNotFound;9    };1011    const path = try std.fmt.allocPrint(arena, "{s}/.nina.conf", .{home});1213    const content = std.Io.Dir.readFileAlloc(.cwd(), io, path, arena, .unlimited) catch {14        errors.error_info.setSuggestion("run nina to set up", .{});15        return error.ConfigNotFound;16    };17    return parse(arena, content, home);18}1920fn parse(arena: std.mem.Allocator, input: []const u8, home: []const u8) !types.NinaConfig {21    var config = types.NinaConfig{};22    var machines: std.ArrayList(types.Machine) = .empty;23    var current: ?types.Machine = null;2425    var lines = std.mem.splitScalar(u8, input, '\n');26    while (lines.next()) |line| {27        const trimmed = std.mem.trim(u8, line, " \t\r");28        if (trimmed.len == 0 or trimmed[0] == '#') continue;2930        if (std.mem.eql(u8, trimmed, "[machine]")) {31            if (current) |m| try machines.append(arena, m);32            current = .{};33            continue;34        }3536        const eq = std.mem.indexOfScalar(u8, trimmed, '=') orelse continue;37        const key = std.mem.trim(u8, trimmed[0..eq], " \t");38        const val = std.mem.trim(u8, trimmed[eq + 1 ..], " \t");3940        if (current) |*m| {41            setMachineField(arena, m, key, val, home) catch {};42        } else {43            setConfigField(arena, &config, key, val) catch {};44        }45    }4647    if (current) |m| try machines.append(arena, m);48    config.machines = try machines.toOwnedSlice(arena);49    return config;50}5152fn setConfigField(arena: std.mem.Allocator, config: *types.NinaConfig, key: []const u8, val: []const u8) !void {53    if (std.mem.eql(u8, key, "editor")) {54        config.editor = try arena.dupe(u8, val);55    } else if (std.mem.eql(u8, key, "generations")) {56        config.generations = std.fmt.parseInt(u32, val, 10) catch 5;57    } else if (std.mem.eql(u8, key, "confirm")) {58        config.confirm = std.mem.eql(u8, val, "true");59    } else if (std.mem.eql(u8, key, "teach")) {60        config.teach = std.mem.eql(u8, val, "true");61    } else if (std.mem.eql(u8, key, "color")) {62        config.color = std.mem.eql(u8, val, "true");63    }64}6566fn setMachineField(arena: std.mem.Allocator, m: *types.Machine, key: []const u8, val: []const u8, home: []const u8) !void {67    if (std.mem.eql(u8, key, "name")) {68        m.name = try arena.dupe(u8, val);69    } else if (std.mem.eql(u8, key, "config")) {70        m.config_path = try arena.dupe(u8, val);71    } else if (std.mem.eql(u8, key, "local")) {72        m.local = std.mem.eql(u8, val, "true");73    } else if (std.mem.eql(u8, key, "default")) {74        m.default = std.mem.eql(u8, val, "true");75    } else if (std.mem.eql(u8, key, "host")) {76        m.host = try arena.dupe(u8, val);77    } else if (std.mem.eql(u8, key, "user")) {78        m.user = try arena.dupe(u8, val);79    } else if (std.mem.eql(u8, key, "ssh_key")) {80        m.ssh_key = try expandHome(arena, val, home);81    }82}8384fn expandHome(arena: std.mem.Allocator, path: []const u8, home: []const u8) ![]const u8 {85    if (!std.mem.startsWith(u8, path, "~")) return arena.dupe(u8, path);86    return std.fmt.allocPrint(arena, "{s}{s}", .{ home, path[1..] });87}8889pub fn resolveMachine(config: *const types.NinaConfig, name: ?[]const u8) !types.Machine {90    if (name) |n| {91        for (config.machines) |m| {92            if (std.mem.eql(u8, m.name, n)) return m;93        }94        errors.error_info.setMessage("'{s}' is not in your config", .{n});95        errors.error_info.setSuggestion("check ~/.nina.conf", .{});96        return error.MachineNotFound;97    }98    for (config.machines) |m| {99        if (m.default) return m;100    }101    if (config.machines.len > 0) return config.machines[0];102    // No machines defined — return a default local machine103    return .{};104}105106test "parse basic config" {107    const input =108        \\editor = hx109        \\generations = 3110        \\confirm = false111        \\teach = true112        \\113        \\[machine]114        \\name = test-box115        \\local = true116        \\default = true117    ;118    const config = try parse(std.testing.allocator, input, "/home/test");119    defer std.testing.allocator.free(config.machines);120121    try std.testing.expectEqualStrings("hx", config.editor);122    try std.testing.expectEqual(@as(u32, 3), config.generations);123    try std.testing.expect(!config.confirm);124    try std.testing.expect(config.teach);125    try std.testing.expectEqual(@as(usize, 1), config.machines.len);126    try std.testing.expectEqualStrings("test-box", config.machines[0].name);127}128129test "resolve machine by name" {130    var machines = [_]types.Machine{131        .{ .name = "alpha", .default = true },132        .{ .name = "beta" },133    };134    const config = types.NinaConfig{ .machines = &machines };135    const m = try resolveMachine(&config, "beta");136    try std.testing.expectEqualStrings("beta", m.name);137}138139test "resolve default machine" {140    var machines = [_]types.Machine{141        .{ .name = "alpha", .default = true },142        .{ .name = "beta" },143    };144    const config = types.NinaConfig{ .machines = &machines };145    const m = try resolveMachine(&config, null);146    try std.testing.expectEqualStrings("alpha", m.name);147}