twin

package module
v0.9.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 21 Imported by: 0

README

Docs Linux CI Windows CI

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.

Features

Demo

The moor pager was built using twin.

So is ftop:

ftop screenshot

Installation

go get github.com/walles/twin

Usage

Here's examples/hello, a complete, runnable program. Run it yourself, after cloning this repo:

go run ./examples/hello

hello example screenshot

// 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.

Why not tcell?

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.

Making a new release

  1. git tag --annotate vX.Y.Z, note the leading v in the version number. Write something descriptive in the annotation message.
  2. git push --tags

Documentation

Overview

Package twin provides Terminal Window Interaction

Index

Constants

This section is empty.

Variables

View Source
var ColorDefault = newColor(ColorCountDefault, 0)

ColorDefault is the terminal's own default foreground / background color, used when no explicit color has been set.

Functions

func Printable

func Printable(char rune) bool

Printable reports whether char should be rendered as-is rather than escaped, covering some cases that unicode.IsPrint() gets wrong for terminal output.

Types

type AttrMask

type AttrMask uint

AttrMask is a bitmask of text attributes (bold, blink, ...), combined with bitwise OR.

const (
	AttrBold AttrMask = 1 << iota
	AttrBlink
	AttrReverse
	AttrUnderline
	AttrDim
	AttrItalic
	AttrStrikeThrough
	AttrHidden
	AttrNone AttrMask = 0 // Normal text
)

AttrMask bit values.

type Color

type Color uint32

Color represents a terminal color. Create one using NewColor16(), NewColor256(), or NewColor24Bit(), or use ColorDefault.

func NewColor16

func NewColor16(colorNumber0to15 int) Color

NewColor16 creates a 4-bit ANSI color (16 colors) from a palette index 0-15.

Ref: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit

func NewColor24Bit

func NewColor24Bit(red uint8, green uint8, blue uint8) Color

NewColor24Bit creates a 24-bit RGB color from its red, green and blue channels.

func NewColor256

func NewColor256(colorNumber uint8) Color

NewColor256 creates an 8-bit ANSI color (256 colors) from a palette index.

Ref: https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit

func NewColorHex

func NewColorHex(rgb uint32) Color

NewColorHex creates a 24-bit RGB color from a packed 0xRRGGBB value.

func (Color) Distance

func (color Color) Distance(other Color) float64

Distance approximates the perceptual difference between two colors, using the formula from https://www.compuphase.com/cmetric.htm, scaled to 0.0-1.0 where 1.0 is the distance between black and white.

Panics if either color is ColorDefault.

func (Color) Mix

func (color Color) Mix(other Color, weight float64) Color

Mix blends color and other, weighted 0.0 (all color) to 1.0 (all other).

Panics if either color is ColorDefault, or if weight is outside 0.0-1.0.

func (Color) RGBA

func (color Color) RGBA() (r, g, b, a uint32)

RGBA implements color.Color. All twin colors are fully opaque, so alpha is always 0xffff.

ColorDefault has no defined RGB value. Calling RGBA() on it panics; this is reserved / unspecified behavior and may change without a major release.

func (Color) String

func (color Color) String() string

type ColorCount

type ColorCount uint8

ColorCount represents the terminal's color capability, one of the ColorCount* constants.

const (
	// ColorCountDefault is no explicit color: the terminal's own default
	// foreground / background.
	ColorCountDefault ColorCount = iota

	// ColorCount8 is 3-bit ANSI color (8 colors):
	// https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
	//
	// Note that this type is only used for output, on input we store 3 bit
	// colors as 4 bit colors since they map to the same values.
	ColorCount8

	// ColorCount16 is 4-bit ANSI color (16 colors):
	// https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
	ColorCount16

	// ColorCount256 is 8-bit ANSI color (256 colors):
	// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
	ColorCount256

	// ColorCount24bit is an RGB color:
	// https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit
	ColorCount24bit
)

type Event

type Event any

Event is the type of value received on a Screen's event channel.

EventRune, EventKeyCode and EventMouse can be constructed directly by embedding applications, to programmatically feed input into the screen.

Ref: https://github.com/walles/moor/pull/456

type EventExit

type EventExit struct {
}

EventExit is sent, and the application should exit, when we're unable to continue showing the screen.

Ref: https://github.com/walles/moor/issues/126

type EventKeyCode

type EventKeyCode struct {
	KeyCode KeyCode
}

EventKeyCode is sent when the user presses a non-printable key.

type EventMouse

type EventMouse struct {
	Buttons MouseButtonMask
}

EventMouse is sent on mouse wheel activity.

type EventResize

type EventResize struct {
}

EventResize is sent when the terminal window is resized. Query Screen.Size() after receiving this to get the new size.

type EventRune

type EventRune struct {
	Rune rune
}

EventRune is sent when the user types a printable rune.

type FakeScreen

type FakeScreen struct {
	// contains filtered or unexported fields
}

FakeScreen is an in-memory Screen implementation, needing no real terminal. Typically used for testing code that renders to a Screen, but also suitable for headless rendering in general.

Create one with NewFakeScreen(width, height) and hand it to the code under test. Let that code call SetCell(), Clear(), Show() etc. as it normally would, then call GetRow() or GetCell() afterwards to inspect what ended up on screen.

Events() always returns nil.

func NewFakeScreen

func NewFakeScreen(width int, height int) *FakeScreen

NewFakeScreen creates a FakeScreen of the given size, with all cells set to a space in the default style.

func (*FakeScreen) Clear

func (screen *FakeScreen) Clear()

Clear erases all screen cells, replacing them with spaces in the default style.

func (*FakeScreen) Close

func (screen *FakeScreen) Close()

Close does nothing, since a FakeScreen owns no real terminal to restore.

func (*FakeScreen) Events

func (screen *FakeScreen) Events() chan Event

Events always returns nil, so anything reading from it will block forever.

func (*FakeScreen) GetCell

func (screen *FakeScreen) GetCell(column int, row int) StyledRune

GetCell returns the StyledRune at the given screen position.

Note that this does not read cells from a physical screen, since there is none, but rather from what was previously set using SetCell().

For out-of-bounds requests, a space with default style is returned.

func (*FakeScreen) GetRow

func (screen *FakeScreen) GetRow(row int) []StyledRune

GetRow returns the row's cells, skipping any cell hidden behind a preceding wide rune.

func (*FakeScreen) PauseAndCall

func (screen *FakeScreen) PauseAndCall(run func() error) error

PauseAndCall runs the given function and returns its result. A FakeScreen has no terminal state to pause and resume, so there is nothing else to do.

func (*FakeScreen) PrintLines added in v0.9.3

func (screen *FakeScreen) PrintLines(int)

PrintLines does nothing, since a FakeScreen has no real terminal to render into.

func (*FakeScreen) SetCell

func (screen *FakeScreen) SetCell(column int, row int, styledRune StyledRune) int

SetCell returns the width of the rune just added, in number of columns.

Note that if you set a wide rune (like '午') in one column, then whatever you put in the next column will be hidden by the wide rune. A wide rune in the last screen column will be replaced by a space, to prevent it from overflowing onto the next line.

func (*FakeScreen) SetProgress

func (screen *FakeScreen) SetProgress(state ProgressState, percent int)

SetProgress does nothing, since a FakeScreen has no terminal to show a progress bar in.

func (*FakeScreen) Show

func (screen *FakeScreen) Show()

Show does nothing, since a FakeScreen has no real terminal to render into.

func (*FakeScreen) Size

func (screen *FakeScreen) Size() (width int, height int)

Size returns the width and height given to NewFakeScreen().

func (*FakeScreen) TerminalBackground

func (screen *FakeScreen) TerminalBackground() *Color

TerminalBackground always returns nil, since a FakeScreen has no real terminal to query.

type KeyCode

type KeyCode uint16

KeyCode identifies a non-printable key the user pressed

const (
	KeyEscape KeyCode = iota
	KeyEnter

	KeyBackspace
	KeyDelete

	KeyUp
	KeyDown
	KeyRight
	KeyLeft

	KeyAltUp
	KeyAltDown
	KeyAltRight
	KeyAltLeft

	KeyHome
	KeyEnd
	KeyPgUp
	KeyPgDown
)

KeyCode values for non-printable keys, sent via EventKeyCode

type Logger

type Logger interface {
	// Debug logs low-level diagnostic messages. This level is recommended for
	// messages that the user has to explicitly ask to see.
	Debug(message string)

	// Info logs high-level status messages. This level and up is recommended
	// for adding to panic reports.
	Info(message string)

	// Error logs a problem the user should be told about whether they asked for
	// it or not.
	Error(message string)
}

Logger receives twin's log messages. Implement it and pass it in Options.Logger to NewScreen to consume those messages.

type MouseButtonMask

type MouseButtonMask uint16

MouseButtonMask is a bitmask of the MouseWheel* constants, used in EventMouse.Buttons.

const (
	// MouseWheelUp is set in EventMouse.Buttons when the wheel scrolls up.
	MouseWheelUp MouseButtonMask = 1 << iota

	// MouseWheelDown is set in EventMouse.Buttons when the wheel scrolls down.
	MouseWheelDown

	// MouseWheelLeft is set in EventMouse.Buttons when the wheel scrolls left.
	MouseWheelLeft

	// MouseWheelRight is set in EventMouse.Buttons when the wheel scrolls right.
	MouseWheelRight
)

type MouseMode

type MouseMode int

MouseMode controls how mouse events are captured. See MouseModeAuto, MouseModeSelect and MouseModeScroll for the available behaviors.

const (
	// MouseModeAuto auto-detects whether to capture mouse events, based on
	// the terminal.
	MouseModeAuto MouseMode = iota

	// MouseModeSelect doesn't capture mouse events. This makes selecting with
	// the mouse work. On some terminals mouse scrolling will work using arrow
	// keys emulation, and on some not.
	MouseModeSelect

	// MouseModeScroll captures mouse events. This makes mouse scrolling work.
	// Special gymnastics will be required for marking with the mouse to copy
	// text.
	MouseModeScroll
)

type Options

type Options struct {
	// MouseMode controls how mouse events are captured. Leave as
	// MouseModeAuto to auto-detect based on the terminal.
	MouseMode MouseMode

	// TerminalColorCount overrides how many colors twin assumes the terminal
	// supports. Leave as ColorCountDefault to auto-detect from the
	// environment.
	TerminalColorCount ColorCount

	// Logger receives twin's own log messages. Leave nil to disable logging.
	Logger Logger
}

Options configures a new Screen, created with NewScreen.

The zero value auto-detects mouse mode and terminal color count from the environment, and disables twin's own logging.

type Progress

type Progress struct {
	State   ProgressState
	Percent int
}

Progress is a terminal progress bar's state and completion percentage.

Ref: https://rockorager.dev/misc/osc-9-4-progress-bars/

type ProgressState

type ProgressState int

ProgressState is a terminal progress bar's state, one of the ProgressState* constants.

const (
	// ProgressStateRemove hides the progress bar.
	ProgressStateRemove ProgressState = 0

	// ProgressStateSet shows the progress bar at Progress.Percent.
	ProgressStateSet ProgressState = 1

	// ProgressStateError shows the progress bar, in an error color, at
	// Progress.Percent.
	ProgressStateError ProgressState = 2

	// ProgressStateIndeterminate shows a busy progress bar with no known
	// percentage.
	ProgressStateIndeterminate ProgressState = 3

	// ProgressStatePause shows the progress bar, in a paused color, at
	// Progress.Percent.
	ProgressStatePause ProgressState = 4
)

ProgressState values. The numbers match the ones from https://rockorager.dev/misc/osc-9-4-progress-bars/.

type Screen

type Screen interface {
	// Close restores the terminal to normal state, must be called after you are
	// done with the screen returned by NewScreen().
	Close()

	// Erases all screen cells, replacing them with spaces in the default
	// style.
	//
	// Like Size(), may apply a pending resize; see Size() for how that
	// affects the rest of the frame.
	Clear()

	// Returns the width of the rune just added, in number of columns.
	//
	// Note that if you set a wide rune (like '午') in one column, then whatever
	// you put in the next column will be hidden by the wide rune. A wide rune
	// in the last screen column will be replaced by a space, to prevent it from
	// overflowing onto the next line.
	SetCell(column int, row int, styledRune StyledRune) int

	// Returns the StyledRune at the given screen position.
	//
	// Note that this does not read cells from the physical screen, but rather
	// from what was previously set using SetCell().
	//
	// For out-of-bounds requests, a space with default style is returned.
	GetCell(column int, row int) StyledRune

	// Ask the terminal to show a progress bar
	//
	// Ref: https://rockorager.dev/misc/osc-9-4-progress-bars/
	SetProgress(state ProgressState, percent int)

	// Render our contents into the terminal window.
	//
	// The first call takes over the terminal: alternate screen, cursor hidden,
	// mouse tracked. Until then the user's own screen is left alone.
	//
	// Renders nothing while somebody else owns the terminal, meaning after
	// Close() or during PauseAndCall(); Ctrl-Z handling makes that possible
	// without your main loop asking for it.
	Show()

	// Can be called after Close()ing the screen to fake retaining its output.
	// Plain Show() is what you'd call during normal operation.
	//
	// Unlike Show(), this one never takes over the terminal; it prints where
	// the cursor already is.
	PrintLines(lineCountToShow int)

	// Returns screen width and height.
	//
	// NOTE: Never cache this response! On window resizes you'll get an
	// EventResize on the Screen.Events channel. The new size takes effect the
	// next time you call Size() or Clear(), whichever comes first after your
	// last Show()/PrintLines() call, and stays consistent for the rest of that
	// frame.
	Size() (width int, height int)

	// The first call may delay up to 50ms while waiting for the terminal to
	// respond to a background color query. After that, it's instant.
	//
	// Can be nil if not (yet?) detected.
	TerminalBackground() *Color

	// This channel is what your main loop should be checking.
	Events() chan Event

	// Pause the screen, run the given function, then resume the screen. Blocks
	// until the function has completed and the screen has been resumed again.
	//
	// Error returns mean that either pausing failed or the run function failed.
	// If resuming fails, this method will panic.
	PauseAndCall(run func() error) error
}

Screen is the main interface for interacting with the terminal, created with NewScreen.

func NewScreen

func NewScreen(options Options) (Screen, error)

NewScreen creates a new Screen according to options. Passing the zero value Options{} auto-detects mouse mode and terminal color count, and disables twin's own logging.

The returned Screen requires Close() to be called after you are done with it, most likely somewhere in your shutdown code.

type Style

type Style struct {
	// contains filtered or unexported fields
}

Style is a foreground color, background color, underline color, and a set of text attributes (bold, italic, ...), applied together to a piece of text.

var StyleDefault Style

StyleDefault is the zero value Style: default foreground and background colors, no attributes, no hyperlink.

func (Style) Background

func (style Style) Background() Color

Background returns style's background color.

func (Style) Equal

func (style Style) Equal(other Style) bool

Equal reports whether style and other render identically: same colors, same attributes, and same hyperlink.

func (Style) Foreground

func (style Style) Foreground() Color

Foreground returns style's foreground color.

func (Style) HasAttr

func (style Style) HasAttr(attr AttrMask) bool

HasAttr reports whether attr is set on style.

func (Style) HyperlinkURL

func (style Style) HyperlinkURL() *string

HyperlinkURL returns the hyperlink URL if set, or nil otherwise.

func (Style) RenderUpdateFrom

func (style Style) RenderUpdateFrom(previous Style, terminalColorCount ColorCount) string

RenderUpdateFrom returns the ANSI escape sequence needed to switch terminal state from previous to style.

func (Style) String

func (style Style) String() string

func (Style) WithAttr

func (style Style) WithAttr(attr AttrMask) Style

WithAttr returns a copy of style with attr added. AttrBold and AttrDim are mutually exclusive, so adding one clears the other.

func (Style) WithBackground

func (style Style) WithBackground(color Color) Style

WithBackground returns a copy of style with its background color set to color.

func (Style) WithForeground

func (style Style) WithForeground(color Color) Style

WithForeground returns a copy of style with its foreground color set to color.

func (style Style) WithHyperlink(hyperlinkURL *string) Style

WithHyperlink returns a copy of style with its hyperlink URL set. Call with nil to remove the link.

func (Style) WithUnderlineColor

func (style Style) WithUnderlineColor(color Color) Style

WithUnderlineColor returns a copy of style with its underline color set to color.

func (Style) WithoutAttr

func (style Style) WithoutAttr(attr AttrMask) Style

WithoutAttr returns a copy of style with attr removed.

type StyledRune

type StyledRune struct {
	Rune  rune
	Style Style
}

StyledRune is a rune with a style to be written to a one or more cells on the screen. Note that a StyledRune may use more than one cell on the screen ('午' for example).

func (StyledRune) Equal

func (styledRune StyledRune) Equal(other StyledRune) bool

Equal reports whether styledRune and other have the same rune and style.

func (StyledRune) String

func (styledRune StyledRune) String() string

func (StyledRune) Width

func (styledRune StyledRune) Width() int

Width returns how many screen cells this rune will cover. Most runes cover one, but some like '午' will cover two.

Directories

Path Synopsis
examples
hello command
Command hello is a minimal, runnable twin demo.
Command hello is a minimal, runnable twin demo.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL