magdalena

Log | Files | Refs | README | LICENSE

commit 69bab0eed9881f0a425140ef8c9677f0fcf7dd4c
parent af46877b6bf86045d68dcfb761c05139c2033a36
Author: mtmn <miro@haravara.org>
Date:   Mon,  4 May 2026 11:48:16 +0200

feat!: add path based overrides

Diffstat:
Msrc/cfg.zig | 32+++++++++++++++++++++++++++++++-
Msrc/cli.zig | 8++++----
Msrc/db.zig | 34++++++++++++++--------------------
Msrc/fzf.zig | 56+++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/main.zig | 13++++++++++---
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| {