If youâve ever wanted to test your typing speed or practice your accuracy, chances are youâve come across monketype.com. At its core, the concept is simple: type a sequence of random words for a set amount of time, and when the timer ends you get stats like words per minute (WPM) and accuracy. Now, I consider myself a pretty fast typist, so I didnât spend much time on Monkeytypeâuntil I got my hands on a Moonlander split keyboard (my baby). Suddenly, I needed a way to retrain my muscle memory and adapt to the new layout. Monkeytype worked fine, but hereâs the thing: Iâm a Vim user. Opening a browser just to type some words? Slow, clunky, and bruises my fragile ego. Thatâs when I had an eureka moment: why not bring Monkeytype into the terminal? Plus it finally gave me an excuse to tinker with Go, which Iâd been meaning to try.
The first step was figuring out what parts of Monkeytype I actually wanted to steal port over. Monkeytype has a ton of featuresâgraphs, charts, different modesâbut honestly, I didnât care about most of them (sorry, WPM graph lovers). I just wanted the essentials. I narrowed it down to three core features:
For the typing box, I basically wanted to clone Monkeytypeâs structure: three visible lines of text at a time. Once you finish one line, it slides up and a new one appears below. I also wanted color indicators for correctness (white for right, red for wrong), along with support for backspace and typo correction.
With the feature list locked down, it was time to actually start building. I decided to use Go as the programming language, along with a terminal UI (TUI) framework called Bubble Teaâwhich, by the way, is absolutely amazing. For styling, I also pulled in Lip Gloss, another Go package that helps with styling terminal apps since Iâm not a masochist and donât enjoy manually typing out the necessary ANSI escape characters. Both libraries have fantastic documentation, so if youâre ever thinking about building a TUI app in Go, I canât recommend them enough.
I wonât turn this into a full âHow Bubble Tea Worksâ article, but hereâs the quick version: Bubble Tea apps are modeled around a single state object (the model). That model gets updated through an update handler whenever events happen (like a key press), and then a view function renders the updated state into the terminal. Most of this project boiled down to manipulating and combining those three things: state, update, and view.
Iâm not going to walk through every single step of the development process hereâbecause letâs be honest, a lot of it would either be boring to read or turn this article into a novel. Instead, I want to zoom in on a couple of parts that I think you would find semi-interesting:
In CSS, centering an element is as easy as slapping on a margin: auto or justify-content: center. Unfortunately, terminals donât play that game. I had to manually calculate how to center the text box so it would scale nicely across different terminal resolutions.
The interesting thing about working in the terminal is that its âresolutionâ isnât measured in pixelsâitâs measured in characters. The width is how many characters fit across, and the height is how many characters fit vertically. That means you donât need to worry about pixels at all; you just need to think in terms of character counts.
Borrowing a bit from my game dev background, the math is straightforward: to center an element A inside a screen S, you shift A down by half the height of S, and right by half the width of S, minus half the width of A and half the height of A respectively.
width := (screenWidth - elementWidth) / 2
height := (screenHeight - elementHeight) / 2
I applied this same logic to the typing box by setting its initial top and left padding, using blank space characters to give the illusion of centered content. But there were some quirks to handle:
In the end, the effect worked really well: the textbox looks centered, the text is left-aligned, and the layout stays stable across different terminal sizes.
One of the biggest lessons I took away from this project is that you donât really understand a system until you try to build it yourself. What seemed like a simple typing app quickly spiraled into a rabbit hole of edge cases that I had never even thought about before.
Take spaces, for example. In MonkeyType, hitting space at the very start of a word skips that word entirely. Sounds simple, right? But then what happens if you hit backspace? Well, in MonkeyType, backspace will âundoâ that skip only if the word was skippedâbut if you had typed part of the word, backspace just deletes the last character instead.
The real challenge wasnât just coding these behaviors individuallyâit was designing a flexible model that could handle every possible typing state without collapsing under its own complexity. Youâve got words being skipped, corrected, partially typed, fully typed, and mistyped, and all of these states interact with one another depending on how the user keeps typing. At one point, I had this tangled mess of conditionals where fixing one bug would create two new ones.
Eventually, I had to step back and think about it more like designing a finite state machine: every word could only be in one of a handful of states (e.g. untyped, active, completed, skipped, corrected), and every keystroke was just a transition between those states. Once I reframed it this way, I was able to cleanly model all the edge cases while keeping the update logic efficient enough to run smoothly in the terminal.
There were plenty of other fun challenges I tackled during this projectâlike figuring out efficient rendering tricks in the terminalâthat I never even got the chance to dive into here. But this articleâs already getting long enough (at least by my standards), so Iâll wrap it up with the part everyone actually wants to see: a quick demo gif.