gocui

TUITerminalMinimalistViewsGo

GitHub Overview

jroimartin/gocui

Minimalist Go package aimed at creating Console User Interfaces.

Stars10,308
Watchers129
Forks625
Created:January 4, 2014
Language:Go
License:BSD 3-Clause "New" or "Revised" License

Topics

cuigogocuigui

Star History

jroimartin/gocui 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

FeaturegocuiBubble Teatview
DesignMinimalistModernFull-featured
WidgetsBasicComponentsRich
Learning CostLowMediumLow
CustomizabilityHighVery HighMedium
ActivityLowVery HighHigh

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.