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 keyEnter:
 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	var cmd tea.Cmd
 78	m.input, cmd = m.input.Update(msg)
 79	return m, cmd
 80}
 81
 82func (m *PasswordPrompt) View() tea.View {
 83	var b strings.Builder
 84
 85	b.WriteString(logoStyle.Render(choiceLogo))
 86	b.WriteString("\n")
 87
 88	lockTitle := lipgloss.NewStyle().
 89		Foreground(lipgloss.Color("#FFFDF5")).
 90		Background(lipgloss.Color("#25A065")).
 91		Padding(0, 1).
 92		Render(t("password_prompt.title"))
 93
 94	b.WriteString(lockTitle)
 95	b.WriteString("\n\n")
 96
 97	if m.verifying {
 98		b.WriteString("  Verifying password...\n")
 99	} else {
100		b.WriteString("  " + m.input.View() + "\n")
101	}
102
103	if m.err != "" {
104		errStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
105		b.WriteString("\n" + errStyle.Render("  "+m.err) + "\n")
106	}
107
108	mainContent := b.String()
109	helpView := helpStyle.Render(t("password_prompt.help"))
110
111	if m.height > 0 {
112		currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
113		gap := m.height - currentHeight
114		if gap > 0 {
115			mainContent += strings.Repeat("\n", gap)
116		}
117	} else {
118		mainContent += "\n\n"
119	}
120
121	return tea.NewView(docStyle.Render(mainContent + helpView))
122}
123
124// verifyPasswordCmd runs password verification in a goroutine.
125func verifyPasswordCmd(password string) tea.Cmd {
126	return func() tea.Msg {
127		key, err := config.VerifyPassword(password)
128		return PasswordVerifiedMsg{Key: key, Err: err}
129	}
130}