termbox-go

TUITerminalLow-levelLightweightGo

GitHub Overview

nsf/termbox-go

Pure Go termbox implementation

Stars4,734
Watchers92
Forks375
Created:January 12, 2012
Language:Go
License:MIT License

Topics

None

Star History

nsf/termbox-go Star History
Data as of: 7/25/2025, 11:09 AM

termbox-go

termbox-go is a simple and lightweight terminal UI library. As a Pure Go implementation of the C termbox library, it allows rapid terminal application development with its minimalist design and minimal dependencies.

Features

Simple API

  • Minimal Features: Only essential functionality
  • Clean Design: Easy-to-understand API
  • Low-Level Operations: Direct cell-level control
  • Event-Driven: Keyboard and mouse events

Performance

  • Lightweight: Minimal memory usage
  • Fast: Efficient rendering
  • Pure Go: No CGO required
  • Zero Dependencies: No external libraries

Compatibility

  • Cross-Platform: Windows, macOS, Linux
  • 256 Color Support: Modern terminal support
  • UTF-8 Support: Unicode character display
  • Mouse Support: Click event support

Buffering

  • Double Buffer: Flicker prevention
  • Cell Buffer: Efficient screen updates
  • Differential Updates: Redraw only necessary parts
  • Smart Rendering: Optimized drawing

Basic Usage

Installation

go get github.com/nsf/termbox-go

Hello World

package main

import (
    "github.com/nsf/termbox-go"
)

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()
    
    // Clear screen
    termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
    
    // Display "Hello, World!"
    text := "Hello, World!"
    for i, ch := range text {
        termbox.SetCell(i+5, 5, ch, termbox.ColorYellow, termbox.ColorBlue)
    }
    
    // Flush
    termbox.Flush()
    
    // Wait for key
    termbox.PollEvent()
}

Event Handling

package main

import (
    "fmt"
    tb "github.com/nsf/termbox-go"
)

func main() {
    err := tb.Init()
    if err != nil {
        panic(err)
    }
    defer tb.Close()
    
    // Enable mouse support
    tb.SetInputMode(tb.InputEsc | tb.InputMouse)
    
    drawAll()
    
    // Event loop
    for {
        switch ev := tb.PollEvent(); ev.Type {
        case tb.EventKey:
            if ev.Key == tb.KeyEsc || ev.Key == tb.KeyCtrlC {
                return
            }
            handleKey(ev)
        case tb.EventMouse:
            handleMouse(ev)
        case tb.EventResize:
            drawAll()
        case tb.EventError:
            panic(ev.Err)
        }
        tb.Flush()
    }
}

func drawAll() {
    w, h := tb.Size()
    tb.Clear(tb.ColorDefault, tb.ColorDefault)
    
    // Header
    drawBox(0, 0, w, 3, "Termbox-go Demo")
    
    // Status bar
    status := fmt.Sprintf(" Size: %dx%d | Press ESC to quit ", w, h)
    drawText(0, h-1, w, status, tb.ColorWhite, tb.ColorBlue)
    
    // Center message
    msg := "Press any key or click the mouse"
    x := (w - len(msg)) / 2
    y := h / 2
    drawText(x, y, len(msg), msg, tb.ColorYellow, tb.ColorDefault)
}

func drawBox(x, y, w, h int, title string) {
    // Top line
    tb.SetCell(x, y, '┌', tb.ColorWhite, tb.ColorDefault)
    for i := 1; i < w-1; i++ {
        tb.SetCell(x+i, y, '─', tb.ColorWhite, tb.ColorDefault)
    }
    tb.SetCell(x+w-1, y, '┐', tb.ColorWhite, tb.ColorDefault)
    
    // Side lines
    for i := 1; i < h-1; i++ {
        tb.SetCell(x, y+i, '│', tb.ColorWhite, tb.ColorDefault)
        tb.SetCell(x+w-1, y+i, '│', tb.ColorWhite, tb.ColorDefault)
    }
    
    // Bottom line
    tb.SetCell(x, y+h-1, '└', tb.ColorWhite, tb.ColorDefault)
    for i := 1; i < w-1; i++ {
        tb.SetCell(x+i, y+h-1, '─', tb.ColorWhite, tb.ColorDefault)
    }
    tb.SetCell(x+w-1, y+h-1, '┘', tb.ColorWhite, tb.ColorDefault)
    
    // Title
    if title != "" {
        titleX := x + (w-len(title))/2
        drawText(titleX, y, len(title), title, tb.ColorCyan, tb.ColorDefault)
    }
}

func drawText(x, y, w int, text string, fg, bg tb.Attribute) {
    for i, ch := range text {
        if i < w {
            tb.SetCell(x+i, y, ch, fg, bg)
        }
    }
}

func handleKey(ev tb.Event) {
    w, h := tb.Size()
    info := fmt.Sprintf("Key: %v, Char: %c", ev.Key, ev.Ch)
    x := (w - len(info)) / 2
    y := h/2 + 2
    
    // Clear
    for i := 0; i < w; i++ {
        tb.SetCell(i, y, ' ', tb.ColorDefault, tb.ColorDefault)
    }
    
    drawText(x, y, len(info), info, tb.ColorGreen, tb.ColorDefault)
}

func handleMouse(ev tb.Event) {
    w, h := tb.Size()
    info := fmt.Sprintf("Mouse: X=%d, Y=%d, Button=%v", ev.MouseX, ev.MouseY, ev.Key)
    x := (w - len(info)) / 2
    y := h/2 + 2
    
    // Clear
    for i := 0; i < w; i++ {
        tb.SetCell(i, y, ' ', tb.ColorDefault, tb.ColorDefault)
    }
    
    drawText(x, y, len(info), info, tb.ColorMagenta, tb.ColorDefault)
    
    // Marker at mouse position
    tb.SetCell(ev.MouseX, ev.MouseY, 'X', tb.ColorRed, tb.ColorDefault)
}

Creating a Game

package main

import (
    "math/rand"
    "time"
    tb "github.com/nsf/termbox-go"
)

type Game struct {
    px, py  int // Player position
    ex, ey  int // Enemy position
    score   int
    gameOver bool
}

func NewGame() *Game {
    w, h := tb.Size()
    return &Game{
        px: w / 2,
        py: h / 2,
        ex: rand.Intn(w),
        ey: rand.Intn(h),
    }
}

func (g *Game) Update(ev tb.Event) {
    if g.gameOver {
        return
    }
    
    w, h := tb.Size()
    
    // Player movement
    switch ev.Ch {
    case 'w', 'W':
        if g.py > 0 {
            g.py--
        }
    case 's', 'S':
        if g.py < h-1 {
            g.py++
        }
    case 'a', 'A':
        if g.px > 0 {
            g.px--
        }
    case 'd', 'D':
        if g.px < w-1 {
            g.px++
        }
    }
    
    // Check if caught enemy
    if g.px == g.ex && g.py == g.ey {
        g.score++
        g.ex = rand.Intn(w)
        g.ey = rand.Intn(h)
    }
    
    // Random enemy movement
    if rand.Float32() < 0.1 {
        g.ex += rand.Intn(3) - 1
        g.ey += rand.Intn(3) - 1
        
        if g.ex < 0 {
            g.ex = 0
        }
        if g.ex >= w {
            g.ex = w - 1
        }
        if g.ey < 0 {
            g.ey = 0
        }
        if g.ey >= h {
            g.ey = h - 1
        }
    }
}

func (g *Game) Draw() {
    tb.Clear(tb.ColorDefault, tb.ColorDefault)
    
    // Player
    tb.SetCell(g.px, g.py, '@', tb.ColorYellow, tb.ColorDefault)
    
    // Enemy
    tb.SetCell(g.ex, g.ey, '*', tb.ColorRed, tb.ColorDefault)
    
    // Score
    scoreText := fmt.Sprintf("Score: %d | WASD to move | ESC to quit", g.score)
    for i, ch := range scoreText {
        tb.SetCell(i, 0, ch, tb.ColorWhite, tb.ColorDefault)
    }
    
    tb.Flush()
}

func main() {
    err := tb.Init()
    if err != nil {
        panic(err)
    }
    defer tb.Close()
    
    rand.Seed(time.Now().UnixNano())
    game := NewGame()
    
    // Event channel for timer
    eventQueue := make(chan tb.Event)
    go func() {
        for {
            eventQueue <- tb.PollEvent()
        }
    }()
    
    // Game loop
    ticker := time.NewTicker(100 * time.Millisecond)
    defer ticker.Stop()
    
    game.Draw()
    
    for {
        select {
        case ev := <-eventQueue:
            if ev.Type == tb.EventKey && (ev.Key == tb.KeyEsc || ev.Key == tb.KeyCtrlC) {
                return
            }
            game.Update(ev)
            game.Draw()
            
        case <-ticker.C:
            game.Update(tb.Event{})
            game.Draw()
        }
    }
}

Custom Widgets

package main

import tb "github.com/nsf/termbox-go"

// Button widget
type Button struct {
    x, y   int
    width  int
    text   string
    active bool
}

func NewButton(x, y int, text string) *Button {
    return &Button{
        x:     x,
        y:     y,
        width: len(text) + 4,
        text:  text,
    }
}

func (b *Button) Draw() {
    fg := tb.ColorWhite
    bg := tb.ColorBlue
    
    if b.active {
        bg = tb.ColorRed
    }
    
    // Button background
    for i := 0; i < b.width; i++ {
        tb.SetCell(b.x+i, b.y, ' ', fg, bg)
    }
    
    // Text
    textX := b.x + (b.width-len(b.text))/2
    for i, ch := range b.text {
        tb.SetCell(textX+i, b.y, ch, fg, bg)
    }
}

func (b *Button) HandleClick(x, y int) bool {
    if y == b.y && x >= b.x && x < b.x+b.width {
        b.active = !b.active
        return true
    }
    return false
}

// Progress bar widget
type ProgressBar struct {
    x, y     int
    width    int
    progress float64
}

func NewProgressBar(x, y, width int) *ProgressBar {
    return &ProgressBar{
        x:     x,
        y:     y,
        width: width,
    }
}

func (p *ProgressBar) SetProgress(progress float64) {
    if progress < 0 {
        progress = 0
    } else if progress > 1 {
        progress = 1
    }
    p.progress = progress
}

func (p *ProgressBar) Draw() {
    // Border
    tb.SetCell(p.x, p.y, '[', tb.ColorWhite, tb.ColorDefault)
    tb.SetCell(p.x+p.width-1, p.y, ']', tb.ColorWhite, tb.ColorDefault)
    
    // Bar
    filled := int(float64(p.width-2) * p.progress)
    for i := 1; i < p.width-1; i++ {
        if i <= filled {
            tb.SetCell(p.x+i, p.y, '█', tb.ColorGreen, tb.ColorDefault)
        } else {
            tb.SetCell(p.x+i, p.y, '░', tb.ColorGray, tb.ColorDefault)
        }
    }
    
    // Percentage
    percent := fmt.Sprintf(" %d%% ", int(p.progress*100))
    percentX := p.x + (p.width-len(percent))/2
    for i, ch := range percent {
        fg := tb.ColorWhite
        if percentX+i-p.x-1 <= filled {
            fg = tb.ColorBlack
        }
        tb.SetCell(percentX+i, p.y, ch, fg, tb.ColorDefault)
    }
}

Advanced Features

256 Color Mode

// Enable 256 color mode
err := tb.SetOutputMode(tb.Output256)
if err != nil {
    // Fallback
    tb.SetOutputMode(tb.OutputNormal)
}

// Use 256 colors
tb.SetCell(x, y, '█', tb.Attribute(196), tb.ColorDefault) // Red

Buffer Operations

// Direct back buffer manipulation
cells := tb.CellBuffer()
w, h := tb.Size()

// Fast full screen fill
for i := 0; i < w*h; i++ {
    cells[i] = tb.Cell{
        Ch: ' ',
        Fg: tb.ColorDefault,
        Bg: tb.ColorBlue,
    }
}

Custom Input Mode

// Enable Alt keys
tb.SetInputMode(tb.InputEsc | tb.InputAlt)

// Event handling
if ev.Type == tb.EventKey && ev.Mod == tb.ModAlt {
    switch ev.Ch {
    case 'f':
        // Alt+F
    }
}

Ecosystem

Projects Using termbox-go

  • micro: Modern text editor
  • lf: File manager
  • hecate: Hex editor
  • goyo: Distraction-free writing

Alternative Libraries

  • tcell: More feature-rich alternative
  • termui: Dashboard-oriented
  • gocui: View-based alternative

Advantages

  • Simple: Easy to learn
  • Lightweight: Minimal resource usage
  • Fast: Efficient rendering
  • Independent: No external dependencies
  • Stable: Mature codebase

Limitations

  • Limited Features: Basic functionality only
  • Maintenance: Development stalled
  • No Widgets: Everything must be built from scratch
  • Modern Features: No True Color support

Comparison with Other Libraries

Featuretermbox-gotcellgocui
LevelLowestLowMedium
FeaturesBasicRichModerate
PerformanceExcellentExcellentGood
DependenciesNoneFewFew
MaintenanceStalledActiveLow

Summary

termbox-go is the ideal choice for developers seeking a simple and lightweight terminal UI library. With minimal features and zero dependencies, it's easy to learn and offers excellent performance. While maintenance has stalled, making alternatives like tcell worth considering for new projects, it remains a useful library for existing projects and simple tools.