~gpanders/wk

8d2bfa718875f455c4b6a1daa8fbda150afb4d12 — Greg Anders 2 months ago aa07609
Reduce redundancy in command definitions

Before, adding a new command required changes in 3 or 4 different
places. Now, adding a new command is as simple as defining in the
command in the appropriate file under the cmd/ folder and making the
Command data structure public, then just adding the @import statement to
the 'commands' array.
M src/cmd.zig => src/cmd.zig +48 -125
@@ 1,92 1,67 @@
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 ChildProcess = std.ChildProcess;

const commands = .{
    @import("cmd/backlinks.zig").cmd,
    @import("cmd/list.zig").cmd,
    @import("cmd/new.zig").cmd,
    @import("cmd/open.zig").cmd,
    @import("cmd/preview.zig").cmd,
    @import("cmd/sync.zig").cmd,
    @import("cmd/search.zig").cmd,
    @import("cmd/show.zig").cmd,
    @import("cmd/tags.zig").cmd,
};

const getZettels = @import("zettel.zig").getZettels;

const backlinks = @import("cmd/backlinks.zig");
const list = @import("cmd/list.zig");
const new = @import("cmd/new.zig");
const open = @import("cmd/open.zig");
const preview = @import("cmd/preview.zig");
const sync = @import("cmd/sync.zig");
const search = @import("cmd/search.zig");
const show = @import("cmd/show.zig");
const tags = @import("cmd/tags.zig");

pub const Command = enum {
    Backlinks,
    List,
    New,
    Open,
    Preview,
    Sync,
    Search,
    Show,
    Tags,

    pub fn usage(self: Command) []const u8 {
        return switch (self) {
            .Backlinks => backlinks.usage,
            .List => list.usage,
            .New => new.usage,
            .Open => open.usage,
            .Preview => preview.usage,
            .Sync => sync.usage,
            .Search => search.usage,
            .Show => show.usage,
            .Tags => tags.usage,
        };
    }

    pub fn desc(self: Command) []const u8 {
        return switch (self) {
            .Backlinks => backlinks.desc,
            .List => list.desc,
            .New => new.desc,
            .Open => open.desc,
            .Preview => preview.desc,
            .Sync => sync.desc,
            .Search => search.desc,
            .Show => show.desc,
            .Tags => tags.desc,
        };
    }
pub const Command = struct {
    name: []const u8,
    aliases: []const []const u8,
    usage: []const u8,
    desc: []const u8,
    run: fn (allocator: *mem.Allocator, args: ?[]const []const u8) Error!void,

    const Self = @This();

    pub const Error = error{
        MissingRequiredArgument,
        CommandFailed,
        NoMatches,
        StreamTooLong,
        InvalidFormat,
    } || mem.Allocator.Error || fs.File.OpenError || fs.File.StatError || fs.File.ReadError || fs.File.WriteError || ChildProcess.SpawnError;

    fn parse(str: []const u8) !Self {
        inline for (commands) |cmd| {
            if (mem.eql(u8, str, cmd.name)) return cmd;
            inline for (cmd.aliases) |alias| {
                if (mem.eql(u8, str, alias)) return cmd;
            }
        }

    pub fn run(self: Command, allocator: *mem.Allocator, args: ?[]const []const u8) !void {
        var zettels = try getZettels(allocator);
        return switch (self) {
            .Backlinks => try backlinks.run(allocator, zettels),
            .List => try list.run(zettels, args),
            .New => try new.run(allocator, zettels, args),
            .Open => try open.run(allocator, zettels, args),
            .Preview => try preview.run(allocator, zettels, args),
            .Sync => try sync.run(allocator),
            .Search => try search.run(zettels, args),
            .Show => try show.run(allocator, zettels, args),
            .Tags => try tags.run(allocator, zettels, args),
        };
        warn("Error: unknown command: {}\n", .{str});
        return error.UnknownCommand;
    }
};

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

    if (cmd) |_| {
        stdout.print("{}\n", .{cmd.?.desc()}) catch return;
        stdout.print("{}\n", .{cmd.?.desc}) catch return;
    } else {
        stdout.print("Available commands:\n", .{}) catch return;
        inline for (@typeInfo(Command).Enum.fields) |command| {
            const lower_cmd_name = [_]u8{ascii.toLower(command.name[0])} ++ command.name[1..];
        inline for (commands) |command| {
            // TODO (zig 0.7.0) Replace with format string specifier
            // stdout.print("{: 8}{: <16}{}\n", ...)
            stdout.print("        {}{}{}\n", .{
                lower_cmd_name,
                " " ** (16 - lower_cmd_name.len),
                @field(Command, command.name).desc(),
                command.name,
                " " ** (16 - command.name.len),
                command.desc,
            }) catch return;
        }
        stdout.print("\n", .{}) catch return;


@@ 96,63 71,11 @@ pub fn printUsage(cmd: ?Command) void {

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

    const cmd = try parseCommand(command);
    const cmd = try Command.parse(command);
    try cmd.run(allocator, args);
}

fn parseCommand(command: []const u8) !Command {
    if (strEq(command, &[_][]const u8{ "bl", "backlinks" })) {
        return .Backlinks;
    }

    if (strEq(command, &[_][]const u8{ "sh", "show" })) {
        return .Show;
    }

    if (strEq(command, &[_][]const u8{ "l", "ls", "list" })) {
        return .List;
    }

    if (strEq(command, &[_][]const u8{ "o", "open" })) {
        return .Open;
    }

    if (strEq(command, &[_][]const u8{ "n", "new" })) {
        return .New;
    }

    if (strEq(command, &[_][]const u8{ "s", "search" })) {
        return .Search;
    }

    if (strEq(command, &[_][]const u8{ "p", "pre", "prev", "preview" })) {
        return .Preview;
    }

    if (strEq(command, &[_][]const u8{"sync"})) {
        return .Sync;
    }

    if (strEq(command, &[_][]const u8{ "t", "tag", "tags" })) {
        return .Tags;
    }

    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 (mem.eql(u8, str, pat)) {
            return true;
        }
    }

    return false;
}

M src/cmd/backlinks.zig => src/cmd/backlinks.zig +11 -3
@@ 3,13 3,21 @@ const fmt = std.fmt;
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 commit = @import("../util.zig").commit;

pub const usage = "bl|backlinks [NOTE [NOTE ..]]";
pub const desc = "Add backlinks to given notes (all notes if no argument given).";
pub const cmd = Command{
    .name = "backlinks",
    .aliases = &[_][]const u8{"bl"},
    .usage = "bl|backlinks [NOTE [NOTE ..]]",
    .desc = "Add backlinks to given notes (all notes if no argument given).",
    .run = run,
};

pub fn run(allocator: *mem.Allocator, zettels: []Zettel) !void {
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const zettels = try getZettels(allocator);
    var modified = std.ArrayList([]const u8).init(allocator);
    for (zettels) |*zet| {
        try zet.writeBacklinks();

M src/cmd/list.zig => src/cmd/list.zig +11 -3
@@ 2,12 2,20 @@ const std = @import("std");
const ascii = std.ascii;
const mem = std.mem;

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

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 const cmd = Command{
    .name = "list",
    .aliases = &[_][]const u8{ "l", "ls" },
    .usage = "l|list [PATTERN [PATTERN ...]]",
    .desc = "With no argument, list all notes. Otherwise list notes matching any of the given patterns.",
    .run = run,
};

pub fn run(zettels: []const Zettel, args: ?[]const []const u8) !void {
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const zettels = try getZettels(allocator);
    for (zettels) |zet| {
        if (args) |patterns| {
            for (patterns) |pat| {

M src/cmd/new.zig => src/cmd/new.zig +11 -3
@@ 2,14 2,21 @@ const std = @import("std");
const fmt = std.fmt;
const mem = std.mem;

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 commit = @import("../util.zig").commit;

pub const usage = "n|new <TITLE>";
pub const desc = "Create a new note with the given title.";
pub const cmd = Command{
    .name = "new",
    .aliases = &[_][]const u8{"n"},
    .usage = "n|new <TITLE>",
    .desc = "Create a new note with the given title.",
    .run = run,
};

pub fn run(allocator: *mem.Allocator, zettels: []Zettel, args: ?[]const []const u8) !void {
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);


@@ 18,6 25,7 @@ pub fn run(allocator: *mem.Allocator, zettels: []Zettel, args: ?[]const []const 

        new_zettel.contents = try new_zettel.read();

        const zettels = try getZettels(allocator);
        const links = try new_zettel.links();
        for (links) |link| {
            for (zettels) |*zet| {

M src/cmd/open.zig => src/cmd/open.zig +11 -4
@@ 3,15 3,22 @@ const mem = std.mem;
const os = std.os;
const warn = std.debug.warn;

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

pub const usage = "o|open [NOTE [NOTE ...]]";
pub const desc = "Open the given notes in your $EDITOR.";
pub const cmd = Command{
    .name = "open",
    .aliases = &[_][]const u8{"o"},
    .usage = "o|open [NOTE [NOTE ...]]",
    .desc = "Open the given notes in your $EDITOR.",
    .run = run,
};

pub fn run(allocator: *mem.Allocator, zettels: []const Zettel, args: ?[]const []const u8) !void {
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const matches = if (args) |_|
        try findZettels(allocator, zettels, args.?)
        try findZettels(allocator, try getZettels(allocator), args.?)
    else
        null;


M src/cmd/preview.zig => src/cmd/preview.zig +12 -5
@@ 6,16 6,23 @@ const mem = std.mem;
const os = std.os;
const warn = std.debug.warn;

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

pub const usage = "p|preview <NOTE> [NOTE ...]";
pub const desc = "View notes as HTML.";
pub const cmd = Command{
    .name = "preview",
    .aliases = &[_][]const u8{ "p", "pre", "prev" },
    .usage = "p|preview <NOTE> [NOTE ...]",
    .desc = "View notes as HTML.",
    .run = run,
};

pub fn run(allocator: *mem.Allocator, zettels: []const Zettel, args: ?[]const []const u8) !void {
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const matches = if (args) |_|
        try findZettels(allocator, zettels, args.?)
        try findZettels(allocator, try getZettels(allocator), args.?)
    else
        return error.MissingRequiredArgument;



@@ 48,7 55,7 @@ pub fn run(allocator: *mem.Allocator, zettels: []const Zettel, args: ?[]const []
    std.time.sleep(1 * std.time.ns_per_s);
}

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

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

M src/cmd/search.zig => src/cmd/search.zig +12 -3
@@ 1,12 1,21 @@
const std = @import("std");
const ascii = std.ascii;
const mem = std.mem;

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

pub const usage = "s|search <PATTERN> [PATTERN ...]";
pub const desc = "Search for notes whose contents match any of the given patterns.";
pub const cmd = Command{
    .name = "search",
    .aliases = &[_][]const u8{"s"},
    .usage = "s|search <PATTERN> [PATTERN ...]",
    .desc = "Search for notes whose contents match any of the given patterns.",
    .run = run,
};

pub fn run(zettels: []const Zettel, args: ?[]const []const u8) !void {
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const zettels = try getZettels(allocator);
    if (args) |patterns| {
        for (zettels) |zet| {
            for (patterns) |pat| {

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

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

pub const usage = "sh|show <NOTE> [NOTE ...]";
pub const desc = "Display contents of notes to stdout.";
pub const cmd = Command{
    .name = "show",
    .aliases = &[_][]const u8{"sh"},
    .usage = "sh|show <NOTE> [NOTE ...]",
    .desc = "Display contents of notes to stdout.",
    .run = run,
};

pub fn run(allocator: *mem.Allocator, zettels: []const Zettel, args: ?[]const []const u8) !void {
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    if (args) |_| {
        const zettels = try getZettels(allocator);
        const matches = try findZettels(allocator, zettels, args.?);
        for (matches) |zet| stdout.print("{}", .{try zet.read()}) catch return;
    } else {

M src/cmd/sync.zig => src/cmd/sync.zig +9 -3
@@ 3,12 3,18 @@ const mem = std.mem;
const stdout = std.io.getStdOut().outStream();
const warn = std.debug.warn;

const Command = @import("../cmd.zig").Command;
const execAndCheck = @import("../util.zig").execAndCheck;

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

pub fn run(allocator: *mem.Allocator) !void {
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    var result = execAndCheck(allocator, &[_][]const u8{ "git", "pull", "--rebase" }) catch |err| switch (err) {
        error.FileNotFound => {
            warn("git not found or is not executable\n", .{});

M src/cmd/tags.zig => src/cmd/tags.zig +11 -3
@@ 3,12 3,20 @@ const ascii = std.ascii;
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;

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 const cmd = Command{
    .name = "tags",
    .aliases = &[_][]const u8{ "t", "tag" },
    .usage = "t|tags [TAG [TAG ...]]",
    .desc = "With no argument, list all tags found in notes. Otherwise list all notes containing any of the given tags.",
    .run = run,
};

pub fn run(allocator: *mem.Allocator, zettels: []const Zettel, args: ?[]const []const u8) !void {
pub fn run(allocator: *mem.Allocator, args: ?[]const []const u8) Command.Error!void {
    const zettels = try getZettels(allocator);
    if (args) |_| {
        outer: for (zettels) |zet| {
            for (zet.tags) |t| {

M src/util.zig => src/util.zig +2 -1
@@ 3,8 3,9 @@ const fs = std.fs;
const mem = std.mem;
const os = std.os;
const warn = std.debug.warn;
const ChildProcess = std.ChildProcess;

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

M src/zettel.zig => src/zettel.zig +5 -5
@@ 66,7 66,7 @@ pub const Zettel = struct {
        var file = try fs.cwd().openFile(self.fname, .{ .read = true });
        defer file.close();

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

    pub fn print(self: Zettel) void {


@@ 227,8 227,8 @@ pub fn getZettels(allocator: *mem.Allocator) ![]Zettel {
    return zettels.toOwnedSlice();
}

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

    for (keywords) |keyword| {
        if (findZettel(zettels, keyword)) |zet| {


@@ 245,9 245,9 @@ pub fn findZettels(allocator: *mem.Allocator, zettels: []const Zettel, keywords:
    return found.toOwnedSlice();
}

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