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 103 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}