config.zig

7.5 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    const cfg = try parse(arena, content, home);18    if (environ.contains("NINA_DEBUG")) debugDumpConfig(cfg);19    return cfg;20}2122fn parse(arena: std.mem.Allocator, input: []const u8, home: []const u8) !types.NinaConfig {23    var config = types.NinaConfig{};24    var machines: std.ArrayList(types.Machine) = .empty;25    var current: ?types.Machine = null;2627    var lines = std.mem.splitScalar(u8, input, '\n');28    while (lines.next()) |line| {29        const trimmed = std.mem.trim(u8, line, " \t\r");30        if (trimmed.len == 0 or trimmed[0] == '#') continue;3132        if (std.mem.eql(u8, trimmed, "[machine]")) {33            if (current) |m| try machines.append(arena, m);34            current = .{};35            continue;36        }3738        const eq = std.mem.indexOfScalar(u8, trimmed, '=') orelse continue;39        const key = std.mem.trim(u8, trimmed[0..eq], " \t");40        const val = std.mem.trim(u8, trimmed[eq + 1 ..], " \t");4142        if (current) |*m| {43            setMachineField(arena, m, key, val, home) catch {};44        } else {45            setConfigField(arena, &config, key, val) catch {};46        }47    }4849    if (current) |m| try machines.append(arena, m);50    config.machines = try machines.toOwnedSlice(arena);51    return config;52}5354fn setConfigField(arena: std.mem.Allocator, config: *types.NinaConfig, key: []const u8, val: []const u8) !void {55    if (std.mem.eql(u8, key, "editor")) {56        config.editor = try arena.dupe(u8, val);57    } else if (std.mem.eql(u8, key, "generations")) {58        config.generations = std.fmt.parseInt(u32, val, 10) catch 5;59    } else if (std.mem.eql(u8, key, "confirm")) {60        config.confirm = parseBool(val, true);61    } else if (std.mem.eql(u8, key, "teach")) {62        config.teach = parseBool(val, false);63    } else if (std.mem.eql(u8, key, "color")) {64        config.color = parseBool(val, true);65    }66}6768fn setMachineField(arena: std.mem.Allocator, m: *types.Machine, key: []const u8, val: []const u8, home: []const u8) !void {69    if (std.mem.eql(u8, key, "name")) {70        m.name = try arena.dupe(u8, val);71    } else if (std.mem.eql(u8, key, "config")) {72        m.config_path = try arena.dupe(u8, val);73    } else if (std.mem.eql(u8, key, "local")) {74        m.local = parseBool(val, true);75    } else if (std.mem.eql(u8, key, "default")) {76        m.default = parseBool(val, false);77    } else if (std.mem.eql(u8, key, "host")) {78        m.host = try arena.dupe(u8, val);79        // A machine with a host is remote unless explicitly marked local. The80        // Machine struct defaults local=true, so without this a [machine] block81        // that only sets host/user/ssh_key would render as "local".82        m.local = false;83    } else if (std.mem.eql(u8, key, "user")) {84        m.user = try arena.dupe(u8, val);85    } else if (std.mem.eql(u8, key, "ssh_key")) {86        m.ssh_key = try expandHome(arena, val, home);87    }88}8990fn parseBool(val: []const u8, default: bool) bool {91    if (std.mem.eql(u8, val, "true") or std.mem.eql(u8, val, "yes") or std.mem.eql(u8, val, "y") or std.mem.eql(u8, val, "on") or std.mem.eql(u8, val, "1")) {92        return true;93    }94    if (std.mem.eql(u8, val, "false") or std.mem.eql(u8, val, "no") or std.mem.eql(u8, val, "n") or std.mem.eql(u8, val, "off") or std.mem.eql(u8, val, "0")) {95        return false;96    }97    return default;98}99100fn debugDumpConfig(cfg: types.NinaConfig) void {101    std.debug.print("NINA_DEBUG config\n", .{});102    std.debug.print("editor={s}\n", .{cfg.editor});103    std.debug.print("generations={d}\n", .{cfg.generations});104    std.debug.print("confirm={}\n", .{cfg.confirm});105    std.debug.print("teach={}\n", .{cfg.teach});106    std.debug.print("color={}\n", .{cfg.color});107    for (cfg.machines, 0..) |m, i| {108        std.debug.print("machine[{d}].name={s}\n", .{ i, m.name });109        std.debug.print("machine[{d}].config={s}\n", .{ i, m.config_path });110        std.debug.print("machine[{d}].local={}\n", .{ i, m.local });111        std.debug.print("machine[{d}].default={}\n", .{ i, m.default });112        if (m.host) |host| std.debug.print("machine[{d}].host={s}\n", .{ i, host });113        if (m.user) |user| std.debug.print("machine[{d}].user={s}\n", .{ i, user });114    }115}116117fn expandHome(arena: std.mem.Allocator, path: []const u8, home: []const u8) ![]const u8 {118    if (!std.mem.startsWith(u8, path, "~")) return arena.dupe(u8, path);119    return std.fmt.allocPrint(arena, "{s}{s}", .{ home, path[1..] });120}121122pub fn resolveMachine(config: *const types.NinaConfig, name: ?[]const u8) !types.Machine {123    if (name) |n| {124        for (config.machines) |m| {125            if (std.mem.eql(u8, m.name, n)) return m;126        }127        errors.error_info.setMessage("'{s}' is not in your config", .{n});128        errors.error_info.setSuggestion("check ~/.nina.conf", .{});129        return error.MachineNotFound;130    }131    for (config.machines) |m| {132        if (m.default) return m;133    }134    if (config.machines.len > 0) return config.machines[0];135    // No machines defined — return a default local machine136    return .{};137}138139test "parse basic config" {140    const input =141        \\editor = hx142        \\generations = 3143        \\confirm = false144        \\teach = true145        \\146        \\[machine]147        \\name = test-box148        \\local = true149        \\default = true150    ;151    const config = try parse(std.testing.allocator, input, "/home/test");152    defer std.testing.allocator.free(config.machines);153154    try std.testing.expectEqualStrings("hx", config.editor);155    try std.testing.expectEqual(@as(u32, 3), config.generations);156    try std.testing.expect(!config.confirm);157    try std.testing.expect(config.teach);158    try std.testing.expectEqual(@as(usize, 1), config.machines.len);159    try std.testing.expectEqualStrings("test-box", config.machines[0].name);160}161162test "parse alternate booleans" {163    const input =164        \\confirm = y165        \\teach = no166        \\color = 1167        \\168        \\[machine]169        \\name = test-box170        \\local = n171        \\default = on172    ;173    const config = try parse(std.testing.allocator, input, "/home/test");174    defer std.testing.allocator.free(config.machines);175176    try std.testing.expect(config.confirm);177    try std.testing.expect(!config.teach);178    try std.testing.expect(config.color);179    try std.testing.expect(!config.machines[0].local);180    try std.testing.expect(config.machines[0].default);181}182183test "resolve machine by name" {184    var machines = [_]types.Machine{185        .{ .name = "alpha", .default = true },186        .{ .name = "beta" },187    };188    const config = types.NinaConfig{ .machines = &machines };189    const m = try resolveMachine(&config, "beta");190    try std.testing.expectEqualStrings("beta", m.name);191}192193test "resolve default machine" {194    var machines = [_]types.Machine{195        .{ .name = "alpha", .default = true },196        .{ .name = "beta" },197    };198    const config = types.NinaConfig{ .machines = &machines };199    const m = try resolveMachine(&config, null);200    try std.testing.expectEqualStrings("alpha", m.name);201}