//! Native startup configuration discovery and loading. //! //! The file is named `pardes` inside the platform's per-user configuration //! directory. Discovery is deliberately outside the sans-IO core: native //! launchers opt in by putting the bytes in `Options.startup_config`, while //! tests and the browser keep the default `null`. const std = @import("std"); const builtin = @import("builtin"); const max_bytes = 1024 * 1024; /// Resolve the native per-user config file with stdlib environment and path /// APIs. XDG_CONFIG_HOME is accepted only when absolute, as required by the /// XDG base-directory specification; an empty/relative value falls back. pub fn path(gpa: std.mem.Allocator, env: *const std.process.Environ.Map) !?[]u8 { if (builtin.os.tag == .windows) { if (nonEmpty(env.get("LOCALAPPDATA"))) |base| return try std.fs.path.join(gpa, &.{ base, "pardes" }); if (nonEmpty(env.get("USERPROFILE"))) |home| return try std.fs.path.join(gpa, &.{ home, "AppData", "Local", "pardes" }); return null; } if (xdgBase(env)) |base| return try std.fs.path.join(gpa, &.{ base, "pardes" }); const home = nonEmpty(env.get("HOME")) orelse return null; if (builtin.os.tag == .macos) return try std.fs.path.join(gpa, &.{ home, "Library", "Application Support", "pardes" }); return try std.fs.path.join(gpa, &.{ home, ".config", "pardes" }); } /// The config file: WHERE it was looked for, and what was there. Missing, /// unreadable, oversized, or otherwise unusable config is simply no config — /// but the path resolves either way, because "nothing is there yet" is the /// answer the Config builtin exists to give and a null would erase it. The /// arena passed by the launcher owns both for the process. pub const Found = struct { path: ?[]const u8 = null, bytes: ?[]const u8 = null, }; pub fn load( io: std.Io, gpa: std.mem.Allocator, env: *const std.process.Environ.Map, ) Found { const config_path = (path(gpa, env) catch return .{}) orelse return .{}; return .{ .path = config_path, .bytes = std.Io.Dir.cwd().readFileAlloc(io, config_path, gpa, .limited(max_bytes)) catch null, }; } fn nonEmpty(value: ?[]const u8) ?[]const u8 { const v = value orelse return null; return if (v.len == 0) null else v; } fn xdgBase(env: *const std.process.Environ.Map) ?[]const u8 { const value = nonEmpty(env.get("XDG_CONFIG_HOME")) orelse return null; return if (std.fs.path.isAbsolute(value)) value else null; } test "config path honors XDG and rejects a relative XDG directory" { if (builtin.os.tag == .windows) return; var env: std.process.Environ.Map = .init(std.testing.allocator); defer env.deinit(); try env.put("HOME", "/home/pardes-test"); try env.put("XDG_CONFIG_HOME", "/var/tmp/pardes-xdg"); const xdg = (try path(std.testing.allocator, &env)).?; defer std.testing.allocator.free(xdg); try std.testing.expectEqualStrings("/var/tmp/pardes-xdg/pardes", xdg); try env.put("XDG_CONFIG_HOME", "relative/config"); const fallback = (try path(std.testing.allocator, &env)).?; defer std.testing.allocator.free(fallback); const expected = if (builtin.os.tag == .macos) "/home/pardes-test/Library/Application Support/pardes" else "/home/pardes-test/.config/pardes"; try std.testing.expectEqualStrings(expected, fallback); } test "config loader is silent when missing and returns exact file bytes" { if (builtin.os.tag == .windows) return; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var base_buf: [std.fs.max_path_bytes]u8 = undefined; const base_len = try tmp.dir.realPath(std.testing.io, &base_buf); var env: std.process.Environ.Map = .init(std.testing.allocator); defer env.deinit(); try env.put("XDG_CONFIG_HOME", base_buf[0..base_len]); const missing = load(std.testing.io, std.testing.allocator, &env); defer std.testing.allocator.free(missing.path.?); const expected = try std.fs.path.join(std.testing.allocator, &.{ base_buf[0..base_len], "pardes" }); defer std.testing.allocator.free(expected); try std.testing.expectEqualStrings(expected, missing.path.?); try std.testing.expect(missing.bytes == null); const source = "Theme dark\nUnknown command\nTheme acme\n"; try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "pardes", .data = source }); const found = load(std.testing.io, std.testing.allocator, &env); defer std.testing.allocator.free(found.path.?); defer std.testing.allocator.free(found.bytes.?); try std.testing.expectEqualStrings(source, found.bytes.?); }