Building a cross-platform TUI CLI app in Go

Building a cross-platform TUI CLI app in Go

I built a cross-platform TUI CLI app in Go. It runs on macOS, Windows and Linux.

The app is named timerrr and you can go install it today.

What is a TUI CLI app?

TUI stands for Terminal User Interface, which is exactly what it sounds like—a user interface in the terminal!

CLI stands for Command Line Interface, which is a tool powered by text that you use in the terminal by typing commands.


timerrr allows you to start timers, save timers so they're ready to start when you need them, and speaks a custom message when done.

Add saved timers

You can save timers with timerrr add. It'll bring up a UI with input fields for filling out your new timer.

  • Name your timer whatever you want. If you don't provide a custom message, the timer name will be spoken aloud on supported platforms

  • Write a duration using duration syntax, e.g. 1m30s for a timer that's 1 minute and 30 seconds, or 45s or a timer that's 45 seconds, or 5m for a timer that's 5 minutes.

  • Customize the text-to-speech message.

View all timers

You can view all timers with the default timerrr command.

Start one-off timers

You can start a one-off timer with the timerrr start command. By default, it'll start a 30 second timer. To configure settings for your timer, including the duration and the message that will be spoken, type timer start --help to get more info.

Text-to-speech when done

By default, it'll say the timer by name is done. You can customize this message with the --say flag for one-off timers, or by inputting it when you create a timer with timerrr add.

Currently, the only operating system that supports TTS is macOS right now, but I have stubbed out the methods so that support for other operating systems can be added.

Resources, Tools, and libraries I used

I used a lot of great tools and libraries to help me build this.

First, I am using Go, which is a language that I don't have much experience with but it has been quick to get started in. The app is written in Go version 1.20.

To manage my versions of Go, I am using gvm (Go version manager). I prefer to use version managers for software development and in the past have used them for Ruby, Python and Node.js. Different projects use different versions of Go, so gvm has come in handy for switching versions when switching projects.

Screenshot of the Go website

I am currently trialing and evaluating GoLand, the JetBrains IDE for Go. I have configured goimports as the auto-formatting tool, which comes preinstalled with GoLand. GoLand has held my hand while venturing through this new land. While it's pretty opinionated about how you should use it, e.g. never type your imports because they'll be cleared manually, after adapting to its ways, I found it very helpful for doing all of the things that you can expect from a JetBrains IDE: advanced refactoring abilities, generating and running tests, etc.

Screenshot of the JetBrains GoLand web page

To power the CLI app, I am using cobra-cli. Cobra powers the argument and flag parsing, displays the help documentation, and allows me to map functions to commands. The tool also generates and scaffolds a CLI app for you.

For the Terminal User Interface (TUI), I am using libraries by Charm. These libraries are the starring attraction of the TUI and what makes it look so good. They have a lot of libraries, and I'll be honest, I don't know what most of them do, but I ended up using a few since their examples used them. The bubbletea library is the main library that provides the command framework—after cobra routes the user to the cobra command, you create a bubbletea application with a bubbletea command. The components I am using are the progress component, the table component, and the text input component. The components in the examples for Bubbletea are from the bubbles library.

I used other tools for testing and publishing, but I'll get into that in another section.

Testing in Go

While doing this project, I learned it was really easy to write tests in Go. You can get an editor like GoLand to bootstrap tests for you, or you can write them out yourself. I used the testing library testify, which plugs into go test seamlessly and helps with mocks and assertions. For this simple, proof-of-concept I didn't write any tests that required mocking, but the assertion library is nice and what you'd expect from an assertion library. I also used the tool cover to allow me to run tests with the -coverage flag to see test coverage.

Testing cross-platform code in Go

Go has a really cool feature that allows you to conditionally run code depending on the operating system simply by adding a suffix to the file name. For example, to run a suite of tests only on Mac, you'd create a file named something like foo_darwin_test.go. GoLand even shows helpful hints to let you know that you've done it correctly.

Screenshot of test files that run only on Mac, Windows and Linux

So, it's one thing to create test files for all these operating systems, but how do you run them? Well, I used Github Actions for that. They have a matrix strategy that allows you to choose different operating systems to run the test suite against. And thanks to the above-mentioned file suffixes, only the tests for the given operating system will run.

The tests I have in this project aren't particularly complex, but they allowed me to get a feel for testing in Go. Also, seeing tests run on different operating systems made me realize I wasn't properly supporting Windows file paths—they use a back slash instead of a forward slash like unix systems.

Publishing with GoReleaser

Another great tool I used was GoReleaser. GoReleaser is a command line tool that compiles your tool for multiple platforms and allows you to publish releases via Github.

I will admit it was very easy to publish with GoReleaser and would say it's probably one of the simplest publishing tools I've ever used. You don't need to sign up for an account, and there's no centralized registry. You publish using Git and they currently support Github, GitLab and Gitea. It has other features, including allowing you to distribute private packages.

Being able to go install and go get anything from a Git URL is a great feature of Go, and GoReleaser seems to be leveraging that to allow developers to ship installable apps really quickly.

Screenshot of the GoReleaser home page


Mostly everything was a breeze, but I did have some layout trouble with the bubbletea library when launching one bubbletea command from another one. While I got help from the lovely people at Charm, there was a UI bug that only seems to show up in the Windows terminal. In the screenshot below, you can see that the table is behind the timer. I likely need to clear the view. Thankfully, calling clear before running a timer fixes that so I'm not in a rush to fix it. If you'd like to fix it, here is the open Github issue.


Another issue I had was with the release, specifically with the module name. I followed the tutorial on the Go website for getting started. It generated a module for me timerrr/main. Turns out, I didn't want the /main at the end. After staring at a few go.mod files, I saw that other projects omitted the /main file.


I don't have big plans for this app, this is pretty much it, but I would like to address a few things.

Fix bugs

I'd like to fix the UI bugs described. While the current one can be fixed by clearing the terminal before running commands, I'd like to not have that as a requirement.

Better cross-platform TTS support

Currently, there is text-to-speech support on macOS. I am using the native say command and randomizing some hardcoded voices.

I would also like to add TTS support in Linux and Windows. Right now, it falls back to the echo command which does nothing in a TUI app.

On Linux, I'd like to use Mycroft's Mimic 3. It sounds much more natural than the common espeak, but I would like to fall back to espeak for users that have that installed but don't have Mimic 3.

On Windows, I am hoping to be able to integrate with the pre-installed voices on Windows. That's what I'm using for my current Twitch Multilingual Text-to-Speech app, which I use for my gaming streams on Twitch so I don't completely neglect chat. In order for this to work, I'd need to make sure that there was a command I could call from the Windows terminal that could execute text-to-speech.

Watch me code it

I coded this on my Twitch stream. It took 13 hours but don't worry, I took brief breaks! I was having too much fun to stop.

I stream software development and gaming on my stream. Some things I've coded on stream:

  • Trackrrr, my own period tracker, a native Android app written in Kotlin

  • a multilingual text-to-speech app on macOS, which I use to power text-to-speech enabled by bits, elevated messages, and subs during coding streams. Built in SwiftUI on macOS.

  • techydrrroid, a chatbot that powers fun channel point redemptions and moderation features in my stream. Built in Kotlin using Ktor.

  • various things in Rust, which include working with the Rocket and Axum web frameworks, Serenity for building Discord bots, basic CLI apps, algorithm problems, and serverless functions that run in web assembly on Cloudflare Workers.

I also play video games sometimes.

View the stream Building a TUI app in Go.

Get the Code and use it

Here is the timerrr code repository.

Here is the wiki which includes installation and usage documentation.