pulumi/sdk/go/common/util/cmdutil/console_input.go

130 lines
3.4 KiB
Go

// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !js
// +build !js
package cmdutil
import (
"io"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
func readConsoleFancy(stdout io.Writer, stdin io.Reader, prompt string, secret bool) (string, error) {
final, err := tea.NewProgram(
newReadConsoleModel(prompt, secret),
tea.WithInput(stdin),
tea.WithOutput(stdout),
).Run()
if err != nil {
return "", err
}
model, ok := final.(readConsoleModel)
contract.Assertf(ok, "expected readConsoleModel, got %T", final)
if model.Canceled {
return "", io.EOF
}
return model.Value, nil
}
// readConsoleModel drives a bubbletea widget that reads from the console.
type readConsoleModel struct {
input textinput.Model
secret bool
// Canceled is set to true when the model finishes
// if the user canceled the operation by pressing Ctrl-C or Esc.
Canceled bool
// Value is the user's response to the prompt.
Value string
}
var _ tea.Model = readConsoleModel{}
func newReadConsoleModel(prompt string, secret bool) readConsoleModel {
input := textinput.New()
input.Cursor.Style = lipgloss.NewStyle().
Foreground(lipgloss.Color("205")) // 205 = hot pink cursor
if secret {
input.EchoMode = textinput.EchoPassword
}
if prompt != "" {
input.Prompt = prompt + ": "
}
input.Focus() // required to receive input
return readConsoleModel{
input: input,
secret: secret,
}
}
// Init initializes the model.
// We don't have any initialization to do, so we just return nil.
func (readConsoleModel) Init() tea.Cmd { return nil }
// Update handles a single tick of the bubbletea loop.
func (m readConsoleModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// If the user pressed enter, Ctrl-C, or Esc,
// it's time to stop the bubbletea loop.
//
// Only Enter is considered a success.
//nolint:exhaustive // We only want special handling for these keys.
switch msg.Type {
case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc:
m.Value = m.input.Value()
m.Canceled = msg.Type != tea.KeyEnter
m.input.Blur() // hide the cursor
if m.secret {
// If we're in secret mode, don't include
// the '*' characters in the final output
// so as not to leak the length of the input.
m.input.EchoMode = textinput.EchoNone
}
var cmds []tea.Cmd
if !m.Canceled {
// If the user accepts the input,
// we'll primnt the prompt to the terminal
// before exiting this loop.
cmds = append(cmds, tea.Println(m.input.View()))
}
cmds = append(cmds, tea.Quit)
return m, tea.Sequence(cmds...)
}
}
var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
return m, cmd
}
// View renders the prompt.
func (m readConsoleModel) View() string {
return m.input.View()
}