~gpanders/wk

6e91c7719d3862d680933bb4b8b0fad9acb8d929 — Greg Anders 8 months ago 4eee30f
Split newline delimited argument into separate lines

When composing commands together, e.g.

    zet open "$(zet tags foobar)"

The command used as the argument must be quoted to prevent the shell
from splitting the output on spaces. However, this causes `zet open` to
have a single argument which is just a long newline-delimited string of
the output of `zet tags foobar`. In these cases manually split the
string on the newlines and treat each line as a separate argument.

This is a total hack: I'm basically doing the shell's job for it, but
it's an important feature to be able to chain commands together in this
way. To do it the bash way, you'd have to use

    readarray -t lines < <(zet tags foobar)
    zet open "${lines[@]}"

Bash sucks sometimes.
6 files changed, 57 insertions(+), 57 deletions(-)

M README.md
M src/cmd/list.zig
M src/cmd/search.zig
M src/cmd/tags.zig
M src/main.zig
M src/zettel.zig
M README.md => README.md +21 -39
@@ 14,7 14,9 @@ running the following from the project root:

    make PREFIX=$PREFIX install

If you use macOS you can use
If `PREFIX` is not specified, `zet` will be installed under `zig-cache/bin/`.

If you use macOS and Homebrew you can use

    brew install gpanders/tap/zet



@@ 25,55 27,35 @@ Notes are stored in the directory represented by the environment variable
`$ZETTEL_DIR`. If `$ZETTEL_DIR` is unset, it defaults to
`$HOME/.local/share/zet/`.

In the following commands, `NOTE` is a note ID, title, or the file name of an
existing note with or without the extension.

List all notes in your Zettelkasten:

    zet list
Notes are created with

Create a new note:
    zet new TITLE

    zet new 'TITLE'

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

If you keep your notes in a git repository, `zet new` will automatically commit
your new note with the title as the commit message. There is (currently) no way
to disable this behavior.

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

    zet open [NOTE]

With no argument, `zet open` will open your `$EDITOR` in the directory
containing your notes.
To list all of your existing notes, use

Display a note's contents to stdout:

    zet show NOTE

Search through note contents and file names and report matches:

    zet search PATTERN

List all note tags:

    zet tags
    zet list

List all notes matching a certain tag:
This will print the title of each existing note along with its ID.

    zet tags TAG
For a full list of available commands, use `zet help`. For more information on
a specific command, use `zet help COMMAND`.

Add or update backlinks:
For commands that take arguments, the argument can be a note ID, title, or file
name (with or without the extension) of an existing note. The output of any
`zet` command can be used with other commands, allowing commands to be
composed. For example, to open all notes containing the tag "foo", use

    zet backlinks [NOTE]
    zet open "$(zet tag foo)"

The argument to the `backlinks` command is optional. If no argument is given,
`zet` will update backlinks in all notes; otherwise, only backlinks to the
given note(s) will be updated.
Note that the surrounding quotes are necessary to prevent the shell from word
splitting the output of the `zet tag foo` command.

Note that `zet new` will automatically update backlinks after creating a new
note.
Most commands can be abbreviated. Use `zet help CMD` for more information.

M src/cmd/list.zig => src/cmd/list.zig +2 -3
@@ 1,5 1,4 @@
const std = @import("std");
const stdout = std.io.getStdOut().outStream();

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



@@ 14,11 13,11 @@ pub fn run(allocator: *std.mem.Allocator, patterns: ?[][]const u8, zettels: []co
                    std.ascii.indexOfIgnoreCase(zet.fname, pat) != null or
                    std.ascii.indexOfIgnoreCase(zet.title, pat) != null)
                {
                    try stdout.print("{} {}\n", .{ zet.id, zet.title });
                    try zet.print();
                }
            }
        } else {
            try stdout.print("{} {}\n", .{ zet.id, zet.title });
            try zet.print();
        }
    }
}

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

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



@@ 37,6 36,6 @@ pub fn run(allocator: *std.mem.Allocator, patterns: [][]const u8, zettels: []con
    }

    for (matches.items) |match, i| {
        try stdout.print("{} {}\n", .{ match.id, match.title });
        try match.print();
    }
}

M src/cmd/tags.zig => src/cmd/tags.zig +12 -12
@@ 7,7 7,18 @@ 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 {
    if (tags == null) {
    if (tags) |_| {
        outer: for (zettels) |zet| {
            for (zet.tags) |t| {
                for (tags.?) |tag| {
                    if (std.ascii.eqlIgnoreCase(tag, t)) {
                        try zet.print();
                        continue :outer;
                    }
                }
            }
        }
    } else {
        // List tags
        var existing_tags = std.ArrayList([]const u8).init(allocator);
        defer existing_tags.deinit();


@@ 27,16 38,5 @@ pub fn run(allocator: *std.mem.Allocator, tags: ?[][]const u8, zettels: []const 
                try stdout.print("{}\n", .{t});
            }
        }
    } else {
        outer: for (zettels) |zet| {
            for (zet.tags) |t| {
                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 +16 -1
@@ 32,7 32,22 @@ pub fn main() anyerror!void {
    try std.process.changeCurDir(dir);

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

    // If first arg is a newline delimited string, split it into separate
    // lines. This can happen when the result of one zet command is used as the
    // argument to another zet command
    if (args != null and std.mem.indexOf(u8, args.?[0], "\n") != null) {
        var lines = std.ArrayList([]const u8).init(allocator);
        defer lines.deinit();

        var it = std.mem.split(args.?[0], "\n");
        while (it.next()) |line| {
            try lines.append(line);
        }

        args = lines.toOwnedSlice();
    }

    cmd.handleCommand(allocator, command, args) catch |err| return switch (err) {
        error.UnknownCommand => {

M src/zettel.zig => src/zettel.zig +5 -0
@@ 1,4 1,5 @@
const std = @import("std");
const stdout = std.io.getStdOut().outStream();
const stderr = std.io.getStdErr().outStream();
const c = @cImport(@cInclude("time.h"));



@@ 57,6 58,10 @@ pub const Zettel = struct {
        self.allocator.free(self.title);
        if (self.tags.len > 0) self.allocator.free(self.tags);
    }

    pub fn print(self: Zettel) !void {
        try stdout.print("{} {}\n", .{ self.id, self.title });
    }
};

pub fn getZettels(allocator: *std.mem.Allocator) !std.ArrayList(Zettel) {