more knecht
This commit is contained in:
157
knecht/tui/form.go
Normal file
157
knecht/tui/form.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
|
||||
labelStyle = lipgloss.NewStyle().Width(32)
|
||||
blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
cursorStyle = focusedStyle
|
||||
)
|
||||
|
||||
// secretKey returns true for keys that likely contain sensitive values.
|
||||
func secretKey(key string) bool {
|
||||
upper := strings.ToUpper(key)
|
||||
for _, word := range []string{"KEY", "TOKEN", "PASSWORD", "SECRET", "PASS"} {
|
||||
if strings.Contains(upper, word) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type formModel struct {
|
||||
title string
|
||||
keys []string
|
||||
inputs []textinput.Model
|
||||
cursor int
|
||||
done bool
|
||||
}
|
||||
|
||||
func newFormModel(title string, keys []string) formModel {
|
||||
inputs := make([]textinput.Model, len(keys))
|
||||
for i, key := range keys {
|
||||
t := textinput.New()
|
||||
t.Placeholder = "enter value"
|
||||
t.CursorStyle = cursorStyle
|
||||
if secretKey(key) {
|
||||
t.EchoMode = textinput.EchoPassword
|
||||
t.EchoCharacter = '•'
|
||||
}
|
||||
if i == 0 {
|
||||
t.Focus()
|
||||
t.PromptStyle = focusedStyle
|
||||
t.TextStyle = focusedStyle
|
||||
}
|
||||
inputs[i] = t
|
||||
}
|
||||
return formModel{title: title, keys: keys, inputs: inputs}
|
||||
}
|
||||
|
||||
func (m formModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m formModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
m.done = true
|
||||
m.keys = nil // signal cancelled
|
||||
return m, tea.Quit
|
||||
|
||||
case "tab", "down":
|
||||
m.cursor = (m.cursor + 1) % len(m.inputs)
|
||||
return m, m.focusAt(m.cursor)
|
||||
|
||||
case "shift+tab", "up":
|
||||
m.cursor = (m.cursor - 1 + len(m.inputs)) % len(m.inputs)
|
||||
return m, m.focusAt(m.cursor)
|
||||
|
||||
case "enter":
|
||||
if m.cursor < len(m.inputs)-1 {
|
||||
m.cursor++
|
||||
return m, m.focusAt(m.cursor)
|
||||
}
|
||||
// Last field — submit
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
// Forward key events to the focused input
|
||||
var cmd tea.Cmd
|
||||
m.inputs[m.cursor], cmd = m.inputs[m.cursor].Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m formModel) focusAt(idx int) tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
for i := range m.inputs {
|
||||
if i == idx {
|
||||
m.inputs[i].PromptStyle = focusedStyle
|
||||
m.inputs[i].TextStyle = focusedStyle
|
||||
cmds = append(cmds, m.inputs[i].Focus())
|
||||
} else {
|
||||
m.inputs[i].Blur()
|
||||
m.inputs[i].PromptStyle = blurredStyle
|
||||
m.inputs[i].TextStyle = blurredStyle
|
||||
}
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m formModel) View() string {
|
||||
var b strings.Builder
|
||||
b.WriteString(titleStyle.Render(m.title) + "\n\n")
|
||||
|
||||
for i, key := range m.keys {
|
||||
label := key
|
||||
if secretKey(key) {
|
||||
label += mutedStyle.Render(" (hidden)")
|
||||
}
|
||||
if i == m.cursor {
|
||||
b.WriteString(focusedStyle.Render("› ") + labelStyle.Render(label) + m.inputs[i].View() + "\n")
|
||||
} else {
|
||||
b.WriteString(blurredStyle.Render(" ") + labelStyle.Render(label) + m.inputs[i].View() + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n" + mutedStyle.Render("tab/↑↓ navigate enter confirm esc cancel"))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// PromptMissingEnv runs a TUI form for the given keys and returns a map of
|
||||
// key → value. Returns nil if the user cancelled.
|
||||
func PromptMissingEnv(stackName string, keys []string) (map[string]string, error) {
|
||||
if len(keys) == 0 {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("Missing env vars for %q", stackName)
|
||||
m := newFormModel(title, keys)
|
||||
p := tea.NewProgram(m)
|
||||
result, err := p.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
final := result.(formModel)
|
||||
if final.keys == nil {
|
||||
return nil, nil // cancelled
|
||||
}
|
||||
|
||||
values := make(map[string]string, len(keys))
|
||||
for i, key := range keys {
|
||||
values[key] = final.inputs[i].Value()
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
Reference in New Issue
Block a user