Files
homelab/knecht/tui/form.go
2026-04-04 15:05:08 +02:00

158 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}