1const std = @import("std");2const types = @import("types.zig");3const errors = @import("errors.zig");4const exec = @import("exec.zig");5const output = @import("output.zig");678910pub fn configPath(arena: std.mem.Allocator, environ: *const std.process.Environ.Map) ![]const u8 {11 if (environ.get("XDG_CONFIG_HOME")) |xdg| {12 if (xdg.len > 0) return std.fs.path.join(arena, &.{ xdg, "nina", "config" });13 }14 const home = environ.get("HOME") orelse {15 errors.error_info.setSuggestion("set HOME environment variable", .{});16 return error.ConfigNotFound;17 };18 return std.fs.path.join(arena, &.{ home, ".config", "nina", "config" });19}2021fn legacyPath(arena: std.mem.Allocator, environ: *const std.process.Environ.Map) ?[]const u8 {22 const home = environ.get("HOME") orelse return null;23 return std.fmt.allocPrint(arena, "{s}/.nina.conf", .{home}) catch null;24}2526fn fileExists(io: std.Io, path: []const u8) bool {27 const f = std.Io.Dir.openFile(.cwd(), io, path, .{}) catch return false;28 f.close(io);29 return true;30}313233343536pub fn migrateLegacyConfig(arena: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) void {37 const new_path = configPath(arena, environ) catch return;38 if (fileExists(io, new_path)) return;39 const legacy = legacyPath(arena, environ) orelse return;40 const content = std.Io.Dir.readFileAlloc(.cwd(), io, legacy, arena, .unlimited) catch return;4142 if (std.fs.path.dirname(new_path)) |dir| exec.ensureDir(arena, io, dir);43 const f = std.Io.Dir.createFile(.cwd(), io, new_path, .{}) catch return;44 f.writePositionalAll(io, content, 0) catch {45 f.close(io);46 return;47 };48 f.close(io);49 std.Io.Dir.deleteFile(.cwd(), io, legacy) catch {};50 output.configMigrated(homeShort(arena, new_path, environ));51}52535455fn homeShort(arena: std.mem.Allocator, path: []const u8, environ: *const std.process.Environ.Map) []const u8 {56 const home = environ.get("HOME") orelse return path;57 if (home.len == 0 or !std.mem.startsWith(u8, path, home)) return path;58 return std.fmt.allocPrint(arena, "~{s}", .{path[home.len..]}) catch path;59}6061pub fn load(arena: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !types.NinaConfig {62 migrateLegacyConfig(arena, io, environ);6364 const path = try configPath(arena, environ);65 const home = environ.get("HOME") orelse "";6667 const content = std.Io.Dir.readFileAlloc(.cwd(), io, path, arena, .unlimited) catch {68 errors.error_info.setSuggestion("run nina to set up", .{});69 return error.ConfigNotFound;70 };71 const cfg = try parse(arena, content, home);72 if (environ.contains("NINA_DEBUG")) debugDumpConfig(cfg);73 return cfg;74}7576fn parse(arena: std.mem.Allocator, input: []const u8, home: []const u8) !types.NinaConfig {77 var config = types.NinaConfig{};78 var machines: std.ArrayList(types.Machine) = .empty;79 var current: ?types.Machine = null;8081 var lines = std.mem.splitScalar(u8, input, '\n');82 while (lines.next()) |line| {83 const trimmed = std.mem.trim(u8, line, " \t\r");84 if (trimmed.len == 0 or trimmed[0] == '#') continue;8586 if (std.mem.eql(u8, trimmed, "[machine]")) {87 if (current) |m| try machines.append(arena, m);88 current = .{};89 continue;90 }9192 const eq = std.mem.indexOfScalar(u8, trimmed, '=') orelse continue;93 const key = std.mem.trim(u8, trimmed[0..eq], " \t");94 const val = std.mem.trim(u8, trimmed[eq + 1 ..], " \t");9596 if (current) |*m| {97 setMachineField(arena, m, key, val, home) catch {};98 } else {99 setConfigField(arena, &config, key, val) catch {};100 }101 }102103 if (current) |m| try machines.append(arena, m);104 config.machines = try machines.toOwnedSlice(arena);105 return config;106}107108fn setConfigField(arena: std.mem.Allocator, config: *types.NinaConfig, key: []const u8, val: []const u8) !void {109 if (std.mem.eql(u8, key, "editor")) {110 config.editor = try arena.dupe(u8, val);111 } else if (std.mem.eql(u8, key, "generations")) {112 config.generations = std.fmt.parseInt(u32, val, 10) catch 5;113 } else if (std.mem.eql(u8, key, "confirm")) {114 config.confirm = parseBool(val, true);115 } else if (std.mem.eql(u8, key, "teach")) {116 config.teach = parseBool(val, false);117 } else if (std.mem.eql(u8, key, "color")) {118 config.color = parseBool(val, true);119 } else if (std.mem.eql(u8, key, "flake")) {120 config.flake = parseBool(val, false);121 }122}123124fn setMachineField(arena: std.mem.Allocator, m: *types.Machine, key: []const u8, val: []const u8, home: []const u8) !void {125 if (std.mem.eql(u8, key, "name")) {126 m.name = try arena.dupe(u8, val);127 } else if (std.mem.eql(u8, key, "config")) {128 m.config_path = try arena.dupe(u8, val);129 } else if (std.mem.eql(u8, key, "local")) {130 m.local = parseBool(val, true);131 } else if (std.mem.eql(u8, key, "default")) {132 m.default = parseBool(val, false);133 } else if (std.mem.eql(u8, key, "host")) {134 m.host = try arena.dupe(u8, val);135 136 137 138 m.local = false;139 } else if (std.mem.eql(u8, key, "user")) {140 m.user = try arena.dupe(u8, val);141 } else if (std.mem.eql(u8, key, "ssh_key")) {142 m.ssh_key = try expandHome(arena, val, home);143 }144}145146fn parseBool(val: []const u8, default: bool) bool {147 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")) {148 return true;149 }150 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")) {151 return false;152 }153 return default;154}155156fn debugDumpConfig(cfg: types.NinaConfig) void {157 std.debug.print("NINA_DEBUG config\n", .{});158 std.debug.print("editor={s}\n", .{cfg.editor});159 std.debug.print("generations={d}\n", .{cfg.generations});160 std.debug.print("confirm={}\n", .{cfg.confirm});161 std.debug.print("teach={}\n", .{cfg.teach});162 std.debug.print("color={}\n", .{cfg.color});163 std.debug.print("flake={}\n", .{cfg.flake});164 for (cfg.machines, 0..) |m, i| {165 std.debug.print("machine[{d}].name={s}\n", .{ i, m.name });166 std.debug.print("machine[{d}].config={s}\n", .{ i, m.config_path });167 std.debug.print("machine[{d}].local={}\n", .{ i, m.local });168 std.debug.print("machine[{d}].default={}\n", .{ i, m.default });169 if (m.host) |host| std.debug.print("machine[{d}].host={s}\n", .{ i, host });170 if (m.user) |user| std.debug.print("machine[{d}].user={s}\n", .{ i, user });171 }172}173174fn expandHome(arena: std.mem.Allocator, path: []const u8, home: []const u8) ![]const u8 {175 if (!std.mem.startsWith(u8, path, "~")) return arena.dupe(u8, path);176 return std.fmt.allocPrint(arena, "{s}{s}", .{ home, path[1..] });177}178179pub fn resolveMachine(config: *const types.NinaConfig, name: ?[]const u8) !types.Machine {180 if (name) |n| {181 for (config.machines) |m| {182 if (std.mem.eql(u8, m.name, n)) return m;183 }184 errors.error_info.setMessage("'{s}' is not in your config", .{n});185 errors.error_info.setSuggestion("check ~/.nina.conf", .{});186 return error.MachineNotFound;187 }188 for (config.machines) |m| {189 if (m.default) return m;190 }191 if (config.machines.len > 0) return config.machines[0];192 193 return .{};194}195196test "parse basic config" {197 const input =198 \\editor = hx199 \\generations = 3200 \\confirm = false201 \\teach = true202 \\203 \\[machine]204 \\name = test-box205 \\local = true206 \\default = true207 ;208 const config = try parse(std.testing.allocator, input, "/home/test");209 defer std.testing.allocator.free(config.machines);210211 try std.testing.expectEqualStrings("hx", config.editor);212 try std.testing.expectEqual(@as(u32, 3), config.generations);213 try std.testing.expect(!config.confirm);214 try std.testing.expect(config.teach);215 try std.testing.expectEqual(@as(usize, 1), config.machines.len);216 try std.testing.expectEqualStrings("test-box", config.machines[0].name);217}218219test "parse alternate booleans" {220 const input =221 \\confirm = y222 \\teach = no223 \\color = 1224 \\225 \\[machine]226 \\name = test-box227 \\local = n228 \\default = on229 ;230 const config = try parse(std.testing.allocator, input, "/home/test");231 defer std.testing.allocator.free(config.machines);232233 try std.testing.expect(config.confirm);234 try std.testing.expect(!config.teach);235 try std.testing.expect(config.color);236 try std.testing.expect(!config.machines[0].local);237 try std.testing.expect(config.machines[0].default);238}239240test "parse flake flag" {241 const with =242 \\flake = true243 \\244 \\[machine]245 \\name = box246 ;247 const cfg_with = try parse(std.testing.allocator, with, "/home/test");248 defer std.testing.allocator.free(cfg_with.machines);249 try std.testing.expect(cfg_with.flake);250251 const without =252 \\[machine]253 \\name = box254 ;255 const cfg_without = try parse(std.testing.allocator, without, "/home/test");256 defer std.testing.allocator.free(cfg_without.machines);257 try std.testing.expect(!cfg_without.flake);258}259260test "resolve machine by name" {261 var machines = [_]types.Machine{262 .{ .name = "alpha", .default = true },263 .{ .name = "beta" },264 };265 const config = types.NinaConfig{ .machines = &machines };266 const m = try resolveMachine(&config, "beta");267 try std.testing.expectEqualStrings("beta", m.name);268}269270test "resolve default machine" {271 var machines = [_]types.Machine{272 .{ .name = "alpha", .default = true },273 .{ .name = "beta" },274 };275 const config = types.NinaConfig{ .machines = &machines };276 const m = try resolveMachine(&config, null);277 try std.testing.expectEqualStrings("alpha", m.name);278}