Skip to content

Interaction with bubbletea #178

@flowchartsman

Description

@flowchartsman

Perhaps similar in spirit to some aspects of #116, I think you could provide a set of simple, interactive inputs using TUI elements provided by bubbletea and bubbles. There are a couple of different approaches that you could take, or combine. A compelling one is a technique I saw used in chezmoi where templates receive a funcmap that provides functions which, when executed, create a bubbles widget on the fly and interpolate its value. Here's a quick (and dirty) prototype.

package main

import (
	"bytes"
	"fmt"
	"log"
	"os"
	"text/template"

	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
)

func main() {
	tmpl, err := template.New("").Funcs(interactiveFns()).Parse(os.Args[1])
	if err != nil {
		log.Fatalf("parsing tempalte: %v", err)
	}
	var buf bytes.Buffer
	tmpl.Execute(&buf, nil)
	os.Stdout.Write(buf.Bytes())
}

type interactiveCtx struct {
	values map[string]any
}

func interactiveFns() template.FuncMap {
	c := &interactiveCtx{
		values: map[string]any{},
	}
	return template.FuncMap{
		"stringInput": stringInput(c),
	}
}

func stringInput(c *interactiveCtx) func(string) (any, error) {
	return func(inputPrompt string) (any, error) {
		if val, found := c.values[inputPrompt]; found {
			return val, nil
		}
		p := tea.NewProgram(initialModel(inputPrompt))
		finalModel, err := p.Run()
		if err != nil {
			return nil, err
		}
		value := finalModel.(textInputModel).Value()
		c.values[inputPrompt] = value
		return value, nil
	}
}

type (
	errMsg error
)

type textInputModel struct {
	prompt    string
	textInput textinput.Model
	err       error
}

func initialModel(inputPrompt string) textInputModel {
	ti := textinput.New()
	ti.Focus()
	ti.CharLimit = 156
	ti.Width = 20

	return textInputModel{
		prompt:    inputPrompt,
		textInput: ti,
		err:       nil,
	}
}

func (m textInputModel) Init() tea.Cmd {
	return textinput.Blink
}

func (m 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.KeyEnter, tea.KeyCtrlC, tea.KeyEsc:
			return m, tea.Quit
		}

	case errMsg:
		m.err = msg
		return m, nil
	}

	m.textInput, cmd = m.textInput.Update(msg)
	return m, cmd
}

func (m textInputModel) View() string {
	return fmt.Sprintf(
		"%s %s",
		m.prompt,
		m.textInput.View(),
	) + "\n"
}

func (m textInputModel) Value() string {
	return m.textInput.Value()
}

Example input/output

go run main.go 'User {{stringInput "First Name"}} {{stringInput "Last Name"}} has a first name of {{
stringInput "First Name"}}'
First Name > Rob                  
Last Name > Robertson            
User Rob Robertson has a first name of Rob

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions