password_prompt.go

  1package tui
  2
  3import (
  4	"strings"
  5
  6	"charm.land/bubbles/v2/textinput"
  7	tea "charm.land/bubbletea/v2"
  8	"charm.land/lipgloss/v2"
  9	"github.com/floatpane/matcha/config"
 10)
 11
 12// PasswordPrompt asks the user for their encryption password to unlock the app.
 13type PasswordPrompt struct {
 14	input     textinput.Model
 15	err       string
 16	width     int
 17	height    int
 18	verifying bool
 19}
 20
 21// NewPasswordPrompt creates a new password prompt screen.
 22func NewPasswordPrompt() *PasswordPrompt {
 23	ti := textinput.New()
 24	ti.Placeholder = t("password_prompt.enter_password")
 25	ti.EchoMode = textinput.EchoPassword
 26	ti.EchoCharacter = '*'
 27	ti.Prompt = "> "
 28	ti.CharLimit = 256
 29	ti.Focus()
 30	ti.SetStyles(ThemedTextInputStyles())
 31
 32	return &PasswordPrompt{
 33		input: ti,
 34	}
 35}
 36
 37func (m *PasswordPrompt) Init() tea.Cmd {
 38	return textinput.Blink
 39}
 40
 41func (m *PasswordPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 42	switch msg := msg.(type) {
 43	case tea.WindowSizeMsg:
 44		m.width = msg.Width
 45		m.height = msg.Height
 46		return m, nil
 47
 48	case tea.KeyPressMsg:
 49		switch msg.String() {
 50		case "enter":
 51			password := m.input.Value()
 52			if password == "" {
 53				m.err = t("password_prompt.error_empty")
 54				return m, nil
 55			}
 56			m.verifying = true
 57			return m, verifyPasswordCmd(password)
 58		case "ctrl+c":
 59			return m, tea.Quit
 60		}
 61		// Clear error on new input
 62		if m.err != "" {
 63			m.err = ""
 64		}
 65
 66	case PasswordVerifiedMsg:
 67		if msg.Err != nil {
 68			m.err = msg.Err.Error()
 69			m.verifying = false
 70			m.input.SetValue("")
 71			return m, nil
 72		}
 73		// Password correct — key is in msg.Key
 74		return m, nil
 75
 76	}
 77
 78	var cmd tea.Cmd
 79	m.input, cmd = m.input.Update(msg)
 80	return m, cmd
 81}
 82
 83func (m *PasswordPrompt) View() tea.View {
 84	var b strings.Builder
 85
 86	b.WriteString(logoStyle.Render(choiceLogo))
 87	b.WriteString("\n")
 88
 89	lockTitle := lipgloss.NewStyle().
 90		Foreground(lipgloss.Color("#FFFDF5")).
 91		Background(lipgloss.Color("#25A065")).
 92		Padding(0, 1).
 93		Render(t("password_prompt.title"))
 94
 95	b.WriteString(lockTitle)
 96	b.WriteString("\n\n")
 97
 98	if m.verifying {
 99		b.WriteString("  Verifying password...\n")
100	} else {
101		b.WriteString("  " + m.input.View() + "\n")
102	}
103
104	if m.err != "" {
105		errStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
106		b.WriteString("\n" + errStyle.Render("  "+m.err) + "\n")
107	}
108
109	mainContent := b.String()
110	helpView := helpStyle.Render(t("password_prompt.help"))
111
112	if m.height > 0 {
113		currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
114		gap := m.height - currentHeight
115		if gap > 0 {
116			mainContent += strings.Repeat("\n", gap)
117		}
118	} else {
119		mainContent += "\n\n"
120	}
121
122	return tea.NewView(docStyle.Render(mainContent + helpView))
123}
124
125// verifyPasswordCmd runs password verification in a goroutine.
126func verifyPasswordCmd(password string) tea.Cmd {
127	return func() tea.Msg {
128		key, err := config.VerifyPassword(password)
129		return PasswordVerifiedMsg{Key: key, Err: err}
130	}
131}