awesome-gocui / gocui

Minimalist Go package aimed at creating Console User Interfaces.
BSD 3-Clause "New" or "Revised" License
356 stars 39 forks source link

GOCUI - Go Console User Interface

github actions Go Report Card GoDoc GitHub tag (latest SemVer)

Minimalist Go package aimed at creating Console User Interfaces. A community fork based on the amazing work of jroimartin For v0 to v1 mirgration help read: migrate-to-v1.md

Features

About fork

This fork has many improvements over the original work from jroimartin.

For information about this org see: awesome-gocui/about.

Installation

Execute:

$ go get github.com/awesome-gocui/gocui

Documentation

Execute:

$ go doc github.com/awesome-gocui/gocui

Or visit godoc.org to read it online.

Example

See the _example folder for more examples

package main

import (
    "fmt"
    "log"

    "github.com/awesome-gocui/gocui"
)

func main() {
    g, err := gocui.NewGui(gocui.OutputNormal, true)
    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 && !errors.Is(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, 0); err != nil {
        if !errors.Is(err, gocui.ErrUnknownView) {
            return err
        }

        if _, err := g.SetCurrentView("hello"); err != nil {
            return err
        }

        fmt.Fprintln(v, "Hello world!")
    }

    return nil
}

func quit(g *gocui.Gui, v *gocui.View) error {
    return gocui.ErrQuit
}

Testing example

You can write simple tests for gocui which let you simulate keyboard and then validate the output drawn to the screen.

  1. Create an instance of gui with OutputSimulator set as the mode g, err := NewGui(OutputSimulator, true)
  2. Call GetTestingScreen to get a testingScreen instance.
  3. On this you can use SendKey to simulate input and GetViewContent to evaluate what is drawn.

Warning: Timing plays a part here, key bindings don't fire synchronously and drawing isn't instant. Here we used time.After to pause, gomega's asynchronous assertions are likely a better alternative for more complex tests.

Here is a simple example showing how this can be used to validate what a view shows and that a key binding is handled correctly:

func TestTestingScreenReturnsCorrectContent(t *testing.T) {
    // Track what happened in the view, we'll assert on these
    didCallCTRLC := false
    expectedViewContent := "Hello world!"
    viewName := "testView1"

    // Create a view specifying the "OutputSimulator" mode
    g, err := NewGui(OutputSimulator, true)
    if err != nil {
        log.Panicln(err)
    }
    g.SetManagerFunc(func(g *Gui) error {
        maxX, maxY := g.Size()
        if v, err := g.SetView(viewName, maxX/2-7, maxY/2, maxX/2+7, maxY/2+2, 0); err != nil {
            if !errors.Is(err, ErrUnknownView) {
                return err
            }

            if _, err := g.SetCurrentView(viewName); err != nil {
                return err
            }

            // Have the view draw "Hello world!"
            fmt.Fprintln(v, expectedViewContent)
        }

        return nil
    })

    // Create a key binding which sets "didCallCTRLC" when triggered
    exampleBindingToTest := func(g *Gui, v *View) error {
        didCallCTRLC = true
        return nil
    }
    if err := g.SetKeybinding("", KeyCtrlC, ModNone, exampleBindingToTest); err != nil {
        log.Panicln(err)
    }

    // Create a test screen and start gocui
    testingScreen := g.GetTestingScreen()
    cleanup := testingScreen.StartGui()
    defer cleanup()

    // Send a key to gocui
    testingScreen.SendKey(KeyCtrlC)

    // Wait for key to be processed
    <-time.After(time.Millisecond * 50)

    // Test that the keybinding fired and set "didCallCTRLC" to true
    if !didCallCTRLC {
        t.Error("Expect the simulator to invoke the key handler for CTRLC")
    }

    // Get the content from the testing screen
    actualContent, err := testingScreen.GetViewContent(viewName)
    if err != nil {
        t.Error(err)
    }

    // Test that it contains the "Hello World!" we thought the view should draw
    if strings.TrimSpace(actualContent) != expectedViewContent {
        t.Error(fmt.Printf("Expected view content to be: %q got: %q", expectedViewContent, actualContent))
    }
}

Note: Under the covers this is using the tcell SimulationScreen.

Screenshots

r2cui

_examples/demo.go

_examples/dynamic.go

Projects using gocui

Note: if your project is not listed here, let us know! :)