config.zig

5.6 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        // A machine with a host is remote unless explicitly marked local. The78        // Machine struct defaults local=true, so without this a [machine] block79        // that only sets host/user/ssh_key would render as "local".80        m.local = false;81    } else if (std.mem.eql(u8, key, "user")) {82        m.user = try arena.dupe(u8, val);83    } else if (std.mem.eql(u8, key, "ssh_key")) {84        m.ssh_key = try expandHome(arena, val, home);85    }86}8788fn expandHome(arena: std.mem.Allocator, path: []const u8, home: []const u8) ![]const u8 {89    if (!std.mem.startsWith(u8, path, "~")) return arena.dupe(u8, path);90    return std.fmt.allocPrint(arena, "{s}{s}", .{ home, path[1..] });91}9293pub fn resolveMachine(config: *const types.NinaConfig, name: ?[]const u8) !types.Machine {94    if (name) |n| {95        for (config.machines) |m| {96            if (std.mem.eql(u8, m.name, n)) return m;97        }98        errors.error_info.setMessage("'{s}' is not in your config", .{n});99        errors.error_info.setSuggestion("check ~/.nina.conf", .{});100        return error.MachineNotFound;101    }102    for (config.machines) |m| {103        if (m.default) return m;104    }105    if (config.machines.len > 0) return config.machines[0];106    // No machines defined — return a default local machine107    return .{};108}109110test "parse basic config" {111    const input =112        \\editor = hx113        \\generations = 3114        \\confirm = false115        \\teach = true116        \\117        \\[machine]118        \\name = test-box119        \\local = true120        \\default = true121    ;122    const config = try parse(std.testing.allocator, input, "/home/test");123    defer std.testing.allocator.free(config.machines);124125    try std.testing.expectEqualStrings("hx", config.editor);126    try std.testing.expectEqual(@as(u32, 3), config.generations);127    try std.testing.expect(!config.confirm);128    try std.testing.expect(config.teach);129    try std.testing.expectEqual(@as(usize, 1), config.machines.len);130    try std.testing.expectEqualStrings("test-box", config.machines[0].name);131}132133test "resolve machine by name" {134    var machines = [_]types.Machine{135        .{ .name = "alpha", .default = true },136        .{ .name = "beta" },137    };138    const config = types.NinaConfig{ .machines = &machines };139    const m = try resolveMachine(&config, "beta");140    try std.testing.expectEqualStrings("beta", m.name);141}142143test "resolve default machine" {144    var machines = [_]types.Machine{145        .{ .name = "alpha", .default = true },146        .{ .name = "beta" },147    };148    const config = types.NinaConfig{ .machines = &machines };149    const m = try resolveMachine(&config, null);150    try std.testing.expectEqualStrings("alpha", m.name);151}