tcell

Tcell Tutorial

Tcell provides a low-level, portable API for building terminal-based programs. A terminal emulator (or a real terminal such as a DEC VT-220) is used to interact with such a program.

Tcell’s interface is fairly low-level. While it provides a reasonably portable way of dealing with all the usual terminal features, it may be easier to utilize a higher level framework. A number of such frameworks are listed on the Tcell main README.

This tutorial provides the details of Tcell, and is appropriate for developers wishing to create their own application frameworks or needing more direct access to the terminal capabilities.

Resize events

Applications receive an event of type EventResize when they are first initialized and each time the terminal is resized. The new size is available as Size.

switch ev := ev.(type) {
case *tcell.EventResize:
	w, h := ev.Size()
	logMessage(fmt.Sprintf("Resized to %dx%d", w, h))
}

Key events

When a key is pressed, applications receive an event of type EventKey. This event describes the modifier keys pressed (if any) and the pressed key or rune.

When a rune key is pressed, an event with its Key set to KeyRune is dispatched.

When a non-rune key is pressed, it is available as the Key of the event.

switch ev := ev.(type) {
case *tcell.EventKey:
    mod, key, ch := ev.Mod(), ev.Key(), ev.Str()
    logMessage(fmt.Sprintf("EventKey Modifiers: %d Key: %d Str: %q", mod, key, ch))
}

Key event restrictions

Terminal-based programs have less visibility into keyboard activity than graphical applications.

When a key is pressed and held, additional key press events are sent by the terminal emulator. The rate of these repeated events depends on the emulator’s configuration. Key release events are not available.

It is not possible to distinguish runes typed while holding shift and runes typed using caps lock. Capital letters are reported without the Shift modifier.

Mouse events

Applications receive an event of type EventMouse when the mouse moves, or a mouse button is pressed or released. Mouse events are only delivered if EnableMouse has been called.

The mouse buttons being pressed (if any) are available as Buttons, and the position of the mouse is available as Position.

switch ev := ev.(type) {
case *tcell.EventMouse:
	mod := ev.Modifiers()
	btns := ev.Buttons()
	x, y := ev.Position()
	logMessage(fmt.Sprintf("EventMouse Modifiers: %d Buttons: %d Position: %d,%d", mod, btns, x, y))
}

Mouse buttons

Identifier Alias Description
Button1 ButtonPrimary Left button
Button2 ButtonSecondary Right button
Button3 ButtonMiddle Middle button
Button4   Side button (thumb/next)
Button5   Side button (thumb/prev)
WheelUp   Scroll wheel up
WheelDown   Scroll wheel down
WheelLeft   Horizontal wheel left
WheelRight   Horizontal wheel right

Usage

To create a Tcell application, first initialize a screen to hold it.

s, err := tcell.NewScreen()
if err != nil {
	log.Fatalf("%+v", err)
}
if err := s.Init(); err != nil {
	log.Fatalf("%+v", err)
}

// Set default text style
defStyle := tcell.StyleDefault.Background(color.Reset).Foreground(color.Reset)
s.SetStyle(defStyle)

// Clear screen
s.Clear()

Text may be drawn on the screen using Put, PutStr, or PutStrStyled.

s.Put(0, 0, 'H', defStyle)
s.Put(1, 0, 'i', defStyle)
s.Put(2, 0, '!', defStyle)

which is equivalent to

s.PutStrStyled(0, 0, "Hi!", defStyle)

To draw text more easily with wrapping, define a render function.

func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
	row := y1
	col := x1
	var width int
	for text != "" {
		text, width = s.Put(col, row, text, style)
		col += width
		if col >= x2 {
			row++
			col = x1
		}
		if row > y2 {
			break
		}
		if width == 0 {
			// incomplete grapheme at end of string
			break
		}
	}
}

Lastly, define an event loop to handle user input and update application state.

quit := func() {
    s.Fini()
    os.Exit(0)
}
for {
    // Update screen
    s.Show()

    // Poll event (can be used in select statement as well)
    ev := <-s.EventQ()

    // Process event
    switch ev := ev.(type) {
    case *tcell.EventResize:
        s.Sync()
    case *tcell.EventKey:
        if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
            quit()
        }
    }
}

Demo application

The following demonstrates how to initialize a screen, draw text/graphics and handle user input.

package main

import (
	"fmt"
	"log"

	"github.com/gdamore/tcell/v3"
	"github.com/gdamore/tcell/v3/color"
)

func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
	row := y1
	col := x1
	var width int
	for text != "" {
		text, width = s.Put(col, row, text, style)
		col += width
		if col >= x2 {
			row++
			col = x1
		}
		if row > y2 {
			break
		}
		if width == 0 {
			// incomplete grapheme at end of string
			break
		}
	}
}

func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
	if y2 < y1 {
		y1, y2 = y2, y1
	}
	if x2 < x1 {
		x1, x2 = x2, x1
	}

	// Fill background
	for row := y1; row <= y2; row++ {
		for col := x1; col <= x2; col++ {
			s.Put(col, row, " ", style)
		}
	}

	// Draw borders
	for col := x1; col <= x2; col++ {
		s.Put(col, y1, string(tcell.RuneHLine), style)
		s.Put(col, y2, string(tcell.RuneHLine), style)
	}
	for row := y1 + 1; row < y2; row++ {
		s.Put(x1, row, string(tcell.RuneVLine), style)
		s.Put(x2, row, string(tcell.RuneVLine), style)
	}

	// Only draw corners if necessary
	if y1 != y2 && x1 != x2 {
		s.Put(x1, y1, string(tcell.RuneULCorner), style)
		s.Put(x2, y1, string(tcell.RuneURCorner), style)
		s.Put(x1, y2, string(tcell.RuneLLCorner), style)
		s.Put(x2, y2, string(tcell.RuneLRCorner), style)
	}

	drawText(s, x1+1, y1+1, x2-1, y2-1, style, text)
}

func main() {
	defStyle := tcell.StyleDefault.Background(color.Reset).Foreground(color.Reset)
	boxStyle := tcell.StyleDefault.Foreground(color.White).Background(color.Purple)

	// Initialize screen
	s, err := tcell.NewScreen()
	if err != nil {
		log.Fatalf("%+v", err)
	}
	if err := s.Init(); err != nil {
		log.Fatalf("%+v", err)
	}
	s.SetStyle(defStyle)
	s.EnableMouse()
	s.EnablePaste()
	s.Clear()

	// Draw initial boxes
	drawBox(s, 1, 1, 42, 7, boxStyle, "Click and drag to draw a box")
	drawBox(s, 5, 9, 32, 14, boxStyle, "Press C to reset")

	quit := func() {
		// You have to catch panics in a defer, clean up, and
		// re-raise them - otherwise your application can
		// die without leaving any diagnostic trace.
		maybePanic := recover()
		s.Fini()
		if maybePanic != nil {
			panic(maybePanic)
		}
	}
	defer quit()

	// Here's how to get the screen size when you need it.
	// xmax, ymax := s.Size()

	// Here's an example of how to inject a keystroke where it will
	// be picked up by a future read of the event queue.  Note that
	// care should be used to avoid blocking writes to the queue if
	// this is done from the same thread that is responsible for reading
	// the queue, or else a single-party deadlock might occur.
	// s.EventQ() <- tcell.NewEventKey(tcell.KeyRune, rune('a'), 0)

	// Event loop
	ox, oy := -1, -1
	for {
		// Update screen
		s.Show()

		// Poll event (this can be in a select statement as well)
		ev := <-s.EventQ()

		// Process event
		switch ev := ev.(type) {
		case *tcell.EventResize:
			s.Sync()
		case *tcell.EventKey:
			if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
				return
			} else if ev.Key() == tcell.KeyCtrlL {
				s.Sync()
			} else if ev.Str() == "C" || ev.Str() == "c" {
				s.Clear()
			}
		case *tcell.EventMouse:
			x, y := ev.Position()

			switch ev.Buttons() {
			case tcell.Button1, tcell.Button2:
				if ox < 0 {
					ox, oy = x, y // record location when click started
				}

			case tcell.ButtonNone:
				if ox >= 0 {
					label := fmt.Sprintf("%d,%d to %d,%d", ox, oy, x, y)
					drawBox(s, ox, oy, x, y, boxStyle, label)
					ox, oy = -1, -1
				}
			}
		}
	}
}