cli
packageAPI reference for the cli
package.
Imports
(16)fmt
STD
strings
PKG
github.com/charmbracelet/bubbletea
PKG
github.com/charmbracelet/lipgloss
STD
reflect
STD
time
PKG
github.com/mirkobrombin/go-cli-builder/v2/pkg/cli
PKG
github.com/mirkobrombin/go-cli-builder/v2/pkg/help
PKG
github.com/mirkobrombin/go-cli-builder/v2/pkg/parser
INT
github.com/vanilla-os/sdk/pkg/v1/roff
PKG
github.com/charmbracelet/bubbles/progress
STD
io
PKG
github.com/charmbracelet/bubbles/list
PKG
github.com/charmbracelet/bubbles/spinner
PKG
github.com/charmbracelet/lipgloss/table
PKG
github.com/charmbracelet/bubbles/textinput
confirmModel
type confirmModel struct
Methods
func (confirmModel) Update(msg tea.Msg) (tea.Model, tea.Cmd)
{
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.err = fmt.Errorf("interrupted")
return m, tea.Quit
case "y", "Y":
m.choice = true
case "n", "N":
m.choice = false
case "left", "right", "h", "l", "tab":
m.choice = !m.choice
case "enter":
m.submitted = true
return m, tea.Quit
}
}
return m, nil
}
Returns
func (confirmModel) View() string
{
var s strings.Builder
s.WriteString("\n" + lipgloss.NewStyle().Bold(true).Render(m.prompt) + "\n\n")
yStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).MarginRight(2)
nStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).MarginRight(2)
if m.choice {
yStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true).MarginRight(2)
} else {
nStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true).MarginRight(2)
}
s.WriteString(yStyle.Render(m.yesText))
s.WriteString(nStyle.Render(m.noText))
s.WriteString("\n")
return s.String()
}
Fields
| Name | Type | Description |
|---|---|---|
| prompt | string | |
| yesText | string | |
| noText | string | |
| choice | bool | |
| err | error | |
| submitted | bool |
initialConfirmModel
Parameters
Returns
func initialConfirmModel(prompt, yesText, noText string, defaultChoice bool) confirmModel
{
return confirmModel{
prompt: prompt,
yesText: yesText,
noText: noText,
choice: defaultChoice,
}
}
Uses
Base
Base is an alias for builder.Base to be used by consumers
type Base builder.Base
Command
Command represents a CLI command.
type Command struct
Methods
StartProgressBar starts a progress bar with a message and a total. The progress bar is stopped automatically when it reaches the total or manually by calling the Stop method on the returned model.
Parameters
Returns
func (Command) StartProgressBar(message string, total int) *ProgressBarModel
{
p := progress.New(
progress.WithGradient("#277eff", "#e0388d"),
progress.WithoutPercentage(),
)
m := progressComponent{
progress: p,
total: total,
message: message,
}
prog := tea.NewProgram(m)
go func() {
if _, err := prog.Run(); err != nil {
fmt.Println("Error running progress bar:", err)
}
}()
return &ProgressBarModel{
program: prog,
total: total,
current: 0,
}
}
progressBar := myApp.CLI.StartProgressBar("Loading the batmobile...", 100)
for i := 0; i < 100; i++ {
progressBar.Increment(1)
time.Sleep(50 * time.Millisecond)
}
StartSpinner starts a spinner with a message. The spinner can be stopped by calling the Stop method on the returned model.
Parameters
Returns
func (Command) StartSpinner(message string) *SpinnerModel
{
p := tea.NewProgram(initialSpinnerComponent(message))
quit := make(chan struct{})
go func() {
if _, err := p.Run(); err != nil {
fmt.Println("Error running spinner:", err)
}
close(quit)
}()
return &SpinnerModel{
program: p,
}
}
spinner := myApp.CLI.StartSpinner("Loading the batmobile...")
time.Sleep(3 * time.Second)
spinner.Stop()
ConfirmAction prompts the user to confirm an action, it supports customizing the prompt and the text for the "yes" and "no" options. If the user does not provide an answer, the default choice is used.
Parameters
Returns
func (*Command) ConfirmAction(prompt, yesText, noText string, defaultChoice bool) (bool, error)
{
// If custom text provided, use it, assuming 'y' maps to yesText and 'n' to noText visual
p := tea.NewProgram(initialConfirmModel(prompt, yesText, noText, defaultChoice))
m, err := p.Run()
if err != nil {
return false, err
}
if m, ok := m.(confirmModel); ok {
if m.err != nil {
return false, m.err
}
if !m.submitted {
return defaultChoice, nil
}
return m.choice, nil
}
return false, fmt.Errorf("could not retrieve confirmation")
}
confirm, err := myApp.CLI.ConfirmAction(
"Do you like Batman?",
"Yes", "No",
true,
)
if err != nil {
fmt.Println(err)
return err
}
if confirm {
fmt.Println("Everybody likes Batman!")
} else {
fmt.Println("You don't like Batman...")
}
Name returns the name of the command
Returns
func (*Command) Name() string
{
return c.Use
}
Execute runs the command
Returns
func (*Command) Execute() error
{
if c.app == nil {
return fmt.Errorf("no application initialized. Use NewCommandFromStruct")
}
return c.app.Run()
}
AddCommand adds a dynamic command to the application.
Parameters
func (*Command) AddCommand(name string, cmd *parser.CommandNode)
{
c.app.AddCommand(name, cmd)
}
SetTranslator sets the translator for the application.
Parameters
func (*Command) SetTranslator(tr help.Translator)
{
c.app.SetTranslator(tr)
if c.manCmd != nil {
c.manCmd.translator = tr
}
}
SetName sets the name of the root command.
Parameters
func (*Command) SetName(name string)
{
c.app.SetName(name)
}
Reload re-parses the root struct to pick up dynamic changes.
Returns
func (*Command) Reload() error
{
return c.app.Reload()
}
GetRoot returns the underlying root struct of the command.
Returns
func (*Command) GetRoot() any
{
return c.root
}
SelectOption prompts the user to select an option from a list of options.
Parameters
Returns
func (*Command) SelectOption(prompt string, options []string) (string, error)
{
if strings.Contains(prompt, "%d") {
prompt = fmt.Sprintf(prompt, len(options))
}
p := tea.NewProgram(initialListModel(prompt, options))
m, err := p.Run()
if err != nil {
return "", err
}
if m, ok := m.(listModel); ok {
if m.err != nil {
return "", m.err
}
return m.selected, nil
}
return "", fmt.Errorf("could not retrieve selection")
}
selected, err := myApp.CLI.SelectOption(
"What is your preferred hero?",
[]string{"Batman", "Ironman", "Spiderman", "Robin", "None"},
)
if err != nil {
fmt.Println(err)
return err
}
fmt.Printf("You selected %s!\n", selected)
Table renders a styled table with the given headers and data using lipgloss.
Parameters
Returns
func (*Command) Table(headers []string, data [][]string) error
{
headerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true).Padding(0, 1)
styledHeaders := make([]string, len(headers))
for i, h := range headers {
styledHeaders[i] = headerStyle.Render(h)
}
t := table.New().
Border(lipgloss.NormalBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("238"))).
Headers(styledHeaders...).
Rows(data...).
StyleFunc(func(row, col int) lipgloss.Style {
switch {
default:
if row%2 == 0 {
return lipgloss.NewStyle().
Foreground(lipgloss.Color("252")).
Padding(0, 1)
}
return lipgloss.NewStyle().
Foreground(lipgloss.Color("246")).
Padding(0, 1)
}
})
fmt.Println(t)
return nil
}
err := myApp.CLI.Table(
[]string{"Name", "Age"},
[][]string{
{"Batman", "35"},
{"Robin", "25"},
},
)
PromptText prompts the user to input a text, it supports customizing the prompt and the placeholder.
Parameters
Returns
func (*Command) PromptText(prompt, placeholder string) (string, error)
{
p := tea.NewProgram(initialTextInputModel(prompt, placeholder))
m, err := p.Run()
if err != nil {
return "", err
}
if m, ok := m.(textInputModel); ok {
if m.err != nil {
return "", m.err
}
if m.textInput.Value() == "" {
return placeholder, nil
}
return m.textInput.Value(), nil
}
return "", fmt.Errorf("could not retrieve manual input")
}
response, err := myApp.CLI.PromptText(
"What is your name?",
"Bruce Wayne",
)
if err != nil {
fmt.Println(err)
return err
}
fmt.Printf("Hello %s!\n", response)
Fields
| Name | Type | Description |
|---|---|---|
| Use | string | |
| Short | string | |
| Long | string | |
| root | any | |
| app | *builder.App | |
| manCmd | *ManCmd |
ManCmd
ManCmd is the command to generate the man page
type ManCmd struct
Methods
Run runs the man command
Returns
func (*ManCmd) Run() error
{
man, err := GenerateManPage(c.root, c.translator)
if err != nil {
return err
}
fmt.Println(man)
return nil
}
manCmd := &cli.ManCmd{root: s}
err := parser.Run(manCmd)
Fields
| Name | Type | Description |
|---|---|---|
| root | any | |
| translator | help.Translator |
NewCommandFromStruct
NewCommandFromStruct returns a new Command created from a struct.
Parameters
Returns
func NewCommandFromStruct(s any) (*Command, error)
{
app, err := builder.New(s)
if err != nil {
return nil, err
}
// We inject the man command
manCmd := &ManCmd{root: s}
manNode, err := parser.Parse("man", manCmd)
if err == nil {
manNode.Description = "Generate man page"
app.AddCommand("man", manNode)
}
node := app.RootNode
c := &Command{
Use: node.Name,
Short: node.Description,
root: s,
app: app,
manCmd: manCmd,
}
return c, nil
}
Example
type RootCmd struct {
cli.Base
Poll PollCmd `cmd:"poll" help:"Ask the user preferred hero"`
Man ManCmd `cmd:"man" help:"Generate man page"`
}
cmd, err := cli.NewCommandFromStruct(&RootCmd{})
if err != nil {
return nil, err
}
err := cmd.Execute()
GenerateManPage
GenerateManPage generates a man page for the declarative struct
Parameters
Returns
func GenerateManPage(root any, tr help.Translator) (string, error)
{
t := reflect.TypeOf(root)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
cleanRoot := reflect.New(t).Interface()
node, err := parser.Parse("root", cleanRoot)
if err != nil {
return "", err
}
description := node.Description
if tr != nil {
description = tr(cleanKey(description))
}
d := roff.NewDocument()
d.Heading(1, node.Name, description, time.Now())
docNode(d, node, tr)
return d.String(), nil
}
Example
type RootCmd struct {
cli.Base
Poll PollCmd `cmd:"poll" help:"Ask the user preferred hero"`
Man ManCmd `cmd:"man" help:"Generate man page"`
}
man, err := cli.GenerateManPage(&RootCmd{}, nil)
if err != nil {
return "", err
}
GenerateManPage automatically uses a zero-value instance of the root struct
to exclude any dynamic commands.
docNode
docNode recursively documents a command node and its children.
Parameters
func docNode(d *roff.Document, node *parser.CommandNode, tr help.Translator)
{
description := node.Description
if tr != nil {
description = tr(cleanKey(description))
}
d.Section("subcommand " + node.Name)
d.Indent(4)
d.Text(description)
d.IndentEnd()
d.EndSection()
// Options
if len(node.Flags) > 0 {
d.SubSection("Options")
for name, meta := range node.Flags {
short := ""
if meta.Short != "" {
short = fmt.Sprintf("-%s, ", meta.Short)
}
desc := meta.Description
if tr != nil {
desc = tr(cleanKey(desc))
}
d.Text(fmt.Sprintf(" %s--%s %s\n", short, name, desc))
}
d.EndSection()
}
// Commands
if len(node.Children) > 0 {
for _, child := range node.Children {
docNode(d, child, tr)
}
}
}
cleanKey
Parameters
Returns
func cleanKey(key string) string
{
if strings.HasPrefix(key, "pr:") {
return strings.TrimPrefix(key, "pr:")
}
return key
}
progressMsg
type progressMsg float64
titleMsg
type titleMsg string
stopMsg
type stopMsg struct
progressComponent
type progressComponent struct
Methods
func (progressComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd)
{
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.Type == tea.KeyCtrlC {
return m, tea.Quit
}
case progressMsg:
var cmd tea.Cmd
if m.current < m.total {
pct := float64(m.current) / float64(m.total)
cmd = m.progress.SetPercent(pct)
return m, cmd
}
case titleMsg:
m.message = string(msg)
case stopMsg:
return m, tea.Quit
case progress.FrameMsg:
progressModel, cmd := m.progress.Update(msg)
m.progress = progressModel.(progress.Model)
return m, cmd
}
return m, nil
}
Returns
func (progressComponent) View() string
{
pad := strings.Repeat(" ", 2)
return "\n" +
pad + m.message + "\n" +
pad + m.progress.View() + "\n\n"
}
Fields
| Name | Type | Description |
|---|---|---|
| progress | progress.Model | |
| total | int | |
| current | int | |
| message | string |
ProgressBarModel
type ProgressBarModel struct
Methods
Parameters
func (*ProgressBarModel) Increment(inc int)
{
m.current += inc
if m.current >= m.total {
m.current = m.total
m.Stop()
return
}
// Bubbles progress takes a 0-1 float.
pct := float64(m.current) / float64(m.total)
m.program.Send(progressMsg(pct))
}
UpdateMessage updates the title logic.
Parameters
func (*ProgressBarModel) UpdateMessage(msg string)
{
m.program.Send(titleMsg(msg))
}
func (*ProgressBarModel) Stop()
{
m.program.Send(stopMsg{})
// Allow cleanup
time.Sleep(100 * time.Millisecond)
}
Fields
| Name | Type | Description |
|---|---|---|
| program | *tea.Program | |
| total | int | |
| current | int |
item
type item string
itemDelegate
type itemDelegate struct
Methods
Parameters
Returns
func (itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd
{ return nil }
Parameters
func (itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item)
{
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))
}
listModel
type listModel struct
Methods
func (listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd)
{
switch msg := msg.(type) {
case tea.KeyMsg:
key := msg.String()
switch key {
case "ctrl+c":
m.err = fmt.Errorf("interrupted")
return m, tea.Quit
case "enter":
if i, ok := m.list.SelectedItem().(item); ok {
m.selected = string(i)
}
return m, tea.Quit
case "1", "2", "3", "4", "5", "6", "7", "8", "9":
idx := int(key[0] - '1')
items := m.list.Items()
if idx >= 0 && idx < len(items) {
if i, ok := items[idx].(item); ok {
m.selected = string(i)
return m, tea.Quit
}
}
}
case tea.WindowSizeMsg:
m.list.SetWidth(msg.Width)
return m, nil
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
Fields
| Name | Type | Description |
|---|---|---|
| list | list.Model | |
| selected | string | |
| err | error |
initialListModel
Parameters
Returns
func initialListModel(prompt string, options []string) listModel
{
items := make([]list.Item, len(options))
for i, opt := range options {
items[i] = item(opt)
}
const defaultWidth = 20
l := list.New(items, itemDelegate{}, defaultWidth, 14)
l.Title = prompt
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
l.Styles.Title = titleStyle
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle
return listModel{list: l}
}
Uses
SpinnerModel
type SpinnerModel struct
Methods
UpdateMessage updates the spinner message dynamically.
Parameters
func (*SpinnerModel) UpdateMessage(message string)
{
if m.program != nil {
m.program.Send(updateMsg(message))
}
}
spinner.UpdateMessage("Loading the batcave...")
func (*SpinnerModel) Stop()
{
if m.program != nil {
m.program.Send(quitMsg{})
// Wait for clear
time.Sleep(100 * time.Millisecond)
}
}
Fields
| Name | Type | Description |
|---|---|---|
| program | *tea.Program | |
| quit | chan struct{} |
spinnerComponent
type spinnerComponent struct
Methods
func (spinnerComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd)
{
switch msg := msg.(type) {
case quitMsg:
m.quitting = true
return m, tea.Quit
case updateMsg:
m.message = string(msg)
return m, nil
}
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
Returns
func (spinnerComponent) View() string
{
if m.quitting {
return ""
}
return fmt.Sprintf("\n %s %s\n\n", m.spinner.View(), m.message)
}
Fields
| Name | Type | Description |
|---|---|---|
| spinner | spinner.Model | |
| message | string | |
| quitting | bool |
quitMsg
type quitMsg struct
updateMsg
type updateMsg string
initialSpinnerComponent
Parameters
Returns
func initialSpinnerComponent(message string) spinnerComponent
{
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return spinnerComponent{
spinner: s,
message: message,
}
}
textInputModel
type textInputModel struct
Methods
func (textInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd)
{
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
m.err = fmt.Errorf("interrupted")
return m, tea.Quit
case tea.KeyEnter:
return m, tea.Quit
}
case tea.WindowSizeMsg:
m.textInput.Width = msg.Width
}
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
Returns
func (textInputModel) View() string
{
var style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return fmt.Sprintf(
"%s\n%s\n",
style.Bold(true).Render(m.prompt),
m.textInput.View(),
)
}
Fields
| Name | Type | Description |
|---|---|---|
| textInput | textinput.Model | |
| err | error | |
| prompt | string |
initialTextInputModel
Parameters
Returns
func initialTextInputModel(prompt, placeholder string) textInputModel
{
ti := textinput.New()
ti.Placeholder = placeholder
ti.Focus()
ti.CharLimit = 156
ti.Width = 40
ti.Prompt = "➜ "
return textInputModel{
textInput: ti,
prompt: prompt,
}
}