183 lines
4 KiB
Go
183 lines
4 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// textInputModel is the internal bubbletea model for text input.
|
|
type textInputModel struct {
|
|
title string
|
|
placeholder string
|
|
value string
|
|
masked bool
|
|
submitted bool
|
|
cancelled bool
|
|
cursorPos int
|
|
validator func(string) error
|
|
err error
|
|
}
|
|
|
|
func newTextInputModel(title, placeholder string) *textInputModel {
|
|
return &textInputModel{
|
|
title: title,
|
|
placeholder: placeholder,
|
|
}
|
|
}
|
|
|
|
func (m *textInputModel) insertChar(ch rune) {
|
|
m.value = m.value[:m.cursorPos] + string(ch) + m.value[m.cursorPos:]
|
|
m.cursorPos++
|
|
}
|
|
|
|
func (m *textInputModel) backspace() {
|
|
if m.cursorPos > 0 {
|
|
m.value = m.value[:m.cursorPos-1] + m.value[m.cursorPos:]
|
|
m.cursorPos--
|
|
}
|
|
}
|
|
|
|
func (m *textInputModel) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
func (m *textInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch msg.Type {
|
|
case tea.KeyEnter:
|
|
if m.validator != nil {
|
|
if err := m.validator(m.value); err != nil {
|
|
m.err = err
|
|
return m, nil
|
|
}
|
|
}
|
|
if m.value == "" && m.placeholder != "" {
|
|
m.value = m.placeholder
|
|
}
|
|
m.submitted = true
|
|
return m, tea.Quit
|
|
case tea.KeyEscape, tea.KeyCtrlC:
|
|
m.cancelled = true
|
|
return m, tea.Quit
|
|
case tea.KeyBackspace:
|
|
m.backspace()
|
|
m.err = nil
|
|
case tea.KeyLeft:
|
|
if m.cursorPos > 0 {
|
|
m.cursorPos--
|
|
}
|
|
case tea.KeyRight:
|
|
if m.cursorPos < len(m.value) {
|
|
m.cursorPos++
|
|
}
|
|
case tea.KeyRunes:
|
|
for _, ch := range msg.Runes {
|
|
m.insertChar(ch)
|
|
}
|
|
m.err = nil
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m *textInputModel) View() string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(BoldStyle.Render(m.title) + "\n\n")
|
|
|
|
display := m.value
|
|
if m.masked {
|
|
display = strings.Repeat("*", len(m.value))
|
|
}
|
|
|
|
if display == "" && m.placeholder != "" {
|
|
sb.WriteString(DimStyle.Render(m.placeholder))
|
|
} else {
|
|
sb.WriteString(display)
|
|
}
|
|
sb.WriteString(AccentStyle.Render("\u2588")) // Cursor block
|
|
|
|
if m.err != nil {
|
|
sb.WriteString("\n" + ErrorStyle.Render(fmt.Sprintf(" %s", m.err)))
|
|
}
|
|
|
|
sb.WriteString("\n\n" + DimStyle.Render("enter submit \u2022 esc cancel"))
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// TextInputOption configures TextInput behaviour.
|
|
type TextInputOption func(*textInputConfig)
|
|
|
|
type textInputConfig struct {
|
|
placeholder string
|
|
masked bool
|
|
validator func(string) error
|
|
}
|
|
|
|
// WithTextPlaceholder sets placeholder text shown when input is empty.
|
|
func WithTextPlaceholder(text string) TextInputOption {
|
|
return func(c *textInputConfig) {
|
|
c.placeholder = text
|
|
}
|
|
}
|
|
|
|
// WithMask hides input characters (for passwords).
|
|
func WithMask() TextInputOption {
|
|
return func(c *textInputConfig) {
|
|
c.masked = true
|
|
}
|
|
}
|
|
|
|
// WithInputValidator adds a validation function for the input.
|
|
func WithInputValidator(fn func(string) error) TextInputOption {
|
|
return func(c *textInputConfig) {
|
|
c.validator = fn
|
|
}
|
|
}
|
|
|
|
// TextInput presents a styled text input prompt and returns the entered value.
|
|
// Returns empty string if cancelled.
|
|
//
|
|
// Falls back to Question() when stdin is not a terminal.
|
|
//
|
|
// name, err := cli.TextInput("Enter your name:", cli.WithTextPlaceholder("Anonymous"))
|
|
// pass, err := cli.TextInput("Password:", cli.WithMask())
|
|
func TextInput(title string, opts ...TextInputOption) (string, error) {
|
|
cfg := &textInputConfig{}
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
// Fall back to simple Question if not a terminal
|
|
if !term.IsTerminal(0) {
|
|
var qopts []QuestionOption
|
|
if cfg.placeholder != "" {
|
|
qopts = append(qopts, WithDefault(cfg.placeholder))
|
|
}
|
|
if cfg.validator != nil {
|
|
qopts = append(qopts, WithValidator(cfg.validator))
|
|
}
|
|
return Question(title, qopts...), nil
|
|
}
|
|
|
|
m := newTextInputModel(title, cfg.placeholder)
|
|
m.masked = cfg.masked
|
|
m.validator = cfg.validator
|
|
|
|
p := tea.NewProgram(m)
|
|
finalModel, err := p.Run()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
final := finalModel.(*textInputModel)
|
|
if final.cancelled {
|
|
return "", nil
|
|
}
|
|
return final.value, nil
|
|
}
|