tcell
GitHub Overview
gdamore/tcell
Tcell is an alternate terminal package, similar in some ways to termbox, but better in others.
Repository:https://github.com/gdamore/tcell
Stars4,879
Watchers72
Forks329
Created:September 27, 2015
Language:Go
License:Apache License 2.0
Topics
None
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
Feature | tcell | termbox-go | ncurses |
---|---|---|---|
Level | Low | Low | Low |
Cross-Platform | Excellent | Good | Unix only |
Modern Features | Complete | Basic | Partial |
Performance | Excellent | Excellent | Good |
Go Native | Yes | Yes | No |
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.