~gpanders/wk

effb62c943733b3f80858bed0b146c4d2cd16d47 — Greg Anders 5 months ago bf6cc36
Rename project to wk
M README.md => README.md +14 -20
@@ 1,58 1,52 @@
zet
===
wk
==

Command line tool to manage a [Zettelkästen][], written in [zig][].
Command line tool to manage a personal wiki, written in [zig][].

[Zettelkästen]: https://zettelkasten.de/posts/overview/
[zig]: https://ziglang.org

Installation
------------

After cloning this repository, you can install `zet` under `$PREFIX/bin` by
After cloning this repository, you can install `wk` under `$PREFIX/bin` by
running the following from the project root:

    make PREFIX=$PREFIX install

If `PREFIX` is not specified, `zet` will be installed under `zig-cache/bin/`.
If `PREFIX` is not specified, `wk` will be installed under `zig-cache/bin/`.

If you use macOS and Homebrew you can use

    brew install --HEAD gpanders/tap/zet
    brew install --HEAD gpanders/tap/wk

Usage
-----

Notes are stored in the directory represented by the environment variable
`$ZETTEL_DIR`. If `$ZETTEL_DIR` is unset, it defaults to
`$HOME/.local/share/zet/`.
`$WIKI_DIR`. If `$WIKI_DIR` is unset, it defaults to `$HOME/.local/share/wk/`.

Notes are created with

    zet new TITLE
    wk new TITLE

The file name of the new note will be the given title prepended with a unique
ID and a `.md` extension. The title can contain spaces, but be sure to wrap it
in quotes so your shell doesn't split it.

If you keep your notes in a git repository, `zet new` will automatically commit
your new note with the title as the commit message. There is (currently) no way
to disable this behavior.

To list all of your existing notes, use

    zet list
    wk list

This will print the title of each existing note along with its ID.

For a full list of available commands, use `zet help`. For more information on
a specific command, use `zet help COMMAND`.
For a full list of available commands, use `wk help`. For more information on
a specific command, use `wk help COMMAND`.

For commands that take arguments, the argument can be a note ID, title, or file
name (with or without the extension) of an existing note. The output of any
`zet` command can be used with other commands, allowing commands to be
`wk` command can be used with other commands, allowing commands to be
composed. For example, to open all notes containing the tag "foo", use

    zet tag foo | zet open
    wk tag foo | wk open

Most commands can be abbreviated. Use `zet help CMD` for more information.
Most commands can be abbreviated. Use `wk help CMD` for more information.

M build.zig => build.zig +1 -1
@@ 13,7 13,7 @@ pub fn build(b: *Builder) void {
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    const exe = b.addExecutable("zet", "src/main.zig");
    const exe = b.addExecutable("wk", "src/main.zig");
    exe.setTarget(target);
    exe.setBuildMode(mode);
    exe.linkSystemLibrary("c");

M src/cmd.zig => src/cmd.zig +2 -2
@@ 49,7 49,7 @@ pub const Command = struct {
};

pub fn printUsage(cmd: ?Command) void {
    stdout.print("Usage: zet {}\n\n", .{if (cmd) |_| cmd.?.usage else "COMMAND"}) catch return;
    stdout.print("Usage: wk {}\n\n", .{if (cmd) |_| cmd.?.usage else "COMMAND"}) catch return;

    if (cmd) |_| {
        stdout.print("{}\n", .{cmd.?.desc}) catch return;


@@ 65,7 65,7 @@ pub fn printUsage(cmd: ?Command) void {
            }) catch return;
        }
        stdout.print("\n", .{}) catch return;
        stdout.print("Use \"zet help COMMAND\" for more information about a command.\n", .{}) catch return;
        stdout.print("Use \"wk help COMMAND\" for more information about a command.\n", .{}) catch return;
    }
}


M src/cmd/backlinks.zig => src/cmd/backlinks.zig +5 -5
@@ 4,9 4,9 @@ const mem = std.mem;
const stdout = std.io.getStdOut().outStream();

const Command = @import("../cmd.zig").Command;
const Zettel = @import("../zettel.zig").Zettel;
const getZettels = @import("../zettel.zig").getZettels;
const updateBacklinks = @import("../zettel.zig").updateBacklinks;
const Page = @import("../core.zig").Page;
const getPages = @import("../core.zig").getPages;
const updateBacklinks = @import("../core.zig").updateBacklinks;

pub const cmd = Command{
    .name = "backlinks",


@@ 17,7 17,7 @@ pub const cmd = Command{
};

pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const zettels = try getZettels(allocator);
    try updateBacklinks(allocator, zettels);
    const pages = try getPages(allocator);
    try updateBacklinks(allocator, pages);
    stdout.print("Updated backlinks in all notes.\n", .{}) catch return;
}

M src/cmd/inbox.zig => src/cmd/inbox.zig +3 -3
@@ 6,8 6,8 @@ const os = std.os;
const warn = std.debug.warn;

const Command = @import("../cmd.zig").Command;
const Zettel = @import("../zettel.zig").Zettel;
const openZettels = @import("../zettel.zig").openZettels;
const Page = @import("../core.zig").Page;
const openPages = @import("../core.zig").openPages;

pub const cmd = Command{
    .name = "inbox",


@@ 18,5 18,5 @@ pub const cmd = Command{
};

pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    try openZettels(allocator, &[_][]const u8{"inbox.md"});
    try openPages(allocator, &[_][]const u8{"inbox.md"});
}

M src/cmd/list.zig => src/cmd/list.zig +6 -6
@@ 4,7 4,7 @@ const mem = std.mem;
const stdout = std.io.getStdOut().outStream();

const Command = @import("../cmd.zig").Command;
const getZettels = @import("../zettel.zig").getZettels;
const getPages = @import("../core.zig").getPages;

pub const cmd = Command{
    .name = "list",


@@ 15,14 15,14 @@ pub const cmd = Command{
};

pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const zettels = try getZettels(allocator);
    for (zettels) |zet| {
    const pages = try getPages(allocator);
    for (pages) |page| {
        if (args) |patterns| {
            if (zet.match(patterns)) {
                stdout.print("{} {}\n", .{ zet.id, zet.title }) catch return;
            if (page.match(patterns)) {
                stdout.print("{} {}\n", .{ page.id, page.title }) catch return;
            }
        } else {
            stdout.print("{} {}\n", .{ zet.id, zet.title }) catch return;
            stdout.print("{} {}\n", .{ page.id, page.title }) catch return;
        }
    }
}

M src/cmd/new.zig => src/cmd/new.zig +14 -14
@@ 6,11 6,11 @@ const mem = std.mem;
const stdout = std.io.getStdOut().outStream();

const Command = @import("../cmd.zig").Command;
const Zettel = @import("../zettel.zig").Zettel;
const Page = @import("../core.zig").Page;
const execAndCheck = @import("../util.zig").execAndCheck;
const getZettels = @import("../zettel.zig").getZettels;
const openZettels = @import("../zettel.zig").openZettels;
const updateBacklinks = @import("../zettel.zig").updateBacklinks;
const getPages = @import("../core.zig").getPages;
const openPages = @import("../core.zig").openPages;
const updateBacklinks = @import("../core.zig").updateBacklinks;

pub const cmd = Command{
    .name = "new",


@@ 23,35 23,35 @@ pub const cmd = Command{
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    if (args) |_| {
        const title = args.?[0];
        var new_zettel = try Zettel.new(allocator, title);
        var new_page = try Page.new(allocator, title);

        // Add new Zettel to git if in a git repo
        // Add new Page to git if in a git repo
        if (fs.cwd().access(".git", .{})) |_| {
            // Fail early if git is not found
            _ = execAndCheck(allocator, &[_][]const u8{
                "git",
                "add",
                new_zettel.fname,
                new_page.fname,
            }) catch |err| switch (err) {
                error.FileNotFound => {},
                else => return err,
            };
        } else |_| {}

        // If stdout is not a tty just print the new Zettel's filename
        // If stdout is not a tty just print the new Page's filename
        if (!io.getStdOut().isTty()) {
            stdout.print("{}\n", .{new_zettel.fname}) catch {};
            stdout.print("{}\n", .{new_page.fname}) catch {};
            return;
        }

        try openZettels(allocator, &[_][]const u8{new_zettel.fname});
        try openPages(allocator, &[_][]const u8{new_page.fname});

        new_zettel.contents = try new_zettel.read();
        new_page.contents = try new_page.read();

        var zettels = std.ArrayList(Zettel).fromOwnedSlice(allocator, try getZettels(allocator));
        try zettels.append(new_zettel);
        var pages = std.ArrayList(Page).fromOwnedSlice(allocator, try getPages(allocator));
        try pages.append(new_page);

        try updateBacklinks(allocator, zettels.items);
        try updateBacklinks(allocator, pages.items);
    } else {
        return error.MissingRequiredArgument;
    }

M src/cmd/open.zig => src/cmd/open.zig +10 -10
@@ 6,29 6,29 @@ const os = std.os;
const warn = std.debug.warn;

const Command = @import("../cmd.zig").Command;
const Zettel = @import("../zettel.zig").Zettel;
const getZettels = @import("../zettel.zig").getZettels;
const openZettels = @import("../zettel.zig").openZettels;
const updateBacklinks = @import("../zettel.zig").updateBacklinks;
const Page = @import("../core.zig").Page;
const getPages = @import("../core.zig").getPages;
const openPages = @import("../core.zig").openPages;
const updateBacklinks = @import("../core.zig").updateBacklinks;

pub const cmd = Command{
    .name = "open",
    .aliases = &[_][]const u8{"o"},
    .usage = "o|open [PATTERN [PATTERN ...]]",
    .desc = "Open notes matching any of the supplied PATTERNs in your editor. With no argument, start your editor in ZETTEL_DIR.",
    .desc = "Open notes matching any of the supplied PATTERNs in your editor. With no argument, start your editor in PAGE_DIR.",
    .run = run,
};

pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    var files = std.ArrayList([]const u8).init(allocator);

    const zettels = try getZettels(allocator);
    const pages = try getPages(allocator);
    if (args) |patterns| {
        for (zettels) |zet| {
            if (zet.match(patterns)) try files.append(zet.fname);
        for (pages) |page| {
            if (page.match(patterns)) try files.append(page.fname);
        }
    }

    try openZettels(allocator, files.items);
    try updateBacklinks(allocator, zettels);
    try openPages(allocator, files.items);
    try updateBacklinks(allocator, pages);
}

M src/cmd/preview.zig => src/cmd/preview.zig +9 -9
@@ 7,8 7,8 @@ const os = std.os;
const warn = std.debug.warn;

const Command = @import("../cmd.zig").Command;
const Zettel = @import("../zettel.zig").Zettel;
const getZettels = @import("../zettel.zig").getZettels;
const Page = @import("../core.zig").Page;
const getPages = @import("../core.zig").getPages;
const execAndCheck = @import("../util.zig").execAndCheck;

pub const cmd = Command{


@@ 22,15 22,15 @@ pub const cmd = Command{
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const patterns = if (args) |_| args.? else return error.MissingRequiredArgument;

    const zettels = try getZettels(allocator);
    const pages = try getPages(allocator);

    var output_files = std.ArrayList([]const u8).init(allocator);
    defer for (output_files.items) |file| fs.cwd().deleteFile(file) catch {};

    for (zettels) |zet| {
        if (!zet.match(patterns)) continue;
    for (pages) |page| {
        if (!page.match(patterns)) continue;

        const html_file = try convert(allocator, zet);
        const html_file = try convert(allocator, page);
        try output_files.append(html_file);

        if (os.getenv("BROWSER")) |browser| {


@@ 55,8 55,8 @@ pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!v
    std.time.sleep(1 * std.time.ns_per_s);
}

fn convert(allocator: *mem.Allocator, zet: Zettel) Command.Error![]const u8 {
    const output = try fmt.allocPrint(allocator, "{}{}", .{ zet.basename, ".html" });
fn convert(allocator: *mem.Allocator, page: Page) Command.Error![]const u8 {
    const output = try fmt.allocPrint(allocator, "{}{}", .{ page.basename, ".html" });

    _ = execAndCheck(allocator, &[_][]const u8{
        "pandoc",


@@ 67,7 67,7 @@ fn convert(allocator: *mem.Allocator, zet: Zettel) Command.Error![]const u8 {
        "markdown",
        "--output",
        output,
        zet.fname,
        page.fname,
    }) catch |err| switch (err) {
        error.FileNotFound => {
            warn("Error: couldn't find pandoc\n", .{});

M src/cmd/show.zig => src/cmd/show.zig +7 -7
@@ 5,8 5,8 @@ const mem = std.mem;

const ColorWriter = @import("../tty.zig").ColorWriter;
const Command = @import("../cmd.zig").Command;
const Zettel = @import("../zettel.zig").Zettel;
const getZettels = @import("../zettel.zig").getZettels;
const Page = @import("../core.zig").Page;
const getPages = @import("../core.zig").getPages;

pub const cmd = Command{
    .name = "show",


@@ 19,13 19,13 @@ pub const cmd = Command{
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    if (args) |patterns| {
        const writer = ColorWriter.new(std.io.getStdOut());
        const zettels = try getZettels(allocator);
        for (zettels) |zet| {
            if (!zet.match(patterns)) continue;
        const pages = try getPages(allocator);
        for (pages) |page| {
            if (!page.match(patterns)) continue;

            writer.setColor(.Blue).print("{}\n", .{zet.fname});
            writer.setColor(.Blue).print("{}\n", .{page.fname});

            var it = mem.split(zet.contents, "\n");
            var it = mem.split(page.contents, "\n");

            // Print front matter fences in yellow
            writer.setColor(.Yellow).print("{}\n", .{it.next()});

M src/cmd/sync.zig => src/cmd/sync.zig +6 -6
@@ 7,23 7,23 @@ const warn = std.debug.warn;

const Command = @import("../cmd.zig").Command;
const execAndCheck = @import("../util.zig").execAndCheck;
const getZettels = @import("../zettel.zig").getZettels;
const strftime = @import("../zettel.zig").strftime;
const getPages = @import("../core.zig").getPages;
const strftime = @import("../core.zig").strftime;

pub const cmd = Command{
    .name = "sync",
    .aliases = &[_][]const u8{},
    .usage = "sync",
    .desc = "Synchronize ZETTEL_DIR with the git remote, if one is configured.",
    .desc = "Synchronize PAGE_DIR with the git remote, if one is configured.",
    .run = run,
};

pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    fs.cwd().access(".git", .{}) catch return;

    const zettels = try getZettels(allocator);
    const pages = try getPages(allocator);

    var add_args = try std.ArrayList([]const u8).initCapacity(allocator, zettels.len + 2);
    var add_args = try std.ArrayList([]const u8).initCapacity(allocator, pages.len + 2);

    // TODO: zig 0.7.0
    // add_args.appendSliceAssumeCapacity(&[_][]const u8{ "git", "add" });


@@ 31,7 31,7 @@ pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!v
        add_args.appendAssumeCapacity(s);
    }

    for (zettels) |zet| add_args.appendAssumeCapacity(zet.fname);
    for (pages) |page| add_args.appendAssumeCapacity(page.fname);

    // Fail early if git is not found
    _ = execAndCheck(allocator, add_args.items) catch |err| switch (err) {

M src/cmd/tags.zig => src/cmd/tags.zig +7 -7
@@ 4,8 4,8 @@ const mem = std.mem;
const stdout = std.io.getStdOut().outStream();

const Command = @import("../cmd.zig").Command;
const Zettel = @import("../zettel.zig").Zettel;
const getZettels = @import("../zettel.zig").getZettels;
const Page = @import("../core.zig").Page;
const getPages = @import("../core.zig").getPages;

pub const cmd = Command{
    .name = "tags",


@@ 16,13 16,13 @@ pub const cmd = Command{
};

pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const zettels = try getZettels(allocator);
    const pages = try getPages(allocator);
    if (args) |tags| {
        outer: for (zettels) |zet| {
            for (zet.tags) |t| {
        outer: for (pages) |page| {
            for (page.tags) |t| {
                for (tags) |tag| {
                    if (ascii.eqlIgnoreCase(tag, t)) {
                        stdout.print("{} {}\n", .{ zet.id, zet.title }) catch return;
                        stdout.print("{} {}\n", .{ page.id, page.title }) catch return;
                        continue :outer;
                    }
                }


@@ 32,7 32,7 @@ pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!v
        // List tags
        var tags = std.ArrayList([]const u8).init(allocator);

        for (zettels) |zet| try tags.appendSlice(zet.tags);
        for (pages) |page| try tags.appendSlice(page.tags);

        std.sort.sort([]const u8, tags.items, struct {
            fn lessThan(lhs: []const u8, rhs: []const u8) bool {

R src/zettel.zig => src/core.zig +36 -36
@@ 14,7 14,7 @@ const id_len = "20200106121000".len;
const extension = ".md";
const bl_header = "## Backlinks";

pub const Zettel = struct {
pub const Page = struct {
    allocator: *mem.Allocator,
    basename: []const u8,
    fname: []const u8,


@@ 23,8 23,8 @@ pub const Zettel = struct {
    title: []const u8,
    contents: []const u8,

    /// Create a new Zettel with the given title.
    pub fn new(allocator: *mem.Allocator, title: []const u8) !Zettel {
    /// Create a new Page with the given title.
    pub fn new(allocator: *mem.Allocator, title: []const u8) !Page {
        const fname = try createFilename(allocator, title);
        var file = try fs.cwd().createFile(fname, .{});
        defer file.close();


@@ 34,7 34,7 @@ pub const Zettel = struct {
        const contents = try fmt.allocPrint(allocator, template, .{ title, date });
        try file.writeAll(contents);

        return Zettel{
        return Page{
            .allocator = allocator,
            .basename = fname[0 .. fname.len - extension.len],
            .fname = fname,


@@ 45,14 45,14 @@ pub const Zettel = struct {
        };
    }

    pub fn read(self: Zettel) ![]const u8 {
    pub fn read(self: Page) ![]const u8 {
        var file = try fs.cwd().openFile(self.fname, .{ .read = true });
        defer file.close();

        return try file.inStream().readAllAlloc(self.allocator, 1 * 1024 * 1024);
    }

    pub fn match(self: Zettel, keywords: []const []const u8) bool {
    pub fn match(self: Page, keywords: []const []const u8) bool {
        for (keywords) |keyword| {
            if (ascii.indexOfIgnoreCase(self.fname, keyword) != null or
                ascii.indexOfIgnoreCase(self.contents, keyword) != null)


@@ 72,8 72,8 @@ pub const Zettel = struct {
        return false;
    }

    /// Return a list of zettels linked to in the given zettel
    pub fn links(self: Zettel) ![]const []const u8 {
    /// Return a list of pages linked to in the given page
    pub fn links(self: Page) ![]const []const u8 {
        var links_list = std.ArrayList([]const u8).init(self.allocator);

        // Ignore any links beneath a preexisting backlinks header


@@ 103,13 103,13 @@ pub const Zettel = struct {
    }
};

pub fn openZettels(allocator: *mem.Allocator, zettels: []const []const u8) !void {
    var argv = try std.ArrayList([]const u8).initCapacity(allocator, zettels.len + 1);
pub fn openPages(allocator: *mem.Allocator, pages: []const []const u8) !void {
    var argv = try std.ArrayList([]const u8).initCapacity(allocator, pages.len + 1);
    argv.appendAssumeCapacity(os.getenv("EDITOR") orelse "vi");

    // TODO zig 0.7.0
    // argv.appendSliceAssumeCapacity(zettels)
    for (zettels) |zet| argv.appendAssumeCapacity(zet);
    // argv.appendSliceAssumeCapacity(pages)
    for (pages) |page| argv.appendAssumeCapacity(page);

    var proc = try std.ChildProcess.init(argv.items, allocator);
    defer proc.deinit();


@@ 125,43 125,43 @@ pub fn openZettels(allocator: *mem.Allocator, zettels: []const []const u8) !void
    }
}

pub fn getZettels(allocator: *mem.Allocator) ![]Zettel {
    var zettels = std.ArrayList(Zettel).init(allocator);
pub fn getPages(allocator: *mem.Allocator) ![]Page {
    var pages = std.ArrayList(Page).init(allocator);

    var dir = try fs.cwd().openDir(".", .{ .iterate = true });
    defer dir.close();

    var it = dir.iterate();
    while (try it.next()) |entry| {
        if (!hasId(entry.name) or !mem.endsWith(u8, entry.name, extension)) {
    while (try it.next()) |file| {
        if (!hasId(file.name) or !mem.endsWith(u8, file.name, extension)) {
            continue;
        }

        const zet = fromEntry(allocator, entry) catch |err| switch (err) {
        const page = fromFile(allocator, file) catch |err| switch (err) {
            error.InvalidFormat => continue,
            else => return err,
        };

        try zettels.append(zet);
        try pages.append(page);
    }

    std.sort.sort(Zettel, zettels.items, struct {
        fn lessThan(lhs: Zettel, rhs: Zettel) bool {
    std.sort.sort(Page, pages.items, struct {
        fn lessThan(lhs: Page, rhs: Page) bool {
            return mem.lessThan(u8, lhs.id, rhs.id);
        }
    }.lessThan);

    return zettels.toOwnedSlice();
    return pages.toOwnedSlice();
}

/// Find all backlinks between notes and write them to file
pub fn updateBacklinks(allocator: *mem.Allocator, zettels: []const Zettel) !void {
    for (zettels) |zet| {
pub fn updateBacklinks(allocator: *mem.Allocator, pages: []const Page) !void {
    for (pages) |page| {
        var backlinks = std.StringHashMap([]const u8).init(allocator);
        for (zettels) |other| {
            if (mem.eql(u8, other.id, zet.id)) continue;
        for (pages) |other| {
            if (mem.eql(u8, other.id, page.id)) continue;
            for (try other.links()) |link| {
                if (mem.eql(u8, link, zet.id)) {
                if (mem.eql(u8, link, page.id)) {
                    _ = try backlinks.put(other.id, other.title);
                }
            }


@@ 169,10 169,10 @@ pub fn updateBacklinks(allocator: *mem.Allocator, zettels: []const Zettel) !void

        if (backlinks.size == 0) continue;

        const start_index = if (mem.indexOf(u8, zet.contents, bl_header)) |s|
        const start_index = if (mem.indexOf(u8, page.contents, bl_header)) |s|
            s - 1
        else
            zet.contents.len;
            page.contents.len;

        var bl_list = try std.ArrayList([]const u8).initCapacity(allocator, backlinks.size);
        var it = backlinks.iterator();


@@ 183,15 183,15 @@ pub fn updateBacklinks(allocator: *mem.Allocator, zettels: []const Zettel) !void
        }

        const new_contents = try mem.join(allocator, "\n", &[_][]const u8{
            zet.contents[0 .. start_index - 1],
            page.contents[0 .. start_index - 1],
            "",
            bl_header,
            "",
            try mem.join(allocator, "\n", bl_list.items),
        });

        if (!mem.eql(u8, new_contents, zet.contents)) {
            var file = try fs.cwd().openFile(zet.fname, .{ .write = true });
        if (!mem.eql(u8, new_contents, page.contents)) {
            var file = try fs.cwd().openFile(page.fname, .{ .write = true });
            defer file.close();

            try file.outStream().writeAll(new_contents);


@@ 208,15 208,15 @@ pub fn strftime(allocator: *mem.Allocator, format: [:0]const u8) ![]const u8 {
    return allocator.shrink(buf, mem.len(buf.ptr));
}

fn fromEntry(allocator: *mem.Allocator, entry: fs.Dir.Entry) !Zettel {
    var file = try fs.cwd().openFile(entry.name, .{ .read = true });
fn fromFile(allocator: *mem.Allocator, page: fs.Dir.Entry) !Page {
    var file = try fs.cwd().openFile(page.name, .{ .read = true });
    defer file.close();

    const contents = try file.inStream().readAllAlloc(allocator, 1 * 1024 * 1024);

    var line_it = mem.split(contents, "\n");
    if (line_it.next()) |line| {
        // If first line is not a front matter fence, skip this entry
        // If first line is not a front matter fence, skip this page
        if (!mem.eql(u8, line, "---")) {
            return error.InvalidFormat;
        }


@@ 268,9 268,9 @@ fn fromEntry(allocator: *mem.Allocator, entry: fs.Dir.Entry) !Zettel {
    // If no title found, skip
    _ = title orelse return error.InvalidFormat;

    const fname = try mem.dupe(allocator, u8, entry.name);
    const fname = try mem.dupe(allocator, u8, page.name);

    return Zettel{
    return Page{
        .allocator = allocator,
        .basename = fname[0 .. fname.len - extension.len],
        .fname = fname,

M src/main.zig => src/main.zig +5 -5
@@ 20,12 20,12 @@ pub fn main() anyerror!void {
        return;
    }

    const dir = if (os.getenv("ZETTEL_DIR")) |dir|
    const dir = if (os.getenv("WIKI_DIR")) |dir|
        try mem.dupe(allocator, u8, dir)
    else if (os.getenv("XDG_DATA_HOME")) |xdg_data_home|
        try fs.path.join(allocator, &[_][]const u8{ xdg_data_home, "zet" })
        try fs.path.join(allocator, &[_][]const u8{ xdg_data_home, "wk" })
    else if (os.getenv("HOME")) |home|
        try fs.path.join(allocator, &[_][]const u8{ home, ".local", "share", "zet" })
        try fs.path.join(allocator, &[_][]const u8{ home, ".local", "share", "wk" })
    else
        unreachable; // $HOME should always be defined



@@ 40,12 40,12 @@ pub fn main() anyerror!void {

    cmd.handleCommand(allocator, command, args) catch |err| return switch (err) {
        error.UnknownCommand => {
            warn("Use \"zet help\" for usage.\n", .{});
            warn("Use \"wk help\" for usage.\n", .{});
            std.process.exit(1);
        },
        error.MissingRequiredArgument => {
            warn("Error: missing required argument\n\n", .{});
            warn("Use \"zet help\" for usage.\n", .{});
            warn("Use \"wk help\" for usage.\n", .{});
            std.process.exit(1);
        },
        error.CommandFailed, error.NoMatches => {