'zk' is a tool for making and organizing hierarchical notes at the command line. It tries to stay out of your way; I wrote it because the only way I'll take notes at all is if it's as easy as possible.
Every note gets a unique ID assigned at creation time. Notes are organized in a tree, starting with note 0 at the root (note 0 is created automatically when you run zk init
).
zk remembers which note you've been working in. When you run a command, zk will act on the current note if no other note id is specified.
The code to interact with zk's file structure is extracted into a library, libzk. This means if you hate the default CLI interface, you can write your own.
Commands in zk can typically be abbreviated to a single letter. zk offers the following commands:
show
(s
): show the current note's title and any subnote titles. If a note id is given as an argument, it will show that note instead.<id>
: set the current note to the given idup
(u
): move up one level in the tree. If a note is linked into multiple places, this may be confusing!print
(p
): print out the current note or the specified note ID.tree
(t
): show the full note tree from the root (0) or from the specified ID.grep
: find notes containing the specified regular expression, e.g.zk grep foo
orzk grep "foo.+bar"
.tgrep
: file notes containing the specified regular expression under the current or specified note, e.g.zk tgrep 17 foobar
to find "foobar" in note 17 or its sub-notes.
Running zk
with no arguments will list the title of the current note and its immediate sub-notes.
new
(n
): create a new note under the current note or under the specified note ID. zk will prompt you for a title and any additional text you want to enter into the note at this time.edit
(e
): edit the current note (or specify a note id as an argument to edit a different one). Uses the $EDITOR variable to determine which editor to run.append
(a
): append to the current note (or specified note id). Reads from standard input.link
: link a note as a sub-note of another.zk link 22 3
will make note 22 a sub-note of note 3.zk link 22
will make note 22 a sub-note of the current note.unlink
: unlink a sub-note from the current note, e.g.zk unlink 22
. As with the link command,zk unlink 22 3
will remove 22 as a sub-note of note 3.
alias
: define a new alias, a human-friendly name for a particular note, e.g.zk alias 7 todo
; you can then use "todo" in place of "7" in future commands.unalias
: remove an alias, e.g.zk unalias todo
.aliases
: list existing aliases.
init
: takes a file path as an argument, sets up a zk in that directory. If the directory already contains zk files, simply sets that as the new default.orphans
: list notes with no parents (excluding note 0). Unlinking a note from the tree entirely makes it an "orphan" and hides it; this lets you see what has been orphaned.rescan
: attempts to re-derive the state from the contents of the zk directory. Sometimes you'll need to run this if you've changed the title (the first line) of a note.
Fetch and build the code; make sure $GOPATH/bin is in your path!
go install github.com/floren/zk@latest
Initialize your zk directory:
zk init ~/zk
After running zk init, you'll have a top-level note and nothing else. We can see the current state like so:
$ zk
0 Top Level
We'll create a new note which will contain all information about my Go projects:
$ zk n
Enter note; the first line will be the title. Ctrl-D when done.
Go hacking
Notes about my Go programming stuff goes under this
^D
The very first line I entered, "Go hacking", becomes the note title; the remainder is body. If we run 'zk' again, we'll see the new note:
$ zk
0 Top Level
1 Go hacking
Note 0 is still the current note; we can set the current note to the new note like so:
$ zk 1
1 Go hacking
We can now create a new note in this Go category:
$ zk n
Enter note; the first line will be the title. Ctrl-D when done.
zk
Notes about my note-taking system (so meta)
$ zk
1 Go hacking
2 zk
The "up" command (abbreviated 'u') takes you up one level in the tree, or you can specify a note ID number directly to go to it:
$ zk u
0 Top Level
1 Go hacking
$ zk 2
2 zk
$ zk u
1 Go hacking
2 zk
$ zk u
0 Top Level
1 Go hacking
$ zk 1
1 Go hacking
2 zk
$ zk 0
0 Top Level
1 Go hacking
I'll create another note under the top-level, make another note under that note, then use the 'tree' command to see all notes:
$ zk n 0
Enter note; the first line will be the title. Ctrl-D when done.
Foo
$ zk
0 Top Level
1 Go hacking
3 Foo
$ zk 3
3 Foo
$ zk n
Enter note; the first line will be the title. Ctrl-D when done.
Bar
$ zk t
0 Top Level
1 Go hacking
2 zk
3 Foo
4 Bar
Notes are never deleted, because a note can appear as the child of multiple other notes; deleting the actual file would leave them hanging. It is, however, possible to 'unlink' a child from the current note so it will not appear any more. This makes it an "orphan"; use zk orphans
to list orphaned notes.
A note can appear at multiple places in the tree. Suppose I have a tree that looks like this:
$ zk t
0 Top Level
1 Go hacking
2 zk
3 Personal Projects
4 Bellwether mouse
Since zk
is a personal project, I'd also like it to appear as a child of note 3, so I use the link
command:
$ zk link 2 3
$ zk t
0 Top Level
1 Go hacking
2 zk
3 Personal Projects
4 Bellwether mouse
2 zk
If I decide that in fact, zk
is so much a personal project that I don't even want to see it under "Go hacking" any more, I can use the unlink
command:
$ zk unlink 2 1
$ zk t
0 Top Level
1 Go hacking
3 Personal Projects
4 Bellwether mouse
2 zk
Perhaps I now realize that "Go hacking" is a silly category to have; if I unlink note 1 from note 0, it will be completely unlinked ("orphaned"). It will no longer appear in the tree.
$ zk unlink 1 0
$ zk t
0 Top Level
3 Personal Projects
4 Bellwether mouse
2 zk
$ zk orphans
1 Go hacking
There are several advantages to unlinking notes rather than deleting them:
- I can refer to "note 1" in other notes and still view it at any time, because it still exists.
- I can reinstate it into the tree whenever I wish with a
link
command. - It will still be included in
zk grep
results (you canzk tgrep 0 <pattern>
to restrict the search to only nodes actually in the tree)
Aliases let you assign human-friendly names to particular notes. Suppose I have a note where I keep a list of tasks to be done:
$ zk p 5
TODO
* TODO update readme
The number 5 isn't particularly conducive to memory, so I use the alias
command to give it another name, "todo":
$ zk alias 5 todo
$ zk aliases
todo → 5 TODO
I can then use the string "todo" anywhere I would have referred to the note by number:
john@frodo:~/hacking/zk$ zk append todo
Ctrl-D when done.
* TODO buy milk
The implementation of zk is split out into a library, libzk. You first init an (empty) directory:
// This will create a default top-level note 0
libzk.InitZK("/path/to/rootdir")
Then you can call NewZK and use it:
var z *ZK
if z, err = NewZK("/path/to/rootdir"); err != nil {
log.Fatal(err)
}
// Create a note as a sub-note of note 0
if err = z.NewNote(0, "Testing\n"); err != nil {
log.Fatal(err)
}
// Now make sure it is listed as a child of note 0
var md NoteMeta
if md, err = z.GetNoteMeta(0); err != nil {
log.Fatal(err)
}
if len(md.Subnotes) != 1 {
log.Fatalf("Wrong number of subnotes on note 0, got %d should be 1", len(md.Subnotes))
}
Notes are stored in numeric directories within your zk dir:
$ ls ~/zk
0/ 1/ 2/ 3/ 4/ 5/ state
Each note is itself a directory, containing the body
file, the metadata
file, and a directory named files
containing any files you have linked with the note (experimental feature).
$ ls ~/zk/3
body files metadata
The metadata file is JSON formatted:
{"Id":3,"Title":"Personal Projects","Subnotes":[4,2],"Files":[],"Parent":0}
Each note has one "canonical" parent. This only comes into play with using the zk up
command, and it faces the same issues as cd ..
does in Unix when dealing with symlinks.