~gpanders/wk

9ed465e1510aff036589635d0b7de12c894e02d4 — Greg Anders 7 months ago 7111493
Import subnamespaces from standard lib

This helps reduce some code noise and shorten line length (just a bit).
M src/cmd.zig => src/cmd.zig +26 -20
@@ 1,8 1,14 @@
const std = @import("std");
const ascii = std.ascii;
const fmt = std.fmt;
const mem = std.mem;
const stdout = std.io.getStdOut().outStream();
const warn = std.debug.warn;

const util = @import("util.zig");
const zettel = @import("zettel.zig");
const commit = @import("util.zig").commit;
const Zettel = @import("zettel.zig").Zettel;
const getZettels = @import("zettel.zig").getZettels;
const findZettels = @import("zettel.zig").findZettels;

pub const backlinks = @import("cmd/backlinks.zig");
pub const list = @import("cmd/list.zig");


@@ 58,7 64,7 @@ pub fn printUsage(cmd: ?Command) void {
    } else {
        stdout.print("Available commands:\n", .{}) catch return;
        inline for (@typeInfo(Command).Enum.fields) |command| {
            const lower_cmd_name = [_]u8{std.ascii.toLower(command.name[0])} ++ command.name[1..];
            const lower_cmd_name = [_]u8{ascii.toLower(command.name[0])} ++ command.name[1..];
            // TODO (zig 0.7.0) Replace with format string specifier
            // stdout.print("{: 8}{: <16}{}\n", ...)
            stdout.print("        {}{}{}\n", .{


@@ 72,20 78,20 @@ pub fn printUsage(cmd: ?Command) void {
    }
}

pub fn handleCommand(allocator: *std.mem.Allocator, command: []const u8, args: ?[][]const u8) !void {
    if (std.mem.eql(u8, command, "help")) {
pub fn handleCommand(allocator: *mem.Allocator, command: []const u8, args: ?[][]const u8) !void {
    if (mem.eql(u8, command, "help")) {
        const subcmd = if (args != null) try parseCommand(args.?[0]) else null;
        printUsage(subcmd);
        return;
    }

    var zettels = try zettel.getZettels(allocator);
    var zettels = try getZettels(allocator);
    defer zettels.deinit();

    switch (try parseCommand(command)) {
        .Backlinks => {
            const matches = if (args) |_|
                try zettel.findZettels(allocator, zettels.items, args.?)
                try findZettels(allocator, zettels.items, args.?)
            else
                zettels.items;



@@ 94,7 100,7 @@ pub fn handleCommand(allocator: *std.mem.Allocator, command: []const u8, args: ?
        },
        .Show => {
            if (args) |_| {
                const matches = try zettel.findZettels(allocator, zettels.items, args.?);
                const matches = try findZettels(allocator, zettels.items, args.?);
                try show.run(allocator, matches);
            } else {
                return error.MissingRequiredArgument;


@@ 105,7 111,7 @@ pub fn handleCommand(allocator: *std.mem.Allocator, command: []const u8, args: ?
        },
        .Open => {
            const matches = if (args) |_|
                try zettel.findZettels(allocator, zettels.items, args.?)
                try findZettels(allocator, zettels.items, args.?)
            else
                null;



@@ 134,7 140,7 @@ pub fn handleCommand(allocator: *std.mem.Allocator, command: []const u8, args: ?
        },
        .Preview => {
            if (args) |_| {
                const matches = try zettel.findZettels(allocator, zettels.items, args.?);
                const matches = try findZettels(allocator, zettels.items, args.?);
                try preview.run(allocator, matches);
            } else {
                return error.MissingRequiredArgument;


@@ 146,20 152,20 @@ pub fn handleCommand(allocator: *std.mem.Allocator, command: []const u8, args: ?
    }
}

fn commitChanges(allocator: *std.mem.Allocator, cmd: Command, zettels: []const zettel.Zettel) !void {
    var modified = try std.ArrayList(zettel.Zettel).initCapacity(allocator, zettels.len);
fn commitChanges(allocator: *mem.Allocator, cmd: Command, zettels: []const Zettel) !void {
    var modified = try std.ArrayList(Zettel).initCapacity(allocator, zettels.len);
    defer modified.deinit();

    for (zettels) |*zet| if (try zet.modified()) modified.appendAssumeCapacity(zet.*);

    if (modified.items.len > 0) {
        const msg = switch (cmd) {
            .Backlinks => try std.fmt.allocPrint(
            .Backlinks => try fmt.allocPrint(
                allocator,
                "Updated backlinks in {} file(s)",
                .{modified.items.len},
            ),
            .New => try std.fmt.allocPrint(
            .New => try fmt.allocPrint(
                allocator,
                "New note: {}",
                .{zettels[zettels.len - 1].title},


@@ 168,14 174,14 @@ fn commitChanges(allocator: *std.mem.Allocator, cmd: Command, zettels: []const z
                var msg_lines = try std.ArrayList([]const u8).initCapacity(allocator, modified.items.len + 1);
                defer msg_lines.deinit();

                var line = try std.fmt.allocPrint(allocator, "Updated {} note(s):\n", .{modified.items.len});
                var line = try fmt.allocPrint(allocator, "Updated {} note(s):\n", .{modified.items.len});
                msg_lines.appendAssumeCapacity(line);
                for (modified.items) |item| {
                    line = try std.fmt.allocPrint(allocator, "- {} {}", .{ item.id, item.title });
                    line = try fmt.allocPrint(allocator, "- {} {}", .{ item.id, item.title });
                    msg_lines.appendAssumeCapacity(line);
                }

                break :blk try std.mem.join(allocator, "\n", msg_lines.items);
                break :blk try mem.join(allocator, "\n", msg_lines.items);
            },
            else => unreachable,
        };


@@ 185,7 191,7 @@ fn commitChanges(allocator: *std.mem.Allocator, cmd: Command, zettels: []const z

        for (modified.items) |item| files.appendAssumeCapacity(item.fname);

        try util.commit(allocator, files.items, msg);
        try commit(allocator, files.items, msg);
    }
}



@@ 222,14 228,14 @@ fn parseCommand(command: []const u8) !Command {
        return .Tags;
    }

    std.debug.warn("Error: unknown command: {}\n", .{command});
    warn("Error: unknown command: {}\n", .{command});
    return error.UnknownCommand;
}

/// Return true if the given `str` matches any of the strings in `patterns`.
fn strEq(str: []const u8, patterns: []const []const u8) bool {
    for (patterns) |pat| {
        if (std.mem.eql(u8, str, pat)) {
        if (mem.eql(u8, str, pat)) {
            return true;
        }
    }

M src/cmd/backlinks.zig => src/cmd/backlinks.zig +23 -19
@@ 1,4 1,8 @@
const std = @import("std");
const ascii = std.ascii;
const fmt = std.fmt;
const fs = std.fs;
const mem = std.mem;
const stdout = std.io.getStdOut().outStream();

const Zettel = @import("../zettel.zig").Zettel;


@@ 9,14 13,14 @@ pub const desc = "Add backlinks to given notes (all notes if no argument given).
const section_header = "## Backlinks";
const link_template = "- [[{}]]";

pub fn run(allocator: *std.mem.Allocator, all_zettels: []const Zettel, zettels: []const Zettel) !void {
pub fn run(allocator: *mem.Allocator, all_zettels: []const Zettel, zettels: []const Zettel) !void {
    // Map from zettel ID to list of IDs that link to this zettel
    var links = std.StringHashMap(std.ArrayList([]const u8)).init(allocator);
    defer links.deinit();

    // Search each zettel in the given list for links to other zettels
    for (zettels) |zet| {
        var file = try std.fs.cwd().openFile(zet.fname, .{ .read = true });
        var file = try fs.cwd().openFile(zet.fname, .{ .read = true });
        defer file.close();

        const size = try file.getEndPos();


@@ 24,26 28,26 @@ pub fn run(allocator: *std.mem.Allocator, all_zettels: []const Zettel, zettels: 
        defer allocator.free(contents);

        // Ignore any links beneath a preexisting backlinks header
        const end_index = std.mem.indexOf(u8, contents, section_header) orelse size;
        const end_index = mem.indexOf(u8, contents, section_header) orelse size;

        var start_index: usize = 0;
        outer: while (std.mem.indexOfPos(u8, contents[0..end_index], start_index, "[[")) |index| : (start_index = index + "[[".len) {
        outer: while (mem.indexOfPos(u8, contents[0..end_index], start_index, "[[")) |index| : (start_index = index + "[[".len) {
            // Check for valid link and ID
            const link = contents[index .. index + "[[".len + zet.id.len + "]]".len];
            if (!std.mem.startsWith(u8, link, "[[") or !std.mem.endsWith(u8, link, "]]")) {
            if (!mem.startsWith(u8, link, "[[") or !mem.endsWith(u8, link, "]]")) {
                continue;
            }

            const id = link["[[".len .. link.len - "]]".len];
            for (id) |c| {
                if (!std.ascii.isDigit(c)) {
                if (!ascii.isDigit(c)) {
                    continue :outer;
                }
            }

            const key = try std.mem.dupe(allocator, u8, id);
            const key = try mem.dupe(allocator, u8, id);
            const links_list = try links.getOrPutValue(key, std.ArrayList([]const u8).init(allocator));
            try links_list.value.append(try std.mem.dupe(allocator, u8, zet.id));
            try links_list.value.append(try mem.dupe(allocator, u8, zet.id));
        }
    }



@@ 55,7 59,7 @@ pub fn run(allocator: *std.mem.Allocator, all_zettels: []const Zettel, zettels: 
        // Find Zettel corresponding to entry
        const zet = blk: {
            for (all_zettels) |zet| {
                if (std.mem.eql(u8, zet.id, entry.kv.key)) {
                if (mem.eql(u8, zet.id, entry.kv.key)) {
                    break :blk zet;
                }
            }


@@ 70,12 74,12 @@ pub fn run(allocator: *std.mem.Allocator, all_zettels: []const Zettel, zettels: 
        defer list_items.deinit();

        for (link_list.items) |link| {
            const item = try std.fmt.allocPrint(allocator, link_template, .{link});
            const item = try fmt.allocPrint(allocator, link_template, .{link});
            list_items.appendAssumeCapacity(item);
        }

        // Read the contents of the file and find where to insert the backlinks
        var file = try std.fs.cwd().openFile(zet.fname, .{ .read = true, .write = true });
        var file = try fs.cwd().openFile(zet.fname, .{ .read = true, .write = true });
        defer file.close();

        const size = try file.getEndPos();


@@ 88,13 92,13 @@ pub fn run(allocator: *std.mem.Allocator, all_zettels: []const Zettel, zettels: 
        // Note that both "---" and "..." are valid closing delimiters of the
        // metadata block.
        const metadata_start = "---".len + 1;
        const metadata_end = std.mem.min(usize, &[_]usize{
            std.mem.indexOf(u8, contents[metadata_start..], "...") orelse size,
            std.mem.indexOf(u8, contents[metadata_start..], "---") orelse size,
        const metadata_end = mem.min(usize, &[_]usize{
            mem.indexOf(u8, contents[metadata_start..], "...") orelse size,
            mem.indexOf(u8, contents[metadata_start..], "---") orelse size,
        });

        const end_index = blk: {
            if (std.mem.lastIndexOf(u8, contents, "---")) |i| {
            if (mem.lastIndexOf(u8, contents, "---")) |i| {
                if (i <= metadata_end) {
                    // If last instance of "---" is in the metadata block, ignore it
                    break :blk size;


@@ 104,12 108,12 @@ pub fn run(allocator: *std.mem.Allocator, all_zettels: []const Zettel, zettels: 
            } else unreachable;
        };

        const start_index = if (std.mem.indexOf(u8, contents, section_header)) |s| s - 1 else end_index;
        const start_index = if (mem.indexOf(u8, contents, section_header)) |s| s - 1 else end_index;

        const backlinks = try std.mem.join(allocator, "\n", list_items.items);
        const backlinks = try mem.join(allocator, "\n", list_items.items);
        defer allocator.free(backlinks);

        const new_contents = try std.fmt.allocPrint(allocator, "{}\n\n{}\n\n{}\n{}", .{
        const new_contents = try fmt.allocPrint(allocator, "{}\n\n{}\n\n{}\n{}", .{
            contents[0 .. start_index - 1],
            section_header,
            backlinks,


@@ 117,7 121,7 @@ pub fn run(allocator: *std.mem.Allocator, all_zettels: []const Zettel, zettels: 
        });
        defer allocator.free(new_contents);

        if (!std.mem.eql(u8, new_contents, zet.contents)) {
        if (!mem.eql(u8, new_contents, zet.contents)) {
            try file.seekTo(0);
            try file.outStream().writeAll(new_contents);
            num_modified += 1;

M src/cmd/list.zig => src/cmd/list.zig +6 -4
@@ 1,17 1,19 @@
const std = @import("std");
const ascii = std.ascii;
const mem = std.mem;

const Zettel = @import("../zettel.zig").Zettel;

pub const usage = "l|list [PATTERN [PATTERN ...]]";
pub const desc = "With no argument, list all notes. Otherwise list notes matching any of the given patterns.";

pub fn run(allocator: *std.mem.Allocator, patterns: ?[][]const u8, zettels: []const Zettel) void {
pub fn run(allocator: *mem.Allocator, patterns: ?[][]const u8, zettels: []const Zettel) void {
    for (zettels) |zet| {
        if (patterns) |pats| {
            for (pats) |pat| {
                if (std.mem.indexOf(u8, zet.id, pat) != null or
                    std.ascii.indexOfIgnoreCase(zet.fname, pat) != null or
                    std.ascii.indexOfIgnoreCase(zet.title, pat) != null)
                if (mem.indexOf(u8, zet.id, pat) != null or
                    ascii.indexOfIgnoreCase(zet.fname, pat) != null or
                    ascii.indexOfIgnoreCase(zet.title, pat) != null)
                {
                    zet.print();
                }

M src/cmd/new.zig => src/cmd/new.zig +2 -1
@@ 1,10 1,11 @@
const std = @import("std");
const mem = std.mem;

const Zettel = @import("../zettel.zig").Zettel;

pub const usage = "n|new <TITLE>";
pub const desc = "Create a new note with the given title.";

pub fn run(allocator: *std.mem.Allocator, title: []const u8) !Zettel {
pub fn run(allocator: *mem.Allocator, title: []const u8) !Zettel {
    return try Zettel.new(allocator, title);
}

M src/cmd/open.zig => src/cmd/open.zig +7 -4
@@ 1,13 1,16 @@
const std = @import("std");
const mem = std.mem;
const os = std.os;
const warn = std.debug.warn;

const Zettel = @import("../zettel.zig").Zettel;

pub const usage = "o|open [NOTE [NOTE ...]]";
pub const desc = "Open the given notes in your $EDITOR.";

pub fn run(allocator: *std.mem.Allocator, zettels: ?[]const Zettel) !void {
pub fn run(allocator: *mem.Allocator, zettels: ?[]const Zettel) !void {
    var args = std.ArrayList([]const u8).init(allocator);
    try args.append(std.os.getenv("EDITOR") orelse "vi");
    try args.append(os.getenv("EDITOR") orelse "vi");
    if (zettels) |_| {
        for (zettels.?) |zet| {
            try args.append(zet.fname);


@@ 21,8 24,8 @@ pub fn run(allocator: *std.mem.Allocator, zettels: ?[]const Zettel) !void {
    switch (term) {
        .Exited => {},
        else => {
            std.debug.warn("The following command terminated unexpectedly:\n", .{});
            for (args.items) |arg| std.debug.warn("{} ", .{arg});
            warn("The following command terminated unexpectedly:\n", .{});
            for (args.items) |arg| warn("{} ", .{arg});
            return error.CommandFailed;
        },
    }

M src/cmd/preview.zig => src/cmd/preview.zig +20 -18
@@ 1,50 1,52 @@
const std = @import("std");
const fmt = std.fmt;
const fs = std.fs;
const mem = std.mem;
const os = std.os;
const warn = std.debug.warn;

const Zettel = @import("../zettel.zig").Zettel;
const util = @import("../util.zig");
const which = @import("../util.zig").which;
const execAndCheck = @import("../util.zig").execAndCheck;

pub const usage = "p|preview <NOTE> [NOTE ...]";
pub const desc = "View notes as HTML.";

pub fn run(allocator: *std.mem.Allocator, zettels: []const Zettel) !void {
    const pandoc = util.which(allocator, "pandoc") orelse {
        std.debug.warn("Couldn't find pandoc on PATH\n", .{});
pub fn run(allocator: *mem.Allocator, zettels: []const Zettel) !void {
    const pandoc = which(allocator, "pandoc") orelse {
        warn("Couldn't find pandoc on PATH\n", .{});
        return error.CommandFailed;
    };

    const open_cmd = if (std.os.getenv("BROWSER")) |browser|
        try std.mem.dupe(allocator, u8, browser)
    else if (util.which(allocator, "xdg-open")) |xdg_open|
    const open_cmd = if (os.getenv("BROWSER")) |browser|
        try mem.dupe(allocator, u8, browser)
    else if (which(allocator, "xdg-open")) |xdg_open|
        xdg_open
    else if (util.which(allocator, "open")) |open|
    else if (which(allocator, "open")) |open|
        open
    else if (util.which(allocator, "firefox")) |firefox|
    else if (which(allocator, "firefox")) |firefox|
        firefox
    else
        null;

    _ = open_cmd orelse {
        std.debug.warn("Error: couldn't find a way to open compiled notes\n", .{});
        warn("Error: couldn't find a way to open compiled notes\n", .{});
        return error.CommandFailed;
    };

    defer allocator.free(open_cmd.?);

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

    for (zettels) |zet| {
        var html_file = try std.fmt.allocPrint(allocator, "{}{}", .{ zet.basename, ".html" });
        // Don't defer free, it's done in the outer defer block
        var html_file = try fmt.allocPrint(allocator, "{}{}", .{ zet.basename, ".html" });
        try output_files.append(html_file);

        _ = try util.execAndCheck(allocator, &[_][]const u8{
        _ = try execAndCheck(allocator, &[_][]const u8{
            pandoc,
            "--standalone",
            "--to",


@@ 56,7 58,7 @@ pub fn run(allocator: *std.mem.Allocator, zettels: []const Zettel) !void {
            zet.fname,
        });

        _ = try util.execAndCheck(allocator, &[_][]const u8{ open_cmd.?, html_file });
        _ = try execAndCheck(allocator, &[_][]const u8{ open_cmd.?, html_file });
    }

    // Sleep so that the chosen `open_cmd` has time to open the file before it's deleted

M src/cmd/search.zig => src/cmd/search.zig +2 -1
@@ 1,4 1,5 @@
const std = @import("std");
const ascii = std.ascii;

const Zettel = @import("../zettel.zig").Zettel;



@@ 8,7 9,7 @@ pub const desc = "Search for notes whose contents match any of the given pattern
pub fn run(patterns: [][]const u8, zettels: []const Zettel) !void {
    for (zettels) |zet| {
        for (patterns) |pat| {
            if (std.ascii.indexOfIgnoreCase(zet.contents, pat)) |_| {
            if (ascii.indexOfIgnoreCase(zet.contents, pat)) |_| {
                zet.print();
                break;
            }

M src/cmd/show.zig => src/cmd/show.zig +4 -2
@@ 1,4 1,6 @@
const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const stdout = std.io.getStdOut().outStream();

const Zettel = @import("../zettel.zig").Zettel;


@@ 6,9 8,9 @@ const Zettel = @import("../zettel.zig").Zettel;
pub const usage = "sh|show <NOTE> [NOTE ...]";
pub const desc = "Display contents of notes to stdout.";

pub fn run(allocator: *std.mem.Allocator, zettels: []const Zettel) !void {
pub fn run(allocator: *mem.Allocator, zettels: []const Zettel) !void {
    for (zettels) |zet| {
        var file = try std.fs.cwd().openFile(zet.fname, .{ .read = true });
        var file = try fs.cwd().openFile(zet.fname, .{ .read = true });
        defer file.close();

        const size = try file.getEndPos();

M src/cmd/tags.zig => src/cmd/tags.zig +6 -4
@@ 1,4 1,6 @@
const std = @import("std");
const ascii = std.ascii;
const mem = std.mem;
const stdout = std.io.getStdOut().outStream();

const Zettel = @import("../zettel.zig").Zettel;


@@ 6,12 8,12 @@ const Zettel = @import("../zettel.zig").Zettel;
pub const usage = "t|tags [TAG [TAG ...]]";
pub const desc = "With no argument, list all tags found in notes. Otherwise list all notes containing any of the given tags.";

pub fn run(allocator: *std.mem.Allocator, tags: ?[][]const u8, zettels: []const Zettel) !void {
pub fn run(allocator: *mem.Allocator, tags: ?[][]const u8, zettels: []const Zettel) !void {
    if (tags) |_| {
        outer: for (zettels) |zet| {
            for (zet.tags) |t| {
                for (tags.?) |tag| {
                    if (std.ascii.eqlIgnoreCase(tag, t)) {
                    if (ascii.eqlIgnoreCase(tag, t)) {
                        zet.print();
                        continue :outer;
                    }


@@ 29,12 31,12 @@ pub fn run(allocator: *std.mem.Allocator, tags: ?[][]const u8, zettels: []const 

        std.sort.sort([]const u8, existing_tags.items, struct {
            fn lessThan(lhs: []const u8, rhs: []const u8) bool {
                return std.mem.lessThan(u8, lhs, rhs);
                return mem.lessThan(u8, lhs, rhs);
            }
        }.lessThan);

        for (existing_tags.items) |t, i| {
            if (i == 0 or !std.mem.eql(u8, t, existing_tags.items[i - 1])) {
            if (i == 0 or !mem.eql(u8, t, existing_tags.items[i - 1])) {
                stdout.print("{}\n", .{t}) catch continue;
            }
        }

M src/main.zig => src/main.zig +18 -14
@@ 1,4 1,8 @@
const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const os = std.os;
const warn = std.debug.warn;

const cmd = @import("cmd.zig");



@@ 16,34 20,34 @@ pub fn main() anyerror!void {
        return;
    }

    const dir = if (std.os.getenv("ZETTEL_DIR")) |dir|
        try std.mem.dupe(allocator, u8, dir)
    else if (std.os.getenv("XDG_DATA_HOME")) |xdg_data_home|
        try std.fs.path.join(allocator, &[_][]const u8{ xdg_data_home, "zet" })
    else if (std.os.getenv("HOME")) |home|
        try std.fs.path.join(allocator, &[_][]const u8{ home, ".local", "share", "zet" })
    const dir = if (os.getenv("ZETTEL_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" })
    else if (os.getenv("HOME")) |home|
        try fs.path.join(allocator, &[_][]const u8{ home, ".local", "share", "zet" })
    else
        unreachable; // $HOME should always be defined

    defer allocator.free(dir);

    try std.fs.cwd().makePath(dir);
    try fs.cwd().makePath(dir);
    try std.process.changeCurDir(dir);

    const command = arglist[1];
    var args: ?[][]const u8 = if (arglist.len > 2 and !std.mem.eql(u8, arglist[2], "-"))
    var args: ?[][]const u8 = if (arglist.len > 2 and !mem.eql(u8, arglist[2], "-"))
        arglist[2..]
    else
        try readFromStdin(allocator);

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


@@ 53,7 57,7 @@ pub fn main() anyerror!void {
    };
}

fn readFromStdin(allocator: *std.mem.Allocator) !?[][]const u8 {
fn readFromStdin(allocator: *mem.Allocator) !?[][]const u8 {
    if (std.io.getStdIn().isTty()) {
        return null;
    }


@@ 64,10 68,10 @@ fn readFromStdin(allocator: *std.mem.Allocator) !?[][]const u8 {
    var args = std.ArrayList([]const u8).init(allocator);
    defer args.deinit();

    var it = std.mem.split(input, "\n");
    var it = mem.split(input, "\n");
    while (it.next()) |line| {
        if (line.len == 0) continue;
        const arg = try std.mem.dupe(allocator, u8, line);
        const arg = try mem.dupe(allocator, u8, line);
        try args.append(arg);
    }


M src/util.zig => src/util.zig +25 -21
@@ 1,13 1,17 @@
const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const os = std.os;
const warn = std.debug.warn;

/// Return full path to the executable that would be run if the given `command` was called.
/// Caller owns the returned string.
pub fn which(allocator: *std.mem.Allocator, command: []const u8) ?[]const u8 {
    if (std.os.getenv("PATH")) |path| {
        var it = std.mem.split(path, &[_]u8{std.fs.path.delimiter});
pub fn which(allocator: *mem.Allocator, command: []const u8) ?[]const u8 {
    if (os.getenv("PATH")) |path| {
        var it = mem.split(path, &[_]u8{fs.path.delimiter});
        while (it.next()) |p| {
            const fullpath = std.fs.path.join(allocator, &[_][]const u8{ p, command }) catch return null;
            std.os.access(fullpath, std.os.F_OK | std.os.X_OK) catch {
            const fullpath = fs.path.join(allocator, &[_][]const u8{ p, command }) catch return null;
            os.access(fullpath, os.F_OK | os.X_OK) catch {
                allocator.free(fullpath);
                continue;
            };


@@ 22,11 26,11 @@ pub fn which(allocator: *std.mem.Allocator, command: []const u8) ?[]const u8 {
test "which" {
    const ls_actual = which(std.testing.allocator, "ls");
    std.testing.expect(ls_actual != null);
    std.testing.expect(std.mem.eql(u8, "/bin/ls", ls_actual.?));
    std.testing.expect(mem.eql(u8, "/bin/ls", ls_actual.?));
    std.testing.allocator.free(ls_actual.?);
}

pub fn execAndCheck(allocator: *std.mem.Allocator, args: [][]const u8) !std.ChildProcess.ExecResult {
pub fn execAndCheck(allocator: *mem.Allocator, args: [][]const u8) !std.ChildProcess.ExecResult {
    const result = try std.ChildProcess.exec(.{
        .allocator = allocator,
        .argv = args,


@@ 34,17 38,17 @@ pub fn execAndCheck(allocator: *std.mem.Allocator, args: [][]const u8) !std.Chil

    switch (result.term) {
        .Exited => |code| if (code != 0) {
            std.debug.warn("The following command failed with exit code {}:\n", .{code});
            for (args) |arg| std.debug.warn("{} ", .{arg});
            std.debug.warn("\n", .{});
            std.debug.warn("{}\n", .{result.stderr});
            warn("The following command failed with exit code {}:\n", .{code});
            for (args) |arg| warn("{} ", .{arg});
            warn("\n", .{});
            warn("{}\n", .{result.stderr});
            return error.CommandFailed;
        },
        else => {
            std.debug.warn("The following command terminated unexpectedly:\n", .{});
            for (args) |arg| std.debug.warn("{} ", .{arg});
            std.debug.warn("\n", .{});
            std.debug.warn("{}\n", .{result.stderr});
            warn("The following command terminated unexpectedly:\n", .{});
            for (args) |arg| warn("{} ", .{arg});
            warn("\n", .{});
            warn("{}\n", .{result.stderr});
            return error.CommandFailed;
        },
    }


@@ 52,8 56,8 @@ pub fn execAndCheck(allocator: *std.mem.Allocator, args: [][]const u8) !std.Chil
    return result;
}

pub fn commit(allocator: *std.mem.Allocator, files: []const []const u8, msg: []const u8) !void {
    std.fs.cwd().access(".git", .{}) catch return;
pub fn commit(allocator: *mem.Allocator, files: []const []const u8, msg: []const u8) !void {
    fs.cwd().access(".git", .{}) catch return;

    const git = which(allocator, "git") orelse return;
    defer allocator.free(git);


@@ 79,10 83,10 @@ pub fn commit(allocator: *std.mem.Allocator, files: []const []const u8, msg: []c
            _ = try execAndCheck(allocator, &[_][]const u8{ git, "commit", "-m", msg });
        },
        else => {
            std.debug.warn("The following command terminated unexpectedly:\n", .{});
            for (diff_index_args) |arg| std.debug.warn("{} ", .{arg});
            std.debug.warn("\n", .{});
            std.debug.warn("{}\n", .{result.stderr});
            warn("The following command terminated unexpectedly:\n", .{});
            for (diff_index_args) |arg| warn("{} ", .{arg});
            warn("\n", .{});
            warn("{}\n", .{result.stderr});
            return error.CommandFailed;
        },
    }

M src/zettel.zig => src/zettel.zig +40 -34
@@ 1,5 1,11 @@
const std = @import("std");
const ascii = std.ascii;
const fmt = std.fmt;
const fs = std.fs;
const mem = std.mem;
const stdout = std.io.getStdOut().outStream();
const warn = std.debug.warn;

const c = @cImport(@cInclude("time.h"));

const ID_LENGTH = "20200106121000".len;


@@ 7,7 13,7 @@ const EXT = ".md";
const TEMPLATE = "---\ntitle: {}\ndate:  {}\ntags:\n...\n";

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


@@ 17,29 23,29 @@ pub const Zettel = struct {
    contents: []const u8,

    /// Create a new Zettel with the given title.
    pub fn new(allocator: *std.mem.Allocator, title: []const u8) !Zettel {
    pub fn new(allocator: *mem.Allocator, title: []const u8) !Zettel {
        const date = try strftime(allocator, "%B %d, %Y");
        defer allocator.free(date);

        const id = try strftime(allocator, "%Y%m%d%H%M%S");
        defer allocator.free(id);

        var fname = try std.fmt.allocPrint(allocator, "{}-{}" ++ EXT, .{ id, title });
        var fname = try fmt.allocPrint(allocator, "{}-{}" ++ EXT, .{ id, title });
        errdefer allocator.free(fname);

        for (fname) |*char| {
            if (std.ascii.isSpace(char.*)) {
            if (ascii.isSpace(char.*)) {
                char.* = '-';
                continue;
            }

            char.* = std.ascii.toLower(char.*);
            char.* = ascii.toLower(char.*);
        }

        var file = try std.fs.cwd().createFile(fname, .{});
        var file = try fs.cwd().createFile(fname, .{});
        defer file.close();

        const contents = try std.fmt.allocPrint(allocator, TEMPLATE, .{ title, date });
        const contents = try fmt.allocPrint(allocator, TEMPLATE, .{ title, date });
        defer allocator.free(contents);
        try file.writeAll(contents);



@@ 49,7 55,7 @@ pub const Zettel = struct {
            .fname = fname,
            .id = fname[0..ID_LENGTH],
            .tags = &[_][]const u8{},
            .title = try std.mem.dupe(allocator, u8, title),
            .title = try mem.dupe(allocator, u8, title),
            .mtime = -1,
            .contents = &[_]u8{},
        };


@@ 62,7 68,7 @@ pub const Zettel = struct {
    }

    pub fn modified(self: Zettel) !bool {
        var file = try std.fs.cwd().openFile(self.fname, .{ .read = true });
        var file = try fs.cwd().openFile(self.fname, .{ .read = true });
        defer file.close();

        if ((try file.stat()).mtime == self.mtime) {


@@ 72,7 78,7 @@ pub const Zettel = struct {
        const contents = try file.inStream().readAllAlloc(self.allocator, 1 * 1024 * 1024);
        defer self.allocator.free(contents);

        if (!std.mem.eql(u8, self.contents, contents)) {
        if (!mem.eql(u8, self.contents, contents)) {
            return true;
        }



@@ 80,11 86,11 @@ pub const Zettel = struct {
    }
};

pub fn getZettels(allocator: *std.mem.Allocator) !std.ArrayList(Zettel) {
pub fn getZettels(allocator: *mem.Allocator) !std.ArrayList(Zettel) {
    var zettels = std.ArrayList(Zettel).init(allocator);
    errdefer zettels.deinit();

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

    var it = dir.iterate();


@@ 103,14 109,14 @@ pub fn getZettels(allocator: *std.mem.Allocator) !std.ArrayList(Zettel) {

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

    return zettels;
}

pub fn findZettels(allocator: *std.mem.Allocator, zettels: []const Zettel, keywords: [][]const u8) ![]const Zettel {
pub fn findZettels(allocator: *mem.Allocator, zettels: []const Zettel, keywords: [][]const u8) ![]const Zettel {
    var found = std.ArrayList(Zettel).init(allocator);
    defer found.deinit();



@@ 118,7 124,7 @@ pub fn findZettels(allocator: *std.mem.Allocator, zettels: []const Zettel, keywo
        if (findZettel(zettels, keyword)) |zet| {
            try found.append(zet);
        } else {
            std.debug.warn("No matches found for '{}'\n", .{keyword});
            warn("No matches found for '{}'\n", .{keyword});
        }
    }



@@ 133,15 139,15 @@ fn findZettel(zettels: []const Zettel, keyword: []const u8) ?Zettel {
    comptime var fields = .{ "id", "fname", "basename", "title" };
    for (zettels) |zet| {
        inline for (fields) |field| {
            if (std.ascii.eqlIgnoreCase(keyword, @field(zet, field))) {
            if (ascii.eqlIgnoreCase(keyword, @field(zet, field))) {
                return zet;
            }
        }

        // Also try id + title
        if (std.mem.startsWith(u8, keyword, zet.id) and
        if (mem.startsWith(u8, keyword, zet.id) and
            keyword.len > ID_LENGTH + 1 and keyword[ID_LENGTH] == ' ' and
            std.ascii.eqlIgnoreCase(keyword[ID_LENGTH + 1 ..], zet.title))
            ascii.eqlIgnoreCase(keyword[ID_LENGTH + 1 ..], zet.title))
        {
            return zet;
        }


@@ 190,17 196,17 @@ test "findZettel" {
    std.testing.expectEqual(@as(?Zettel, null), findZettel(zettels, "Test Three"));
}

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

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

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


@@ 213,13 219,13 @@ fn fromEntry(allocator: *std.mem.Allocator, entry: std.fs.Dir.Entry) !Zettel {
    defer tags.deinit();

    outer: while (line_it.next()) |line| {
        if (std.mem.eql(u8, line, "---") or std.mem.eql(u8, line, "...")) break;
        if (mem.eql(u8, line, "---") or mem.eql(u8, line, "...")) break;

        // Read title
        if (std.mem.startsWith(u8, line, "title: ")) {
        if (mem.startsWith(u8, line, "title: ")) {
            comptime var k = "title: ".len - 1;
            for (line[k..]) |char, i| {
                if (!std.fmt.isWhiteSpace(char)) {
                if (!fmt.isWhiteSpace(char)) {
                    title = line[k + i ..];
                    continue :outer;
                }


@@ 230,13 236,13 @@ fn fromEntry(allocator: *std.mem.Allocator, entry: std.fs.Dir.Entry) !Zettel {
        }

        // Read tags
        if (std.mem.startsWith(u8, line, "tags: ")) {
        if (mem.startsWith(u8, line, "tags: ")) {
            comptime var k = "tags: ".len - 1;
            for (line[k..]) |char, i| {
                if (!std.fmt.isWhiteSpace(char)) {
                    var it = std.mem.split(line[k + i ..], ",");
                if (!fmt.isWhiteSpace(char)) {
                    var it = mem.split(line[k + i ..], ",");
                    while (it.next()) |tag| {
                        const new_tag = std.fmt.trim(tag);
                        const new_tag = fmt.trim(tag);
                        try tags.append(new_tag);
                    }



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

    const fname = try std.mem.dupe(allocator, u8, entry.name);
    const fname = try mem.dupe(allocator, u8, entry.name);
    errdefer allocator.free(fname);

    var zettel = Zettel{


@@ 273,7 279,7 @@ fn hasId(name: []const u8) bool {
    if (name.len < ID_LENGTH) return false;

    for (name[0..ID_LENGTH]) |char| {
        if (!std.ascii.isDigit(char)) return false;
        if (!ascii.isDigit(char)) return false;
    }

    return true;


@@ 289,7 295,7 @@ test "hasId" {

fn hasExtension(name: []const u8) bool {
    if (name.len < EXT.len) return false;
    return std.mem.eql(u8, name[name.len - EXT.len ..], EXT);
    return mem.eql(u8, name[name.len - EXT.len ..], EXT);
}

test "hasExtension" {


@@ 297,11 303,11 @@ test "hasExtension" {
    std.testing.expect(!hasExtension("test"));
}

fn strftime(allocator: *std.mem.Allocator, format: [:0]const u8) ![]const u8 {
fn strftime(allocator: *mem.Allocator, format: [:0]const u8) ![]const u8 {
    var t = c.time(0);
    var tmp = c.localtime(&t);

    var buf = try allocator.allocSentinel(u8, 2 * format.len, 0);
    _ = c.strftime(buf.ptr, buf.len, format.ptr, tmp);
    return allocator.shrink(buf, std.mem.len(buf.ptr));
    return allocator.shrink(buf, mem.len(buf.ptr));
}