Multi-select, filterable command-line interface with inquirer
15 June 2024 | 12:00 am

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.

Multiselect checkbox view on the command line, showing a list of 15 posts, some of them selected

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.

Screenshot of filtered list of posts. Query is rss and it shows two posts out of of which the first one is selected.

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! ❤️


Crafting tabletop games
12 June 2024 | 12:00 am

Andrei invited us to create something and share it for this month’s IndieWeb Carnival, themed “DIY — Something from (Almost) Nothing”:

Create something either using a skill you already have (leather working, woodworking, coding, painting, cooking, gardening). Instead of going to the store and buying something that supports the consumerist world we live in, make something from scratch that makes your life a little bit better. If it helps one more person, even better.

You can find all my carnival entries in at the category page for IndieWeb Carnival if you wanna see what I’ve written previously about core memories, async friendships, accessibility in personal web, being good enough and creative environments.

This month, I want to talk about my hobby of making physical things: tabletop games.

History of my relationship with crafting and DIY

The school system did a fantastic job in making me feel completely inadequate about creating anything physical. Whether it’s woodwork, needlework, metalwork, drawing, painting or anything else do-it-yourself (DIY) related, for most part of my life I considered it a practice out of my reach.

In school, I always felt there was such a divide between the “knowledge subjects” like math, science, languages and history and the “doing stuff subjects” like sports, woodwork and arts.

In the knowledge subjects, we spent almost the entire time learning how to do things and practicing the core skills. In doing stuff, we were just expected to make stuff and if you didn’t know how or didn’t have the skills already, there was very little education, at least compared to the other subjects.

So I learned very quickly that I’m not good enough for it and cut it out of my life for a good couple of decades. I focused on creating digital things with my fingers running around the keyboard: mainly software and writing and out of necessity, a bit of graphical design.

Once I became what some people might call “an adult”, I started to discover my interest towards making things and started finding avenues where I could get into making and creating.

Redesigning and crafting tabletop projects

Tabletop games (an umbrella term I use to cover all sorts of board, card and dice games) are a hobby of mine and in addition to playing, I’ve always been fascinated by the rules and systems that those games run on top of.

I’ve been attempting designing my own games a couple of times with no success (I haven’t really pushed through and have given up too early). A few years back, I started designing a Secret Santa style drafting card game for BoardGameGeek’s 2020 Christmas PnP Game Design Contest but couldn’t find an enjoyable game loop.

A few years before that, I spent a few weeks prototyping a worker placement game which didn’t even get into a playable prototype phase.

Right now, I’m designing my most recent attempt, an engine-building worker placement game where players are space merchants and pirates traveling across planets to win in the game of intergalactic capitalism.

I’m less focused and interested in making a great game (which I hope to still achieve) but rather I go against the grain of every tabletop game design advice and start with an interesting theme or mechanic (or in this case, how cool tabletop games look like in C cassette boxes):

Couple of hand-drawn playing cards on index papers inside transparent sleeves, a few dice scattered around and two C cassette boxes, one with Roll the Rest text logo in front and another one with Skulls of Sedlec text logo in the spine.

In the picture, I have a few prototype cards for my space capitalism game and two C cassette box designs I’ve made for Skulls of Sedlec and my current in-progress redesign called Roll the Rest.

C cassette boxes are absolutely wonderful: they fit 40 non-sleeved or 22 sleeved cards, they look stunning when stacked up next to each other on a shelf and you can print simplified game rules to the inside cover.

The DIY part that I truly enjoy has many parts. One is the prototyping of games and game parts like cards and tokens. I have an assortments box full of different stuff: meeples, dice, tokens, coins and other goodies that I can pick up and start experimenting with:

A box with a lot of small compartments filled with variety of colorful board game tokens, meeples and dice.

I also have hundreds of card sleeves (both transparent and one-sided) and a healthy stack of index cards that I use to prototype and design cards. Sometimes I just slide an index card into a sleeve and test it out or if I need something bit more sturdy for easier shuffling and dealing, I pop in an extra Pokemon or MtG card and slide the paper in front.

Games that fit into a pocket

My niche in tabletop games is with small box sizes. It all started with the Minimal Travel Table Top Game Collection back in 2019-2020.

I travel a lot around Finland and rest of Europe and am often waiting for trains or ferries, traveling in trains and in general hanging out with a lot of people. So I wanted to create something that I could always carry with me in my backpack and have a selection of games to play with people.

A black deckbox, few grey flat circle tokens, three sets of small dice in different colors, two larger translucent dice, a smaller deck box and a deck of cards in red sleeves.

I took a bunch of existing games, some designed to be small and some sold in large boxes in retail stores, and redesigned them to be playable with cards, dice and tokens and everything fit into a deck box.

Then came the pandemic and I couldn’t travel and I still felt excited about the first project. So I took a look at the print and play solo games people had designed in BoardGameGeek and created Minimal Travel Table Top Game Collection 2: Social Distancing Edition that features 8 games that can be played alone.

A fan out of nine game cards with illustrated scenery. Next to them, 9 wooden blocks and 9 dice, 3 of each in green, orange and red.

It was a lot of fun and helped in part getting through the solitary of the pandemic.

My hobby then took me to discover the concept of Universal Card Systems like Everdeck, The Deck of Many Dice and many others. Instead of creating a small collection of individual games, I could create a deck of cards that can be used to play many games.

I took some of my favourite card games like 6 Nimmt! and Texas Showdown as a baseline and built Minimal Travel Table Top Collection 3: Project 108 around them. It was by then the best out of the projects but had the worst name.

Three rows of cards slightly overlapping each other.

And it was glorious. The world started to open up again and I kept bringing the deck everywhere with me and had so many great moments playing with people.

But I wasn’t quite satisfied. This third MTTTC was my first foray into the world of these universal systems and the more I played with it, the more I noticed small places for improvement.

Dissatisfied with the name and few bad design choices, I set out to make a better version.

Meet Potluck, the fourth Minimal Travel Table Top Game Collection:

A deck with a few cards fanned out face-down, showing a dark blue background with white logo saying Potluck. Below them, five white cards face up showing numbers 1, 11, 21, 31 and 71 and a variety of other elements in the cards. Below them, a single card listing a bunch of games and credits for the project.

It was a big improvement. The brand turned out really sleek, the card designs fixed a bunch of issues with the older version and introduced some new games into the mix. Right now, this deck plays 2200+ different games and I never leave home without it. It is built on the same foundation of games than the third one.

Sometime in the middle of all these projects, I also shrank some dinosaurs. A wonderful dinosaur drafting game Draftosaurus was not available for purchase in Finland so opened up Affinity Designer and created cards and then bought a set of Here to Slay meeples to act as dinosaurs (the picture is pre-HtS meeples):

A three photo collage showing cards, meeples and a bag for Draftosaurus board game redesign

It fits into a bit larger deckbox and can play up to five people and provides as much fun as the main big box version.

Hobbies for the sake of hobbies

I was reading through Austin Kleon’s Keep Going after discovering it through Claudine’s last month’s carnival entry and a chapter about doing art for the sake of art rather than trying to monetise it resonated with me a lot:

We used to have hobbies; now we “side hustles”. As things continue to get wrose in America, as the safety net gets torn up, and as steady joobs keep disappearing, the free-time activities that used to soothe us and take our minds off work and add meaning to our lives are now presented to us as potential income streams, or ways out of having a traditional job.

When I first introduced Minimal Travel Table Top Game Collection IV: Potluck to the world, I got a lot of “you should sell these, they are cool” messages from people. And I did consider it for a while because I was proud of how it turned out and it was the closest thing to “a product” that I’ve ever made.

But over time, I’ve realised I don’t want to sell them. I don’t want to add the burden of setting up shop, dealing with print orders and logistics and accounting and taxes to distribute something I created for my own enjoyment – part for practical reasons but in big part as an experiment, learning project and an artistic exploration.

I’m happy to have a hobby where I can just create for myself and not worry about others’ opinions or feedback. A hobby that is completely different from my day job.


Webmention bookmarklet
10 June 2024 | 12:00 am

Lately, I’ve been having some issues with automated Webmention sending tools so I’ve been sending a bunch of mentions manually with curl which can be a bit cumbersome.

The flow with sending them manually normally looks like this:

  1. Open my blog post
  2. Go through each link
  3. For each link, inspect the page source and search for rel=”webmention” element
  4. Copy my source post URL, add to curl command, then copy the target URL, add to curl command and finally copy the rel=”webmention” URL and add to curl

With my new Webmention bookmarklet, I get the entire 2a and two-thirds of the 2b of the workflow above for free with a click of a button.

What the bookmarklet does is that it searches for rel="webmention" link and if one exists, it grabs that and the URL of the page and crafts them into curl command that I can just paste into the terminal. If not, it’ll give me an alert so I know I can continue to next link.

So the new flow now looks like:

  1. Open my blog post
  2. Go through each link
  3. For each link, click my bookmarklet
  4. If webmention endpoint is found, a text of -d target="website url" webmention_endpoint gets added to clipboard
  5. Write curl -i -d source="my url" and hit paste and hit enter

Here’s the bookmarklet

(() => {
  const copy = () => {
    const textArea = document.createElement("textarea");
    const webmention = document.querySelector('link[rel="webmention"]')
    if(webmention) {
			const href = webmention.href
	
			const url = window.location;
			
	    textArea.value = `-d target="${url}" ${href}`;
	    textArea.style.top = "0";
	    textArea.style.left = "0";
	    textArea.style.position = "fixed";
	    document.body.appendChild(textArea);
	    textArea.focus();
	    textArea.select();
	    try {
	      document.execCommand("copy");
	    } catch (err) {
	      console.error("Copying err", err);
	    }
	
	    document.body.removeChild(textArea);
    } else {
	    alert('No webmention endpoint')
    }
  };

  copy();
})();

and minified to be copied to a new bookmarklet’s URL field:

javascript:(function()%7B(()%20%3D%3E%20%7B%0A%20%20const%20copy%20%3D%20()%20%3D%3E%20%7B%0A%20%20%20%20const%20textArea%20%3D%20document.createElement(%22textarea%22)%3B%0A%20%20%20%20const%20webmention%20%3D%20document.querySelector('link%5Brel%3D%22webmention%22%5D')%0A%20%20%20%20if(webmention)%20%7B%0A%20%20%20%20%20%20const%20href%20%3D%20webmention.href%0A%0A%20%20%20%20textArea.value%20%3D%20href%3B%0A%20%20%20%20textArea.style.top%20%3D%20%220%22%3B%0A%20%20%20%20textArea.style.left%20%3D%20%220%22%3B%0A%20%20%20%20textArea.style.position%20%3D%20%22fixed%22%3B%0A%20%20%20%20document.body.appendChild(textArea)%3B%0A%20%20%20%20textArea.focus()%3B%0A%20%20%20%20textArea.select()%3B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20document.execCommand(%22copy%22)%3B%0A%20%20%20%20%7D%20catch%20(err)%20%7B%0A%20%20%20%20%20%20console.error(%22Copying%20err%22%2C%20err)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20document.body.removeChild(textArea)%3B%0A%20%20%20%20%7D%0A%20%20%7D%3B%0A%0A%20%20copy()%3B%0A%7D)()%3B%7D)()%3B



More News from this Feed See Full Web Site