tl;dr; til is a wrapper around a few other tools to simplify the management of a TIL repo, such ashttps://github.com/pjambet/til. I recently published it as a ruby gem.

The code can be found on GitHub.

Intro

At first glance this small CLI might not seem like it does much, and it really doesn’t, but while building it I ended learning quite a few things, three to be exact:

  • How to use an external command line tool, such as fzf, and feed it input, similar to using a unix pipe, like echo "a\nb\nc" | fzf, as well as reading the output of the command, but programmatically, in Ruby.

Before jumping in, here’s a summary of what til actually does:

  • It first loads the list of all existing categories in your TIL repo, and then uses fzf to prompt you to pick the category for your new TIL. You can also choose to add a new category.

Using fzf

I don’t actually use it that much, but I love how improves CLI interactions. I thought it would be a great addition for the workflow I wanted with til. Most of the time I will be reusing existing categories, and as someone who makes a lot of typos, I always look for ways to avoid having to type anything.

The main way that fzf gets its input in from STDIN. You can test that from the command line: echo "a\nb\nc" | fzf will load fzf with three items a, b & c.

I was able to get the list of folders using the GitHub API ruby library, Octokit. Each folder is a category, and I needed to feed that to fzf, through STDIN, in Ruby.

It’s worth mentioning that I could have used the backtick approach, but that was a tiny bit too much magic for me and also, there are few blog posts out there that recommend against using it, and favor the system method. In this case we're not really dealing with user input, so it doesn't matter that much from a security standpoint.

The Process.spawn method accepts a bunch of options, there are a lot, but these are the ones that are interesting to us here:

:in : the file descriptor 0 which is the standard input 
:out : the file descriptor 1 which is the standard output

So, we can create a with and pass the reader as the :in argument to spawn, so that we can write with the writer from the main process and the process on the other end, the one created by spawn will receive it.

Note: it is important to close the writer in the initial process, if you don't, fzf still thinks that there might be more to read, and it shows it by displaying a spinner in the bottom left corner of the terminal.

We can use the same approach to get the content that fzf will output. Once a selection is made, fzf writes it to STDOUT. So we create another pipe, and give the writer as the :out option, the process started by spawn will be able to write to it and we can then use the other end of the pipe to read from it and get the selection from the user in the main process.

This is what til does, as you can see on GitHub.

A quick look at the C code defining the backtick function shows that it uses the pipe syscall, so we're essentially reimplementing something fairly similar to what ruby does for us with`echo 'a' | fzf`.

Note (another one): As I was writing this post, I realized that Ruby has another method related to spawning new processes and dealing with STDIN and STDOUT: . I haven’t looked too much into it yet, but it looks like it could simplify my code a little bit. That being said, the overall approach described above is still valid.

Using an external editor

For years I used git from the cli and relied on its commit workflow without really wondering how it actually made that happen. In case you're not familiar with it, from the CLI, when you commit with git commit you essentially have two options, you either provide the commit message inline, through the-m/--message option, or you leave this option blank and git opens an editor for you, by default vim.

What is actually really cool with this is that you don’t have to use vim, you could use pretty much any other editors, that being said, you probably want to pick one that is quick to start so you don't have to wait just to type a commit message. It's a little bit trickier with editors using a dedicated window such as vscode or macvim. GitHub's documentation has a page explaing how you can use the most common visual editors as git editors.

So, as a git cli enthusiast, I wanted to replicate the same worflow: you picked a category, now let’s write the content, in the editor you like using, so you can format things the way you want.

It turns out that it’s apparently “the right approach” to first look at the VISUAL environment variable and then at EDITOR as explained here and there.

Reimplementing the git commit workflow turned out to be very little work, you first create a file, Ruby makes that easy with the class, we then use either system or spawn with the value in $EDITOR or $VISUAL (we default to vi, just in case, so we have something).

The main difference between spawn and system here is how they return, system does not return until the process is over, whereas spawn returns a pid. Since we basically want to wait for the user to close the editor, system is easier, with spawn we would have had to use waitpid to wait.

Once the child process is done, we can read the content of the file, and VOILA! We have the content, formatted by the user, in their favorite editor (unless they ended up in vi and couldn't figure out how to exit).

You can see that til does exactly what I just described

Creating a commit using the GitHub API

And now, the final piece of the puzzle, we have a category and a string representing a new TIL, it’s time to create a new commit on GitHub. The GitHub API must have an easy way to do this right?

Well?

You can do it! But is it easy? I’ll let you answer on your own.

The new commit needs to contain two changes, the new file we want to create, but also, and this is really one of the reasons why I wanted to create this tool in the first place, the updates to the README file, to keep the table of content and the links up to the date with the new TIL.

This blog post was really helpful but the fact that it didn’t include any code examples means that I spent a few hours (😭) trying to get things workings, here’s a summary of what I’m doing to create a new commit, I hope you’re ready:

You can find documentation about the API endpoints I’m using on the following pages:

See it in action

Image for post
Image for post

Conclusion

The current version (0.0.4) is very very basic, but it gets the job done, and I’ve been using it for a few days already. I have a few thoughts about what I would like to do next:

  • A “real” cli, probably written in go, so that it’s easier to distribute, with brew for instance. There doesn't seem to be any formulae/casks namedtil, so I should hurry up!

Questions? Comments? Hit me up on Twitter!

Existing gems

There are a few similar gems, both in names and features available on ruby gems, I checked all of them before publishing mine:

  • til_cli, there were two issues with this one. First, it does not seem to work with latest ruby 2.7.1 because of json 1.8.1 not compiling. And also, it runs git commands locally, it basically wraps git calls, which is great, but means that the tool is not really a standalone tool, I wanted something I could run from anywhere, and that would work on its own.

Originally published at https://blog.pjam.me.

Written by

Software Engineer by day, Runner by day as well.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store