commit 69bab0eed9881f0a425140ef8c9677f0fcf7dd4c
parent af46877b6bf86045d68dcfb761c05139c2033a36
Author: mtmn <miro@haravara.org>
Date: Mon, 4 May 2026 11:48:16 +0200
feat!: add path based overrides
Diffstat:
5 files changed, 106 insertions(+), 37 deletions(-)
diff --git a/src/cfg.zig b/src/cfg.zig
@@ -4,9 +4,10 @@ pub const Config = struct {
openers: []Opener = &.{},
ignored_patterns: []const []const u8 = &.{},
fzf_opts: []const []const u8 = &.{},
- fav_dirs: []const []const u8 = &.{},
+ favorites: []const []const u8 = &.{},
editor: ?[]const u8 = null,
max_depth: usize = 3,
+ overrides: []FolderOverride = &.{},
pub const Opener = struct {
extensions: []const []const u8,
@@ -15,6 +16,35 @@ pub const Config = struct {
};
};
+// Per-directory settings applied when cwd matches `path` exactly.
+// All override fields are optional; only non-null ones replace the base config.
+pub const FolderOverride = struct {
+ path: []const u8,
+ openers: ?[]Config.Opener = null,
+ ignored_patterns: ?[]const []const u8 = null,
+ fzf_opts: ?[]const []const u8 = null,
+ favorites: ?[]const []const u8 = null,
+ editor: ?[]const u8 = null,
+ max_depth: ?usize = null,
+};
+
+// Return a Config with the first override whose path exactly matches cwd applied on top.
+// The returned Config borrows memory from the parsed Config — no extra allocation needed.
+pub fn applyFolderOverride(base: Config, cwd: []const u8) Config {
+ for (base.overrides) |*ov| {
+ if (!std.mem.eql(u8, cwd, ov.path)) continue;
+ var result = base;
+ if (ov.openers) |v| result.openers = v;
+ if (ov.ignored_patterns) |v| result.ignored_patterns = v;
+ if (ov.fzf_opts) |v| result.fzf_opts = v;
+ if (ov.favorites) |v| result.favorites = v;
+ if (ov.editor) |v| result.editor = v;
+ if (ov.max_depth) |v| result.max_depth = v;
+ return result;
+ }
+ return base;
+}
+
pub fn loadConfig(allocator: std.mem.Allocator) ?std.json.Parsed(Config) {
const home = std.posix.getenv("HOME") orelse return null;
const path = std.fs.path.join(allocator, &.{ home, ".config", "magdalena", "config.json" }) catch return null;
diff --git a/src/cli.zig b/src/cli.zig
@@ -5,7 +5,7 @@ const io = @import("io.zig");
pub const Action = enum {
recent_dirs,
recent_files,
- fav_dirs,
+ favorites,
search,
goto_dir,
goto_file,
@@ -79,8 +79,8 @@ pub fn parseArgs(allocator: std.mem.Allocator) !Args {
res.action = .recent_dirs;
} else if (std.mem.eql(u8, cmd_str, "recent-files")) {
res.action = .recent_files;
- } else if (std.mem.eql(u8, cmd_str, "fav-dirs")) {
- res.action = .fav_dirs;
+ } else if (std.mem.eql(u8, cmd_str, "favorites")) {
+ res.action = .favorites;
} else if (std.mem.eql(u8, cmd_str, "search")) {
res.action = .search;
} else if (std.mem.eql(u8, cmd_str, "goto-dir")) {
@@ -125,7 +125,7 @@ pub fn printUsage() !void {
\\Usage:
\\ magdalena recent-dirs
\\ magdalena recent-files
- \\ magdalena fav-dirs
+ \\ magdalena favorites
\\ magdalena goto-dir
\\ magdalena goto-file
\\ magdalena log-dir <path>
diff --git a/src/db.zig b/src/db.zig
@@ -75,7 +75,7 @@ pub const Db = struct {
};
defer file.close();
- const content = try file.readToEndAlloc(self.allocator, std.math.maxInt(usize));
+ const content = try file.readToEndAlloc(self.allocator, 64 * 1024 * 1024);
errdefer self.allocator.free(content);
var arena = try self.allocator.create(std.heap.ArenaAllocator);
@@ -123,7 +123,7 @@ pub const Db = struct {
};
defer file.close();
- const content = try file.readToEndAlloc(self.allocator, std.math.maxInt(usize));
+ const content = try file.readToEndAlloc(self.allocator, 64 * 1024 * 1024);
errdefer self.allocator.free(content);
var arena = try self.allocator.create(std.heap.ArenaAllocator);
@@ -222,12 +222,9 @@ pub const Db = struct {
const abs_path = try self.resolvePath(dir_path);
defer self.allocator.free(abs_path);
- const file = std.fs.cwd().openFile(self.dirs_path, .{ .mode = .write_only }) catch |err| {
- if (err == error.FileNotFound) {
- _ = try std.fs.cwd().createFile(self.dirs_path, .{ .truncate = false });
- return try self.logDir(dir_path);
- }
- return err;
+ const file = std.fs.cwd().openFile(self.dirs_path, .{ .mode = .write_only }) catch |err| switch (err) {
+ error.FileNotFound => try std.fs.cwd().createFile(self.dirs_path, .{ .truncate = false }),
+ else => return err,
};
defer file.close();
try file.seekFromEnd(0);
@@ -238,8 +235,8 @@ pub const Db = struct {
try now.format(&ts_writer);
const ts = ts_buf[0..ts_writer.end];
- var line_buf: [4096]u8 = undefined;
- const line = try std.fmt.bufPrint(&line_buf, "{s}|{s}\n", .{ ts, abs_path });
+ const line = try std.fmt.allocPrint(self.allocator, "{s}|{s}\n", .{ ts, abs_path });
+ defer self.allocator.free(line);
try file.writeAll(line);
}
@@ -247,12 +244,9 @@ pub const Db = struct {
const abs_path = try self.resolvePath(file_path);
defer self.allocator.free(abs_path);
- const file = std.fs.cwd().openFile(self.files_path, .{ .mode = .write_only }) catch |err| {
- if (err == error.FileNotFound) {
- _ = try std.fs.cwd().createFile(self.files_path, .{ .truncate = false });
- return try self.logFile(file_path, file_type, action);
- }
- return err;
+ const file = std.fs.cwd().openFile(self.files_path, .{ .mode = .write_only }) catch |err| switch (err) {
+ error.FileNotFound => try std.fs.cwd().createFile(self.files_path, .{ .truncate = false }),
+ else => return err,
};
defer file.close();
try file.seekFromEnd(0);
@@ -263,8 +257,8 @@ pub const Db = struct {
try now.format(&ts_writer);
const ts = ts_buf[0..ts_writer.end];
- var line_buf: [4096]u8 = undefined;
- const line = try std.fmt.bufPrint(&line_buf, "{s}|{s}|{s}|{s}\n", .{ ts, file_type, action, abs_path });
+ const line = try std.fmt.allocPrint(self.allocator, "{s}|{s}|{s}|{s}\n", .{ ts, file_type, action, abs_path });
+ defer self.allocator.free(line);
try file.writeAll(line);
}
@@ -277,8 +271,8 @@ pub const Db = struct {
io.warn("Cleaning up {s}...\n", .{path});
const file = try std.fs.cwd().openFile(path, .{});
- const content = try file.readToEndAlloc(self.allocator, std.math.maxInt(usize));
- file.close();
+ defer file.close();
+ const content = try file.readToEndAlloc(self.allocator, 64 * 1024 * 1024);
defer self.allocator.free(content);
var seen = std.StringHashMap(void).init(self.allocator);
diff --git a/src/fzf.zig b/src/fzf.zig
@@ -22,6 +22,32 @@ fn gotoFile(allocator: std.mem.Allocator, db: ?*hist.Db, file_path: []const u8,
if (command != null) break;
}
+ const tty = std.fs.openFileAbsolute("/dev/tty", .{ .mode = .read_write }) catch |err| blk: {
+ io.warn("failed to open /dev/tty: {}\n", .{err});
+ break :blk null;
+ };
+ defer if (tty) |t| t.close();
+
+ const saved_stdin = std.posix.dup(std.posix.STDIN_FILENO) catch |err| blk: {
+ io.warn("failed to save stdin: {}\n", .{err});
+ break :blk null;
+ };
+ defer if (saved_stdin) |fd| std.posix.close(fd);
+ const saved_stdout = std.posix.dup(std.posix.STDOUT_FILENO) catch |err| blk: {
+ io.warn("failed to save stdout: {}\n", .{err});
+ break :blk null;
+ };
+ defer if (saved_stdout) |fd| std.posix.close(fd);
+
+ if (tty) |t| {
+ std.posix.dup2(t.handle, std.posix.STDIN_FILENO) catch |err| io.warn("failed to redirect stdin to tty: {}\n", .{err});
+ std.posix.dup2(t.handle, std.posix.STDOUT_FILENO) catch |err| io.warn("failed to redirect stdout to tty: {}\n", .{err});
+ }
+ defer {
+ if (saved_stdin) |fd| std.posix.dup2(fd, std.posix.STDIN_FILENO) catch |err| io.warn("failed to restore stdin: {}\n", .{err});
+ if (saved_stdout) |fd| std.posix.dup2(fd, std.posix.STDOUT_FILENO) catch |err| io.warn("failed to restore stdout: {}\n", .{err});
+ }
+
const term = if (command) |cmd| blk: {
var child = std.process.Child.init(&.{ cmd, file_path }, allocator);
child.stdin_behavior = .Inherit;
@@ -98,6 +124,7 @@ const Fzf = struct {
}
};
+// ignored_patterns entries are matched by exact path component name, not globs.
fn shouldSkip(path: []const u8, ignored_patterns: []const []const u8) bool {
var it = std.mem.splitScalar(u8, path, std.fs.path.sep);
while (it.next()) |component| {
@@ -190,9 +217,9 @@ pub fn recentDir(allocator: std.mem.Allocator, db: *hist.Db, config: *const cfg.
try out.end();
}
-pub fn favDirs(allocator: std.mem.Allocator, config: *const cfg.Config) !void {
- if (config.fav_dirs.len == 0) {
- io.warn("No favorite directories found in config.\n", .{});
+pub fn favorites(allocator: std.mem.Allocator, db: *hist.Db, config: *const cfg.Config) !void {
+ if (config.favorites.len == 0) {
+ io.warn("No favorites found in config.\n", .{});
return;
}
@@ -202,18 +229,29 @@ pub fn favDirs(allocator: std.mem.Allocator, config: *const cfg.Config) !void {
items.deinit(allocator);
}
- for (config.fav_dirs) |dir| {
- const expanded = try expandPath(allocator, dir);
+ for (config.favorites) |entry| {
+ const expanded = try expandPath(allocator, entry);
try items.append(allocator, expanded);
}
const selected = try runFzfPicker(allocator, config.fzf_opts, items.items);
defer allocator.free(selected);
- var out_buf: [4096]u8 = undefined;
- var out = io.getStdout(&out_buf);
- try out.print("{s}\n", .{selected});
- try out.end();
+ const is_dir = blk: {
+ var dir = std.fs.openDirAbsolute(selected, .{}) catch break :blk false;
+ dir.close();
+ break :blk true;
+ };
+
+ if (is_dir) {
+ try db.logDir(selected);
+ var out_buf: [4096]u8 = undefined;
+ var out = io.getStdout(&out_buf);
+ try out.print("{s}\n", .{selected});
+ try out.end();
+ } else {
+ try gotoFile(allocator, db, selected, null, config);
+ }
}
fn expandPath(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
diff --git a/src/main.zig b/src/main.zig
@@ -25,7 +25,14 @@ pub fn main() void {
const parsed_config = cfg.loadConfig(allocator);
defer if (parsed_config) |c| c.deinit();
const default_config = cfg.Config{};
- const config = if (parsed_config) |c| c.value else default_config;
+ const base_config = if (parsed_config) |c| c.value else default_config;
+
+ var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const cwd = std.process.getCwd(&cwd_buf) catch |err| blk: {
+ io.warn("failed to get cwd for folder overrides: {}\n", .{err});
+ break :blk "";
+ };
+ const config = cfg.applyFolderOverride(base_config, cwd);
const db_path = hist.getDefaultDbPath(allocator) catch |err| {
io.fatal("failed to get database path: {}", .{err});
@@ -64,8 +71,8 @@ fn runAction(allocator: std.mem.Allocator, db: *hist.Db, args: cli.Args, config:
try writer.print("{s}|{s}|{s}|{s}\n", .{ f.timestamp orelse "-", f.file_type, f.action, f.path });
}
},
- .fav_dirs => {
- try fzf.favDirs(allocator, config);
+ .favorites => {
+ try fzf.favorites(allocator, db, config);
},
.search => {
if (args.query) |query| {