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 80 81 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 136 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}