DEV Community

Cover image for How to create a TUI utility on GO
Denis
Denis

Posted on

How to create a TUI utility on GO

Here is the translated text with all formatting preserved:

In this article, I will explain how to create a console TUI utility in Go using ready-made components, show examples of interfaces, and build an application using Goreleaser.

In other words, we will go through the entire cycle of creating an application up to its release.

Everything described in this article is collected in this repository:

https://github.com/deniskorbakov

Background

I really like TUI; with it, terminal utilities come to life with new colors, and the pleasure of using these programs increases significantly.

Recently, with the team at https://жыбийрыр.рф/, we participated in a hackathon where we created a utility (Go + Ansible) for automated deployment and building of all possible applications on a server (a dystopian task).

For this solution, we also used TUI components for the utility, which helped us stand out among the participants and ultimately win.

What we will use to create the utility

1) Cobra - https://github.com/spf13/cobra

2) Fang - https://github.com/charmbracelet/fang

3) Huh - https://github.com/charmbracelet/huh

Cobra

This library is used for creating console applications in Go.

It provides a necessary set of tools for building utilities:

  • A convenient API for creating commands and defining logic
  • Support for flags
  • Suggestions for incorrect command input, etc.

Fang

This library enhances the description of the utility, making it more informative and vibrant, and integrates seamlessly with Cobra.

By adding Fang, you get the following interface:

Huh

This library provides a set of ready-made TUI components and fits perfectly into the color palette of Fang.

Here are the components you can get by using this library:

Creating the Utility

Clone the template:

git clone https://github.com/deniskorbakov/skeleton-cli-go.git  
Enter fullscreen mode Exit fullscreen mode

Navigate to the project folder:

cd skeleton-cli-go  
Enter fullscreen mode Exit fullscreen mode

Build our application:

make build  
Enter fullscreen mode Exit fullscreen mode

Run the template utility:

./cli  
Enter fullscreen mode Exit fullscreen mode

Application Architecture

cmd/cli - contains a directory with the name of our application, which includes the main file that initializes cobra.

package main  

import "github.com/deniskorbakov/skeleton-cli-go/internal/command"  

func main() {  
    command.Run()  
}
Enter fullscreen mode Exit fullscreen mode

configs/constname - contains constants that describe (name, short, and long descriptions of commands).

example.go

package constname  

const (  
    // UseExampleCmd Name example command    
    UseExampleCmd = `example`  

    // ShortExampleCmd Short description example command
    ShortExampleCmd = `Example test command`  
    ...
)
Enter fullscreen mode Exit fullscreen mode

root.go

package constname  

const (  
    // UseRootCmd App name and default name command    
    UseRootCmd = `cli`  

    // LongRootCmd Long description root command    
    LongRootCmd = `cli is a example util`  
    ...
)
Enter fullscreen mode Exit fullscreen mode

internal/command - contains the application’s commands.

example.go

package command  

import (  
    "github.com/deniskorbakov/skeleton-cli-go/configs/constname"  
    "github.com/deniskorbakov/skeleton-cli-go/internal/component/form"  
    "github.com/deniskorbakov/skeleton-cli-go/internal/component/output"  
    "github.com/spf13/cobra"  
)  

// exampleCmd Command for build pipeline 
var exampleCmd = &cobra.Command{  
    Use:   constname.UseExampleCmd,  
    Short: constname.ShortExampleCmd,  
    Long:  constname.LongExampleCmd,  
    RunE: func(cmd *cobra.Command, args []string) error {  
       fields, err := form.Run()  
       if err != nil {  
          return err  
       }  

       output.Green("Success green output: ", fields.ExampleInput)  
       output.Red("This command will be run successfully")  

       return nil  
    },  
}
Enter fullscreen mode Exit fullscreen mode

Here is an example of a command that we will need to add to root.go.

root.go

package command  

import (  
    "context"  
    "os"  
    "github.com/charmbracelet/fang"  
    "github.com/deniskorbakov/skeleton-cli-go/configs/constname"  
    "github.com/deniskorbakov/skeleton-cli-go/internal/component/output"  
    "github.com/deniskorbakov/skeleton-cli-go/internal/version"  
    "github.com/spf13/cobra"  
)  

// Run Start app with cobra cmd
func Run() {  
    cmd := &cobra.Command{  
       Use:     constname.UseRootCmd,  
       Long:    constname.LongRootCmd,  
       Example: constname.ExampleRootCmd,  
    }  

    // Disable default options cmd  
    cmd.Root().CompletionOptions.DisableDefaultCmd = true  

    // Add all command in your app  
    cmd.AddCommand(  
       exampleCmd,  
    )  

    if err := fang.Execute(  
       context.Background(),  
       cmd,  
       fang.WithVersion(version.Get()),  
    ); err != nil {  
       output.Red("The app is broken")  
       os.Exit(1)  
    }  
}
Enter fullscreen mode Exit fullscreen mode

This file contains the logic for describing the main command, configuring the utility, listing added commands, and initializing Cobra.

internal/component - contains the application’s components.

internal/component/form/form.go

package form  

import "github.com/charmbracelet/huh"  

// Run Main function that runs an interactive form
func Run() (*Fields, error) {  
    fields := &Fields{}  

    err := huh.NewForm(  
       huh.NewGroup(  
          huh.NewInput().  
             Title("Example Title").  
             Description("Example description input").  
             Value(&fields.ExampleInput),  
       ),  
    ).WithShowHelp(true).Run()  
    if err != nil {  
       return nil, err  
    }  

    return fields, nil  
}
Enter fullscreen mode Exit fullscreen mode

Here we create a form using huh.

internal/component/form/fields.go

package form  

// Fields struct for huh form
type Fields struct {  
    ExampleInput string  
}
Enter fullscreen mode Exit fullscreen mode

This structure contains the fields we specify when creating the form.

In your application, you can remove this implementation if you consider it unnecessary and use any other output.

internal/component/output/output.go

package output  

import (  
    "fmt"  

    "github.com/charmbracelet/lipgloss/v2")  

var (  
    green = lipgloss.Color("#04B575")  
    red   = lipgloss.Color("#D4634C")  
)  

func output(colorText string) {  
    fmt.Println(colorText)  
}  

func Green(str ...string) {  
    output(lipgloss.NewStyle().Foreground(green).Render(str...))  
}  

func Red(str ...string) {  
    output(lipgloss.NewStyle().Foreground(red).Render(str...))  
}
Enter fullscreen mode Exit fullscreen mode

This component is used for outputting text to the console.

You can also easily extend this component by adding your own colors using lipgloss.

internal/version - contains the logic for parsing the application version.

internal/version/version.go

package version  

import (  
    "regexp"  
    "runtime/debug")  

var (  
    regexVersion = `^v?(\d+\.\d+\.\d+)`  
    emptyVersion = "none version"  
)  

// Get return version from debug main version
func Get() string {  
    if info, ok := debug.ReadBuildInfo(); ok {  
       version := info.Main.Version  

       re := regexp.MustCompile(regexVersion)  
       if matches := re.FindStringSubmatch(version); len(matches) > 1 {  
          return matches[1]  
       }  
       return emptyVersion  
    } else {  
       return emptyVersion  
    }  
}
Enter fullscreen mode Exit fullscreen mode

We retrieve the specified version during the application build and use a regular expression to get the version value; if it’s not found, we indicate that the version was not found.

Changing the utility name and its description

Currently, our project’s utility is called cli - let’s replace it with the name of your utility.

Change the path to your application to the name of your utility: cmd/cli -> cmd/your_name_cli.

In the .goreleaser.yaml file, you need to change the name cli to the name of your utility:

version: 2  

env:  
  - GO111MODULE=on  
project_name: your_name_cli  
Enter fullscreen mode Exit fullscreen mode

Also, replace the application name in the Makefile:

build:  
    go mod vendor    
    go build -ldflags "-w -s" -o your_name_cli cmd/your_name_cli/main.go  
Enter fullscreen mode Exit fullscreen mode

Change the application description in the root command - configs/constname/root.go.

Goreleaser

This is a tool for automating the process of creating releases for projects across different distributions and OS.

The project already includes a .goreleaser.yaml file and a configured GitHub Action.

You only need to add a secret to the repository named GO_RELEASER, which will contain your personal GitHub token.

Conclusion

In this article, I showed you how to create your own TUI utility in Go using a template.

GitHub - I’d be happy if you follow me.

Thank you for reading this article!

Top comments (0)