tcell

TUITerminalLow-levelCross-platformGo

GitHub Overview

gdamore/tcell

Tcell is an alternate terminal package, similar in some ways to termbox, but better in others.

Stars4,879
Watchers72
Forks329
Created:September 27, 2015
Language:Go
License:Apache License 2.0

Topics

None

Star History

gdamore/tcell Star History
Data as of: 7/25/2025, 11:09 AM

tcell

tcell is a low-level terminal manipulation library for Go. It works cross-platform and supports modern terminal features. It serves as the foundation for many high-level TUI libraries such as tview and cview.

Features

Cross-Platform

  • OS Support: Windows, macOS, Linux, and many Unix-like systems
  • Terminal Compatibility: xterm, VT100, Windows Console, and more
  • SSH Support: Works over SSH sessions
  • Built-in Themes: 256 colors and True Color support

Modern Features

  • Unicode Support: Full Unicode support (including emojis and combining characters)
  • Mouse Support: Click, drag, and wheel events
  • Resize Events: Terminal size change detection
  • Performance: Efficient cell buffering

Low-Level API

  • Direct Cell Manipulation: Character drawing with coordinate specification
  • Attribute Control: Colors, bold, underline, blink, etc.
  • Event System: Keyboard, mouse, and resize events
  • Buffering: Double buffering to prevent flicker

Extensibility

  • Custom Terminals: Add new terminal types
  • Simulation: Virtual screen for testing
  • Theme System: Custom color schemes
  • Internationalization: Locale support

Basic Usage

Installation

go get github.com/gdamore/tcell/v2

Hello World

package main

import (
    "fmt"
    "os"
    
    "github.com/gdamore/tcell/v2"
)

func main() {
    // Initialize screen
    s, err := tcell.NewScreen()
    if err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
    if err := s.Init(); err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
    defer s.Fini()
    
    // Default style
    defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
    s.SetStyle(defStyle)
    
    // Clear screen
    s.Clear()
    
    // Display "Hello, World!"
    text := "Hello, World!"
    x, y := 10, 5
    style := tcell.StyleDefault.Foreground(tcell.ColorCyan).Background(tcell.ColorBlue)
    
    for i, r := range text {
        s.SetContent(x+i, y, r, nil, style)
    }
    
    // Show
    s.Show()
    
    // Event loop
    for {
        ev := s.PollEvent()
        switch ev := ev.(type) {
        case *tcell.EventKey:
            if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
                return
            }
        case *tcell.EventResize:
            s.Sync()
        }
    }
}

Interactive Application

package main

import (
    "fmt"
    "os"
    "time"
    
    "github.com/gdamore/tcell/v2"
)

type App struct {
    screen tcell.Screen
    px, py int
    mx, my int
    style  tcell.Style
}

func NewApp() (*App, error) {
    s, err := tcell.NewScreen()
    if err != nil {
        return nil, err
    }
    if err := s.Init(); err != nil {
        return nil, err
    }
    
    s.EnableMouse()
    s.EnablePaste()
    s.Clear()
    
    return &App{
        screen: s,
        px:     10,
        py:     10,
        style:  tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorWhite),
    }, nil
}

func (a *App) Run() {
    defer a.screen.Fini()
    
    // Draw status bar
    go a.drawStatus()
    
    // Main loop
    for {
        a.draw()
        a.screen.Show()
        
        ev := a.screen.PollEvent()
        if !a.handleEvent(ev) {
            return
        }
    }
}

func (a *App) draw() {
    w, h := a.screen.Size()
    
    // Draw border
    for x := 0; x < w; x++ {
        a.screen.SetContent(x, 0, '─', nil, a.style)
        a.screen.SetContent(x, h-2, '─', nil, a.style)
    }
    for y := 1; y < h-2; y++ {
        a.screen.SetContent(0, y, '│', nil, a.style)
        a.screen.SetContent(w-1, y, '│', nil, a.style)
    }
    
    // Corners
    a.screen.SetContent(0, 0, '┌', nil, a.style)
    a.screen.SetContent(w-1, 0, '┐', nil, a.style)
    a.screen.SetContent(0, h-2, '└', nil, a.style)
    a.screen.SetContent(w-1, h-2, '┘', nil, a.style)
    
    // Draw player
    playerStyle := tcell.StyleDefault.Foreground(tcell.ColorYellow)
    a.screen.SetContent(a.px, a.py, '@', nil, playerStyle)
    
    // Draw mouse position
    if a.mx > 0 && a.my > 0 {
        mouseStyle := tcell.StyleDefault.Foreground(tcell.ColorRed)
        a.screen.SetContent(a.mx, a.my, 'X', nil, mouseStyle)
    }
}

func (a *App) drawStatus() {
    for {
        w, h := a.screen.Size()
        statusStyle := tcell.StyleDefault.Background(tcell.ColorBlue).Foreground(tcell.ColorWhite)
        
        // Clear status bar
        for x := 0; x < w; x++ {
            a.screen.SetContent(x, h-1, ' ', nil, statusStyle)
        }
        
        // Display info
        status := fmt.Sprintf(" Pos: (%d,%d) | Size: %dx%d | Time: %s | ESC: Exit ", 
            a.px, a.py, w, h, time.Now().Format("15:04:05"))
        
        for i, r := range status {
            if i < w {
                a.screen.SetContent(i, h-1, r, nil, statusStyle)
            }
        }
        
        a.screen.Show()
        time.Sleep(1 * time.Second)
    }
}

func (a *App) handleEvent(ev tcell.Event) bool {
    switch ev := ev.(type) {
    case *tcell.EventKey:
        return a.handleKey(ev)
    case *tcell.EventMouse:
        return a.handleMouse(ev)
    case *tcell.EventResize:
        a.screen.Sync()
        return true
    }
    return true
}

func (a *App) handleKey(ev *tcell.EventKey) bool {
    w, h := a.screen.Size()
    
    switch ev.Key() {
    case tcell.KeyEscape, tcell.KeyCtrlC:
        return false
    case tcell.KeyUp:
        if a.py > 1 {
            a.py--
        }
    case tcell.KeyDown:
        if a.py < h-3 {
            a.py++
        }
    case tcell.KeyLeft:
        if a.px > 1 {
            a.px--
        }
    case tcell.KeyRight:
        if a.px < w-2 {
            a.px++
        }
    }
    
    switch ev.Rune() {
    case 'h':
        if a.px > 1 {
            a.px--
        }
    case 'j':
        if a.py < h-3 {
            a.py++
        }
    case 'k':
        if a.py > 1 {
            a.py--
        }
    case 'l':
        if a.px < w-2 {
            a.px++
        }
    }
    
    return true
}

func (a *App) handleMouse(ev *tcell.EventMouse) bool {
    x, y := ev.Position()
    a.mx, a.my = x, y
    
    switch ev.Buttons() {
    case tcell.Button1:
        // Move on left click
        w, h := a.screen.Size()
        if x > 0 && x < w-1 && y > 0 && y < h-2 {
            a.px, a.py = x, y
        }
    }
    
    return true
}

func main() {
    app, err := NewApp()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
    
    app.Run()
}

Unicode and Emoji Support

package main

import (
    "github.com/gdamore/tcell/v2"
    "github.com/mattn/go-runewidth"
)

func drawText(s tcell.Screen, x, y int, style tcell.Style, text string) {
    for _, r := range text {
        s.SetContent(x, y, r, nil, style)
        x += runewidth.RuneWidth(r)
    }
}

func main() {
    s, _ := tcell.NewScreen()
    s.Init()
    defer s.Fini()
    
    s.Clear()
    
    // Display emojis and Japanese
    style := tcell.StyleDefault
    drawText(s, 5, 2, style, "🌟 Star ⭐")
    drawText(s, 5, 3, style, "🚀 Rocket")
    drawText(s, 5, 4, style, "🌈 Rainbow")
    drawText(s, 5, 6, style, "Hello World 🌏")
    drawText(s, 5, 7, style, "Text-based UI 💻")
    
    s.Show()
    
    // Wait for key
    for {
        ev := s.PollEvent()
        if ev, ok := ev.(*tcell.EventKey); ok {
            if ev.Key() == tcell.KeyEscape {
                return
            }
        }
    }
}

Creating Custom Widgets

package main

import (
    "github.com/gdamore/tcell/v2"
)

// Button widget
type Button struct {
    x, y   int
    width  int
    text   string
    style  tcell.Style
    hover  bool
    click  func()
}

func NewButton(x, y int, text string) *Button {
    return &Button{
        x:     x,
        y:     y,
        width: len(text) + 4,
        text:  text,
        style: tcell.StyleDefault.Background(tcell.ColorBlue).Foreground(tcell.ColorWhite),
    }
}

func (b *Button) Draw(s tcell.Screen) {
    style := b.style
    if b.hover {
        style = style.Background(tcell.ColorLightBlue)
    }
    
    // Button background
    for i := 0; i < b.width; i++ {
        s.SetContent(b.x+i, b.y, ' ', nil, style)
    }
    
    // Text
    textX := b.x + (b.width-len(b.text))/2
    for i, r := range b.text {
        s.SetContent(textX+i, b.y, r, nil, style)
    }
}

func (b *Button) HandleMouse(x, y int, buttons tcell.ButtonMask) {
    if x >= b.x && x < b.x+b.width && y == b.y {
        b.hover = true
        if buttons&tcell.Button1 != 0 && b.click != nil {
            b.click()
        }
    } else {
        b.hover = false
    }
}

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

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

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

func (p *ProgressBar) Draw(s tcell.Screen) {
    // Border
    s.SetContent(p.x, p.y, '[', nil, p.style)
    s.SetContent(p.x+p.width-1, p.y, ']', nil, p.style)
    
    // Bar
    filled := int(float64(p.width-2) * p.progress)
    for i := 1; i < p.width-1; i++ {
        if i <= filled {
            s.SetContent(p.x+i, p.y, '█', nil, p.style.Foreground(tcell.ColorGreen))
        } else {
            s.SetContent(p.x+i, p.y, '░', nil, p.style.Foreground(tcell.ColorDarkGray))
        }
    }
}

Advanced Features

True Color Support

// Using RGB colors
style := tcell.StyleDefault.
    Background(tcell.NewRGBColor(32, 32, 32)).
    Foreground(tcell.NewRGBColor(255, 128, 0))

// Using HSL colors
hslColor := tcell.NewHSLColor(120, 100, 50) // Green

Simulation Screen

// Virtual screen for testing
simScreen := tcell.NewSimulationScreen("UTF-8")
simScreen.Init()
simScreen.SetSize(80, 24)

// Inject events
simScreen.InjectKey(tcell.KeyEnter, ' ', tcell.ModNone)
simScreen.InjectMouse(10, 5, tcell.Button1, tcell.ModNone)

Custom Terminal Definition

import "github.com/gdamore/tcell/v2/terminfo"

// Create custom terminal info
ti := &terminfo.Terminfo{
    Name:      "myterm",
    Columns:   80,
    Lines:     24,
    Colors:    256,
    // Other settings...
}

// Register
terminfo.AddTerminfo(ti)

Ecosystem

Libraries Based on tcell

  • tview: Rich widget library
  • cview: Fork of tview (concurrency focused)
  • tcell-term: Terminal emulator
  • gowid: Widget library

Adoption Examples

  • micro: Modern text editor
  • aerc: Email client
  • fzf: Fuzzy finder (optional)

Advantages

  • Cross-Platform: Wide OS and terminal support
  • Modern: Unicode, True Color, mouse support
  • Performance: Efficient rendering
  • Stability: Mature codebase
  • Flexibility: Low-level API for complete control

Limitations

  • Low-Level: Need to build high-level widgets yourself
  • Learning Curve: Understanding of direct cell manipulation required
  • Boilerplate: Need to implement basic features yourself
  • Documentation: API documentation focused

Comparison with Other Libraries

Featuretcelltermbox-goncurses
LevelLowLowLow
Cross-PlatformExcellentGoodUnix only
Modern FeaturesCompleteBasicPartial
PerformanceExcellentExcellentGood
Go NativeYesYesNo

Summary

tcell is a powerful low-level library for building cross-platform terminal applications in Go. It fully supports modern terminal features and serves as the foundation for many popular TUI libraries. While high-level widgets require libraries like tview, tcell is the ideal choice when you need complete control or want to build custom TUI frameworks.