package tui import ( "fmt" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // passwordPromptMode determines what the password prompt is for type passwordPromptMode int const ( // passwordModeSetup — first time enabling encryption, ask for new password passwordModeSetup passwordPromptMode = iota // passwordModeUnlock — encryption already enabled, ask for existing password passwordModeUnlock ) // PasswordPromptTab shows a password prompt for encryption setup or unlock type PasswordPromptTab struct { mode passwordPromptMode inputs []textinput.Model focus int width int height int err error dataDir string onComplete func(password string) // called with password on success } // NewPasswordPromptTab creates a new password prompt tab func NewPasswordPromptTab(mode passwordPromptMode, dataDir string, onComplete func(string)) *PasswordPromptTab { p := &PasswordPromptTab{ mode: mode, dataDir: dataDir, onComplete: onComplete, } // Password field passwordInput := textinput.New() passwordInput.Placeholder = "Enter master password" passwordInput.EchoMode = textinput.EchoPassword passwordInput.EchoCharacter = '•' passwordInput.Focus() // Confirm password field (only for setup mode) confirmInput := textinput.New() confirmInput.Placeholder = "Confirm password" confirmInput.EchoMode = textinput.EchoPassword confirmInput.EchoCharacter = '•' if mode == passwordModeSetup { p.inputs = []textinput.Model{passwordInput, confirmInput} } else { p.inputs = []textinput.Model{passwordInput} } return p } func (p *PasswordPromptTab) Name() string { if p.mode == passwordModeSetup { return "Setup Encryption" } return "Unlock Storage" } func (p *PasswordPromptTab) Init() tea.Cmd { return textinput.Blink } func (p *PasswordPromptTab) Update(msg tea.Msg) (Tab, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: p.width = msg.Width p.height = msg.Height return p, nil case tea.KeyMsg: switch msg.String() { case "tab", "down": p.focus++ if p.focus >= len(p.inputs) { p.focus = 0 } for i := range p.inputs { if i == p.focus { p.inputs[i].Focus() } else { p.inputs[i].Blur() } } return p, nil case "shift+tab", "up": p.focus-- if p.focus < 0 { p.focus = len(p.inputs) - 1 } for i := range p.inputs { if i == p.focus { p.inputs[i].Focus() } else { p.inputs[i].Blur() } } return p, nil case "enter": password := p.inputs[0].Value() if password == "" { p.err = fmt.Errorf("password cannot be empty") return p, nil } if p.mode == passwordModeSetup && len(p.inputs) > 1 { confirm := p.inputs[1].Value() if password != confirm { p.err = fmt.Errorf("passwords do not match") return p, nil } } // Success — call onComplete if p.onComplete != nil { p.onComplete(password) } return p, func() tea.Msg { return passwordSetMsg{} } case "esc": // Cancel — go back or quit return p, func() tea.Msg { return closeFormMsg{} } default: // Update current input var cmd tea.Cmd p.inputs[p.focus], cmd = p.inputs[p.focus].Update(msg) return p, cmd } } return p, nil } func (p *PasswordPromptTab) Close() { // Nothing to clean up } func (p *PasswordPromptTab) View() string { var b strings.Builder title := "Setup Master Password" if p.mode == passwordModeUnlock { title = "Enter Master Password" } b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center, AppTitleStyle.Render(title))) b.WriteString("\n\n") if p.mode == passwordModeSetup { b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center, SubtitleStyle.Render("Encrypt all sensitive data (passwords, keys) with AES-256"))) b.WriteString("\n") b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center, SubtitleStyle.Render("You will need this password to access your data"))) b.WriteString("\n\n") } // Input fields contentW := p.width - 8 if contentW > 60 { contentW = 60 } box := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("62")). Padding(1, 2). Width(contentW) var fields strings.Builder for i, input := range p.inputs { label := "Password:" if i == 1 { label = "Confirm:" } fields.WriteString(SubtitleStyle.Render(" " + label)) fields.WriteString("\n") fields.WriteString(input.View()) fields.WriteString("\n\n") } b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center, box.Render(fields.String()))) if p.err != nil { b.WriteString("\n") b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center, ErrorStyle.Render(p.err.Error()))) } // Footer footerText := "Enter:confirm Esc:cancel Tab:next field" b.WriteString("\n\n") for _, line := range strings.Split(wrapFooter(footerText, p.width), "\n") { b.WriteString(lipgloss.PlaceHorizontal(p.width, lipgloss.Center, SubtitleStyle.Render(line))) } return b.String() } // passwordSetMsg is sent when password is successfully set type passwordSetMsg struct { password string }