summaryrefslogtreecommitdiff
path: root/src/user_config.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-18 14:21:38 -0300
committerGabriel Schneider <[email protected]>2026-08-18 23:45:30 -0300
commit382abe3dafb872b3e6c9792a9cc0abcf6267e180 (patch)
treee30f47d911561db9b02e714287a1658822c77a09 /src/user_config.zig
parentc24a9e40215bc30c68c9db7675f1c6a07d9bbae3 (diff)
downloadpardes-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.zig94
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);
+}