35% off Custom React Hooks Crash Course
Skip to content
Writer Buddy

Writer Buddy

Published: at 06:03 AM

How often should I publish? - I'm getting the question now and then.

The only writing routine that works is the one you can stick to.

It's different for everyone. I pivoted from early morning writings to writing at night, between 6 and 8 PM. I write for an hour and a half intensely, but I have already established a habit of writing each day.

When I was in the progress of creating this habit, I knew that setting short-term, realistic goals might help.

Motivation

I thought I could build an app to set daily word-count goals, and it'll track how many words I've written that day!

I was also writing in different places, sometimes in the Hashnode editor, sometimes in Notes or VSCode, whatever felt more suitable at that time.

So I wanted to make a desktop app independent of specific editors or websites.

This is how Writer Buddy was born! (already looking for a better name 😄)

The app consists of a panel where you can set the number of words you want to type and a progress bar.

The progress bar displays how many words you typed and acts as an area where you can start the app's word-tracking feature and close the app.

Here's how it works:

Here are some of the challenges I faced while building this app.

Capturing global keyboard events

One of the challenging parts was finding a reliable, global keyboard listener that works with Node.js and doesn't need extra dependencies (such as jnativehook, which requires Java).

I chose iohook despite not being updated recently, but it worked perfectly.

On Mac, it needs BigSur or older, but it only works with Intel processors for now.

iohook has an event listener, such as an HTML element would have where you can listen to the usual event you would listen to on a web page, keydown, keyup, mousedown, mouseup, etc.:

  ioHook.on('keydown', (event) => {
    // event.keycode
  });

iohook does not track these events by default, so make sure you call ioHook.start(); otherwise, your event listeners won't fire.

Generating a dynamic System Tray Image

The dynamic progress bar in the system tray is nothing more than a PNG generated each time the user types a word.

I'm using pureimage, which provides the HTML Canvas 2D API for Node.js.

This code creates a png image 100 by 20 with a white background with the text wordCount/goal with Roboto font type and saves it to the path returned by getProgressBarPath().

async function createProgressBarImage() {
  const progressInPercent = (wordCount / goal) * 100;
  var img = PImage.make(100, 20);
  var ctx = img.getContext('2d');
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, 100, 20);
  ctx.fillStyle = '#00ff00';
  ctx.fillRect(0, 0, wordCount < goal ? progressInPercent : 100, 20);
  ctx.fillStyle = '#0000ff';
  ctx.font = "20pt Roboto";
  ctx.fillText(`${wordCount}/${goal}`, 5, 17);
  await PImage.encodePNGToStream(img, fs.createWriteStream(getProgressBarPath()));
}

And I'm calling this function every time a word is typed.

The word-capturing mechanism checks if the current key is a Space Bar or an Enter and if the previous character wasn't a Space Bar or an Enter. This could be improved a lot because it produces some false positives:

  ioHook.on('keydown', (event) => {
    if (!isTracking) return; // must click Start tracking in the tray

    if (event.keycode === SPACE || event.keycode === ENTER) {
      if (previousEvent.keycode !== SPACE && previousEvent.keycode !== ENTER) {
        wordCount += 1;
        reloadTrayTitle(tray)
        updateProgressBar(tray);
      }
    }

    previousEvent = event;
  });

GitHub

You'll find the complete repository here: https://github.com/akoskm/writer-buddy.

Pull requests are welcome!

Here are a couple of ideas that I'd like to add to the project:

How could I improve this app so you would use it in your writing journey?