Multi-select, filterable command-line interface with inquirer
In this blog, I have a feature that lets me connect multiple blog posts to each other. I then show them as “Related posts” in the right sidebar of this blog (if read on large enough viewport).
Technically, this is maintained in the front matter of each post under
related_posts
key:
---
related_posts:
- title: "Another post"
slug: another-post
- title: "Second related post"
slug: second-related-post
---
This works very well but there’s one big issue and it’s something I’ve been struggling to figure out for a long time. Managing these connections is cumbersome. If I write a new post and want to connect it with three other, existing posts, I need to manually copy-paste the titles and slugs of each and add to multiple files and so on.
I’ve built so many prototypes to try and figure it out but now I finally got the right inspiration.
Desired workflow
I started designing this with my desired workflow.
I already use a bunch of custom Javascript scripts to manage different parts of my workflow (like downloading posts from Notion, fetching popular posts from analytcis and so on) for this website so I decided to make this as one.
The key reason why I use this approach is that it keeps the tooling in the repository.
After I’ve written a new post and downloaded it from Notion, I want to run
npm run related
and want it to start an interactive CLI session where I could choose all the posts I want to connect with each other and I want it to be smart enough to add or update the connections, as needed.
Building CLI tool with inquirer
I have previously used commander.js to build CLI tooling for this project and it’s very nice for building non-interactive command-line interfaces. This time, I needed to build something more interactive so I decided to go with inquirer that I’ve used before for building similar tooling.
I started with the multi-select feature and used inquirer’s checkbox prompt which provides it out of the box:
const posts = listBlogPosts();
import("@inquirer/checkbox").then(async ({ default: checkbox }) => {
const selections = await checkbox({
message: "Choose posts to connect",
choices: posts,
pageSize: 15,
loop: true,
});
});
My listBlogPosts()
reads in all the blog
post titles and slugs from the repository.
When I now run this script, I get a paginated list of 15 blog post titles per
page and I can select and unselect them. Once I submit the selection,
selections
becomes a list of the slugs
of selected posts.
At the time of building this, I had almost 400 blog posts so going through them in alphabetical order one by one wasn’t gonna work.
Next, I wanted to add a way to filter the list by text queries. As I researched it further, I found inquirer-checkbox-search that does exactly what I want. It’s a 7-year old project but it works like a charm.
To implement it into my project, I added this as my main CLI code:
import("inquirer").then(async ({ default: inquirer }) => {
inquirer.registerPrompt(
"checkbox-search",
require("inquirer-checkbox-search")
);
const slugs = await inquirer.prompt({
type: "checkbox-search",
name: "posts",
message: "Choose posts to connect",
source: filterPosts,
});
connectPosts(slugs.posts);
});
Two custom pieces of code are
filterPosts
and
connectPosts
.
function filterPosts(answers, input) {
input = input || "";
const inputArray = input.split(" ");
const posts = listBlogPosts();
return new Promise((resolve) => {
resolve(
posts.filter((post) => {
let shouldInclude = true;
inputArray.forEach((inputChunk) => {
// if any term to filter by doesn't exist, exclude
if (!post.title.toLowerCase().includes(inputChunk.toLowerCase())) {
shouldInclude = false;
}
});
return shouldInclude;
})
);
});
}
I picked up the code from the repository’s example and adjusted it to use my posts instead of the list of states. It takes in an input, splits it into multiple queries and checks which posts they all match to and returns that filtered list to inquirer.
The workflow is so nice. I can quickly search and select the posts I want to
connect with each other.
connectPosts
then takes those posts,
reads their existing frontmatter, adds missing relations into
related_posts
key in frontmatter and
saves the information back to the files.
Big thanks to Simon and Lauren for the amazing tooling you’ve built! ❤️