~gpanders/wk

d68709c736254d05cfd2650f1551f85bca5d87dd — Greg Anders 6 months ago
Initial commit
5 files changed, 274 insertions(+), 0 deletions(-)

A .gitignore
A LICENSE
A README.md
A build.zig
A src/main.zig
A  => .gitignore +1 -0
@@ 1,1 @@
zig-cache/

A  => LICENSE +19 -0
@@ 1,19 @@
Copyright 2020 Greg Anders

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

A  => README.md +41 -0
@@ 1,41 @@
zet
===

Command line tool to manage a Zettelkasten written in [zig][].

[zig]: https://ziglang.org

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

To install `zet` under `$PREFIX/bin` use

    zig build --prefix $PREFIX install

Usage
-----

Set an environment variable `ZETTEL_DIR` to the directory containing your
Zettelkasten.

List all notes in your Zettelkasten:

    zet list

Create a new note:

    zet new 'TITLE'

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

Open a note in your `$EDITOR` (defaults to `vi` if `$EDITOR` is unset):

    zet open NOTE

Where `NOTE` is the name of an existing note **without** the `.txt` extension.

Display a note's contents to stdout:

    zet show NOTE

A  => build.zig +24 -0
@@ 1,24 @@
const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = b.standardTargetOptions(.{});

    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

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

    const run_cmd = exe.run();
    run_cmd.step.dependOn(b.getInstallStep());

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

A  => src/main.zig +189 -0
@@ 1,189 @@
const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const fmt = std.fmt;
const stdout = std.io.getStdOut().outStream();
const c = @cImport({
    @cInclude("time.h");
});

const extension = ".txt";

fn printUsage(exe: []const u8) !void {
    try stdout.print("Usage: {} COMMAND\n", .{fs.path.basename(exe)});
    try stdout.print("\n", .{});
    try stdout.print("Available commands:\n", .{});
    try stdout.print("\tshow\n", .{});
    try stdout.print("\tlist\n", .{});
    try stdout.print("\topen\n", .{});
    try stdout.print("\tnew\n", .{});
}

fn isHidden(entry: fs.Dir.Entry) bool {
    return entry.name[0] == '.';
}

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

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

    // fs.cwd() is not opened with iterate access, so this is a simple hack to open an iterable
    // reference to the current working directory
    var dir = try fs.cwd().openDir(".", .{ .iterate = true });
    defer dir.close();

    var it = dir.iterate();
    while (try it.next()) |entry| {
        if (!isHidden(entry) and hasExtension(entry.name)) {
            try entries.append(entry.name);
        }
    }

    return entries.toOwnedSlice();
}

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

    const size = 2 * format.len;
    var buf = try allocator.alloc(u8, size);
    _ = c.strftime(buf.ptr, size, format.ptr, tmp);

    var nbytes: u8 = 0;
    for (buf) |byte| {
        if (byte == 0) {
            break;
        }
        nbytes += 1;
    }

    buf = allocator.shrink(buf, nbytes);
    return buf;
}

fn showZettel(name: []const u8, allocator: *mem.Allocator) !void {
    var buf = try allocator.alloc(u8, name.len + extension.len);
    defer allocator.free(buf);

    const fname = try fmt.bufPrint(buf, "{}{}", .{ name, extension });
    var file = try fs.cwd().openFile(fname, .{ .read = true });
    defer file.close();

    const size = try file.getEndPos();

    var contents = try allocator.alloc(u8, size);
    defer allocator.free(contents);

    _ = try file.read(contents);
    try stdout.print("{}", .{contents});
}

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

    var fname = try allocator.alloc(u8, id.len + 1 + title.len + extension.len);
    defer allocator.free(fname);

    _ = try fmt.bufPrint(fname, "{} {}{}", .{ id, title, extension });
    var file = try fs.cwd().createFile(fname, .{});
    defer file.close();

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

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

    const contents = try fmt.bufPrint(buf, "---\ntitle:    {}\ndate:     {}\nkeywords:\n---\n", .{ title, date });
    _ = try file.write(contents);

    try openEditor(fname, allocator);
}

fn openEditor(name: ?[]const u8, allocator: *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 (!hasExtension(n)) {
            buf = try allocator.alloc(u8, n.len + extension.len);
            fname = try fmt.bufPrint(buf.?, "{}{}", .{ n, extension });
        }

        argv = &[_][]const u8{ editor, fname };
    } else {
        argv = &[_][]const u8{editor};
    }

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

    _ = try proc.spawnAndWait();

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

fn getZettelDir() ![]const u8 {
    const path = std.os.getenv("ZETTEL_DIR") orelse return error.MissingZettelDir;
    var buf: [1024]u8 = undefined;
    return try fs.realpath(path, &buf);
}

fn handleCommand(cmd: []const u8, arg: ?[]const u8, allocator: *mem.Allocator) !void {
    if (mem.eql(u8, cmd, "show")) {
        try showZettel(arg.?, allocator);
    } else if (mem.eql(u8, cmd, "list")) {
        const zettels = try getZettels(allocator);
        for (zettels) |item| {
            try stdout.print("{}\n", .{item[0 .. item.len - extension.len]});
        }
    } else if (mem.eql(u8, cmd, "open")) {
        try openEditor(arg, allocator);
    } else if (mem.eql(u8, cmd, "new")) {
        try newZettel(arg.?, allocator);
    } else {
        return error.UnknownCommand;
    }
}

fn parseArgs(allocator: *mem.Allocator) ![][]const u8 {
    var arglist = std.ArrayList([]const u8).init(allocator);

    var args = std.process.args();
    while (args.next(allocator)) |item_or_error| {
        const item = try item_or_error;
        try arglist.append(item);
    }

    return arglist.toOwnedSlice();
}

pub fn main() anyerror!void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();

    var allocator = &arena.allocator;

    const arglist = try parseArgs(allocator);
    if (arglist.len == 1) {
        try printUsage(arglist[0]);
        return;
    }

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

    const dir = try getZettelDir();
    try std.process.changeCurDir(dir);

    try handleCommand(cmd, arg, allocator);
}