Heading image

The recent evening’s project was to generate a CLI helper for myself to quick sketch up rough diagrams and flows at work as PNG images.

In action

The tool in action

Static output

Static output

Why? After recent events at work, I am helping to take over our department’s developer tool that enables other users to get up and running quickly at work.

This tool is written in Golang and requires an understanding of certain concepts such as named pipes, POSIX standard interrupts, RPCs and more. The hope is that I can quickly generate rough diagram flows to help illustrate what is happening.

The MVP for this was to just get auto-sized rectangles that would flow from 1..n with lines between each using a little bit of math.


This post uses concepts that were taken from previous blog posts. Please check them out first to understand how a bunch of the screenshots and argument parsing is working.

  1. Screenshots with Puppeteer — Blog Post
  2. Intro Yargs Parser — Blog Post

Getting started

Setup a new project:

The above is what we’ll use in the Node script. As for RoughJS itself, we are going to use CDNs for RoughJS to load in the vanilla HTML template. This does mean that our project will only work if connected to the internet.

There are other ways to use Nodejs and Nodejs canvas locally, but this was for me to get it done quickly. I may cover another post with node-canvas another time as it is super handy!

Writing the script part by part

Let’s begin our script with requirements and a simple help message:

Here I am requiring in puppeteer and yargs-parser, then writing a template string for me help. This isn't as useful as other libraries that can help you write out nice CLI "help" options, but it will do. We are going MVP here.

If you run node index.js --help or node index.js with no following arguments, it will now print the help out.

Help example

Output from help

First attempt as legible parsing

In the help printed, we have an example rough "First box, no options {} | Second | Third with red fill {fill: 'red'}". What's happening here?

The endgame is to have a command “rough” that I can use anywhere on the local machine, so running that during development would actually be node index.js "First box, no options {} | Second | Third with red fill {fill: 'red'}"

As for the part within the quotations, that is my attempt to ensure that string becomes the first argument of argv._ and then I will parse the string to grab out the parts between the pipes | and use that to pipe from one text to the next.

The parts within the brackets {} will be the options I pass to Rough in order to add in the colours, fills, etc.

Adding in the constants and helpers

This part is the tricky part. We’re going to add in the constants and help functions. I’ve added some comment annotations to try explain, but the gist of it is that we’re going set a set HEIGHT for each rectangle for now, and a set DISPLACEMENT for the space that we want between each rectangle. That will help us calculate the height and help with adding lines.

As for PER_CHAR, that's an arbitrary, allocated size I've come up with after a couple of tests for how much larger I want the rectangle to grow based on the amount of characters added.

The PADDING is used to give space between the edge of the canvas and the box.

The helper functions addRectangle, addTextToRectangle and addLine return strings that we will interpolate into the HTML. Those strings returned come from RoughJS and a text-onto-canvas w3schools page that helped me figure out what was valid JS and usage of those libraries.

Finally, the generateString is the difficult part. The general idea is that the shapes global constant that was split on the pipes will iterate. If it contains options {} it will attempt to slice that out to differentiate the text and options, else it will just assume it is all text.

It will then push these to a global shapeObjs array. There is no reason for this to be global, but I wanted to hack a way to log it out through the main function (coming later).

We calculate the WIDTH value at runtime to help with the different sizing based on the text. This is based on a PER_CHAR and DISPLACEMENT values. This just required jotting down some arithmetic. It isn't complex math, but I still always need to remember how high school algebra works when figuring it out.

Finally, I am iterating over that shapeObjs and building out a string that will be valid JavaScript. The validity is important here. A lot of this is really a big hack, and since these are all strings, your linters may not be there to help you.

Note: There isn’t really an error handler here. Just MVP’ing. That’s my excuse, anyway.

Phew! That is the complex part out of the way.

Adding the script and valid HTML

We now use that generateString function to generate out a script variable.

This script variable will then be injected into the html variable below. The script has two runtime helpers in addTextToRectangle and addTextToCircle. The circle hasn't been implemented at time of writing (MVP, MVP, MVP!), but that other function helps us add in the text to the rectangles since it does come with RoughJS... at least, I didn't see it in the documentation.

That helper will center the text.

calcLongestTextSize is another helper function to determine the size for the canvas. This is actually repeated code (RIP DRY principle). MVP, I'm telling you!

Finally, we have the html variable which is valid HTML.

There are two important tags in the head which load an Open Sans font that I downloaded from Google Fonts and a script to load RoughJS from a CDN:

Running this altogether

Finally, the magic. Again, this basically comes from another blog post on screenshots with Puppeteer, so I won’t explain too much here:


Let’s now run some of the examples to see what we get!

This will output to rough.png, so if we check it out we can see our success!

First output

First output

Let’s run a couple more to see our success.

Second output

Second output

Third output

Third output

Note: This output actually doesn’t have much spacing. That means I should adjust the PER_CHAR value to something more suitable.

Fourth output

Fourth output

Great success! That is all for today, I am already late for standup!

Resources and Further Reading

  1. Open Sans
  2. Text onto Canvas
  3. RoughJS
  4. Puppeteer
  5. Yargs Parser
  6. Screenshots with Puppeteer — Blog Post
  7. Intro Yargs Parser — Blog Post

Image credit: Bekky Bekks

Originally posted on my blog.

Senior Engineer @ Culture Amp. Tinkerer and professional self-isolator in 2020.

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