diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-18 14:21:38 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:45:30 -0300 |
| commit | 382abe3dafb872b3e6c9792a9cc0abcf6267e180 (patch) | |
| tree | e30f47d911561db9b02e714287a1658822c77a09 /src/user_config.zig | |
| parent | c24a9e40215bc30c68c9db7675f1c6a07d9bbae3 (diff) | |
| download | pardes-382abe3dafb872b3e6c9792a9cc0abcf6267e180.tar.gz pardes-382abe3dafb872b3e6c9792a9cc0abcf6267e180.zip | |
file_watch + builtins + config: richer watch semantics, new builtins, config docs
Diffstat (limited to 'src/user_config.zig')
| -rw-r--r-- | src/user_config.zig | 94 |
1 files changed, 85 insertions, 9 deletions
diff --git a/src/user_config.zig b/src/user_config.zig index aca8cbf3..15253ab2 100644 --- a/src/user_config.zig +++ b/src/user_config.zig @@ -1,7 +1,8 @@ //! Native startup configuration discovery and loading. //! -//! The file is named `pardes` inside the platform's per-user configuration -//! directory. Discovery is deliberately launcher-owned: native +//! `pardes` is a directory inside the platform's per-user configuration +//! directory; its startup command file is `pardes/init`. Discovery is +//! deliberately launcher-owned: native //! launchers opt in by putting the bytes in `Options.startup_config`, while //! tests and the browser keep the default `null`. @@ -10,7 +11,10 @@ const builtin = @import("builtin"); const max_bytes = 1024 * 1024; -/// Resolve the native per-user config file with stdlib environment and path +pub const init_name = "init"; +pub const builtin_themes_subdir = "themes/builtin"; + +/// Resolve the native per-user config directory 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 { @@ -31,12 +35,14 @@ pub fn path(gpa: std.mem.Allocator, env: *const std.process.Environ.Map) !?[]u8 return try std.fs.path.join(gpa, &.{ home, ".config", "pardes" }); } -/// The config file: WHERE it was looked for, and what was there. Missing, +/// The config directory and its init file: WHERE they were 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. +/// arena passed by the launcher owns every returned slice for the process. pub const Found = struct { + dir: ?[]const u8 = null, path: ?[]const u8 = null, bytes: ?[]const u8 = null, }; @@ -46,8 +52,10 @@ pub fn load( gpa: std.mem.Allocator, env: *const std.process.Environ.Map, ) Found { - const config_path = (path(gpa, env) catch return .{}) orelse return .{}; + const config_dir = (path(gpa, env) catch return .{}) orelse return .{}; + const config_path = std.fs.path.join(gpa, &.{ config_dir, init_name }) catch return .{ .dir = config_dir }; return .{ + .dir = config_dir, .path = config_path, .bytes = std.Io.Dir.cwd().readFileAlloc(io, config_path, gpa, .limited(max_bytes)) catch null, }; @@ -85,7 +93,7 @@ test "config path honors XDG and rejects a relative XDG directory" { try std.testing.expectEqualStrings(expected, fallback); } -test "config loader is silent when missing and returns exact file bytes" { +test "config loader reads init inside the config directory" { if (builtin.os.tag == .windows) return; var tmp = std.testing.tmpDir(.{}); @@ -98,15 +106,83 @@ test "config loader is silent when missing and returns exact file bytes" { 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.dir.?); defer std.testing.allocator.free(missing.path.?); - const expected = try std.fs.path.join(std.testing.allocator, &.{ base_buf[0..base_len], "pardes" }); + const expected_dir = try std.fs.path.join(std.testing.allocator, &.{ base_buf[0..base_len], "pardes" }); + defer std.testing.allocator.free(expected_dir); + const expected = try std.fs.path.join(std.testing.allocator, &.{ expected_dir, init_name }); defer std.testing.allocator.free(expected); + try std.testing.expectEqualStrings(expected_dir, missing.dir.?); 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 }); + try tmp.dir.createDir(std.testing.io, "pardes", .default_dir); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "pardes/init", .data = source }); const found = load(std.testing.io, std.testing.allocator, &env); + defer std.testing.allocator.free(found.dir.?); defer std.testing.allocator.free(found.path.?); defer std.testing.allocator.free(found.bytes.?); + try std.testing.expectEqualStrings(expected_dir, found.dir.?); try std.testing.expectEqualStrings(source, found.bytes.?); } + +/// Write one editable `.zon` file per compiled theme. The caller supplies the +/// ring so this filesystem-only module does not import the core. Existing +/// generated copies are replaced; unrelated files in the directory are left +/// alone, which lets a user keep custom themes beside the reference set. +pub fn dumpThemes( + io: std.Io, + gpa: std.mem.Allocator, + config_dir: []const u8, + theme_values: anytype, +) ![]u8 { + const out_dir = try std.fs.path.join(gpa, &.{ config_dir, builtin_themes_subdir }); + errdefer gpa.free(out_dir); + try std.Io.Dir.cwd().createDirPath(io, out_dir); + + for (theme_values) |theme_value| { + var encoded: std.Io.Writer.Allocating = .init(gpa); + defer encoded.deinit(); + try std.zon.stringify.serialize(theme_value, .{ .whitespace = true }, &encoded.writer); + + const filename = try std.fmt.allocPrint(gpa, "{s}.zon", .{theme_value.name}); + defer gpa.free(filename); + const output_path = try std.fs.path.join(gpa, &.{ out_dir, filename }); + defer gpa.free(output_path); + try std.Io.Dir.cwd().writeFile(io, .{ + .sub_path = output_path, + .data = encoded.written(), + }); + } + return out_dir; +} + +test "theme dump creates the builtin subdirectory and ZON files" { + const io = std.testing.io; + const gpa = std.testing.allocator; + 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(io, &base_buf); + const Sample = struct { name: []const u8, rgb: [3]u8 }; + const samples = [_]Sample{ + .{ .name = "one", .rgb = .{ 1, 2, 3 } }, + .{ .name = "two", .rgb = .{ 4, 5, 6 } }, + }; + const output = try dumpThemes(io, gpa, base_buf[0..base_len], &samples); + defer gpa.free(output); + const expected = try std.fs.path.join(gpa, &.{ base_buf[0..base_len], builtin_themes_subdir }); + defer gpa.free(expected); + try std.testing.expectEqualStrings(expected, output); + + const one_path = try std.fs.path.join(gpa, &.{ output, "one.zon" }); + defer gpa.free(one_path); + const bytes = try std.Io.Dir.cwd().readFileAlloc(io, one_path, gpa, .limited(4096)); + defer gpa.free(bytes); + const source = try gpa.dupeZ(u8, bytes); + defer gpa.free(source); + const parsed = try std.zon.parse.fromSliceAlloc(Sample, gpa, source, null, .{}); + defer std.zon.parse.free(gpa, parsed); + try std.testing.expectEqualStrings("one", parsed.name); + try std.testing.expectEqual([3]u8{ 1, 2, 3 }, parsed.rgb); +} |
