Commit Diff


commit - /dev/null
commit + 39a38fc7fa002b6abe4dfd4a8b1a94c0af2ec2ab
blob - /dev/null
blob + 8b94e4b8bca4e126e932d1cb67626ab1f4c987c0 (mode 644)
--- /dev/null
+++ BUILD
@@ -0,0 +1,37 @@
+load("@rules_zig//zig:defs.bzl", "zig_binary", "zig_library")
+load("//bazel:local-deploy.bzl", "local_deploy")
+
+package(default_visibility = ["//visibility:public"])
+
+zig_binary(
+    name = "magdalena",
+    main = "src/main.zig",
+    srcs = glob(["src/*.zig"]),
+    zigopts = [
+        "-O",
+        "ReleaseFast",
+        "-Doptimize=ReleaseFast",
+    ],
+    deps = [
+        "@zul//:zul",
+    ],
+)
+
+zig_binary(
+    name = "magdalena_debug",
+    main = "src/main.zig",
+    srcs = glob(["src/*.zig"]),
+    zigopts = [
+        "-O",
+        "Debug",
+        "-Doptimize=Debug",
+    ],
+    deps = [
+        "@zul//:zul",
+    ],
+)
+
+local_deploy(
+    name = "deploy",
+    srcs = [":magdalena"],
+)
blob - /dev/null
blob + 2777750ab4b2ee7dd970ae75b1abafc5de5c16cd (mode 644)
--- /dev/null
+++ README.md
@@ -0,0 +1,4 @@
+# magdalena
+Interactive shell navigation and history in zig.
+
+[![Sandra - Maria Magdalena](https://i.discogs.com/90fMDrWx3ZBUyp3-54G3H4k6XL13lsFzzpF4TMp52sA/rs:fit/g:sm/q:90/h:524/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTI1NTM5/OC0xNDc3NjY3OTEw/LTI5NjcucG5n.jpeg)](https://youtu.be/qwrrpBc7xWA/)
blob - /dev/null
blob + d490ab6424f8a704fce958f59de761edacbdad0d (mode 644)
--- /dev/null
+++ build.zig
@@ -0,0 +1,34 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+    const target = b.standardTargetOptions(.{});
+    const optimize = b.standardOptimizeOption(.{});
+
+    const zul = b.dependency("zul", .{
+        .target = target,
+        .optimize = optimize,
+    });
+
+    const exe = b.addExecutable(.{
+        .name = "magdalena",
+        .root_module = b.createModule(.{
+            .root_source_file = b.path("src/main.zig"),
+            .target = target,
+            .optimize = optimize,
+            .imports = &.{
+                .{ .name = "zul", .module = zul.module("zul") },
+            },
+        }),
+    });
+    exe.linkLibC();
+
+    b.installArtifact(exe);
+
+    const run_cmd = b.addRunArtifact(exe);
+    run_cmd.step.dependOn(b.getInstallStep());
+    if (b.args) |args| {
+        run_cmd.addArgs(args);
+    }
+    const run_step = b.step("run", "Run the app");
+    run_step.dependOn(&run_cmd.step);
+}
blob - /dev/null
blob + 7f3f0e5721a9b50a53b856ad331799cc2f918fcc (mode 644)
--- /dev/null
+++ build.zig.zon
@@ -0,0 +1,19 @@
+.{
+    .name = .magdalena,
+    .version = "0.1.4",
+    .fingerprint = 0xd065e535ca3c8b31,
+    .minimum_zig_version = "0.15.2",
+    .dependencies = .{
+        .zul = .{
+            .url = "https://github.com/karlseguin/zul/archive/e770047f208cf4538fcd9fc64550c84fd5492fc4.tar.gz",
+            .hash = "1220c0fe8bdde860b533f5625b72daaf2c0c686ac7eaf72e123aaf6998d785439b93",
+        },
+    },
+    .paths = .{
+        "build.zig",
+        "build.zig.zon",
+        "src",
+        "LICENSE",
+        "README.md",
+    },
+}
blob - /dev/null
blob + 363423ce542886f5c6200ba943b7498789b457df (mode 644)
--- /dev/null
+++ config.json
@@ -0,0 +1,51 @@
+{
+  "editor": "vim",
+  "max_depth": 5,
+  "ignored_patterns": [
+    ".git",
+    ".jj",
+    ".zig-cache",
+    "node_modules",
+    "target"
+  ],
+  "fzf_opts": [
+    "--highlight-line",
+    "--ansi",
+    "--layout=reverse",
+    "--border=rounded",
+    "--color=bg+:#2a1f2e,bg:-1,spinner:#c084b8,hl:#e08060",
+    "--color=fg:#c8b8bf,header:#c4607a,info:#7a6e7a,pointer:#c084b8",
+    "--color=marker:#e06fad,fg+:#f0dfe5,prompt:#7a9fd4,hl+:#f0a070",
+    "--color=border:#3d2a42"
+  ],
+  "fav_dirs": [
+    "/home/miro/src"
+  ],
+  "openers": [
+    {
+      "extensions": ["mp4", "mkv", "avi", "webm"],
+      "action": "media_player",
+      "command": "mpv"
+    },
+    {
+      "extensions": ["flac", "aiff", "aif", "mp3", "wav"],
+      "action": "audio_player",
+      "command": "mpv"
+    },
+    {
+      "extensions": ["jpg", "jpeg", "png", "webp", "svg"],
+      "action": "image_viewer",
+      "command": "nsxiv-rifle"
+    },
+    {
+      "extensions": ["pdf", "epub", "mobi"],
+      "action": "document_reader",
+      "command": "sioyek"
+    },
+    {
+      "extensions": ["html"],
+      "action": "web_browser",
+      "command": "firefox"
+    }
+  ]
+}
blob - /dev/null
blob + 67e2ed3367ecce3d57d2a7a6b4a71c68590cc255 (mode 644)
--- /dev/null
+++ extension.bzl
@@ -0,0 +1,75 @@
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+def _magdalena_deps_impl(_ctx):
+    http_archive(
+        name = "zul",
+        sha256 = "1dee366aaab6257e9d2b7d8b54dfb470bc2c0404be5975d76365b8ef6fd51ab7",
+        strip_prefix = "zul-e770047f208cf4538fcd9fc64550c84fd5492fc4",
+        url = "https://github.com/karlseguin/zul/archive/e770047f208cf4538fcd9fc64550c84fd5492fc4.tar.gz",
+        build_file_content = """
+load("@rules_zig//zig:defs.bzl", "zig_library")
+zig_library(
+    name = "zul",
+    main = "src/zul.zig",
+    srcs = glob(["src/**/*.zig"]),
+    visibility = ["//visibility:public"],
+)
+""",
+    )
+
+    http_archive(
+        name = "zio",
+        sha256 = "bcda5af0639c1392f55f909536759bb0df254c9966d1f9bdd9f828212c221816",
+        strip_prefix = "zio-3ac234eae165177cd75742f922851dc5d373fd12",
+        url = "https://github.com/lalinsky/zio/archive/3ac234eae165177cd75742f922851dc5d373fd12.tar.gz",
+        build_file_content = """
+load("@rules_zig//zig:defs.bzl", "zig_library")
+
+genrule(
+    name = "zio_options_gen",
+    outs = ["zio_options.zig"],
+    cmd = 'echo \\'pub const backend: ?[]const u8 = "epoll";\\' > $@',
+)
+
+zig_library(
+    name = "zio_options",
+    main = "zio_options.zig",
+    visibility = ["//visibility:public"],
+)
+
+zig_library(
+    name = "zio",
+    main = "src/zio.zig",
+    srcs = glob(["src/**/*.zig"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        ":zio_options",
+        "@rules_zig//zig/lib:libc",
+    ],
+)
+""",
+    )
+
+    http_archive(
+        name = "memcached",
+        sha256 = "702822a8d4c41b8549c49f42df48e703e279644f3a5e523b7e221b7c57b1c175",
+        strip_prefix = "memcached.zig-d4e93b1663074db714ec4d6ade513d023ab92ee4",
+        url = "https://github.com/lalinsky/memcached.zig/archive/d4e93b1663074db714ec4d6ade513d023ab92ee4.tar.gz",
+        build_file_content = """
+load("@rules_zig//zig:defs.bzl", "zig_library")
+zig_library(
+    name = "memcached",
+    main = "src/root.zig",
+    srcs = glob(["src/**/*.zig"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "@zio//:zio",
+        "@rules_zig//zig/lib:libc",
+    ],
+)
+""",
+    )
+
+magdalena_deps = module_extension(
+    implementation = _magdalena_deps_impl,
+)
blob - /dev/null
blob + 4b77e22df703651cdc018ffca0ae539c24ac9eb8 (mode 644)
--- /dev/null
+++ justfile
@@ -0,0 +1,5 @@
+run opt="Debug":
+    zig build run -Doptimize={{opt}}
+
+build opt="Debug":
+    zig build -Doptimize={{opt}} -p build/{{opt}}
blob - /dev/null
blob + b75234e499966821a6a53c29f28a09d61bc7081f (mode 644)
--- /dev/null
+++ src/cfg.zig
@@ -0,0 +1,36 @@
+const std = @import("std");
+
+pub const Config = struct {
+    openers: []Opener = &.{},
+    ignored_patterns: []const []const u8 = &.{},
+    fzf_opts: []const []const u8 = &.{},
+    fav_dirs: []const []const u8 = &.{},
+    editor: ?[]const u8 = null,
+    max_depth: usize = 3,
+
+    pub const Opener = struct {
+        extensions: []const []const u8,
+        action: []const u8,
+        command: []const u8,
+    };
+};
+
+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;
+    defer allocator.free(path);
+
+    const file = std.fs.cwd().openFile(path, .{}) catch return null;
+    defer file.close();
+
+    const content = file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch return null;
+    defer allocator.free(content);
+
+    return std.json.parseFromSlice(Config, allocator, content, .{
+        .ignore_unknown_fields = true,
+        .allocate = .alloc_always,
+    }) catch |err| {
+        std.log.err("failed to parse config: {any}", .{err});
+        return null;
+    };
+}
blob - /dev/null
blob + 9466b39f6645858d5d72f5278de807e3d9a8efb4 (mode 644)
--- /dev/null
+++ src/cli.zig
@@ -0,0 +1,144 @@
+const std = @import("std");
+const zul = @import("zul");
+const io = @import("io.zig");
+
+pub const Action = enum {
+    recent_dirs,
+    recent_files,
+    fav_dirs,
+    search,
+    goto_dir,
+    goto_file,
+    look_file,
+    look_dir,
+    grep,
+    cleanup,
+    log_dir,
+    log_file,
+    help,
+};
+
+pub const Args = struct {
+    action: Action,
+    query: ?[]const u8 = null,
+    log_path: ?[]const u8 = null,
+    file_type: ?[]const u8 = null,
+    file_action: ?[]const u8 = null,
+    depth: ?usize = null,
+
+    _cla: zul.CommandLineArgs,
+
+    pub fn deinit(self: Args) void {
+        self._cla.deinit();
+    }
+};
+
+pub fn parseArgs(allocator: std.mem.Allocator) !Args {
+    const cla = try zul.CommandLineArgs.parse(allocator);
+
+    var res = Args{
+        .action = .help,
+        ._cla = cla,
+    };
+
+    if (cla.contains("help") or cla.contains("-h") or cla.contains("--help")) {
+        res.action = .help;
+        return res;
+    }
+
+    if (cla.contains("clean") or cla.contains("c")) {
+        res.action = .cleanup;
+        return res;
+    }
+
+    if (cla.get("depth")) |d| {
+        res.depth = try std.fmt.parseInt(usize, d, 10);
+    } else if (cla.get("d")) |d| {
+        res.depth = try std.fmt.parseInt(usize, d, 10);
+    }
+
+    if (res.depth == null) {
+        for (cla.tail, 0..) |arg, i| {
+            if (std.mem.startsWith(u8, arg, "--depth=")) {
+                res.depth = try std.fmt.parseInt(usize, arg["--depth=".len..], 10);
+            } else if (std.mem.eql(u8, arg, "--depth") or std.mem.eql(u8, arg, "-d")) {
+                if (i + 1 < cla.tail.len) {
+                    res.depth = try std.fmt.parseInt(usize, cla.tail[i + 1], 10);
+                }
+            }
+            if (res.depth != null) break;
+        }
+    }
+
+    if (cla.tail.len == 0) {
+        return res;
+    }
+
+    const cmd_str = cla.tail[0];
+    if (std.mem.eql(u8, cmd_str, "recent-dirs")) {
+        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, "search")) {
+        res.action = .search;
+    } else if (std.mem.eql(u8, cmd_str, "goto-dir")) {
+        res.action = .goto_dir;
+    } else if (std.mem.eql(u8, cmd_str, "goto-file")) {
+        res.action = .goto_file;
+    } else if (std.mem.eql(u8, cmd_str, "look-file")) {
+        res.action = .look_file;
+    } else if (std.mem.eql(u8, cmd_str, "look-dir")) {
+        res.action = .look_dir;
+    } else if (std.mem.eql(u8, cmd_str, "grep")) {
+        res.action = .grep;
+    } else if (std.mem.eql(u8, cmd_str, "log-dir")) {
+        res.action = .log_dir;
+    } else if (std.mem.eql(u8, cmd_str, "log-file")) {
+        res.action = .log_file;
+    } else {
+        res.action = .help;
+        return res;
+    }
+
+    if (cla.tail.len > 1) {
+        const arg = cla.tail[1];
+        if (res.action == .search) {
+            res.query = arg;
+        } else if (res.action == .log_dir) {
+            res.log_path = arg;
+        } else if (res.action == .log_file) {
+            res.log_path = arg;
+            if (cla.tail.len > 2) res.file_type = cla.tail[2];
+            if (cla.tail.len > 3) res.file_action = cla.tail[3];
+        }
+    }
+
+    return res;
+}
+
+pub fn printUsage() !void {
+    var buf: [4096]u8 = undefined;
+    var writer = io.getStdout(&buf);
+    try writer.print(
+        \\Usage:
+        \\  magdalena recent-dirs
+        \\  magdalena recent-files
+        \\  magdalena fav-dirs
+        \\  magdalena goto-dir
+        \\  magdalena goto-file
+        \\  magdalena log-dir <path>
+        \\  magdalena log-file <path> [type] [action]
+        \\  magdalena search <query>
+        \\  magdalena look-file [--depth <n>]
+        \\  magdalena look-dir [--depth <n>]
+        \\  magdalena grep
+        \\
+        \\Options:
+        \\  -c, --clean             Perform cleanup
+        \\  -h, --help              Show this help
+        \\
+    , .{});
+    try writer.end();
+}
blob - /dev/null
blob + 1526c962e7f2772dadd64acd77bcb51390208003 (mode 644)
--- /dev/null
+++ src/db.zig
@@ -0,0 +1,347 @@
+const std = @import("std");
+const zul = @import("zul");
+const io = @import("io.zig");
+
+pub const DirectoryEntry = struct {
+    path: []const u8,
+    timestamp: ?[]const u8 = null,
+};
+
+pub const FileEntry = struct {
+    path: []const u8,
+    file_type: []const u8,
+    action: []const u8,
+    timestamp: ?[]const u8 = null,
+};
+
+pub const SearchResult = struct {
+    directories: []DirectoryEntry,
+    files: []FileEntry,
+};
+
+pub const Db = struct {
+    allocator: std.mem.Allocator,
+    base_path: []const u8,
+    dirs_path: []const u8,
+    files_path: []const u8,
+
+    pub fn init(allocator: std.mem.Allocator, path: []const u8) !Db {
+        const base_path = try allocator.dupe(u8, path);
+        errdefer allocator.free(base_path);
+
+        const dirs_path = try std.fs.path.join(allocator, &.{ base_path, "dirs.log" });
+        errdefer allocator.free(dirs_path);
+
+        const files_path = try std.fs.path.join(allocator, &.{ base_path, "files.log" });
+        errdefer allocator.free(files_path);
+
+        const res = Db{
+            .allocator = allocator,
+            .base_path = base_path,
+            .dirs_path = dirs_path,
+            .files_path = files_path,
+        };
+
+        // Ensure directory exists
+        std.fs.cwd().makePath(base_path) catch |err| {
+            if (err != error.PathAlreadyExists) return err;
+        };
+
+        // Ensure files exist
+        for (&[_][]const u8{ dirs_path, files_path }) |p| {
+            var f = std.fs.cwd().openFile(p, .{ .mode = .read_write }) catch |err| {
+                if (err == error.FileNotFound) {
+                    _ = try std.fs.cwd().createFile(p, .{ .truncate = false });
+                    continue;
+                }
+                return err;
+            };
+            f.close();
+        }
+
+        return res;
+    }
+
+    pub fn deinit(self: *Db) void {
+        self.allocator.free(self.base_path);
+        self.allocator.free(self.dirs_path);
+        self.allocator.free(self.files_path);
+    }
+
+    pub fn recentDirs(self: *Db) !zul.Managed([]DirectoryEntry) {
+        const file = std.fs.cwd().openFile(self.dirs_path, .{}) catch |err| {
+            if (err == error.FileNotFound) return zul.Managed([]DirectoryEntry).fromJson(try std.json.parseFromSlice([]DirectoryEntry, self.allocator, "[]", .{}));
+            return err;
+        };
+        defer file.close();
+
+        const content = try file.readToEndAlloc(self.allocator, std.math.maxInt(usize));
+        errdefer self.allocator.free(content);
+
+        var arena = try self.allocator.create(std.heap.ArenaAllocator);
+        errdefer {
+            arena.deinit();
+            self.allocator.destroy(arena);
+        }
+        arena.* = std.heap.ArenaAllocator.init(self.allocator);
+        const allocator = arena.allocator();
+
+        var list = std.ArrayListUnmanaged(DirectoryEntry){};
+
+        var seen = std.StringHashMap(void).init(allocator);
+
+        // Use splitBackwardsScalar to read the newest entries first
+        var it = std.mem.splitBackwardsScalar(u8, content, '\n');
+        while (it.next()) |line| {
+            const trimmed = std.mem.trim(u8, line, " \r\t");
+            if (trimmed.len == 0) continue;
+
+            var line_it = std.mem.splitScalar(u8, trimmed, '|');
+            const ts = line_it.next() orelse continue;
+            const dir_path = line_it.next() orelse continue;
+
+            if (seen.contains(dir_path)) continue;
+            try seen.put(dir_path, {});
+
+            try list.append(allocator, .{
+                .path = try allocator.dupe(u8, dir_path),
+                .timestamp = try allocator.dupe(u8, ts),
+            });
+        }
+
+        self.allocator.free(content);
+        return .{
+            .arena = arena,
+            .value = try list.toOwnedSlice(allocator),
+        };
+    }
+
+    pub fn recentFiles(self: *Db) !zul.Managed([]FileEntry) {
+        const file = std.fs.cwd().openFile(self.files_path, .{}) catch |err| {
+            if (err == error.FileNotFound) return zul.Managed([]FileEntry).fromJson(try std.json.parseFromSlice([]FileEntry, self.allocator, "[]", .{}));
+            return err;
+        };
+        defer file.close();
+
+        const content = try file.readToEndAlloc(self.allocator, std.math.maxInt(usize));
+        errdefer self.allocator.free(content);
+
+        var arena = try self.allocator.create(std.heap.ArenaAllocator);
+        errdefer {
+            arena.deinit();
+            self.allocator.destroy(arena);
+        }
+        arena.* = std.heap.ArenaAllocator.init(self.allocator);
+        const allocator = arena.allocator();
+
+        var list = std.ArrayListUnmanaged(FileEntry){};
+
+        var seen = std.StringHashMap(void).init(allocator);
+
+        var it = std.mem.splitBackwardsScalar(u8, content, '\n');
+        while (it.next()) |line| {
+            const trimmed = std.mem.trim(u8, line, " \r\t");
+            if (trimmed.len == 0) continue;
+
+            var line_it = std.mem.splitScalar(u8, trimmed, '|');
+            const ts = line_it.next() orelse continue;
+            const ftype = line_it.next() orelse continue;
+            const action = line_it.next() orelse continue;
+            const fpath = line_it.next() orelse continue;
+
+            if (seen.contains(fpath)) continue;
+            try seen.put(fpath, {});
+
+            try list.append(allocator, .{
+                .path = try allocator.dupe(u8, fpath),
+                .file_type = try allocator.dupe(u8, ftype),
+                .action = try allocator.dupe(u8, action),
+                .timestamp = try allocator.dupe(u8, ts),
+            });
+        }
+
+        self.allocator.free(content);
+        return .{
+            .arena = arena,
+            .value = try list.toOwnedSlice(allocator),
+        };
+    }
+
+    pub fn searchHistory(self: *Db, query_str: []const u8) !zul.Managed(SearchResult) {
+        const managed_dirs = try self.recentDirs();
+        defer managed_dirs.deinit();
+        const managed_files = try self.recentFiles();
+        defer managed_files.deinit();
+
+        var arena = try self.allocator.create(std.heap.ArenaAllocator);
+        errdefer {
+            arena.deinit();
+            self.allocator.destroy(arena);
+        }
+        arena.* = std.heap.ArenaAllocator.init(self.allocator);
+        const allocator = arena.allocator();
+
+        var dir_list = std.ArrayListUnmanaged(DirectoryEntry){};
+        for (managed_dirs.value) |d| {
+            if (std.mem.indexOf(u8, d.path, query_str) != null) {
+                try dir_list.append(allocator, .{
+                    .path = try allocator.dupe(u8, d.path),
+                    .timestamp = if (d.timestamp) |ts| try allocator.dupe(u8, ts) else null,
+                });
+            }
+        }
+
+        var file_list = std.ArrayListUnmanaged(FileEntry){};
+        for (managed_files.value) |f| {
+            if (std.mem.indexOf(u8, f.path, query_str) != null) {
+                try file_list.append(allocator, .{
+                    .path = try allocator.dupe(u8, f.path),
+                    .file_type = try allocator.dupe(u8, f.file_type),
+                    .action = try allocator.dupe(u8, f.action),
+                    .timestamp = if (f.timestamp) |ts| try allocator.dupe(u8, ts) else null,
+                });
+            }
+        }
+
+        return .{
+            .arena = arena,
+            .value = .{
+                .directories = try dir_list.toOwnedSlice(allocator),
+                .files = try file_list.toOwnedSlice(allocator),
+            },
+        };
+    }
+
+    fn resolvePath(self: *Db, path: []const u8) ![]const u8 {
+        return std.fs.cwd().realpathAlloc(self.allocator, path) catch {
+            return try std.fs.path.resolve(self.allocator, &[_][]const u8{path});
+        };
+    }
+
+    pub fn logDir(self: *Db, dir_path: []const u8) !void {
+        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;
+        };
+        defer file.close();
+        try file.seekFromEnd(0);
+
+        const now = zul.DateTime.now();
+        var ts_buf: [64]u8 = undefined;
+        var ts_writer = std.io.Writer.fixed(&ts_buf);
+        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 });
+        try file.writeAll(line);
+    }
+
+    pub fn logFile(self: *Db, file_path: []const u8, file_type: []const u8, action: []const u8) !void {
+        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;
+        };
+        defer file.close();
+        try file.seekFromEnd(0);
+
+        const now = zul.DateTime.now();
+        var ts_buf: [64]u8 = undefined;
+        var ts_writer = std.io.Writer.fixed(&ts_buf);
+        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 });
+        try file.writeAll(line);
+    }
+
+    pub fn cleanup(self: *Db) !void {
+        try self.cleanupFile(self.dirs_path, 1);
+        try self.cleanupFile(self.files_path, 3);
+    }
+
+    fn cleanupFile(self: *Db, path: []const u8, path_col_idx: usize) !void {
+        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 self.allocator.free(content);
+
+        var seen = std.StringHashMap(void).init(self.allocator);
+        defer seen.deinit();
+
+        var lines = std.ArrayListUnmanaged([]const u8){};
+        defer lines.deinit(self.allocator);
+
+        var total_entries: usize = 0;
+        var missing_entries: usize = 0;
+        var it = std.mem.splitBackwardsScalar(u8, content, '\n');
+        while (it.next()) |line| {
+            const trimmed = std.mem.trim(u8, line, " \r\t");
+            if (trimmed.len == 0) continue;
+            total_entries += 1;
+
+            var line_it = std.mem.splitScalar(u8, trimmed, '|');
+            var item_path: ?[]const u8 = null;
+            var i: usize = 0;
+            while (line_it.next()) |col| : (i += 1) {
+                if (i == path_col_idx) {
+                    item_path = col;
+                    break;
+                }
+            }
+
+            const p = item_path orelse continue;
+
+            if (seen.contains(p)) continue;
+
+            std.fs.cwd().access(p, .{}) catch |err| {
+                if (err == error.FileNotFound) {
+                    missing_entries += 1;
+                    continue;
+                }
+                return err;
+            };
+
+            try seen.put(p, {});
+            try lines.append(self.allocator, trimmed);
+        }
+
+        const duplicates = total_entries - lines.items.len - missing_entries;
+        io.warn("  Entries: {d} total, {d} unique, {d} missing, {d} duplicates removed\n", .{ total_entries, lines.items.len, missing_entries, duplicates });
+
+        const write_file = try std.fs.cwd().createFile(path, .{ .truncate = true });
+        defer write_file.close();
+
+        var write_buf: [4096]u8 = undefined;
+        var writer = write_file.writer(&write_buf);
+
+        var i: usize = lines.items.len;
+        while (i > 0) {
+            i -= 1;
+            try writer.interface.print("{s}\n", .{lines.items[i]});
+        }
+        try writer.end();
+    }
+};
+
+pub fn getDefaultDbPath(allocator: std.mem.Allocator) ![]const u8 {
+    if (std.posix.getenv("HOME")) |home| {
+        return std.fs.path.join(allocator, &.{ home, ".magdalena" });
+    }
+    return error.HomeNotFound;
+}
blob - /dev/null
blob + 50989ef8c80a2225b9e99a9dff9648184a88c400 (mode 644)
--- /dev/null
+++ src/fzf.zig
@@ -0,0 +1,557 @@
+const std = @import("std");
+
+const cache = @import("memcached.zig");
+const cfg = @import("cfg.zig");
+const hist = @import("db.zig");
+const io = @import("io.zig");
+
+fn gotoFile(allocator: std.mem.Allocator, db: ?*hist.Db, file_path: []const u8, line: ?[]const u8, config: *const cfg.Config) !void {
+    const ext = if (std.mem.lastIndexOfScalar(u8, file_path, '.')) |idx| file_path[idx + 1 ..] else "";
+
+    var action: []const u8 = "text_editor";
+    var command: ?[]const u8 = null;
+
+    for (config.openers) |opener| {
+        for (opener.extensions) |e| {
+            if (std.mem.eql(u8, e, ext)) {
+                action = opener.action;
+                command = opener.command;
+                break;
+            }
+        }
+        if (command != null) break;
+    }
+
+    const term = if (command) |cmd| blk: {
+        var child = std.process.Child.init(&.{ cmd, file_path }, allocator);
+        child.stdin_behavior = .Inherit;
+        child.stdout_behavior = .Inherit;
+        child.stderr_behavior = .Inherit;
+        break :blk try child.spawnAndWait();
+    } else blk: {
+        const editor = config.editor orelse
+            std.process.getEnvVarOwned(allocator, "EDITOR") catch |err| switch (err) {
+            error.EnvironmentVariableNotFound => return error.EditorNotSet,
+            else => return err,
+        };
+        defer if (config.editor == null) allocator.free(editor);
+
+        if (line) |l| {
+            const line_arg = try std.fmt.allocPrint(allocator, "+{s}", .{l});
+            defer allocator.free(line_arg);
+            var child = std.process.Child.init(&.{ editor, line_arg, file_path }, allocator);
+            child.stdin_behavior = .Inherit;
+            child.stdout_behavior = .Inherit;
+            child.stderr_behavior = .Inherit;
+            break :blk try child.spawnAndWait();
+        }
+
+        var child = std.process.Child.init(&.{ editor, file_path }, allocator);
+        child.stdin_behavior = .Inherit;
+        child.stdout_behavior = .Inherit;
+        child.stderr_behavior = .Inherit;
+        break :blk try child.spawnAndWait();
+    };
+
+    if (term == .Exited and term.Exited == 0) {
+        if (db) |d| {
+            try d.logFile(file_path, ext, action);
+        }
+    }
+}
+
+const Fzf = struct {
+    child: std.process.Child,
+    argv: [][]const u8,
+    allocator: std.mem.Allocator,
+
+    pub fn init(allocator: std.mem.Allocator, fzf_opts: []const []const u8, extra_opts: []const []const u8) !Fzf {
+        var argv = try allocator.alloc([]const u8, 1 + fzf_opts.len + extra_opts.len);
+        errdefer allocator.free(argv);
+
+        var i: usize = 0;
+        argv[i] = "fzf";
+        i += 1;
+        for (fzf_opts) |opt| {
+            argv[i] = opt;
+            i += 1;
+        }
+        for (extra_opts) |opt| {
+            argv[i] = opt;
+            i += 1;
+        }
+
+        var child = std.process.Child.init(argv, allocator);
+        child.stdin_behavior = .Pipe;
+        child.stdout_behavior = .Pipe;
+        child.stderr_behavior = .Inherit;
+
+        return .{
+            .child = child,
+            .argv = argv,
+            .allocator = allocator,
+        };
+    }
+
+    pub fn deinit(self: *Fzf) void {
+        self.allocator.free(self.argv);
+    }
+};
+
+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| {
+        for (ignored_patterns) |pattern| {
+            if (std.mem.eql(u8, component, pattern)) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+fn runFzfPicker(allocator: std.mem.Allocator, fzf_opts: []const []const u8, items: []const []const u8) ![]u8 {
+    var fzf = try Fzf.init(allocator, fzf_opts, &[_][]const u8{});
+    defer fzf.deinit();
+    const child = &fzf.child;
+    try child.spawn();
+
+    if (child.stdin) |*stdin| {
+        // Write items in batches to prevent excessive buffer usage
+        const batch_size = 1000;
+        for (0..((items.len + batch_size - 1) / batch_size)) |batch_idx| {
+            const start = batch_idx * batch_size;
+            const end = @min(start + batch_size, items.len);
+
+            for (start..end) |i| {
+                stdin.writeAll(items[i]) catch |err| {
+                    if (err == error.BrokenPipe) break;
+                    return err;
+                };
+                stdin.writeAll("\n") catch |err| {
+                    if (err == error.BrokenPipe) break;
+                    return err;
+                };
+            }
+        }
+        stdin.close();
+        child.stdin = null;
+    }
+
+    var stdout_buf = std.ArrayListUnmanaged(u8){};
+    defer stdout_buf.deinit(allocator);
+
+    if (child.stdout) |stdout| {
+        var buffer: [1024]u8 = undefined;
+        while (true) {
+            const bytes_read = try stdout.read(&buffer);
+            if (bytes_read == 0) break;
+            try stdout_buf.appendSlice(allocator, buffer[0..bytes_read]);
+        }
+    }
+
+    const term = try child.wait();
+    if (term != .Exited or term.Exited != 0) return error.UserAbort;
+    const selected = std.mem.trim(u8, stdout_buf.items, " \n\r\t");
+    if (selected.len == 0) return error.UserAbort;
+
+    return allocator.dupe(u8, selected);
+}
+
+pub fn recentDir(allocator: std.mem.Allocator, db: *hist.Db, config: *const cfg.Config) !void {
+    const managed_dirs = try db.recentDirs();
+    defer managed_dirs.deinit();
+    const dirs = managed_dirs.value;
+
+    if (dirs.len == 0) {
+        io.warn("No recent directories found.\n", .{});
+        return;
+    }
+
+    var items = std.ArrayListUnmanaged([]const u8){};
+    defer items.deinit(allocator);
+
+    for (dirs) |dir| {
+        if (shouldSkip(dir.path, config.ignored_patterns)) continue;
+        try items.append(allocator, dir.path);
+    }
+
+    const selected = try runFzfPicker(allocator, config.fzf_opts, items.items);
+    defer allocator.free(selected);
+
+    std.fs.accessAbsolute(selected, .{}) catch |err| {
+        io.warn("Selected directory no longer exists: {s} ({})\n", .{ selected, err });
+        return error.UserAbort;
+    };
+
+    var out_buf: [4096]u8 = undefined;
+    var out = io.getStdout(&out_buf);
+    try out.print("{s}\n", .{selected});
+    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", .{});
+        return;
+    }
+
+    var items = std.ArrayListUnmanaged([]const u8){};
+    defer {
+        for (items.items) |item| allocator.free(item);
+        items.deinit(allocator);
+    }
+
+    for (config.fav_dirs) |dir| {
+        const expanded = try expandPath(allocator, dir);
+        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();
+}
+
+fn expandPath(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
+    var res = std.ArrayListUnmanaged(u8){};
+    errdefer res.deinit(allocator);
+
+    var i: usize = 0;
+    while (i < path.len) {
+        if (std.mem.startsWith(u8, path[i..], "$HOME")) {
+            const home = std.posix.getenv("HOME") orelse "/home/user";
+            try res.appendSlice(allocator, home);
+            i += 5;
+        } else {
+            try res.append(allocator, path[i]);
+            i += 1;
+        }
+    }
+
+    return res.toOwnedSlice(allocator);
+}
+
+pub fn recentFile(allocator: std.mem.Allocator, db: *hist.Db, config: *const cfg.Config) !void {
+    const managed_files = try db.recentFiles();
+    defer managed_files.deinit();
+    const files = managed_files.value;
+
+    if (files.len == 0) {
+        io.warn("No recent files found.\n", .{});
+        return;
+    }
+
+    var items = std.ArrayListUnmanaged([]const u8){};
+    defer items.deinit(allocator);
+
+    for (files) |file| {
+        if (shouldSkip(file.path, config.ignored_patterns)) continue;
+        try items.append(allocator, file.path);
+    }
+
+    const selected = try runFzfPicker(allocator, config.fzf_opts, items.items);
+    defer allocator.free(selected);
+
+    try std.fs.accessAbsolute(selected, .{});
+
+    try gotoFile(allocator, db, selected, null, config);
+}
+
+pub fn grep(allocator: std.mem.Allocator, db: *hist.Db, config: *const cfg.Config) !void {
+    const initial_query = "";
+
+    var rg_cmd = std.ArrayListUnmanaged(u8){};
+    defer rg_cmd.deinit(allocator);
+    try rg_cmd.appendSlice(allocator, "change:reload:rg --column --line-number --no-heading --color=always --smart-case");
+    for (config.ignored_patterns) |pattern| {
+        try rg_cmd.appendSlice(allocator, " --glob '!");
+        try rg_cmd.appendSlice(allocator, pattern);
+        try rg_cmd.appendSlice(allocator, "/*'");
+        try rg_cmd.appendSlice(allocator, " --glob '!");
+        try rg_cmd.appendSlice(allocator, pattern);
+        try rg_cmd.appendSlice(allocator, "'");
+    }
+    try rg_cmd.appendSlice(allocator, " {q} || true");
+
+    var fzf = try Fzf.init(allocator, config.fzf_opts, &[_][]const u8{
+        "--disabled",
+        "--query",
+        initial_query,
+        "--bind",
+        rg_cmd.items,
+        "--delimiter",
+        ":",
+        "--preview",
+        "bat --highlight-line {2} {1}",
+        "--preview-window",
+        "right,60%,border-left,+{2}+3/3,~3",
+    });
+    defer fzf.deinit();
+    const child = &fzf.child;
+
+    child.stdin_behavior = .Inherit;
+    child.stdout_behavior = .Pipe;
+    child.stderr_behavior = .Inherit;
+
+    try child.spawn();
+
+    var stdout_buf = std.ArrayListUnmanaged(u8){};
+    defer stdout_buf.deinit(allocator);
+
+    if (child.stdout) |stdout| {
+        var buffer: [1024]u8 = undefined;
+        while (true) {
+            const bytes_read = try stdout.read(&buffer);
+            if (bytes_read == 0) break;
+            try stdout_buf.appendSlice(allocator, buffer[0..bytes_read]);
+        }
+    }
+
+    const term = try child.wait();
+    if (term != .Exited or term.Exited != 0) return error.UserAbort;
+    const selected = std.mem.trim(u8, stdout_buf.items, " \n\r\t");
+    if (selected.len == 0) return error.UserAbort;
+
+    var it = std.mem.splitScalar(u8, selected, ':');
+    if (it.next()) |file| {
+        const line = it.next();
+        try gotoFile(allocator, db, file, line, config);
+    }
+}
+
+pub fn lookFile(allocator: std.mem.Allocator, db: *hist.Db, config: *const cfg.Config, depth: ?usize) !void {
+    try runFzfWithWalker(allocator, db, .file, config, depth);
+}
+
+pub fn lookDir(allocator: std.mem.Allocator, db: *hist.Db, config: *const cfg.Config, depth: ?usize) !void {
+    try runFzfWithWalker(allocator, db, .directory, config, depth);
+}
+
+const Entry = struct {
+    path: []const u8,
+    mtime: i128,
+};
+
+fn getMaxDirMtime(allocator: std.mem.Allocator, ignored_patterns: []const []const u8, max_depth: usize) !i128 {
+    var max_mtime: i128 = 0;
+
+    if (std.fs.cwd().statFile(".")) |stat| {
+        max_mtime = stat.mtime;
+    } else |_| {}
+
+    const StackEntry = struct {
+        dir: std.fs.Dir,
+        depth: usize,
+    };
+    var stack = std.ArrayListUnmanaged(StackEntry){};
+    defer {
+        for (stack.items) |*se| se.dir.close();
+        stack.deinit(allocator);
+    }
+
+    try stack.append(allocator, .{ .dir = try std.fs.cwd().openDir(".", .{ .iterate = true }), .depth = 0 });
+
+    while (stack.items.len > 0) {
+        var current = stack.pop() orelse unreachable;
+        defer current.dir.close();
+
+        var it = current.dir.iterate();
+        while (try it.next()) |entry| {
+            var skip = false;
+            for (ignored_patterns) |pattern| {
+                if (std.mem.eql(u8, entry.name, pattern)) {
+                    skip = true;
+                    break;
+                }
+            }
+            if (skip) continue;
+
+            const is_dir = entry.kind == .directory;
+            if (is_dir or entry.kind == .sym_link) {
+                const stat = current.dir.statFile(entry.name) catch |err| {
+                    if (err != error.FileNotFound and err != error.AccessDenied) {
+                        io.warn("failed to stat {s}: {}\n", .{ entry.name, err });
+                    }
+                    continue;
+                };
+                if (stat.kind == .directory) {
+                    if (stat.mtime > max_mtime) max_mtime = stat.mtime;
+
+                    if (is_dir and current.depth + 1 < max_depth) {
+                        const sub_dir = current.dir.openDir(entry.name, .{ .iterate = true }) catch |err| {
+                            io.warn("failed to open directory {s}: {}\n", .{ entry.name, err });
+                            continue;
+                        };
+                        try stack.append(allocator, .{ .dir = sub_dir, .depth = current.depth + 1 });
+                    }
+                }
+            }
+        }
+    }
+
+    return max_mtime;
+}
+
+fn runFzfWithWalker(allocator: std.mem.Allocator, db: *hist.Db, kind: std.fs.File.Kind, config: *const cfg.Config, depth: ?usize) !void {
+    const is_dir = kind == .directory;
+    const max_depth = depth orelse config.max_depth;
+
+    var entries = std.ArrayListUnmanaged(Entry){};
+    defer {
+        for (entries.items) |e| allocator.free(e.path);
+        entries.deinit(allocator);
+    }
+
+    var mc = cache.Memcached.init(allocator, "127.0.0.1", 11211) catch |err| blk: {
+        if (err != error.ConnectionRefused) {
+            io.warn("failed to initialize memcached: {}\n", .{err});
+        }
+        break :blk null;
+    };
+    defer if (mc) |*m| m.deinit();
+
+    var cache_key: ?[]const u8 = null;
+    defer if (cache_key) |k| allocator.free(k);
+
+    var cache_hit = false;
+    if (mc) |*m| {
+        const cwd = blk: {
+            break :blk std.process.getCwdAlloc(allocator) catch |err| {
+                io.warn("failed to get cwd for cache: {}\n", .{err});
+                break :blk null;
+            };
+        };
+        if (cwd) |c| {
+            defer allocator.free(c);
+
+            const hash = std.hash.Crc32.hash(c);
+            const max_dir_mtime = getMaxDirMtime(allocator, config.ignored_patterns, max_depth) catch blk: {
+                break :blk @as(i128, 0);
+            };
+            const mtime = @divTrunc(max_dir_mtime, std.time.ns_per_s);
+            cache_key = try std.fmt.allocPrint(allocator, "magdalena:look:{s}:{x}:{d}:{d}", .{ if (is_dir) "dir" else "file", hash, mtime, max_depth });
+            if (cache_key) |key| {
+                const cached_data = try m.get(key);
+                if (cached_data) |data| {
+                    defer allocator.free(data);
+                    const parsed = try std.json.parseFromSlice([]Entry, allocator, data, .{});
+                    defer parsed.deinit();
+                    for (parsed.value) |e| {
+                        try entries.append(allocator, .{
+                            .path = try allocator.dupe(u8, e.path),
+                            .mtime = e.mtime,
+                        });
+                    }
+                    cache_hit = true;
+                }
+            }
+        }
+    }
+
+    if (!cache_hit) {
+        const StackEntry = struct {
+            dir: std.fs.Dir,
+            path: ?[]const u8,
+            depth: usize,
+        };
+        var stack = std.ArrayListUnmanaged(StackEntry){};
+        defer {
+            for (stack.items) |*se| se.dir.close();
+            stack.deinit(allocator);
+        }
+
+        try stack.append(allocator, .{ .dir = try std.fs.cwd().openDir(".", .{ .iterate = true }), .path = null, .depth = 0 });
+
+        while (stack.items.len > 0) {
+            var current = stack.pop() orelse unreachable;
+            defer {
+                current.dir.close();
+                if (current.path) |p| allocator.free(p);
+            }
+
+            var it = current.dir.iterate();
+            while (try it.next()) |entry| {
+                const entry_path = if (current.path) |p|
+                    try std.fs.path.join(allocator, &.{ p, entry.name })
+                else
+                    try allocator.dupe(u8, entry.name);
+                errdefer allocator.free(entry_path);
+
+                if (shouldSkip(entry_path, config.ignored_patterns)) {
+                    allocator.free(entry_path);
+                    continue;
+                }
+
+                if (entry.kind == kind or entry.kind == .sym_link) {
+                    if (current.dir.statFile(entry.name)) |stat| {
+                        if (stat.kind == kind) {
+                            try entries.append(allocator, .{
+                                .path = try allocator.dupe(u8, entry_path),
+                                .mtime = stat.mtime,
+                            });
+                        }
+                    } else |err| {
+                        if (err != error.FileNotFound and err != error.AccessDenied) {
+                            io.warn("failed to stat {s}: {}\n", .{ entry.name, err });
+                        }
+                    }
+                }
+
+                if (entry.kind == .directory and current.depth + 1 < max_depth) {
+                    const sub_dir = current.dir.openDir(entry.name, .{ .iterate = true }) catch |err| {
+                        io.warn("failed to open directory {s}: {}\n", .{ entry.name, err });
+                        allocator.free(entry_path);
+                        continue;
+                    };
+                    try stack.append(allocator, .{ .dir = sub_dir, .path = entry_path, .depth = current.depth + 1 });
+                } else {
+                    allocator.free(entry_path);
+                }
+            }
+        }
+
+        if (mc) |*m| {
+            if (cache_key) |key| {
+                const json_data = try std.json.Stringify.valueAlloc(allocator, entries.items, .{});
+                defer allocator.free(json_data);
+                try m.set(key, json_data, 3600);
+            }
+        }
+    }
+
+    std.mem.sortUnstable(Entry, entries.items, {}, struct {
+        fn lessThan(_: void, a: Entry, b: Entry) bool {
+            return a.mtime > b.mtime;
+        }
+    }.lessThan);
+
+    var items = std.ArrayListUnmanaged([]const u8){};
+    defer items.deinit(allocator);
+    for (entries.items) |entry| {
+        try items.append(allocator, entry.path);
+    }
+
+    const selected = try runFzfPicker(allocator, config.fzf_opts, items.items);
+    defer allocator.free(selected);
+
+    const clean_path = if (std.mem.startsWith(u8, selected, "./")) selected[2..] else selected;
+
+    if (clean_path.len > 0) {
+        std.fs.cwd().access(clean_path, .{}) catch |err| {
+            io.warn("Selected item no longer exists or is inaccessible: {s} ({})\n", .{ clean_path, err });
+            return error.UserAbort;
+        };
+        if (is_dir) {
+            var out_buf: [4096]u8 = undefined;
+            var out = io.getStdout(&out_buf);
+            try out.print("{s}\n", .{clean_path});
+            try out.end();
+        } else {
+            try gotoFile(allocator, db, clean_path, null, config);
+        }
+    }
+}
blob - /dev/null
blob + 684a010edcc132405af3fb664f9de7b24a698094 (mode 644)
--- /dev/null
+++ src/io.zig
@@ -0,0 +1,48 @@
+const std = @import("std");
+
+pub const Writer = struct {
+    inner: std.fs.File.Writer,
+    failed: bool = false,
+
+    pub fn print(self: *Writer, comptime fmt: []const u8, args: anytype) !void {
+        if (self.failed) return;
+        self.inner.interface.print(fmt, args) catch |err| {
+            if (err == error.WriteFailed) {
+                self.failed = true;
+                return;
+            }
+            return err;
+        };
+    }
+
+    pub fn end(self: *Writer) !void {
+        if (self.failed) return;
+        self.inner.end() catch |err| {
+            if (err == error.WriteFailed) {
+                self.failed = true;
+                return;
+            }
+            return err;
+        };
+    }
+};
+
+pub fn getStdout(buf: []u8) Writer {
+    return .{ .inner = std.fs.File.stdout().writer(buf) };
+}
+
+pub fn warn(comptime fmt: []const u8, args: anytype) void {
+    var buf: [4096]u8 = undefined;
+    var writer = std.fs.File.stderr().writer(&buf);
+    writer.interface.print(fmt, args) catch |err| {
+        std.log.warn("failed to write to stderr: {}", .{err});
+    };
+    writer.end() catch |err| {
+        std.log.warn("failed to end stderr writer: {}", .{err});
+    };
+}
+
+pub fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
+    warn(fmt ++ "\n", args);
+    std.process.exit(1);
+}
blob - /dev/null
blob + 364f5c862db1c24fdcad9eb47d5e04a21b292543 (mode 644)
--- /dev/null
+++ src/main.zig
@@ -0,0 +1,117 @@
+const std = @import("std");
+const cli = @import("cli.zig");
+const cfg = @import("cfg.zig");
+const fzf = @import("fzf.zig");
+const hist = @import("db.zig");
+const io = @import("io.zig");
+
+pub fn main() void {
+    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+    defer _ = gpa.deinit();
+    const allocator = gpa.allocator();
+
+    const args = cli.parseArgs(allocator) catch |err| {
+        io.fatal("failed to parse arguments: {}", .{err});
+    };
+    defer args.deinit();
+
+    if (args.action == .help) {
+        cli.printUsage() catch |err| {
+            io.fatal("failed to print usage: {}", .{err});
+        };
+        return;
+    }
+
+    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 db_path = hist.getDefaultDbPath(allocator) catch |err| {
+        io.fatal("failed to get database path: {}", .{err});
+    };
+    defer allocator.free(db_path);
+
+    var db = hist.Db.init(allocator, db_path) catch |err| {
+        io.fatal("failed to initialize database: {}", .{err});
+    };
+    defer db.deinit();
+
+    runAction(allocator, &db, args, &config) catch |err| {
+        if (err == error.UserAbort) {
+            std.process.exit(1);
+        }
+        io.fatal("action failed: {}", .{err});
+    };
+}
+
+fn runAction(allocator: std.mem.Allocator, db: *hist.Db, args: cli.Args, config: *const cfg.Config) !void {
+    var buf: [4096]u8 = undefined;
+    var writer = io.getStdout(&buf);
+
+    switch (args.action) {
+        .recent_dirs => {
+            const managed = try db.recentDirs();
+            defer managed.deinit();
+            for (managed.value) |d| {
+                try writer.print("{s}|{s}\n", .{ d.timestamp orelse "-", d.path });
+            }
+        },
+        .recent_files => {
+            const managed = try db.recentFiles();
+            defer managed.deinit();
+            for (managed.value) |f| {
+                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);
+        },
+        .search => {
+            if (args.query) |query| {
+                const managed = try db.searchHistory(query);
+                defer managed.deinit();
+                const results = managed.value;
+
+                for (results.directories) |d| {
+                    try writer.print("D|{s}\n", .{d.path});
+                }
+                for (results.files) |f| {
+                    try writer.print("F|{s}|{s}|{s}\n", .{ f.file_type, f.action, f.path });
+                }
+            }
+        },
+        .goto_dir => {
+            try fzf.recentDir(allocator, db, config);
+        },
+        .goto_file => {
+            try fzf.recentFile(allocator, db, config);
+        },
+        .look_file => {
+            try fzf.lookFile(allocator, db, config, args.depth);
+        },
+        .look_dir => {
+            try fzf.lookDir(allocator, db, config, args.depth);
+        },
+        .grep => {
+            try fzf.grep(allocator, db, config);
+        },
+        .cleanup => {
+            try db.cleanup();
+        },
+        .log_dir => {
+            if (args.log_path) |path| {
+                try db.logDir(path);
+            }
+        },
+        .log_file => {
+            if (args.log_path) |path| {
+                try db.logFile(path, args.file_type orelse "other", args.file_action orelse "open");
+            }
+        },
+        .help => {
+            try cli.printUsage();
+        },
+    }
+    try writer.end();
+}
blob - /dev/null
blob + a9bfd025d5140fb762317db8350bb8f2ee704197 (mode 644)
--- /dev/null
+++ src/memcached.zig
@@ -0,0 +1,230 @@
+const std = @import("std");
+const io = @import("io.zig");
+
+pub const Memcached = struct {
+    allocator: std.mem.Allocator,
+    stream: std.net.Stream,
+    write_buf: [4096]u8 = undefined,
+    write_end: usize = 0,
+    read_buf: [65536]u8 = undefined,
+    read_start: usize = 0,
+    read_end: usize = 0,
+
+    pub fn init(allocator: std.mem.Allocator, host: []const u8, port: u16) !Memcached {
+        const address = try std.net.Address.parseIp4(host, port);
+        const stream = try std.net.tcpConnectToAddress(address);
+        return .{
+            .allocator = allocator,
+            .stream = stream,
+        };
+    }
+
+    pub fn deinit(self: *Memcached) void {
+        self.flushWrite() catch |err| {
+            io.warn("failed to flush memcached on deinit: {}\n", .{err});
+        };
+        self.stream.close();
+    }
+
+    fn flushWrite(self: *Memcached) !void {
+        if (self.write_end == 0) return;
+        try self.stream.writeAll(self.write_buf[0..self.write_end]);
+        self.write_end = 0;
+    }
+
+    fn bufferedWrite(self: *Memcached, data: []const u8) !void {
+        if (data.len >= self.write_buf.len) {
+            try self.flushWrite();
+            try self.stream.writeAll(data);
+            return;
+        }
+        const available = self.write_buf.len - self.write_end;
+        if (data.len > available) {
+            try self.flushWrite();
+        }
+        @memcpy(self.write_buf[self.write_end..][0..data.len], data);
+        self.write_end += data.len;
+    }
+
+    const chunk_size = 900 * 1024;
+    const chunk_prefix = "\x00chunked:";
+
+    pub fn set(self: *Memcached, key: []const u8, value: []const u8, exptime: u32) !void {
+        if (value.len <= chunk_size) {
+            try self.sendSet(key, value, exptime);
+            return;
+        }
+
+        const num_chunks = (value.len + chunk_size - 1) / chunk_size;
+        var meta_buf: [128]u8 = undefined;
+        const meta = try std.fmt.bufPrint(&meta_buf, chunk_prefix ++ "{d}:{d}", .{ num_chunks, value.len });
+        try self.sendSet(key, meta, exptime);
+
+        var i: usize = 0;
+        while (i < num_chunks) : (i += 1) {
+            const start = i * chunk_size;
+            const end = @min((i + 1) * chunk_size, value.len);
+            var chunk_key_buf: [512]u8 = undefined;
+            const chunk_key = try std.fmt.bufPrint(&chunk_key_buf, "{s}:{d}", .{ key, i });
+            try self.sendSet(chunk_key, value[start..end], exptime);
+        }
+    }
+
+    fn sendSet(self: *Memcached, key: []const u8, value: []const u8, exptime: u32) !void {
+        var header_buf: [512]u8 = undefined;
+        const header = try std.fmt.bufPrint(&header_buf, "ms {s} {d} T{d} q\r\n", .{ key, value.len, exptime });
+
+        try self.bufferedWrite(header);
+        if (value.len > 0) {
+            try self.bufferedWrite(value);
+        }
+        try self.bufferedWrite("\r\n");
+    }
+
+    fn readLine(self: *Memcached, buf: []u8) !?[]const u8 {
+        while (true) {
+            if (self.read_start < self.read_end) {
+                if (std.mem.indexOfScalar(u8, self.read_buf[self.read_start..self.read_end], '\n')) |newline_offset| {
+                    const line_start = self.read_start;
+                    const line_end = self.read_start + newline_offset;
+                    self.read_start = line_end + 1;
+                    const line = self.read_buf[line_start..line_end];
+                    const trimmed = std.mem.trim(u8, line, "\r");
+                    if (buf.len < trimmed.len) return error.BufferTooSmall;
+                    @memcpy(buf[0..trimmed.len], trimmed);
+                    return buf[0..trimmed.len];
+                }
+            }
+            if (self.read_start > 0) {
+                const remaining = self.read_end - self.read_start;
+                @memcpy(self.read_buf[0..remaining], self.read_buf[self.read_start..self.read_end]);
+                self.read_end = remaining;
+                self.read_start = 0;
+            }
+            if (self.read_end >= self.read_buf.len) return error.LineTooLong;
+            const n = try self.stream.read(self.read_buf[self.read_end..]);
+            if (n == 0) return null;
+            self.read_end += n;
+        }
+    }
+
+    fn readExact(self: *Memcached, buf: []u8) !void {
+        var offset: usize = 0;
+        while (offset < buf.len) {
+            if (self.read_start < self.read_end) {
+                const available = self.read_end - self.read_start;
+                const needed = buf.len - offset;
+                const to_copy = @min(available, needed);
+                @memcpy(buf[offset..][0..to_copy], self.read_buf[self.read_start..][0..to_copy]);
+                self.read_start += to_copy;
+                offset += to_copy;
+                continue;
+            }
+            self.read_start = 0;
+            self.read_end = 0;
+            const remaining = buf.len - offset;
+            if (remaining >= self.read_buf.len) {
+                const n = try self.stream.read(buf[offset..]);
+                if (n == 0) return error.IncompleteRead;
+                offset += n;
+            } else {
+                const n = try self.stream.read(self.read_buf[0..]);
+                if (n == 0) return error.IncompleteRead;
+                self.read_end = n;
+            }
+        }
+    }
+
+    fn consumeCrlf(self: *Memcached) void {
+        if (self.read_start < self.read_end) {
+            const available = self.read_end - self.read_start;
+            if (available >= 2 and self.read_buf[self.read_start] == '\r' and self.read_buf[self.read_start + 1] == '\n') {
+                self.read_start += 2;
+                return;
+            }
+            if (available >= 1 and self.read_buf[self.read_start] == '\n') {
+                self.read_start += 1;
+                return;
+            }
+        }
+        var discard: [2]u8 = undefined;
+        _ = self.stream.read(&discard) catch return;
+    }
+
+    pub fn get(self: *Memcached, key: []const u8) !?[]u8 {
+        try self.flushWrite();
+        var cmd_buf: [512]u8 = undefined;
+        const cmd = try std.fmt.bufPrint(&cmd_buf, "mg {s} v\r\n", .{key});
+        try self.stream.writeAll(cmd);
+
+        var line_buf: [512]u8 = undefined;
+        const line = (try self.readLine(&line_buf)) orelse return null;
+
+        if (std.mem.startsWith(u8, line, "VA ")) {
+            const rest = line[3..];
+            const space_idx = std.mem.indexOfScalar(u8, rest, ' ') orelse rest.len;
+            const size = try std.fmt.parseInt(usize, rest[0..space_idx], 10);
+
+            const data = try self.allocator.alloc(u8, size);
+            errdefer self.allocator.free(data);
+
+            if (size > 0) {
+                try self.readExact(data);
+            }
+
+            self.consumeCrlf();
+
+            if (std.mem.startsWith(u8, data, chunk_prefix)) {
+                defer self.allocator.free(data);
+                return try self.reassembleChunks(key, data);
+            }
+
+            return data;
+        }
+
+        return null;
+    }
+
+    fn reassembleChunks(self: *Memcached, key: []const u8, meta: []const u8) ![]u8 {
+        var it = std.mem.splitScalar(u8, meta[chunk_prefix.len..], ':');
+        const num_chunks_str = it.next() orelse return error.InvalidCacheData;
+        const total_size_str = it.next() orelse return error.InvalidCacheData;
+
+        const num_chunks = try std.fmt.parseInt(usize, num_chunks_str, 10);
+        const total_size = try std.fmt.parseInt(usize, total_size_str, 10);
+
+        const result = try self.allocator.alloc(u8, total_size);
+        errdefer self.allocator.free(result);
+
+        var i: usize = 0;
+        while (i < num_chunks) : (i += 1) {
+            var chunk_key_buf: [512]u8 = undefined;
+            const chunk_key = try std.fmt.bufPrint(&chunk_key_buf, "{s}:{d}", .{ key, i });
+
+            var cmd_buf: [512]u8 = undefined;
+            const cmd = try std.fmt.bufPrint(&cmd_buf, "mg {s} v\r\n", .{chunk_key});
+            try self.stream.writeAll(cmd);
+
+            var line_buf: [512]u8 = undefined;
+            const line = (try self.readLine(&line_buf)) orelse return error.CacheChunkMissing;
+
+            if (!std.mem.startsWith(u8, line, "VA ")) return error.CacheChunkMissing;
+
+            const rest = line[3..];
+            const space_idx = std.mem.indexOfScalar(u8, rest, ' ') orelse rest.len;
+            const size = try std.fmt.parseInt(usize, rest[0..space_idx], 10);
+
+            const chunk_start = i * chunk_size;
+            const chunk_end = @min((i + 1) * chunk_size, total_size);
+            if (size != chunk_end - chunk_start) return error.InvalidChunkSize;
+
+            if (size > 0) {
+                try self.readExact(result[chunk_start..chunk_end]);
+            }
+
+            self.consumeCrlf();
+        }
+
+        return result;
+    }
+};
blob - /dev/null
blob + 4ef717fd00201a8b1c439ca17c022cc8aa2a6fc4 (mode 644)
--- /dev/null
+++ themes/bat/magdalena.tmTheme
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>name</key>
+	<string>magdalena</string>
+	<key>settings</key>
+	<array>
+		<dict>
+			<key>settings</key>
+			<dict>
+				<key>background</key>
+				<string>#2a1f2e</string>
+				<key>divider</key>
+				<string>#3d2a42</string>
+				<key>foreground</key>
+				<string>#c8b8bf</string>
+				<key>caret</key>
+				<string>#f0dfe5</string>
+				<key>invisibles</key>
+				<string>#3d2a42</string>
+				<key>lineHighlight</key>
+				<string>#3d2a42</string>
+				<key>selection</key>
+				<string>#c084b8</string>
+				<key>selectionForeground</key>
+				<string>#f0dfe5</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>name</key>
+			<string>Comments</string>
+			<key>scope</key>
+			<string>comment</string>
+			<key>settings</key>
+			<dict>
+				<key>foreground</key>
+				<string>#7a6e7a</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>name</key>
+			<string>Keywords</string>
+			<key>scope</key>
+			<string>keyword, storage.type, storage.modifier</string>
+			<key>settings</key>
+			<dict>
+				<key>foreground</key>
+				<string>#c084b8</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>name</key>
+			<string>Strings</string>
+			<key>scope</key>
+			<string>string</string>
+			<key>settings</key>
+			<dict>
+				<key>foreground</key>
+				<string>#e08060</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>name</key>
+			<string>Functions</string>
+			<key>scope</key>
+			<string>entity.name.function, support.function</string>
+			<key>settings</key>
+			<dict>
+				<key>foreground</key>
+				<string>#7a9fd4</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>name</key>
+			<string>Types</string>
+			<key>scope</key>
+			<string>entity.name.type, entity.other.inherited-class, support.type, support.class</string>
+			<key>settings</key>
+			<dict>
+				<key>foreground</key>
+				<string>#c4607a</string>
+			</dict>
+		</dict>
+	</array>
+</dict>
+</plist>