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.
(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:
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:
- Add "descriptions" to each script in the form of the commands being run.
- Filter out the
pre
andpost
lifecycle hook scripts since these aren't usually invoked manually. - Accept an optional argument of the script name. This would allow Don to entirely take the place of
npm run
, saving me two characters per script invocation if I happen to recall the script name I want to run! If the function gets an argument, just run the npm script named by that argument. If not, show the choices just like it does now.
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: