Outsourcing My Memory to Gum

My memory is notoriously bad. I forgot what I ate today or whether I washed my hair. I walk into rooms and forget why. I tell friends stories they were themselves involved in.

You better believe that, when I return to some sort of web development project after several months, I've long since forgotten how to do anything with it. This is compounded by the fact that every tech stack has their own default way of doing things. For example, some npm project bootstrappers will create a script called start while others will create one called develop, both of which are used to start a local development server. I will sometimes add scripts for various types of deployment, which are different depending on the project, the tech stack it's on, where it's deployed, and how good I am at naming things on that day. πŸ˜…

My Crappy Workaround

I usually work around these problems on Node-based projects by starting with npm run when I return to a project to get an overview of the project's scripts so I can understand how to jump in.

An animation of a terminal session. The user runs `npm run` to get a list of the available scripts in an npm project. The user then examines the output and types `npm run start` having chosen the script they want to run.

(Confession time: in truth, I cat package.json, but npm run is what I should have been doing instead.)

This is cumbersome and clunky though. I'm manually looking at a file or at some output based on a file to figure out what options I have. Then, I manually run the option that does what I'm trying to do. The data that I'm looking at is already nicely structured in JSON, so it seems like a natural fit for the computer to handle the first part for me and just present me with options I can run directly. Now, let's see how to make that happen…

A Cleaner Approach with Gum

That's when I stumbled upon Charm's Gum. Gum is basically a UI component library for shell scripts. Gum has a choose command that outputs a handy option chooser component. This would be a great UI for my use case. I will need to pipe the available options into it and then run the result. Let's start putting the pieces together.

I would like a single command I can run from any npm project. To accomplish this, I'll write a function into my shell's config. In my case, that's ~/.zshrc. I call the function donpm which you can interpret as either "do npm" or "Don P.M." who I imagine as a cool person with aviator shades who only comes out at night (yes, even with the shades; they're that cool) and can figure out exactly how all your npm projects work. 😎

# loads of other stuff

function donpm() {
  $SELECTION=(jq -r '.scripts|to_entries[]|((.key))' package.json | gum choose)
  npm run $SELECTION
}

# loads more stuff

This function has a dependency on jq. I could probably do this with sed, but it's easier with jq and this is just for me to run on my system so I'm going to use it. With jq, I'm reading the contents of package.json and parsing out the scripts. I pipe that over to gum choose to display the chooser component.

gum will spit back the selection. I need to capture that so I can run the selected command, hence assigning the result to a variable (SELECTION).

Once I have that captured, I can then run npm run $SELECTION to interpolate the selected command into my npm run. (I tried piping to this, but that didn't seem to work. Maybe there's a fancier, better way, but this one seems to work well enough… well, almost. πŸ˜‰)

Adding an Option

This is cool and it (mostly) works, but I want to add a few features. First, I'd like to add a "Cancel" option to make it easy for a user (i.e., me) to change their mind and do nothing.

# loads of other stuff

function donpm() {
  OPTIONS=$(jq -r '.scripts|to_entries[]|((.key))' package.json)
  OPTIONS+="\nπŸ›‘ Cancel"
  SELECTION=$(echo $OPTIONS | gum choose --header "Choose a script to run")

  if [[ "$SELECTION" == "πŸ›‘ Cancel" ]]; then
    gum log --level info "Execution was cancelled"
    return 1
  else
    npm run $SELECTION
  fi
}

# loads more stuff

Now, I've split things up a bit differently. I need to separate the steps of grabbing the scripts from package.json and piping them into gum choose. I'm doing this to give me the opportunity to add an option of my own. I capture to the OPTIONS variable and then add my "Cancel" option to that variable. Then I can echo the whole thing back to pipe it into gum choose.

This also requires me to make a special case: if the selection is the "Cancel" option, I log out a message (using gum log) and exit with a 1 since nothing was executed here. Not sure if that's the right move since the command was actually "successful" in doing what the user wanted, which happened to be nothing, but it should be fine. In any other case, I run the selection through npm run just like before.

Yeah, but What Happens When I REALLY Forget?

It's mostly good at this point, but there's one more key piece I want to get in here. This whole thing is about me forgetting. So, what happens if I think a project is an npm project and try to run donpm even though it isn't? Maybe it's a Python project or maybe I'm just in the directory where I keep scans of old video game magazines. Let's handle that.

# loads of other stuff

function donpm() {
  if [ ! -f "package.json" ]; then
    gum log --level error "package.json not found in the current directory"
    return 1
  fi

  OPTIONS=$(jq -r '.scripts|to_entries[]|((.key))' package.json)
  OPTIONS+="\nπŸ›‘ Cancel"
  SELECTION=$(echo $OPTIONS | gum choose --header "Choose a script to run")

  if [[ "$SELECTION" == "πŸ›‘ Cancel" ]]; then
    gum log --level info "Execution was cancelled"
    return 1
  else
    npm run $SELECTION
  fi
}

# loads more stuff

This gives me some safety so I can fully forget almost everything I know and still at least get a solid error message out of this function.

The Final Product

After all of these improvements, here's what my workflow looks like when I come back to a project I've forgotten everything I ever knew about:

An animation of a terminal session. The user runs `donpm` to get a list of the available scripts in an npm project with a caret pointing at the top option (`start`). The user examines the output and arrows down to the `build` command, pressing enter. The build command starts with some preliminary output.

Can it Get Any Better Than This?

Well, yes, of course it can! What you're looking at is the function I'm using now, but I have some ideas for improvements I may make someday if motivation strikes:

Feel free to take those on yourself if you're one of those "students surpassing the teacher" types.

Bonus: VHS

I spend a lot of time in the terminal, and since that's Charm's whole game, I poked around to see all the tools they have available. Another one I found intriguing aside from gum was VHS.

VHS lets you build programmatic GIF animations of terminal activity. No more screen recording the terminal emulator. Cool! So cool, in fact, I used it for the GIFs you've seen in this post.

If you want to see how these are defined, check out the code for the two that appear here: