Yar Kravtsov

pin: A Small CLI Spinner for Go

· go

I wrote pin, a terminal spinner for Go that uses nothing but the standard library. It exists because most Go CLI spinner libraries have the same problem: they want to be frameworks.

You pull in one dependency for a tiny rotating character in your terminal, and suddenly you’re carrying transitive dependencies, ANSI abstractions you didn’t ask for, and an API surface that assumes you’re building a dashboard.

All I wanted was a dot that spins.

Zero Dependencies

pin does colors, prefixes, separators and live message updates, and that’s it. The part I care about most is that it imports only the Go standard library (zero external dependencies).

Dependencies are promises. Every go get you run is a bet that some stranger’s weekend project will keep working the way you expect. For something as trivial as a spinner, that bet is genuinely not worth taking. A package with no dependencies is easy to audit and easy to vendor, and it will never surprise you with a breaking change three levels deep in your dependency tree.

Features

The list is short on purpose:

  • You can set the spinner color, the text color, a prefix and a separator. These are the small formatting knobs you actually need.
  • You can update the message while the work is in progress, because “Loading…” is not helpful for thirty seconds straight.
  • When stdout is not a terminal, pin turns the animation off so you don’t get escape codes in your log files (try ./myapp | tee output.txt).
  • It uses the standard library only. I keep saying it because that is the point.

Quick Start

Install it with go get:

go get github.com/yarlson/pin

Here is a complete program that starts a spinner, changes its message halfway through the work, and stops it:

package main

import (
	"context"
	"time"

	"github.com/yarlson/pin"
)

func main() {
	p := pin.New("Loading...",
		pin.WithSpinnerColor(pin.ColorCyan),
		pin.WithTextColor(pin.ColorYellow),
	)
	cancel := p.Start(context.Background())
	defer cancel()

	// Simulate work
	time.Sleep(3 * time.Second)

	p.UpdateMessage("Almost done...")
	time.Sleep(2 * time.Second)

	p.Stop("Done!")
}

pin.New takes the first message and the color options. Start takes a context and gives you back a cancel function, UpdateMessage swaps the text while the spinner keeps going, and Stop takes the final message.

Look, it’s a spinner. The API should fit in your head in thirty seconds, and this one does.

Fair question. If you’re already deep in a CLI framework that bundles its own spinner, use that. Seriously.

But maybe you’re writing a small tool, such as a deployment script, a code generator or a quick utility, and you want a clean loading indicator without adopting someone else’s opinions about terminal rendering. pin exists for exactly that moment. A spinner with no dependencies is a spinner that does not break because somebody else shipped a bad release on a Friday afternoon.

The source is on GitHub at github.com/yarlson/pin. It’s small enough that you can read the whole thing in one sitting.