diff options
Diffstat (limited to 'src/user_config.zig')
| -rw-r--r-- | src/user_config.zig | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/src/user_config.zig b/src/user_config.zig new file mode 100644 index 00000000..455e8a3c --- /dev/null +++ b/src/user_config.zig @@ -0,0 +1,97 @@ +//! 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" }); +} + +/// Missing, unreadable, oversized, or otherwise unusable config is simply no +/// config. The arena passed by main owns successful bytes for the process. +pub fn load( + io: std.Io, + gpa: std.mem.Allocator, + env: *const std.process.Environ.Map, +) ?[]u8 { + const config_path = path(gpa, env) catch return null; + defer if (config_path) |p| gpa.free(p); + const p = config_path orelse return null; + return std.Io.Dir.cwd().readFileAlloc(io, p, 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]); + + try std.testing.expect(load(std.testing.io, std.testing.allocator, &env) == null); + const source = "Theme dark\nUnknown command\nTheme acme\n"; + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "pardes", .data = source }); + const bytes = load(std.testing.io, std.testing.allocator, &env).?; + defer std.testing.allocator.free(bytes); + try std.testing.expectEqualStrings(source, bytes); +} |
