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 78 79 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 107 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}