~gpanders/wk

04ca0b41183b46e85b6a4865dd10b3e1dfd2434f — Greg Anders 8 months ago 8f07e73
Allow subcommands to take multiple arguments
M src/cmd.zig => src/cmd.zig +18 -29
@@ 44,52 44,41 @@ pub const Command = enum {
    }
};

pub fn handleCommand(command: []const u8, arg: ?[]const u8, allocator: *std.mem.Allocator) !void {
pub fn handleCommand(allocator: *std.mem.Allocator, command: []const u8, args: ?[][]const u8) !void {
    const zettels = try zettel.getZettels(allocator);
    defer allocator.free(zettels);

    switch (try parseCommand(command)) {
        .Show => if (arg != null) {
            if (zettel.findZettel(zettels, arg.?)) |zet| {
                try show.run(allocator, zet);
            } else {
                return error.DoesNotExist;
            }
        .Show => if (args != null) {
            const matches = try zettel.findZettels(allocator, zettels, args.?);
            try show.run(allocator, matches);
        } else {
            return error.MissingRequiredArgument;
        },
        .List => try list.run(allocator, arg, zettels),
        .Open => {
            if (arg != null) {
                if (zettel.findZettel(zettels, arg.?)) |zet| {
                    try open.run(allocator, zet);
                } else {
                    return error.DoesNotExist;
                }
            } else {
                try open.run(allocator, null);
            }
        .List => try list.run(allocator, args, zettels),
        .Open => if (args != null) {
            const matches = try zettel.findZettels(allocator, zettels, args.?);
            try open.run(allocator, matches);
        } else {
            try open.run(allocator, null);
        },
        .New => if (arg != null) {
            try new.run(allocator, arg.?);
        .New => if (args != null) {
            try new.run(allocator, args.?[0]);
        } else {
            return error.MissingRequiredArgument;
        },
        .Search => if (arg != null) {
            try search.run(allocator, arg.?, zettels);
        .Search => if (args != null) {
            try search.run(allocator, args.?, zettels);
        } else {
            return error.MissingRequiredArgument;
        },
        .Preview => if (arg != null) {
            if (zettel.findZettel(zettels, arg.?)) |zet| {
                try preview.run(allocator, zet);
            } else {
                return error.DoesNotExist;
            }
        .Preview => if (args != null) {
            const matches = try zettel.findZettels(allocator, zettels, args.?);
            try preview.run(allocator, matches);
        } else {
            return error.MissingRequiredArgument;
        },
        .Tags => try tags.run(allocator, arg, zettels),
        .Tags => try tags.run(allocator, args, zettels),
    }
}


M src/cmd/list.zig => src/cmd/list.zig +13 -4
@@ 3,12 3,21 @@ const stdout = std.io.getStdOut().outStream();

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

pub const usage = "l|list [pattern]";
pub const desc = "With no argument, list all notes. Otherwise list notes matching the given pattern";
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, pattern: ?[]const u8, zettels: []const Zettel) !void {
pub fn run(allocator: *std.mem.Allocator, patterns: ?[][]const u8, zettels: []const Zettel) !void {
    for (zettels) |zet| {
        if (pattern == null or std.ascii.indexOfIgnoreCase(zet.fname, pattern.?) != null) {
        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)
                {
                    try stdout.print("{} {}\n", .{ zet.id, zet.title });
                }
            }
        } else {
            try stdout.print("{} {}\n", .{ zet.id, zet.title });
        }
    }

M src/cmd/new.zig => src/cmd/new.zig +6 -6
@@ 5,14 5,14 @@ const open = @import("../cmd.zig").open;
const Zettel = @import("../zettel.zig").Zettel;
const util = @import("../util.zig");

pub const usage = "n|new <title>";
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) !void {
    const new_zettel = try Zettel.new(allocator, title);
    defer new_zettel.deinit();

    try open.run(allocator, new_zettel);
    try open.run(allocator, &[_]Zettel{new_zettel});
    try commit(allocator, new_zettel);
}



@@ 22,7 22,7 @@ fn commit(allocator: *std.mem.Allocator, zet: Zettel) !void {
    const git = util.expandPath(allocator, "git") orelse return;
    defer allocator.free(git);

    try util.execAndCheck(allocator, &[_][]const u8{
    _ = try util.execAndCheck(allocator, &[_][]const u8{
        git,
        "stash",
        "push",


@@ 30,7 30,7 @@ fn commit(allocator: *std.mem.Allocator, zet: Zettel) !void {
        "--quiet",
    });

    try util.execAndCheck(allocator, &[_][]const u8{ git, "add", zet.fname });
    try util.execAndCheck(allocator, &[_][]const u8{ git, "commit", "-m", zet.title });
    try util.execAndCheck(allocator, &[_][]const u8{ git, "stash", "pop", "--quiet" });
    _ = try util.execAndCheck(allocator, &[_][]const u8{ git, "add", zet.fname });
    _ = try util.execAndCheck(allocator, &[_][]const u8{ git, "commit", "-m", zet.title });
    _ = try util.execAndCheck(allocator, &[_][]const u8{ git, "stash", "pop", "--quiet" });
}

M src/cmd/open.zig => src/cmd/open.zig +18 -7
@@ 1,19 1,30 @@
const std = @import("std");
const stderr = std.io.getStdErr().outStream();

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

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

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

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

    _ = try proc.spawnAndWait();
    const term = try proc.spawnAndWait();
    switch (term) {
        .Exited => {},
        else => {
            try stderr.print("The following command terminated unexpectedly:\n", .{});
            for (args.items) |arg| try stderr.print("{} ", .{arg});
            return error.ExecFailed;
        },
    }
}

M src/cmd/preview.zig => src/cmd/preview.zig +44 -32
@@ 4,47 4,59 @@ const stderr = std.io.getStdErr().outStream();
const Zettel = @import("../zettel.zig").Zettel;
const util = @import("../util.zig");

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

pub fn run(allocator: *std.mem.Allocator, zet: Zettel) !void {
pub fn run(allocator: *std.mem.Allocator, zettels: []const Zettel) !void {
    const pandoc = util.expandPath(allocator, "pandoc") orelse {
        try stderr.print("Couldn't find pandoc on PATH\n", .{});
        return;
    };

    var html_file = try std.fmt.allocPrint(allocator, "{}{}", .{ zet.basename, ".html" });
    defer allocator.free(html_file);

    try util.execAndCheck(allocator, &[_][]const u8{
        pandoc,
        "--standalone",
        "--to",
        "html",
        "--from",
        "markdown",
        "--output",
        html_file,
        zet.fname,
    });

    var open_cmd: []const u8 = undefined;
    if (std.os.getenv("BROWSER")) |browser| {
        open_cmd = try std.mem.dupe(allocator, u8, browser);
    } else if (util.expandPath(allocator, "xdg-open")) |xdg_open| {
        open_cmd = xdg_open;
    } else if (util.expandPath(allocator, "open")) |open| {
        open_cmd = open;
    } else if (util.expandPath(allocator, "firefox")) |firefox| {
        open_cmd = firefox;
    } else {
        return error.NoAvailableBrowser;
    }
    const open_cmd = blk: {
        if (std.os.getenv("BROWSER")) |browser| {
            break :blk try std.mem.dupe(allocator, u8, browser);
        } else if (util.expandPath(allocator, "xdg-open")) |xdg_open| {
            break :blk xdg_open;
        } else if (util.expandPath(allocator, "open")) |open| {
            break :blk open;
        } else if (util.expandPath(allocator, "firefox")) |firefox| {
            break :blk firefox;
        } else {
            return error.NoAvailableBrowser;
        }
    };
    defer allocator.free(open_cmd);

    try util.execAndCheck(allocator, &[_][]const u8{ open_cmd, html_file });
    var output_files = std.ArrayList([]const u8).init(allocator);
    defer {
        for (output_files.items) |file| {
            std.fs.cwd().deleteFile(file) catch {};
            allocator.free(file);
        }
        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
        try output_files.append(html_file);

        _ = try util.execAndCheck(allocator, &[_][]const u8{
            pandoc,
            "--standalone",
            "--to",
            "html",
            "--from",
            "markdown",
            "--output",
            html_file,
            zet.fname,
        });

        _ = try util.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
    std.time.sleep(1 * std.time.ns_per_s);
    try std.fs.cwd().deleteFile(html_file);
}

M src/cmd/search.zig => src/cmd/search.zig +8 -4
@@ 3,17 3,21 @@ const stdout = std.io.getStdOut().outStream();

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

pub const usage = "s|search <pattern>";
pub const desc = "Search notes for the given pattern";
pub const usage = "s|search <PATTERN> [PATTERN ...]";
pub const desc = "Search for notes whose contents match any of the given patterns";

pub fn run(allocator: *std.mem.Allocator, pattern: []const u8, zettels: []const Zettel) !void {
pub fn run(allocator: *std.mem.Allocator, patterns: [][]const u8, zettels: []const Zettel) !void {
    var matches = std.ArrayList(Zettel).init(allocator);
    defer matches.deinit();

    var args = std.ArrayList([]const u8).init(allocator);
    defer args.deinit();

    try args.appendSlice(&[_][]const u8{ "grep", "-Fil", pattern });
    try args.appendSlice(&[_][]const u8{ "grep", "-Fil" });
    for (patterns) |pat| {
        try args.appendSlice(&[_][]const u8{ "-e", pat });
    }

    for (zettels) |zet| {
        try args.append(zet.fname);
    }

M src/cmd/show.zig => src/cmd/show.zig +11 -9
@@ 4,16 4,18 @@ const stderr = std.io.getStdErr().outStream();

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

pub const usage = "sh|show <name>";
pub const desc = "Display contents of a note to stdout";
pub const usage = "sh|show <NOTE> [NOTE ...]";
pub const desc = "Display contents of notes to stdout";

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

    const size = try file.getEndPos();
    const contents = try file.inStream().readAllAlloc(allocator, size);
    defer allocator.free(contents);
        const size = try file.getEndPos();
        const contents = try file.inStream().readAllAlloc(allocator, size);
        defer allocator.free(contents);

    try stdout.print("{}", .{contents});
        try stdout.print("{}", .{contents});
    }
}

M src/cmd/tags.zig => src/cmd/tags.zig +16 -14
@@ 3,36 3,38 @@ const stdout = std.io.getStdOut().outStream();

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

pub const usage = "t|tags [tag]";
pub const desc = "With no argument, list all tags found in notes. Otherwise list all notes containing the given tag";
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, tag: ?[]const u8, zettels: []const Zettel) !void {
    if (tag == null) {
pub fn run(allocator: *std.mem.Allocator, tags: ?[][]const u8, zettels: []const Zettel) !void {
    if (tags == null) {
        // List tags
        var tags = std.ArrayList([]const u8).init(allocator);
        defer tags.deinit();
        var existing_tags = std.ArrayList([]const u8).init(allocator);
        defer existing_tags.deinit();

        for (zettels) |zet| {
            try tags.appendSlice(zet.tags);
            try existing_tags.appendSlice(zet.tags);
        }

        std.sort.sort([]const u8, tags.items, struct {
        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);
            }
        }.lessThan);

        for (tags.items) |t, i| {
            if (i == 0 or !std.mem.eql(u8, t, tags.items[i - 1])) {
        for (existing_tags.items) |t, i| {
            if (i == 0 or !std.mem.eql(u8, t, existing_tags.items[i - 1])) {
                try stdout.print("{}\n", .{t});
            }
        }
    } else {
        for (zettels) |zet| {
        outer: for (zettels) |zet| {
            for (zet.tags) |t| {
                if (std.ascii.eqlIgnoreCase(tag.?, t)) {
                    try stdout.print("{} {}\n", .{ zet.id, zet.title });
                    break;
                for (tags.?) |tag| {
                    if (std.ascii.eqlIgnoreCase(tag, t)) {
                        try stdout.print("{} {}\n", .{ zet.id, zet.title });
                        continue :outer;
                    }
                }
            }
        }

M src/main.zig => src/main.zig +8 -5
@@ 11,7 11,7 @@ fn printUsage(exe: []const u8) void {
    inline for (@typeInfo(cmd.Command).Enum.fields) |command| {
        comptime var usage = @field(cmd.Command, command.name).usage();
        stdout.print("    {}", .{usage}) catch return;
        stdout.print("{}{}\n", .{ " " ** (24 - usage.len), @field(cmd.Command, command.name).desc() }) catch return;
        stdout.print("{}{}\n", .{ " " ** (36 - usage.len), @field(cmd.Command, command.name).desc() }) catch return;
    }
}



@@ 65,9 65,9 @@ pub fn main() anyerror!void {
    try std.process.changeCurDir(dir);

    const command = arglist[1];
    const arg: ?[]const u8 = if (arglist.len > 2) arglist[2] else null;
    const args: ?[][]const u8 = if (arglist.len > 2) arglist[2..] else null;

    cmd.handleCommand(command, arg, allocator) catch |err| return switch (err) {
    cmd.handleCommand(allocator, command, args) catch |err| return switch (err) {
        error.UnknownCommand => {
            try stderr.print("Unknown command: {}\n", .{command});
            printUsage(exe);


@@ 78,8 78,11 @@ pub fn main() anyerror!void {
            printUsage(exe);
            std.process.exit(1);
        },
        error.DoesNotExist => {
            try stderr.print("Couldn't find any zettels matching {}\n", .{arg.?});
        error.NoAvailableBrowser => {
            try stderr.print("Couldn't find a way to open compiled notes\n", .{});
            std.process.exit(1);
        },
        error.NoMatches => {
            std.process.exit(1);
        },
        else => return err,

M src/util.zig => src/util.zig +3 -1
@@ 15,7 15,7 @@ pub fn expandPath(allocator: *std.mem.Allocator, command: []const u8) ?[]const u
    return null;
}

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


@@ 37,4 37,6 @@ pub fn execAndCheck(allocator: *std.mem.Allocator, args: [][]const u8) !void {
            return error.ExecFailed;
        },
    }

    return result;
}

M src/zettel.zig => src/zettel.zig +21 -3
@@ 1,7 1,6 @@
const std = @import("std");
const c = @cImport({
    @cInclude("time.h");
});
const stderr = std.io.getStdErr().outStream();
const c = @cImport(@cInclude("time.h"));

const ID_LENGTH = "20200106121000".len;
const EXT = ".md";


@@ 87,6 86,25 @@ pub fn getZettels(allocator: *std.mem.Allocator) ![]const Zettel {
    return entries.toOwnedSlice();
}

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

    for (keywords) |keyword| {
        if (findZettel(zettels, keyword)) |zet| {
            try found.append(zet);
        } else {
            try stderr.print("No matches found for '{}'\n", .{keyword});
        }
    }

    if (found.items.len == 0) {
        return error.NoMatches;
    }

    return found.toOwnedSlice();
}

pub fn findZettel(zettels: []const Zettel, keyword: []const u8) ?Zettel {
    comptime var fields = .{ "id", "fname", "basename", "title" };
    for (zettels) |zet| {