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}