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}