magdalena

interactive shell navigation and history
Log | Files | Refs | README | LICENSE

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 }