Skip to content

brtholomy/um

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

157 Commits
 
 
 
 
 
 

Repository files navigation

um

An (U)ltralight zettelkasten for (M)arkdown composition.

um is a zettelkasten specification, an emacs toolkit, and a CLI for organizing writing into larger compositions. It uses unique filenames, plaintext tags, the builtin emacs project package, and simple commandline conventions. It's grown out of my own scripts and emacs hacks over the years.

It tries to be stupid-simple on the filesystem side, while offering powerful conveniences on the tooling side. The idea is to prioritize the moment of creation and get all noise out of the way.

It consists of two parts:

  1. Elisp for functionality within emacs. Much of it is integrated directly with the project interface, and consists of the ability to find a file from a filename and search for tags.

  2. A commandline interface written in Go. I prefer a CLI for file creation and management rather than more emacs functions, because chaining shell commands is easy and I think of the commandline as the point of reference for all filesystem management.

Conceptually, um is somewhat like org-roam, except without any database dependency. And it assumes Markdown rather than org, which I find too heavy-handed.

This "database" depends on a few simple ideas:

  1. A sequentially numbered filename specification which serves as unique id, like this: 001.foo.md. The filesystem is the database. Note that the string descriptor is optional. In regex terms:

    ls | egrep '^[[:digit:]]+\.?.*\.md|txt'

    Or:

    digits.[descriptor.]md
    
  2. A very simple file header consisting of the title and date, with optional tags and place marker.

    # 001.foo.md
    : 2024.01.14
    - place_optional
    + tag_optional
  3. Using a "root" project defined by um-root-glob, where source files should first be composed and where we can assume a file exists if not elsewhere. This matters when trying to navigate back to a source file.

  4. Using the built-in emacs project package and a powerful CLI to organize compositions built from these source files.

installation

Clone it:

git clone https://github.com/brtholomy/um.git ~/.emacs.d/um

Build the um binary:

cd ~/.emacs.d/um/go
go build -o um .

Symlink somewhere in your PATH:

ln -s ~/.emacs.d/um/go/um /usr/local/bin/um

config

My config looks like this:

(use-package um
  :after (project embark)

  :custom (um-root-glob ".*/writing/journal")

  :bind
  ("M-s t" . um-tag-grep)
  ("M-r t" . um-tag-dwim)

  :config
  (advice-add 'embark-target-file-at-point :around 'um-target-file-at-point-advice)

  :load-path "um/elisp"
)

Note the optional setup of embark, which allows us to find files intelligently, explained below.

ultralight "database"

What I consider the killer feature of um is the ability to jump to files from filenames listed in any "child" project back to the "source" project, defined via um-root-glob. This means that child projects can be initially defined as simple lists of filenames, which I do to compose larger pieces. A simple filename anywhere can also serve as a "link".

There are two primary ways this is accessed:

  1. find-file and next-history-element, which will work as C-x C-f M-n out of the box. See um-find-file-at-point.
  2. By invoking emark-dwim on any filename. Requires embark and the advice-add shown above.

CLI

The "command line interface" is a set of conveniences: since this is all plain text, we could just as easily create everything manually.

Try:

um help
um tag --help

seed

To get started, create an empty directory to serve as content origin. It doesn't matter where or what it's called, since the CLI only assumes a sequentially numbered collection of files. Then create your first file, while seeding the zero-width. 4 zeros is plenty, since that means 10k files. My zettelkasten is 20 years old and has about 3000 entries with almost a million words:

touch 0000.md

um next

To create a new file:

um next

This will create the file, open it with emacsclient, and run um-journal-header in that file:

# 01.md
: 2024.01.14

If you save this file and run um next again you get:

# 02.md
: 2024.01.14

Create a new file with an optional descriptor:

um next foo

Yields:

# 03.foo.md
: 2024.01.14

Create a new file with an optional descriptor and tag:

um next foo bar

Yields:

# 04.foo.md
: 2024.01.14
+ bar

Or just append + to add the descriptor as a tag:

um next foo +

Yields:

# 05.foo.md
: 2024.01.14
+ foo

Or send a list of tags separated by commas. + still works:

um next foo +,bar,baz

Yields:

# 05.foo.md
: 2024.01.14
+ foo
+ bar
+ baz

um last

um last will print the name of the last numbered file.

um mv

The um mv command makes it easier to add or change the string descriptor, while also updating the header:

um mv 02.foo.md bar

And we get 02.bar.md:

# 02.bar.md
: 2024.01.14

um tag

The file header allows for an optional list of tags, one per line, marked by a leading +:

# 02.md
: 2024.01.14
+ foo
+ bar
# 03.md
: 2024.01.14
+ foo

This is the most powerful aspect of um: a simple list of tags applied to source files. It encourages small files organized from the bottom up, rather than topdown management - which gets in the way of good creative moods.

We can then search for files containing these tags. This list just goes to stdout, so the idea is to pipe it into a file for reordering within emacs:

um tag foo > ./some/filelist.md

The query supports union as + and intersection as ,:

> um tag foo+bar

02.md

> um tag foo,bar

02.md
03.md

And the complement:

> um tag foo --invert

03.md

Pipe them together to use a big union from which to subtract:

um tag foo,bar | um tag baz --invert

Run um tag --help to see what it can do.

um sort

When working with the filelists produced by um tag, we'll want to rearrange the order of files and add or remove tags. Then when we update our filelist by rerunning um tag, we want the output to respect our updated order. um sort does this:

um tag foo+bar | um sort --key some/filelist.md

um cat

This command is designed to work with the filelists produced by um tag. It separates files with a Markdown horizontal rule --- while stripping their headers:

um tag foo | um cat

As the last step in composing larger pieces, it accepts a filelist and a base directory to find those files. We can then pipe the output wherever we like:

um cat filelist.md --base ../ > finished.md

And there you have the virtue of the Unix philosophy.

About

An (U)ltralight zettelkasten for (M)arkdown composition

Topics

Resources

Stars

Watchers

Forks

Contributors