~gpanders/wk

191f7dbcdb00083439302d77348b142e285214eb — Greg Anders 8 months ago 3766d5b
Refactor to parse items into structs

The `getZettels` method now opens and reads the metadata from each file
to create a `Zettel` struct that contains the Zettel's ID, title, and
filename. This makes it much easier to decouple the filename, ID, and
title and also allows more information to be abstracted away into the
common `Zettel` interface. The only "downside" is that collecting the
Zettels is now (slightly) slower as each one has to be opened and the
first few lines read.
10 files changed, 269 insertions(+), 192 deletions(-)

M src/cmd.zig
M src/cmd/list.zig
M src/cmd/new.zig
M src/cmd/open.zig
M src/cmd/preview.zig
M src/cmd/search.zig
M src/cmd/show.zig
M src/main.zig
D src/util.zig
A src/zettel.zig
M src/cmd.zig => src/cmd.zig +49 -26
@@ 1,53 1,76 @@
const std = @import("std");

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

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

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

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

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

    switch (try parseCommand(command)) {
        .Show => if (arg) |a| {
            try show.run(a, ext, allocator);
        .Show => if (arg != null) {
            if (zettel.findZettel(zettels, arg.?)) |zet| {
                try show.run(allocator, zet);
            } else {
                return error.DoesNotExist;
            }
        } else {
            return error.MissingRequiredArgument;
        },
        .List => try list.run(arg, ext, allocator),
        .Open => try open.run(arg, ext, allocator),
        .New => if (arg) |a| {
            try new.run(a, ext, allocator);
        .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);
            }
        },
        .New => if (arg != null) {
            try new.run(allocator, arg.?);
        } else {
            return error.MissingRequiredArgument;
        },
        .Search => if (arg) |a| {
            try search.run(a, ext, allocator);
        .Search => if (arg != null) {
            try search.run(allocator, arg.?, zettels);
        } else {
            return error.MissingRequiredArgument;
        },
        .Preview => if (arg) |a| {
            try preview.run(a, ext, allocator);
        .Preview => if (arg != null) {
            if (zettel.findZettel(zettels, arg.?)) |zet| {
                try preview.run(allocator, zet);
            } else {
                return error.DoesNotExist;
            }
        } else {
            return error.MissingRequiredArgument;
        },


@@ 56,27 79,27 @@ pub fn handleCommand(command: []const u8, arg: ?[]const u8, ext: []const u8, all

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

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

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

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

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

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

    return error.UnknownCommand;

M src/cmd/list.zig => src/cmd/list.zig +6 -10
@@ 1,18 1,14 @@
const std = @import("std");
const stdout = std.io.getStdOut().outStream();
const util = @import("../util.zig");

pub const usage = "l|list [pattern]";

pub fn run(pattern: ?[]const u8, ext: []const u8, allocator: *std.mem.Allocator) !void {
    const zettels = try util.getZettels(ext, allocator);
    defer allocator.free(zettels);
const Zettel = @import("../zettel.zig").Zettel;

    util.sort(zettels);
pub const usage = "l|list [pattern]";

    for (zettels) |item| {
        if (pattern == null or std.ascii.indexOfIgnoreCase(item, pattern.?) != null) {
            stdout.print("{}\n", .{item[0 .. item.len - ext.len]}) catch unreachable;
pub fn run(allocator: *std.mem.Allocator, pattern: ?[]const u8, zettels: []Zettel) !void {
    for (zettels) |zet| {
        if (pattern == null or std.ascii.indexOfIgnoreCase(zet.fname, pattern.?) != null) {
            try stdout.print("{} {}\n", .{ zet.id, zet.title });
        }
    }
}

M src/cmd/new.zig => src/cmd/new.zig +5 -19
@@ 1,25 1,11 @@
const std = @import("std");

const open = @import("../cmd.zig").open;
const strftime = @import("../util.zig").strftime;
const zettel = @import("../zettel.zig");

pub const usage = "n|new <title>";

pub fn run(title: []const u8, ext: []const u8, allocator: *std.mem.Allocator) !void {
    var id = try strftime(allocator, "%Y%m%d%H%M%S");
    defer allocator.free(id);

    var date = try strftime(allocator, "%B %d, %Y");
    defer allocator.free(date);

    var fname = try std.fmt.allocPrint(allocator, "{} {}{}", .{ id, title, ext });
    defer allocator.free(fname);

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

    comptime var tpl = "---\ntitle:    {}\ndate:     {}\nkeywords:\n---\n";
    const contents = try std.fmt.allocPrint(allocator, tpl, .{ title, date });
    try file.writeAll(contents);

    try open.run(fname, ext, allocator);
pub fn run(allocator: *std.mem.Allocator, title: []const u8) !void {
    const new_zettel = try zettel.Zettel.new(allocator, title);
    try open.run(allocator, new_zettel);
}

M src/cmd/open.zig => src/cmd/open.zig +8 -20
@@ 1,30 1,18 @@
const std = @import("std");
const util = @import("../util.zig");

pub const usage = "o|open [name]";
const zettel = @import("../zettel.zig");

pub fn run(name: ?[]const u8, ext: []const u8, allocator: *std.mem.Allocator) !void {
    const editor = std.os.getenv("EDITOR") orelse "vi";
    var argv: []const []const u8 = undefined;
    var buf: ?[]u8 = undefined;
    if (name) |n| {
        var fname: []const u8 = n;
        if (!util.hasExtension(n, ext)) {
            buf = try allocator.alloc(u8, n.len + ext.len);
            fname = try std.fmt.bufPrint(buf.?, "{}{}", .{ n, ext });
        }
pub const usage = "o|open [name]";

        argv = &[_][]const u8{ editor, fname };
    } else {
        argv = &[_][]const u8{editor};
pub fn run(allocator: *std.mem.Allocator, zet: ?zettel.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);
    }

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

    _ = try proc.spawnAndWait();

    if (buf) |b| {
        allocator.free(b);
    }
}

M src/cmd/preview.zig => src/cmd/preview.zig +19 -16
@@ 1,28 1,16 @@
const std = @import("std");
const assert = std.debug.assert;
const stderr = std.io.getStdErr().outStream();

const c = @cImport({
    @cInclude("stdlib.h");
});

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

pub const usage = "p|preview <name>";

pub fn run(name: []const u8, ext: []const u8, allocator: *std.mem.Allocator) !void {
    const fname = try std.fmt.allocPrint(allocator, "{}{}", .{ name, ext });
    defer allocator.free(fname);

    std.fs.cwd().access(fname, .{ .read = true }) catch |err| {
        if (err == error.FileNotFound) {
            try stderr.print("Zettel '{}' not found\n", .{name});
        }

        return err;
    };

    var html_file = try std.fmt.allocPrint(allocator, "{}{}", .{ name, ".html" });
pub fn run(allocator: *std.mem.Allocator, zet: Zettel) !void {
    var html_file = try std.fmt.allocPrint(allocator, "{}{}", .{ zet.fname, ".html" });

    // Replace spaces in filename with underscores
    for (html_file) |*char| {


@@ 42,7 30,7 @@ pub fn run(name: []const u8, ext: []const u8, allocator: *std.mem.Allocator) !vo
        "markdown",
        "--output",
        html_file,
        fname,
        zet.fname,
    });

    var result = try std.ChildProcess.exec(.{


@@ 77,3 65,18 @@ pub fn run(name: []const u8, ext: []const u8, allocator: *std.mem.Allocator) !vo
    std.time.sleep(1 * std.time.ns_per_s);
    try std.fs.cwd().deleteFile(html_file);
}

/// Return full path to executable `command` as it exists on the `PATH`.
fn expandPath(allocator: *std.mem.Allocator, command: []const u8) ?[]const u8 {
    const path = std.os.getenv("PATH") orelse unreachable;

    var it = std.mem.split(path, ":");
    while (it.next()) |p| {
        const fullpath = std.fs.path.join(allocator, &[_][]const u8{ p, "/", command }) catch unreachable;
        if (std.os.access(fullpath, std.os.R_OK | std.os.X_OK)) |_| {
            return fullpath;
        } else |_| continue;
    }

    return null;
}

M src/cmd/search.zig => src/cmd/search.zig +13 -15
@@ 1,21 1,21 @@
const std = @import("std");
const util = @import("../util.zig");
const stdout = std.io.getStdOut().outStream();

pub const usage = "s|search <pattern>";
const Zettel = @import("../zettel.zig").Zettel;

pub fn run(pattern: []const u8, ext: []const u8, allocator: *std.mem.Allocator) !void {
    const zettels = try util.getZettels(ext, allocator);
    defer allocator.free(zettels);
pub const usage = "s|search <pattern>";

    var matches = std.ArrayList([]const u8).init(allocator);
pub fn run(allocator: *std.mem.Allocator, pattern: []const u8, zettels: []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(zettels);
    for (zettels) |zet| {
        try args.append(zet.fname);
    }

    const result = try std.ChildProcess.exec(.{
        .allocator = allocator,


@@ 24,16 24,14 @@ pub fn run(pattern: []const u8, ext: []const u8, allocator: *std.mem.Allocator) 

    var it = std.mem.split(result.stdout, "\n");
    while (it.next()) |match| {
        if (match.len > 0) try matches.append(match);
    }
        if (match.len == 0) continue;

    util.sort(matches.items);

    for (matches.items) |match, i| {
        if (i > 0 and std.mem.eql(u8, match, matches.items[i - 1])) {
            continue;
        for (zettels) |zet| {
            if (std.mem.eql(u8, zet.fname, match)) try matches.append(zet);
        }
    }

        stdout.print("{}\n", .{match[0 .. match.len - ext.len]}) catch unreachable;
    for (matches.items) |match, i| {
        try stdout.print("{} {}\n", .{ match.id, match.title });
    }
}

M src/cmd/show.zig => src/cmd/show.zig +6 -12
@@ 2,23 2,17 @@ const std = @import("std");
const stdout = std.io.getStdOut().outStream();
const stderr = std.io.getStdErr().outStream();

pub const usage = "sh|show <name>";

pub fn run(name: []const u8, ext: []const u8, allocator: *std.mem.Allocator) !void {
    const fname = try std.fmt.allocPrint(allocator, "{}{}", .{ name, ext });
    defer allocator.free(fname);
const Zettel = @import("../zettel.zig").Zettel;

    var file = std.fs.cwd().openFile(fname, .{ .read = true }) catch |err| {
        if (err == error.FileNotFound) {
            try stderr.print("Zettel '{}' not found\n", .{name});
        }
pub const usage = "sh|show <name>";

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

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

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

M src/main.zig => src/main.zig +4 -5
@@ 1,10 1,8 @@
const std = @import("std");
const cmd = @import("cmd.zig");

const stdout = std.io.getStdOut().outStream();
const stderr = std.io.getStdErr().outStream();

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

fn printUsage(exe: []const u8) void {
    stdout.print("Usage: {} COMMAND\n", .{std.fs.path.basename(exe)}) catch unreachable;


@@ 67,7 65,7 @@ pub fn main() anyerror!void {
    const command = arglist[1];
    const arg: ?[]const u8 = if (arglist.len > 2) arglist[2] else null;

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


@@ 78,7 76,8 @@ pub fn main() anyerror!void {
            printUsage(exe);
            std.process.exit(1);
        },
        error.FileNotFound => {
        error.DoesNotExist => {
            try stderr.print("Couldn't find any zettels matching {}\n", .{arg.?});
            std.process.exit(1);
        },
        else => return err,

D src/util.zig => src/util.zig +0 -69
@@ 1,69 0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @cImport({
    @cInclude("time.h");
});

pub fn getZettels(ext: []const u8, allocator: *std.mem.Allocator) ![][]const u8 {
    var entries = std.ArrayList([]const u8).init(allocator);
    defer entries.deinit();

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

    var it = dir.iterate();
    while (try it.next()) |entry| {
        if (entry.name[0] != '.' and hasExtension(entry.name, ext)) {
            try entries.ensureCapacity(entries.items.len + 1);
            entries.appendAssumeCapacity(try std.mem.dupe(allocator, u8, entry.name));
        }
    }

    return entries.toOwnedSlice();
}

pub fn hasExtension(name: []const u8, ext: []const u8) bool {
    return std.mem.eql(u8, name[name.len - ext.len ..], ext);
}

pub fn strftime(allocator: *std.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));
}

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

/// Return full path to executable `command` as it exists on the `PATH`.
pub fn expandPath(allocator: *std.mem.Allocator, command: []const u8) ?[]const u8 {
    const path = std.os.getenv("PATH") orelse unreachable;

    var it = std.mem.split(path, ":");
    while (it.next()) |p| {
        const fullpath = std.fs.path.join(allocator, &[_][]const u8{ p, "/", command }) catch unreachable;
        if (std.os.access(fullpath, std.os.R_OK | std.os.X_OK)) |_| {
            return fullpath;
        } else |_| continue;
    }

    return null;
}

test "hasExtension" {
    assert(hasExtension("foo.txt", ".txt"));
    assert(!hasExtension("foo.md", ".txt"));
}

test "expandPath" {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    assert(std.mem.eql(u8, expandPath(&arena.allocator, "ls").?, "/bin/ls"));
}

A src/zettel.zig => src/zettel.zig +159 -0
@@ 0,0 1,159 @@
const std = @import("std");
const c = @cImport({
    @cInclude("time.h");
});

const ID_LENGTH = "20200106121000".len;
const EXT = ".md";
const TEMPLATE = "---\ntitle:    {}\ndate:     {}\ntags:\n---\n";

pub const Zettel = struct {
    id: []const u8,
    fname: []const u8,
    title: []const u8,
    tags: [][]const u8,

    pub fn new(allocator: *std.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");
        errdefer allocator.free(id);

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

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

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

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

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

        return Zettel{
            .id = id,
            .title = try std.mem.dupe(allocator, u8, title),
            .fname = fname,
        };
    }
};

pub fn getZettels(allocator: *std.mem.Allocator) ![]Zettel {
    var entries = std.ArrayList(Zettel).init(allocator);
    defer entries.deinit();

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

    var it = dir.iterate();
    while (try it.next()) |entry| {
        if (!hasId(entry.name) or !hasExtension(entry.name)) {
            continue;
        }

        if (try fromEntry(allocator, entry)) |zet| {
            try entries.append(zet);
        }
    }

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

    return entries.toOwnedSlice();
}

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

            // Check for match in filename without extension
            const fname_no_ext = zet.fname[0 .. zet.fname.len - EXT.len];
            if (std.mem.eql(u8, field, "fname") and std.ascii.eqlIgnoreCase(keyword, fname_no_ext)) {
                return zet;
            }
        }
    }

    return null;
}

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

    var buf = try allocator.alloc(u8, 256);
    defer allocator.free(buf);

    if (try file.inStream().readUntilDelimiterOrEof(buf, '\n')) |line| {
        // If first line is not a metadata delimiter, skip this entry
        if (!std.mem.eql(u8, line, "---")) {
            return null;
        }
    }

    var title: ?[]u8 = undefined;

    while (try file.inStream().readUntilDelimiterOrEof(buf, '\n')) |line| {
        if (std.mem.eql(u8, line, "---")) break;

        if (std.mem.eql(u8, line[0.."title:".len], "title:")) {
            var i = "title: ".len;
            while (i <= line.len) : (i += 1) {
                if (!std.ascii.isSpace(line[i])) break;
            }

            title = try std.mem.dupe(allocator, u8, line[i..]);
        }
    }

    if (title == null) {
        return null;
    }

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

    const id = try std.mem.dupe(allocator, u8, entry.name[0..ID_LENGTH]);
    errdefer allocator.free(id);

    return Zettel{ .fname = fname, .id = id, .title = title.? };
}

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;
    }

    return true;
}

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

fn strftime(allocator: *std.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));
}