gocui
GitHub Overview
jroimartin/gocui
Minimalist Go package aimed at creating Console User Interfaces.
Repository:https://github.com/jroimartin/gocui
Stars10,308
Watchers129
Forks625
Created:January 4, 2014
Language:Go
License:BSD 3-Clause "New" or "Revised" License
Topics
cuigogocuigui
Star History
Data as of: 7/25/2025, 11:09 AM
gocui
gocui is a minimalist TUI library for Go. It employs a view-based architecture and focuses on window management and key bindings. With its simple and lightweight design, you can quickly build TUI applications.
Features
View-Based Architecture
- View Management: Overlapping windows
- Focus Management: Active view switching
- Layout: Custom layout managers
- Coordinate System: Support for absolute and relative coordinates
Simple API
- Minimalist Design: Only essential features
- Event Handling: Key binding system
- Editor Mode: Built-in text editing capabilities
- Cursor Management: Cursor control within views
Performance
- Lightweight: Minimal dependencies
- Efficient: Fast rendering
- Low Memory: Small memory footprint
- Thread-Safe: Concurrent processing support
Extensibility
- Custom Handlers: Add event handlers
- Layout Customization: Implement custom layouts
- View Customization: Change view appearance
- Built-in Editor: Extend text editing features
Basic Usage
Installation
go get github.com/jroimartin/gocui
Hello World
package main
import (
"fmt"
"log"
"github.com/jroimartin/gocui"
)
func main() {
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
g.SetManagerFunc(layout)
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView("hello", maxX/2-7, maxY/2, maxX/2+7, maxY/2+2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
fmt.Fprintln(v, "Hello World!")
}
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
Multiple View Management
package main
import (
"fmt"
"log"
"github.com/jroimartin/gocui"
)
func main() {
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
g.Highlight = true
g.Cursor = true
g.SelFgColor = gocui.ColorGreen
g.SetManagerFunc(layout)
// Key bindings
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil {
log.Panicln(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
// Sidebar
if v, err := g.SetView("side", -1, -1, 30, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Sidebar"
v.Highlight = true
v.SelBgColor = gocui.ColorGreen
v.SelFgColor = gocui.ColorBlack
fmt.Fprintln(v, "Item 1")
fmt.Fprintln(v, "Item 2")
fmt.Fprintln(v, "Item 3")
if _, err := g.SetCurrentView("side"); err != nil {
return err
}
}
// Main view
if v, err := g.SetView("main", 30, -1, maxX, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Main"
v.Wrap = true
v.Autoscroll = true
fmt.Fprintln(v, "Main content area")
fmt.Fprintln(v, "Detailed information is displayed here")
}
return nil
}
func nextView(g *gocui.Gui, v *gocui.View) error {
if v == nil || v.Name() == "side" {
_, err := g.SetCurrentView("main")
return err
}
_, err := g.SetCurrentView("side")
return err
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
Editor Features
package main
import (
"fmt"
"log"
"strings"
"github.com/jroimartin/gocui"
)
func main() {
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
g.SetManagerFunc(layout)
// Editor key bindings
if err := g.SetKeybinding("editor", gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("editor", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("editor", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
// Editor view
if v, err := g.SetView("editor", 0, 0, maxX-1, maxY-5); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Editor"
v.Editable = true
v.Wrap = true
fmt.Fprintln(v, "Type text here...")
fmt.Fprintln(v, "Press Enter to display the current line below")
if _, err := g.SetCurrentView("editor"); err != nil {
return err
}
}
// Output view
if v, err := g.SetView("output", 0, maxY-5, maxX-1, maxY-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Output"
v.Wrap = true
v.Autoscroll = true
}
return nil
}
func getLine(g *gocui.Gui, v *gocui.View) error {
var l string
var err error
_, cy := v.Cursor()
if l, err = v.Line(cy); err != nil {
l = ""
}
// Display in output view
outputView, err := g.View("output")
if err != nil {
return err
}
fmt.Fprintln(outputView, strings.TrimSpace(l))
return nil
}
func cursorDown(g *gocui.Gui, v *gocui.View) error {
if v != nil {
cx, cy := v.Cursor()
if err := v.SetCursor(cx, cy+1); err != nil {
ox, oy := v.Origin()
if err := v.SetOrigin(ox, oy+1); err != nil {
return err
}
}
}
return nil
}
func cursorUp(g *gocui.Gui, v *gocui.View) error {
if v != nil {
ox, oy := v.Origin()
cx, cy := v.Cursor()
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
if err := v.SetOrigin(ox, oy-1); err != nil {
return err
}
}
}
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
Custom Layout
package main
import (
"fmt"
"log"
"github.com/jroimartin/gocui"
)
type ViewConfig struct {
name string
x0, y0 float32
x1, y1 float32
title string
}
func main() {
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
// View configuration
views := []ViewConfig{
{"header", 0, 0, 1, 0.1, "Header"},
{"sidebar", 0, 0.1, 0.3, 0.9, "Sidebar"},
{"main", 0.3, 0.1, 1, 0.7, "Main"},
{"bottom", 0.3, 0.7, 1, 0.9, "Bottom"},
{"footer", 0, 0.9, 1, 1, "Footer"},
}
g.SetManagerFunc(func(g *gocui.Gui) error {
return dynamicLayout(g, views)
})
// Key bindings
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil {
log.Panicln(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func dynamicLayout(g *gocui.Gui, views []ViewConfig) error {
maxX, maxY := g.Size()
for _, vc := range views {
x0 := int(float32(maxX) * vc.x0)
y0 := int(float32(maxY) * vc.y0)
x1 := int(float32(maxX)*vc.x1) - 1
y1 := int(float32(maxY)*vc.y1) - 1
if v, err := g.SetView(vc.name, x0, y0, x1, y1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = vc.title
v.Wrap = true
// Add content
switch vc.name {
case "header":
fmt.Fprintln(v, "Application v1.0")
case "sidebar":
fmt.Fprintln(v, "Menu 1")
fmt.Fprintln(v, "Menu 2")
fmt.Fprintln(v, "Menu 3")
case "main":
fmt.Fprintln(v, "Main content area")
case "footer":
fmt.Fprintln(v, "Ctrl+C: Quit | Tab: Switch")
}
}
}
return nil
}
func nextView(g *gocui.Gui, v *gocui.View) error {
views := []string{"header", "sidebar", "main", "bottom", "footer"}
currentView := g.CurrentView()
if currentView == nil {
return g.SetCurrentView(views[0])
}
currentName := currentView.Name()
for i, name := range views {
if name == currentName {
nextIndex := (i + 1) % len(views)
return g.SetCurrentView(views[nextIndex])
}
}
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
Advanced Features
Mouse Support
g.Mouse = true
// Mouse click handler
if err := g.SetKeybinding("viewname", gocui.MouseLeft, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
_, err := g.SetCurrentView(v.Name())
return err
}); err != nil {
return err
}
Overlay Views
func showModal(g *gocui.Gui, message string) error {
maxX, maxY := g.Size()
if v, err := g.SetView("modal", maxX/2-20, maxY/2-2, maxX/2+20, maxY/2+2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Modal"
fmt.Fprintln(v, message)
// Display on top
if _, err := g.SetViewOnTop("modal"); err != nil {
return err
}
if _, err := g.SetCurrentView("modal"); err != nil {
return err
}
}
return nil
}
Asynchronous Updates
func updateAsync(g *gocui.Gui) {
go func() {
for {
time.Sleep(1 * time.Second)
g.Update(func(g *gocui.Gui) error {
v, err := g.View("status")
if err != nil {
return err
}
v.Clear()
fmt.Fprintf(v, "Time: %s", time.Now().Format("15:04:05"))
return nil
})
}
}()
}
Ecosystem
Derivative Projects
- awesome-gocui: Collection of resources and examples
- gocui-component: Reusable components
Adoption Examples
- lazygit: Git TUI client
- lazydocker: Docker management tool
- docui: Docker TUI
- sshportal: SSH gateway
Advantages
- Simple: Minimalist design
- Lightweight: Small dependencies
- Flexible: Easy custom layouts
- Stable: Mature codebase
- Popular: Adopted by many projects
Limitations
- Limited Features: Only basic functionality
- Widget Shortage: Few built-in widgets
- Documentation: Limited documentation
- Maintenance: Low update frequency
Comparison with Other Libraries
Feature | gocui | Bubble Tea | tview |
---|---|---|---|
Design | Minimalist | Modern | Full-featured |
Widgets | Basic | Components | Rich |
Learning Cost | Low | Medium | Low |
Customizability | High | Very High | Medium |
Activity | Low | Very High | High |
Summary
gocui is an excellent library for building simple TUI applications in Go. With its minimalist design, it's easy to learn and implement custom layouts. While it doesn't provide advanced widgets, its flexibility makes it highly customizable, which is why it's adopted by many popular tools like lazygit and lazydocker. It's the ideal choice for developers seeking a simple and lightweight TUI library.