Building an Awesome Terminal User Interface Using Go, Bubble Tea, and Lip Gloss
In the world of command-line interfaces (CLI), creating intuitive and visually appealing tools can greatly enhance user experience and productivity. In this tutorial, we'll explore how to build a modern CLI tool using Bubble Tea and Lip Gloss — two powerful libraries for developing terminal user interfaces (TUIs) in Go.
Introduction to Bubble Tea and Lip Gloss
Bubble Tea
Bubble Tea is a functional and declarative framework designed for building user interfaces in Go. It follows a model-update-view pattern, where application state updates based on events (e.g., user input), and the view renders accordingly.
Lip Gloss
Lip Gloss complements Bubble Tea by offering a simple yet powerful way to style terminal output. It allows you to define styles for text and containers, making it easy to create visually appealing CLI applications.
Setting Up Your Development Environment
Before diving into coding, ensure Go is installed on your machine. Use Go modules to install Bubble Tea and Lip Gloss:
go get github.com/charmbracelet/bubbletea
go get github.com/charmbracelet/lipgloss
go get github.com/charmbracelet/bubbles/progress
go get github.com/charmbracelet/bubbles/textinput
go get github.com/charmbracelet/bubbles/list
Example: Building a Simple File Downloader Tool with Bubble Tea and Lip Gloss
In this example, we'll build a CLI tool using Bubble Tea and Lip Gloss to download files from a provided URL. The application will allow users to input a URL, select file permissions, and monitor the download progress — all within the terminal.
Step 1: Setting Up Lip Gloss Styles
First, define Lip Gloss styles for enhanced terminal output:
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
generalStatementStyle = lipgloss.NewStyle().BorderForeground().Bold(true)
successStatementStyle = lipgloss.NewStyle().
Bold(true).
Border(lipgloss.NormalBorder()).
Background(lipgloss.Color("#04B575")).
Foreground(lipgloss.Color("#000000")).
Width(50).
AlignHorizontal(lipgloss.Center)
placeHolderStyle = lipgloss.NewStyle().Italic(true)
infoStyle = lipgloss.NewStyle().
Bold(true).
Border(lipgloss.HiddenBorder()).
Foreground(lipgloss.Color("#0000FF"))
Step 2: Implementing Text Input Model
Building a Text Input Model with Bubble Tea and Lip Gloss
The textInput package demonstrates how to create a text input model using Bubble Tea and Lip Gloss in Go. This model allows users to input a URL for downloading a file in a command-line interface (CLI).
Key Components:
- textModel Struct: Defines a model that wraps around the
textinput.Modeland handles user input events. - textInputModel Function: Initializes a new instance of
textinput.Modelwith placeholder text and styles. - Init Method: Implements the
tea.Modelinterface methodInit, which initializes the text input with a blinking cursor. - Update Method: Handles user input events such as pressing Enter to capture the input URL and quitting with Ctrl+C.
- View Method: Renders the text input view, showing the input field and placeholder text styled with Lip Gloss.
Usage:
The StartInputTextModel function initializes and runs the Bubble Tea program, allowing users to interactively enter a URL in the CLI for further processing, such as file downloading.
type textModel struct {
textInput textinput.Model
err error
}
var (
placeHolderStyle = lipgloss.NewStyle().Italic(true)
OutputValue *string
)
func textInputModel() textModel {
ti := textinput.New()
ti.Placeholder = "Enter the url to be downloaded"
ti.PlaceholderStyle = placeHolderStyle
ti.Focus()
return textModel{
textInput: ti,
err: nil,
}
}
func (m textModel) Init() tea.Cmd {
return textinput.Blink
}
func (m textModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEnter:
value := m.textInput.Value()
OutputValue = &value
return m, tea.Quit
case tea.KeyCtrlC:
return nil, tea.Quit
}
}
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
func (m textModel) View() string {
return (m.textInput.View())
}
func StartInputTextModel() {
p := tea.NewProgram(textInputModel())
p.Run()
}
Step 3: Implementing List Model
Create a model for selecting file permissions using a list:
Key Components:
- listModel Struct: Implements the Bubble Tea
tea.Modelinterface to manage a list of options for selecting file permissions. - Item and ItemDelegate: Defines item rendering and interaction logic for the list of permissions.
- optionsRenderer Function: Renders the list of options using Bubble Tea's
list.Modeland handles user interaction to select permissions. - setPermissions Function: Sets file permissions based on the selected option ("RO", "RW", "RWX") using
os.Chmod. - StartListModel Function: Initializes and runs the Bubble Tea program to present the list of permissions and handle user selection.
const listHeight = 10
var (
generalStatementStyle = lipgloss.NewStyle().BorderForeground().Bold(true)
successStatementStyle = lipgloss.NewStyle().
Bold(true).
Border(lipgloss.NormalBorder()).
Background(lipgloss.Color("#04B575")).
Foreground(lipgloss.Color("#000000")).
Width(50).
AlignHorizontal(lipgloss.Center)
SelectedOptions string
OverrideValue string
DefaultValue string
permissions = []list.Item{
Item("RO"),
Item("RW"),
Item("RWX"),
}
)
type Item string
func (i Item) FilterValue() string { return "" }
type ItemDelegate struct{}
func (d ItemDelegate) Height() int { return 1 }
func (d ItemDelegate) Spacing() int { return 0 }
func (d ItemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
itemStyle := lipgloss.NewStyle().PaddingLeft(4)
selecteditemStyle := lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("63"))
i, ok := listItem.(Item)
if !ok {
return
}
str := fmt.Sprintf("%d. %s", index+1, i)
fn := itemStyle.Render
if index == m.Index() {
fn = func(s ...string) string {
return selecteditemStyle.Render("> " + strings.Join(s, " "))
}
}
fmt.Fprint(w, fn(str))
}
func StartListModel() {
optionsRenderer("Please select the file permissions", permissions)
}
Step 4: Implementing Download Model
Create a model for managing file download progress:
Key Components:
- StartDownloaderModel Function: Initializes a Bubble Tea program to manage the download process.
- progressWriter Struct: Implements the
io.Writerinterface to manage and update the download progress. - downloadModel Struct: Implements the Bubble Tea
tea.Modelinterface to manage the download's UI state.
func (m downloadModel) Init() tea.Cmd {
return nil
}
func (m downloadModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m, tea.Quit
case tea.WindowSizeMsg:
m.progress.Width = msg.Width - padding*2 - 4
if m.progress.Width > maxWidth {
m.progress.Width = maxWidth
}
return m, nil
case progressErrMsg:
m.err = msg.err
return m, tea.Quit
case progressMsg:
var cmds []tea.Cmd
if msg >= 1.0 {
cmds = append(cmds, tea.Sequence(finalPause(), tea.Quit))
}
cmds = append(cmds, m.progress.SetPercent(float64(msg)))
return m, tea.Batch(cmds...)
case progress.FrameMsg:
progressModel, cmd := m.progress.Update(msg)
m.progress = progressModel.(progress.Model)
return m, cmd
default:
return m, nil
}
}
func (m downloadModel) View() string {
if m.err != nil {
return "Error downloading: " + m.err.Error() + "\n"
}
pad := strings.Repeat(" ", padding)
return "\n" + pad + m.progress.View() + "\n\n" + pad + helpStyle("Press any key to quit")
}
Final Entrypoint
var (
infoStyle = lipgloss.NewStyle().
Bold(true).
Border(lipgloss.HiddenBorder()).
Foreground(lipgloss.Color("#0000FF"))
)
func main() {
fmt.Println(infoStyle.Render(
"Welcome to File-Manager Cli ! \nWhich is useful to download online files and set permissions instantly.."))
textInput.StartInputTextModel()
downloader.StartDownloaderModel()
list.StartListModel()
}
Now we can build the binary and test the output.