termbox-go
GitHub Overview
Stars4,734
Watchers92
Forks375
Created:January 12, 2012
Language:Go
License:MIT License
Topics
None
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
Feature | termbox-go | tcell | gocui |
---|---|---|---|
Level | Lowest | Low | Medium |
Features | Basic | Rich | Moderate |
Performance | Excellent | Excellent | Good |
Dependencies | None | Few | Few |
Maintenance | Stalled | Active | Low |
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.