cfg.zig (4176B)
1 const std = @import("std"); 2 const io = @import("io.zig"); 3 4 pub const Config = struct { 5 openers: []Opener = &.{}, 6 ignored_patterns: []const []const u8 = &.{}, 7 fzf_opts: []const []const u8 = &.{}, 8 favorites: []const []const u8 = &.{}, 9 editor: ?[]const u8 = null, 10 max_depth: usize = 3, 11 overrides: []FolderOverride = &.{}, 12 13 pub const Opener = struct { 14 extensions: []const []const u8, 15 action: []const u8, 16 command: []const u8, 17 }; 18 }; 19 20 // Per-directory settings applied when cwd matches `path` exactly. 21 // All override fields are optional; only non-null ones replace the base config. 22 pub const FolderOverride = struct { 23 path: []const u8, 24 openers: ?[]Config.Opener = null, 25 ignored_patterns: ?[]const []const u8 = null, 26 fzf_opts: ?[]const []const u8 = null, 27 favorites: ?[]const []const u8 = null, 28 editor: ?[]const u8 = null, 29 max_depth: ?usize = null, 30 }; 31 32 // Return a Config with the first override whose path exactly matches cwd applied on top. 33 // The returned Config borrows memory from the parsed Config — no extra allocation needed. 34 pub fn applyFolderOverride(base: Config, cwd: []const u8) Config { 35 for (base.overrides) |*ov| { 36 if (!std.mem.eql(u8, cwd, ov.path)) continue; 37 var result = base; 38 if (ov.openers) |v| result.openers = v; 39 if (ov.ignored_patterns) |v| result.ignored_patterns = v; 40 if (ov.fzf_opts) |v| result.fzf_opts = v; 41 if (ov.favorites) |v| result.favorites = v; 42 if (ov.editor) |v| result.editor = v; 43 if (ov.max_depth) |v| result.max_depth = v; 44 return result; 45 } 46 return base; 47 } 48 49 pub fn loadConfig(allocator: std.mem.Allocator) ?std.json.Parsed(Config) { 50 const home = io.getenv("HOME") orelse return null; 51 const path = std.fs.path.join(allocator, &.{ home, ".config", "magdalena", "config.json" }) catch return null; 52 defer allocator.free(path); 53 54 const content = std.Io.Dir.cwd().readFileAlloc(io.rt(), path, allocator, .unlimited) catch return null; 55 defer allocator.free(content); 56 57 return std.json.parseFromSlice(Config, allocator, content, .{ 58 .ignore_unknown_fields = true, 59 .allocate = .alloc_always, 60 }) catch |err| { 61 std.log.err("failed to parse config: {any}", .{err}); 62 return null; 63 }; 64 } 65 66 const testing = std.testing; 67 68 test "applyFolderOverride: no overrides returns base" { 69 const base = Config{ .max_depth = 3, .editor = "vim" }; 70 const got = applyFolderOverride(base, "/anywhere"); 71 try testing.expectEqual(@as(usize, 3), got.max_depth); 72 try testing.expectEqualStrings("vim", got.editor.?); 73 } 74 75 test "applyFolderOverride: matching path overrides only provided fields" { 76 const ignored = [_][]const u8{ "node_modules", ".git" }; 77 var overrides = [_]FolderOverride{ 78 .{ .path = "/work", .max_depth = 9, .editor = "nvim" }, 79 }; 80 const base = Config{ 81 .max_depth = 3, 82 .editor = "vim", 83 .ignored_patterns = &ignored, 84 .overrides = &overrides, 85 }; 86 87 const got = applyFolderOverride(base, "/work"); 88 try testing.expectEqual(@as(usize, 9), got.max_depth); 89 try testing.expectEqualStrings("nvim", got.editor.?); 90 // A field left null on the override keeps the base value. 91 try testing.expectEqual(base.ignored_patterns.ptr, got.ignored_patterns.ptr); 92 } 93 94 test "applyFolderOverride: non-matching path leaves base untouched" { 95 var overrides = [_]FolderOverride{ 96 .{ .path = "/work", .max_depth = 9 }, 97 }; 98 const base = Config{ .max_depth = 3, .editor = "vim", .overrides = &overrides }; 99 100 const got = applyFolderOverride(base, "/elsewhere"); 101 try testing.expectEqual(@as(usize, 3), got.max_depth); 102 try testing.expectEqualStrings("vim", got.editor.?); 103 } 104 105 test "applyFolderOverride: first matching override wins" { 106 var overrides = [_]FolderOverride{ 107 .{ .path = "/work", .max_depth = 5 }, 108 .{ .path = "/work", .max_depth = 8 }, 109 }; 110 const base = Config{ .max_depth = 3, .overrides = &overrides }; 111 112 const got = applyFolderOverride(base, "/work"); 113 try testing.expectEqual(@as(usize, 5), got.max_depth); 114 }