94f2142d3d
- PasswordPromptTab: setup/unlock modes, password + confirm fields - --encrypt flag: shows password prompt on TUI startup - Model.ShowEncryptPrompt() triggers prompt on first render - passwordSetMsg stores password in Model.storagePassword - Tab interface: added Close() method to PasswordPromptTab
219 lines
5.1 KiB
Go
219 lines
5.1 KiB
Go
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
|
|
}
|