Twin is a low-level library for drawing to the terminal screen: you own the widgets and layout, twin owns the terminal. Originally built for the moor pager, it's been battle-tested across Linux, macOS and Windows ever since.
- Clickable hyperlinks in terminals that support them
- Automatic color downsampling, so full 24-bit color degrades gracefully on terminals that don't support truecolor
- Self-healing raw mode: detects when another program resets your terminal settings behind your back and restores them
- Transparent suspend/resume: Ctrl-Z drops you to the shell and back cleanly
- Efficient rendering that only redraws what changed on screen, not the whole frame every time
- Wide-character support, so CJK and other double-width characters render without corrupting the layout
- Native progress indicators, reporting task progress to the terminal / taskbar, including error and indeterminate states
- Terminal background color detection, so your program can adapt to it if needed
The moor pager was built using twin.
So is ftop:
go get github.com/walles/twin
Here's examples/hello, a complete, runnable program. Run it yourself, after cloning this repo:
go run ./examples/hello
// Command hello is a minimal, runnable twin demo. It draws some wide-character
// text over a diagonal color gradient, redrawing on resize, then waits for a
// keypress before exiting cleanly.
package main
import (
"fmt"
"os"
"github.com/walles/twin"
)
func main() {
screen, err := twin.NewScreen(twin.Options{})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer screen.Close()
screen.SetProgress(twin.ProgressStateIndeterminate, 0)
draw(screen)
screen.Show()
for event := range screen.Events() {
switch event.(type) {
case twin.EventExit, twin.EventKeyCode, twin.EventRune:
return
case twin.EventResize:
draw(screen)
screen.Show()
}
}
}
// draw renders the whole demo screen: the gradient background plus the
// greeting text on top of it. Called both up front and on every resize.
func draw(screen twin.Screen) {
titleStyle := twin.StyleDefault.WithForeground(twin.NewColor24Bit(255, 230, 120)).WithAttr(twin.AttrBold)
bodyStyle := twin.StyleDefault.WithForeground(twin.NewColor24Bit(230, 230, 230))
url := "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
linkStyle := bodyStyle.WithHyperlink(&url)
drawText(screen, 2, 1, "Hello, 世界!", titleStyle)
column := drawText(screen, 2, 2, "Drawn with ", bodyStyle)
drawText(screen, column, 2, "github.com/walles/twin", linkStyle)
drawText(screen, 2, 3, "Press any key to exit", bodyStyle)
drawGradientBackground(screen)
}
// drawText writes text into screen starting at (column, row), advancing by each
// rune's actual on-screen width so wide characters don't overlap what follows
// them. Returns the column right after the text, for chaining
// differently-styled text on the same line.
func drawText(screen twin.Screen, column int, row int, text string, style twin.Style) int {
for _, r := range text {
width := screen.SetCell(column, row, twin.StyledRune{Rune: r, Style: style})
column += width
}
return column
}
// drawGradientBackground paints every cell's background in a diagonal gradient
// from top-left to bottom-right. It runs after the text has already been drawn,
// and reads each cell back with GetCell() so it only changes the background,
// leaving that cell's rune and foreground color untouched.
func drawGradientBackground(screen twin.Screen) {
topLeft := twin.NewColor24Bit(20, 20, 60)
bottomRight := twin.NewColor24Bit(200, 70, 160)
width, height := screen.Size()
for row := range height {
for column := range width {
t := float64(column+row) / float64(width+height-2)
cell := screen.GetCell(column, row)
cell.Style = cell.Style.WithBackground(topLeft.Mix(bottomRight, t))
screen.SetCell(column, row, cell)
}
}
}Twin opens an alternate screen buffer that it draws into.
See the full API docs at pkg.go.dev/github.com/walles/twin.
Twin's API is similar to tcell's because twin started out as a from-scratch reimplementation of tcell for moor.
But the real case for twin isn't a longer feature list than tcell's, it's years of continuous, real-world use.
The trigger in 2021 was tcell's PollEvent(): it hands you one event at a time
and blocks until the next one arrives, so moor had to redraw after every single
event. On a trackpad fling-scroll that meant redrawing once per queued scroll
tick, long after the user's finger had left the trackpad. Twin's Events() is a
plain channel instead, so moor could drain everything queued up and redraw once,
and scrolling immediately felt
right.
Tcell later shipped its own answer to this:
ChannelEvents(),
so today either library can drain events non-blockingly before redrawing. But by
then twin was already built, and it's been running moor — and later
ftop — in production ever since.
git tag --annotate vX.Y.Z, note the leadingvin the version number. Write something descriptive in the annotation message.git push --tags

