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
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.
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:
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
Navigate to the project folder:
cd skeleton-cli-go
Build our application:
make build
Run the template utility:
./cli
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()
}
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`
...
)
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`
...
)
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
},
}
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)
}
}
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
}
Here we create a form using huh.
internal/component/form/fields.go
package form
// Fields struct for huh form
type Fields struct {
ExampleInput string
}
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...))
}
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
}
}
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
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
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)